"The caller does not have permission - google-apps-script" - google-apps-script

I am trying to create an extension to google classroom using google-apps-script but I seem to be running into permission problems- "the caller does not have permission". Can someone tell me why this is happening and what I need to do
I have tried accessing all the libraries provided by google cloud platform hoping that one of them gives the permission that is required all to no avail.
function listCourses() {
var optionalArgs = {
pageSize: 10
};
var response = Classroom.Courses.list(optionalArgs);
var courses = response.courses;
if (courses && courses.length > 0) {
for (i = 0; i < courses.length; i++) {
var course = courses[i];
Logger.log('%s (%s)', course.name, course.id);
var submissions = Classroom.Courses.CourseWork.list(course.id, optionalArgs);
for (i = 0; i < submissions.length; i++) {
var submission = submissions[i];
Logger.log('%s', submission);
}
}
} else {
Logger.log('No courses found.');
}
}
The code is meant to list out the course ids of courses in a particular class

This solution involves doing 2 things -
Updating the manifest file (appsscript.json)
Updating the Code.gs file
Task 1
Updating the manifest file (appsscript.json)
The appropriate Classroom API reference for this task is here.
Looks like even after enabling Advanced Google services..., you only get the following OAuth Scopes added -
https://www.googleapis.com/auth/classroom.courses
https://www.googleapis.com/auth/classroom.coursework.me.readonly
https://www.googleapis.com/auth/classroom.profile.emails
https://www.googleapis.com/auth/classroom.profile.photos
https://www.googleapis.com/auth/classroom.rosters
You can view these by navigating to File > Project properties > Scopes.
However, when you try the API from the documentation link, under the Credentials > Google OAuth 2.0 tab, it shows 4 more, completely different OAuth scopes; those are as follows -
https://www.googleapis.com/auth/classroom.coursework.me
https://www.googleapis.com/auth/classroom.coursework.me.readonly
https://www.googleapis.com/auth/classroom.coursework.students
https://www.googleapis.com/auth/classroom.coursework.students.readonly
You need to add all 8 of these manually in your Apps Script manifest file. To do that, navigate to View & check the Show manifest file. There you need to add this code, perhaps below dependencies -
"oauthScopes": [
"https://www.googleapis.com/auth/classroom.courses",
"https://www.googleapis.com/auth/classroom.coursework.me.readonly",
"https://www.googleapis.com/auth/classroom.profile.emails",
"https://www.googleapis.com/auth/classroom.profile.photos",
"https://www.googleapis.com/auth/classroom.rosters",
"https://www.googleapis.com/auth/classroom.coursework.me",
"https://www.googleapis.com/auth/classroom.coursework.me.readonly",
"https://www.googleapis.com/auth/classroom.coursework.students",
"https://www.googleapis.com/auth/classroom.coursework.students.readonly"
],
Note1: Only adding the newer 4 will not do the trick as the script would assume only these and not the original 5 that were auto-populated when your script ran for the first time.
Note2: The blank line is simply to differentiate between the scopes that get generated automatically vs. the ones you need to add manually (its redundant).
My appsscript.json file looks like this; yours might differ -
{
"timeZone": "Asia/Kolkata",
"dependencies": {
"enabledAdvancedServices": [{
"userSymbol": "Classroom",
"serviceId": "classroom",
"version": "v1"
}]
},
"oauthScopes": [
"https://www.googleapis.com/auth/classroom.courses",
"https://www.googleapis.com/auth/classroom.coursework.me.readonly",
"https://www.googleapis.com/auth/classroom.profile.emails",
"https://www.googleapis.com/auth/classroom.profile.photos",
"https://www.googleapis.com/auth/classroom.rosters",
"https://www.googleapis.com/auth/classroom.coursework.me",
"https://www.googleapis.com/auth/classroom.coursework.me.readonly",
"https://www.googleapis.com/auth/classroom.coursework.students",
"https://www.googleapis.com/auth/classroom.coursework.students.readonly"
],
"exceptionLogging": "STACKDRIVER"
}
Task 2
Updating the Code.gs file
Once you have the right permissions, you're then free to play around with the actual code - the one you've originally shared lacks a few components and I've modified the same to get it working here -
function listCourses() {
var optionalArgs = {
pageSize: 10
};
var response = Classroom.Courses.list(optionalArgs);
var courses = response.courses;
if (courses && courses.length > 0) {
for (var i = 0; i < courses.length; i++) {
var course = courses[i];
Logger.log('%s (%s)', course.name, course.id);
var submissions = Classroom.Courses.CourseWork.list(course.id, optionalArgs);
for (var j = 0; j < submissions.courseWork.length; j++) {
var submission = submissions.courseWork[j];
Logger.log('%s', submission);
}
}
} else {
Logger.log('No courses found.');
}
}
Hope this helps :) but I don't actually know why these scopes are not added automatically, perhaps open up an issue with Google for it.
Edit note: Rectified grammatical errors

Related

Google App Script timing out while removing Viewers, How to make the script more efficient?

I have a GAS that I run every month or so to remove Viewers and Editors from GoogleDocs and GoogleSheets that were created over 1 year ago. I have not found a way to return ONLY the documents which have the specific users I want to remove.
So the code is setup to loop thru all the documents in a specific folder and if the Viewers/Editors do not match the 2 owners, then it removes their access.
The problem is a few folders have a large number of files and it is timing out just reading thru to find out if any Viewers/Editors need to be removed.
Any ideas on how this code could be streamlined or if there is a way to query for only the documents not owned by a specific user?
var folder = folders.next(); //assume the match is the first one
folder = DriveApp.getFolderById(folder.getId()); //use the folderID of the year folder
processFolder(folder); //this starts in with the newest folder modified date under the Proposals/Year folder and works down thru the list until it times out after 5 minutes of running
function processFolder(folder) {
var asset;
var users;
var email;
var files = folder.getFiles();
var todaysDate = new Date();
while (files.hasNext()) {
var file = files.next();
var daysCreated = parseInt(((todaysDate - file.getDateCreated()) / 86400000)); //how many days since the document was created 24/3600/1000 = 86,400,000
if (daysCreated > RETENTION_DAYS) {
asset = DriveApp.getFileById(file.getId());
for (var i = 0; i < 2; i++) {
if (i == 0) {
users = asset.getEditors();
} else {
users = asset.getViewers();
}
for (var cnt = 0; cnt < users.length; cnt++) {
email = users[cnt].getEmail().toLowerCase();
if (email != "xxx1#gmail.com" && email != "xxx2#gmail.com") {
if (i == 0) { //Editors
asset.removeEditor(email);
} else { //Viewers
asset.removeViewer(email);
}
}
}
}
}
} //processFolder
About how this code could be streamlined or if there is a way to query for only the documents not owned by a specific user?, for example, if you want to retrieve only the files without including xxx1#gmail.com and xxx2#gmail.com as the writer and the viewer, how about using searchFiles instead of getFiles? When your script is modified, it becomes as follows.
Modified script:
function processFolder(folder) {
var emails = ["xxx1#gmail.com", "xxx2#gmail.com"]; // Please set the email addresses.
var query = emails.map(e => `not '${e}' in writers and not '${e}' in readers`).join(" and ") + " and trashed=false";
var files = folder.searchFiles(query);
var todaysDate = new Date();
while (files.hasNext()) {
var file = files.next();
var daysCreated = parseInt(((todaysDate - file.getDateCreated()) / 86400000));
if (daysCreated > RETENTION_DAYS) {
file.getEditors().forEach(e => file.removeEditor(e));
file.getViewers().forEach(e => file.removeViewer(e));
}
}
}
When this script is run, the writers and the viewers of the files without including "xxx1#gmail.com" and "xxx2#gmail.com" as the writer and the viewer are removed.
Note:
When this sample script is run, the writers and the viewers of the files without including "xxx1#gmail.com" and "xxx2#gmail.com" as the writer and the viewer are removed. So, I would like to recommend testing this script using the sample files. Please be careful about this.
Reference:
searchFiles(params)
Added:
From your replying, as another approach, in this case, how about the following sample script? In this sample, the following flow is used.
Retrieve all file IDs just under the specific folder using Drive API.
Retrieve permission IDs from the files using Drive API.
Create the requests for deleting the permissions except for "emails".
Delete permissions using Drive API.
Usage:
1. Install a Google Apps Script library.
In this sample, the batch request is used. In this case, I created a Google Apps Script library for this. So, please install the library. About the method for installing it, you can see it at here.
2. Enable Drive API.
This script uses Drive API. So, please enable Drive API at Advanced Google services.
3. Sample script:
Please copy and paste the following script to the script editor and set emails and folderId. And please run sample(). By this, the script is run.
function sample() {
var emails = ["xxx1#gmail.com", "xxx2#gmail.com"]; // Please set the email addresses.
var folderId = "###"; // Please set the folder ID.
// 1. Retrieve all file IDs just under the specific folder using Drive API.
var list = [];
var pageToken = "";
do {
var obj = Drive.Files.list({q: `'${folderId}' in parents`, maxResults: 1000, pageToken, fields: "items(id),nextPageToken"});
if (obj.items.length > 0) list = [...list, ...obj.items.map(({id}) => id)];
pageToken = obj.nextPageToken;
} while(pageToken);
// 2. Retrieve permission IDs from the files using Drive API.
var req1 = list.map(id => ({method: "GET", endpoint: `https://www.googleapis.com/drive/v3/files/${id}/permissions?pageSize=100&fields=permissions(id%2CemailAddress%2Crole)`}))
var token = ScriptApp.getOAuthToken();
var requests1 = {
batchPath: "batch/drive/v3", // batch path. This will be introduced in the near future.
requests: req1,
accessToken: token
};
var result1 = BatchRequest.EDo(requests1);
// 3. Create the requests for deleting the permissions except for "emails".
var req2 = list.reduce((ar, id, i) => {
var p = result1[i].permissions;
if (p.length > 0) {
p.forEach(e => {
if (e.role != "owner" && e.emailAddress && !emails.includes(e.emailAddress)) {
ar.push({method: "DELETE", endpoint: `https://www.googleapis.com/drive/v3/files/${id}/permissions/${e.id}`});
}
})
}
return ar;
}, []);
// 4. Delete permissions using Drive API.
var requests2 = {
batchPath: "batch/drive/v3",
requests: req2,
accessToken: token
};
var result2 = BatchRequest.EDo(requests2);
}
When this script is run, about all files just under the specific folder, all permissions except for the owner and emails are removed.
Note:
This script removes the permissions. Please be careful about this. So in this case, I would like to propose to test using a sample permitted files.
Reference:
BatchRequest

Last modified date in Google App Script GAS sheets

I am trying to get the last modified date of a sheet from a GAS add on which I am developing.
My current idea is to get the Drive revision list and then take the last value. This seems a bit overkill for just getting the last modified, I am also worried that this will break if the number of revisions exceeds 1000 as per this link suggests.
https://developers.google.com/drive/api/v3/reference/revisions/list
Ideally I would like to know the range which has changed too, but I do not think this is possible.
I cannot use the onEdit event because I would like to track edits made by users who have not installed the add-on.
var fileId = SpreadsheetApp.getActiveSpreadsheet().getId();
var revisions = Drive.Revisions.list(fileId);
var revisionLength = revisions.items.length;
if(revisionLength > 0){
var revision = revisions.items[revisionLength-1];
var date = new Date(revision.modifiedDate);
Logger.log(date.toString());
}
I believe you can do that as follow
var lastUpdated = DriveApp.getFileById(fileId).getLastUpdated();
See function reference
Given that you need users of your add-on to have access to revision information from non-add-on users, the Drive revision list is precisely what you need. Happily, you can get the content of revisions, so if you wish you can compute diffs. I don't know what your data looks like, so that might be easy or nigh-impossible.
Aside: to your point about more than 1000 revisions, if there are more than 1000 (or whatever your page size is) revisions, you'll get a nextPageToken like so:
{
"kind": "drive#revisionList",
"nextPageToken": "BHNMJKHJKHKVJHyugaiohasdzT1JyUmlQWG10RUJ1emx1S2xNDg4EgQzMzY1GAI=",
"revisions": [
...
]
}
If you see that you'll need to list revisions again, providing that token.
Anyway, when you list revisions, each revision will look something like this:
{
"kind": "drive#revision",
"etag": "\"som3-e-tAg\"",
"id": "3365",
"selfLink": "https://www.googleapis.com/drive/v2/files/dummydummydummy/revisions/3365",
"mimeType": "application/vnd.google-apps.spreadsheet",
"modifiedDate": "2018-10-19T19:05:41.762Z",
"published": false,
"exportLinks": {
"application/x-vnd.oasis.opendocument.spreadsheet": "https://docs.google.com/spreadsheets/export?id=dummydummydummy&revision=3365&exportFormat=ods",
"text/tab-separated-values": "https://docs.google.com/spreadsheets/export?id=dummydummydummy&revision=3365&exportFormat=tsv",
"application/pdf": "https://docs.google.com/spreadsheets/export?id=dummydummydummy&revision=3365&exportFormat=pdf",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "https://docs.google.com/spreadsheets/export?id=dummydummydummy&revision=3365&exportFormat=xlsx",
"text/csv": "https://docs.google.com/spreadsheets/export?id=dummydummydummy&revision=3365&exportFormat=csv",
"application/zip": "https://docs.google.com/spreadsheets/export?id=dummydummydummy&revision=3365&exportFormat=zip",
"application/vnd.oasis.opendocument.spreadsheet": "https://docs.google.com/spreadsheets/export?id=dummydummydummy&revision=3365&exportFormat=ods"
},
"lastModifyingUserName": "Joe User",
"lastModifyingUser": {
"kind": "drive#user",
"displayName": "Joe User",
"picture": {
"url": "https://lh3.googleusercontent.com/-asdfsadf/AAAAAAAAAAI/AAAAAAAAFOk/OIPUYOIUGO/s64/photo.jpg"
},
"isAuthenticatedUser": true,
"permissionId": "123456789",
"emailAddress": "user#gmail.com"
}
}
Provided your data isn't insanely complex or large, you could fetch the target the text/csv export link for the revisions you wish to compare, and then do that comparison in Apps Script.
That might look something like:
var fileId = SpreadsheetApp.getActiveSpreadsheet().getId();
var revisions = Drive.Revisions.list(fileId);
var revisionLength = revisions.items.length;
if(revisionLength > 1) { // something to compare!
var revision = revisions.items[revisionLength-1];
var newContent = UrlFetchApp.fetch(revision.exportLinks["text/csv"]).getContent();
newContent = Utilities.parseCsv(newContent);
var oldRevision = revisions.items[revisionLength-2];
var oldContent = UrlFetchApp.fetch(oldRevision.exportLinks["text/csv"]).getContent();
oldContent = Utilities.parseCsv(oldContent);
# TODO check they're the same size!
# where do they differ?
for (var row = 0; row < newContent.length; row++) {
for (var col = 0; col < newContent[0].length; col++) {
if (newContent[row][col] != oldContent[row][col]) {
Logger.log('Change on row ' + (row + 1) + ' column ' + (col + 1));
}
}
# when did it change?
var date = new Date(revision.modifiedDate);
Logger.log(date.toString());
}

List Google app script projects

I'm trying to list all my google script files using the Google Drive File API as documented here. However I always get back an empty [] list. I think my token and my scopes are fine as I'm getting back this from Google OAuth2:
{ "access_token": "xxxxx", "expires_in": 3600, "refresh_token": "yyyyy",
"scope": "https://www.googleapis.com/auth/drive.scripts https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive",
"token_type": "Bearer"
}
But then when I issue the query using mimeType filtering (I just want to get google app scripts list):
mimeType = 'application/vnd.google-apps.script'
I'm only getting back an empty items list ([]), even if I've just created a google script inside a Google Sheet:
{ "kind": "drive#fileList", "etag": "....", "selfLink": "https://www.googleapis.com/drive/v2/files?q=mimeType+%3D+'application/vnd.google-apps.script'", "incompleteSearch": false, "items": []}
I'm guessing that app-script means something else than google script code which is part of a Google Sheet... Any help appreciated :)
About retrieving the file list of projects of the container-bound script type and exporting each script from the project, please check the following answer.
Retrieve file list of projects of container-bound script type
Unfortunately, the file list of projects of the container-bound script type cannot be retrieved yet, while the file list of projects of the standalone type can be retrieved using drive.files.list of Drive API.
This has been reported as a future request. https://issuetracker.google.com/issues/111149037
Export scripts in a project
When you want to export each script from a project of the containter-bound script type and the standalone type, you can do it using Google Apps Script API.
This might become a GAS sample script for your situation. here.
Also you can do it using a GAS library like this.
A Patchwork Quilt of a Work Around to get a list of Standalone and Bound Project Ids
I posted this here because I ran across this page several times during the last couple of days and in case there is someone else who would really like to get a complete list of their projects and doesn't mind having to jump through a few hoops to get it then this might be useful to them.
I found a way to get all of the my Apps Script Project File Ids. It's not pretty but but I got all 1393 of them bound and standalone. I went into Google Developer Hub and noticed that my project id's could be found in an attribute name data-script-id="project id" and by continuing to page down to the bottom of the list I was able to get the page to keep seeking more pages until there were none left. Then I went into Chrome developers tools and I found the div that contained all the divs with the above attribute and for me this one was named <div class="uBvHbd" and I copied the entire div and tried pasting it into an ascii file on my Google Account but I found that to be a problem so I opened up my copy of UltraEdit and pasted it in there. I played around with the regex for a while and spent the day retraining myself on the UltraEdit Document Model and their version of the Javascript Engine and developed the following routines which allowed me to build a complete list of projectids.
UltraEdits Scripting Language:
function getDocumentPaths() {
UltraEdit.outputWindow.write('File Paths:');
for(var i=0;i<UltraEdit.document.length;i++) {
UltraEdit.outputWindow.write('[' + i + ']: ' + UltraEdit.document[i].path);
}
}
function getFileNames() {
var nA=[];
var fnToIdx={fnA:[]};
for(var i=0;i<UltraEdit.document.length;i++) {
var p=UltraEdit.document[i].path;
var pA=p.split(/\\/);
var fn=pA[pA.length-1].split('.')[0];
fnToIdx.fnA.push(fn);
fnToIdx[fn]=i;
}
UltraEdit.outputWindow.write('FileNames: \r\n' + fnToIdx.fnA.join('\r\n'));
return fnToIdx;
}
function getScriptIdsIndex() {
for(var i=0;i<UltraEdit.document.length;i++) {
if(UltraEdit.document[i].isName('ScriptIds')) {
return i;
}
}
}
function getFileIndexByName(name) {
var name=name||'ScriptIds';
if(name) {
for(var i=0;i<UltraEdit.document.length;i++) {
if(UltraEdit.document[i].isName(name)) {
return i;
}
}
}else{
UltraEdit.messageBox("Invalid or Missing Inputs at getFileIndexByName().","Alert")
}
}
function getAllFileIndices(obj) {
UltraEdit.outputWindow.write('File Indices:');
var fnIndicesA=[]
for(var j=0;j<obj.fnA.length;j++) {
fnIndicesA.push({name:obj.fnA[j] , index:obj[obj.fnA[j]]})
}
var_dump(fnIndicesA);
}
function updateWorkingTabs(index) {
UltraEdit.outputWindow.write('Index: ' + index);
UltraEdit.document[index].selectAll();
UltraEdit.document[index].copy();
var workingTabsA=[];
for(var i=0;i<UltraEdit.document.length;i++) {
if(UltraEdit.document[i].path.slice(0,-1)=='Edit') {
UltraEdit.document[i].paste();
workingTabsA.push(i);
}
}
return workingTabsA;
}
function findAllIds() {
var fnToIndex=getFileNames();
UltraEdit.document[fnToIndex['ScriptIds']].selectAll();
UltraEdit.document[fnToIndex['ScriptIds']].copy();
var s=UltraEdit.clipboardContent;
var re=/data-script-id="[^"]+"/g;
var matchA=s.match(re);
UltraEdit.document[fnToIndex['FileIds']].selectAll();
UltraEdit.document[fnToIndex['FileIds']].cut();
for(var i=0;i<matchA.length;i++) {
UltraEdit.document[fnToIndex['FileIds']].write(matchA[i].slice(16,-1) + '\r\n');
}
}
function removeDuplicates() {
var fnToIndex=getFileNames();
UltraEdit.document[fnToIndex['FileIds']].selectAll();
UltraEdit.document[fnToIndex['FileIds']].copy();
var fnA=UltraEdit.clipboardContent.split('\r\n');
if(!fnA[fnA.length-1]) {
fnA.pop();
}
var uA=[];
for(var i=0;i<fnA.length;i++) {
if(uA.indexOf(fnA[i])==-1) {
uA.push(fnA[i]);
}
}
var s='';
for(var i=0;i<uA.length;i++) {
if(i>0){
s+='\r\n';
}
s+=uA[i];
}
UltraEdit.document[fnToIndex['FileIds']].selectAll();
UltraEdit.document[fnToIndex['FileIds']].cut();
UltraEdit.document[fnToIndex['FileIds']].write(s);
}
UltraEdit.outputWindow.clear();
//UltraEdit.open('E:\\Projects\\ScriptIds\\ScriptIds.txt');
//var ScriptIds_idx=getScriptIdsIndex();
//var wtA=updateWorkingTabs(ScriptIds_idx);
//findAllIds()
removeDuplicates();
The input file was 24 MB and the output list was 81KB and it took the script running on my laptop less then 5 seconds to strip out the id's and remove duplicates (didn't find any either).
I ran these two routines in Google Apps Script to get script info from Google Apps Script API so that I could figure out which were Standalone and which were container bound. And for the container bound I was able to get the Container Filename and id.
function getInfo(projectId) {
var projectId=projectId||pid;
var rObj={'ProjectId':projectId};
var params = {muteHttpExceptions:true,headers: {"Authorization": "Bearer " + ScriptApp.getOAuthToken()}};
try {
var url=Utilities.formatString('https://script.googleapis.com/v1/projects/%s',projectId);
}
catch(e) {
return rObj['Error']=e;
}
var resp=UrlFetchApp.fetch(url,params);
var data=JSON.parse(resp.getContentText());
if(data.hasOwnProperty('parentId')) {
var pFile=DriveApp.getFileById(data.parentId)
var parentName=pFile.getName();
var pfldrA=[];
var pFolders=pFile.getParents();
while(pFolders.hasNext()) {
var folder=pFolders.next();
pfldrA.push({id:folder.getId(),name:folder.getName()});
}
rObj['ProjectName']=data.title;
rObj['ParentName']=parentName;
rObj['ParentId']=data.parentId;
rObj['ParentFolders']='';
rObj['ParentFolderIds']='';
//var html=Utilities.formatString('<br /><b>Project Name:</b>%s<br /><b>ParentName:</b> %s<br /><b>ParentId:</b> %s',data.title,parentName,data.parentId);
//html+=Utilities.formatString('<br /><b>Parent Folders:</b>');
for(var i=0;i<pfldrA.length;i++) {
if(i>0) {
rObj.ParentFolders+=', ';
rObj.ParentFolderIds+=', ';
}
//html+=Utilities.formatString('<br /><b>Name:</b> %s <b>Id:</b> %s',pfldrA[i].name,pfldrA[i].id);
rObj.ParentFolders+=pfldrA[i].name;
rObj.ParentFolderIds+=pfldrA[i].id;
}
}else{
rObj['ProjectName']=data.title;
//var html=Utilities.formatString('<br /><b>StandAlone Project</b><br /><b>Project Name:</b>%s<br /><b>ProjectId:</b>%s',data.title,projectId);
}
//Logger.log(data);
//var userInterface=HtmlService.createHtmlOutput(html);
//SpreadsheetApp.getUi().showModelessDialog(userInterface, "Project Info");
return rObj;
}
function updateProjects() {
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName('Projects');
var idx={hA:[],hr:1}
idx.hA=sh.getRange(1,1,1,sh.getLastColumn()).getValues()[0];
idx.hA.forEach(function(e,i){idx[e]=i;});
var lr=sh.getLastRow();
var sr=getColumnHeight(1,sh,ss) + 1;
var cnt=25;
setGlobal('count',cnt);
if(lr-sr+1<cnt) {
cnt=lr-sr+1;
}
if(sr>=lr) {
setGlobal('count',0);
return;
}
//Item ProjectId ProjectName ParentId ParentName ParentFolders ParentFolderIds
var rg=sh.getRange(sr,1,cnt,sh.getLastColumn());
var vA=rg.getValues();
for(var i=0;i<vA.length;i++) {
if(!vA[i][idx.Item] && vA[i][idx.ProjectId]) {
var pObj=getInfo(vA[i][idx.ProjectId]);
if(!pObj.hasOwnProperty('Error') && pObj.hasOwnProperty('ParentId')) {
vA[i][idx.Item]=i+sr;
vA[i][idx.ProjectName]=pObj.ProjectName;
vA[i][idx.ParentId]=pObj.ParentId;
vA[i][idx.ParentName]=pObj.ParentName;
vA[i][idx.ParentFolders]=pObj.ParentFolders;
vA[i][idx.ParentFolderIds]=pObj.ParentFolderIds;
}else if(!pObj.hasOwnProperty('Error') && !pObj.hasOwnProperty('ParentId')){
vA[i][idx.Item]=i+sr;
vA[i][idx.ProjectName]=pObj.ProjectName;
vA[i][idx.ParentName]='Standalone Project';
}else{
vA[i][idxItem]=i+sr;
vA[i][idx.ProjectName]=(pObj.hasOwnProperty('Error'))?pObj.Error:"Unknown Problem";
}
Utilities.sleep(1000);
}
}
rg.setValues(vA);
}
I'll come back later and put some comments in the code. It's not pretty but I now have a list of all my projects and where to find them and the names of their containers if they have one. I have to thank #Tanaike for the Apps Script library. It really helped me to figure out how access the Apps Script API which turned out to be a lot less difficult than I thought it would be.
A Glimpse at a portion of the final list:

Writing Add-on manifest files

I created a GAS script file on my Drive using the example from Packt's book 'Learning Google Apps Script. My question is how do I make this into an Add-On. Not quite sure on the manifest file I need to create (even after reading https://developers.google.com/gmail/add-ons/how-tos/building).
function saveEmailAttachmentsToDrive(){
// Create 'Gmail Attachments' folder if not exists.
createFolder_('Gmail attachments');
// Get inbox threads starting from the latest one to 100.
var threads = GmailApp.getInboxThreads(0, 100);
var messages = GmailApp.getMessagesForThreads(threads);
var folderID = PropertiesService.getUserProperties()
.getProperty("FOLDER");
var file, folder = DriveApp.getFolderById(folderID);
for (var i = 0 ; i < messages.length; i++) {
for (var j = 0; j < messages[i].length; j++) {
if(!messages[i][j].isUnread()){
var msgId = messages[i][j].getId();
// Assign '' if MSG_ID is undefined.
var oldMsgId = PropertiesService.getUserProperties()
.getProperty('MSG_ID') || '';
if(msgId > oldMsgId){
var attachments = messages[i][j].getAttachments();
for (var k = 0; k < attachments.length; k++) {
PropertiesService.getUserProperties()
.setProperty('MSG_ID', messages[i][j].getId());
try {
file = folder.createFile(attachments[k]);
Utilities.sleep(1000);// Wait before next iteration.
} catch (e) {
Logger.log(e);
}
}
}
else return;
}
}
}
};
function createFolder_(name) {
var folder, folderID, found = false;
/*
* Returns collection of all user folders as an iterator.
* That means it do not return all folder names at once,
* but you should get them one by one.
*
*/
var folders = DriveApp.getFolders();
while (folders.hasNext()) {
folder = folders.next();
if (folder.getName() == name) {
folderID = folder.getId();
found = true;
break;
}
};
if (!found) {
folder = DriveApp.createFolder(name);
folderID = folder.getId();
};
PropertiesService.getUserProperties()
.setProperty("FOLDER", folderID);
return folderID;
}
My question is how do I make this into an Add-On. Not quite sure on the manifest file I need to create (even after reading https://developers.google.com/gmail/add-ons/how-tos/building).
The JSON for the manifest file for the gmail part is:
"gmail": {
"name": "My Gmail Add-on",
"logoUrl": "https://www.example.com/hosted/images/2x/my-icon.png",
"primaryColor": "#4285F4",
"secondaryColor": "#00BCD4",
"authorizationCheckFunction": "get3PAuthorizationUrls",
"contextualTriggers": [{
"unconditional": {},
"onTriggerFunction": "buildAddOn"
}]
That must be in addition to what is already there. A manifest file at it's very basic, looks like this:
{
"timeZone": "America/New_York",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER"
}
You are going to make it look like the following:
{
"timeZone": "America/New_York",
"dependencies": {
},
"gmail": {
"name": "My Gmail Add-on",
"logoUrl": "https://www.example.com/hosted/images/2x/my-icon.png",
"primaryColor": "#4285F4",
"secondaryColor": "#00BCD4",
"authorizationCheckFunction": "get3PAuthorizationUrls",
"contextualTriggers": [{
"unconditional": {},
"onTriggerFunction": "buildAddOn"
}]
},
"exceptionLogging": "STACKDRIVER"
}
Use your timeZone, and your settings. Note that there are no dependencies listed, but leave that part in.
manifest.json is generated automatically by the Apps Script engine. It is hidden by default, but you can view it by toggling View > manifest file to see the structure.
When you're ready to test the AddOn, you can go to Publish > Deploy as web add on. If you're uploading from the cloud editor, you do not need to add a separate manifest file in the web store listing.
The development documentation covers deployment methods in detail.

How to enable Google Classroom courseWorkList

I've been testing the Google Classroom Api and can't seem to get the courseWorkList method to run getting a caller error
I've basically cut and pasted code from the reference and it works if i remove the classCourseWork line. Otherwise I get a "The caller does not have permission" error. I've enabled the API services, approved it to run, etc. Any help in getting this one sorted?
function listCourses() {
var optionalArgs = {
pageSize: 10
};
var response = Classroom.Courses.list(optionalArgs);
var courses = response.courses;
if (courses && courses.length > 0) {
for (i = 0; i < courses.length; i++) {
var course = courses[i];
var classCourseWork = Classroom.Courses.CourseWork.list(course.id);
}
Logger.log('%s (%s)', course.name, course.id);
} else {
Logger.log('No courses found.');
}
}
I've tried your code, here are the scenarios:
As a student:
Your code is working fine.
As a teacher:
I've also received "The caller does not have permission" error.
You might want to check this open issue tracker, that a teacher cannot access the coursework (or the students' turned-in assignments) in their course, getting the same permission error. It was updated and was filed a ticket internally, we can follow this ticket to be updated.
Hope this helps.
Does this thread sound like the same issue? Http://stackoverflow.com/questions/42796630/google-classroom-apps-script-coursework-list-permission-error