Google Apps Script Permissions for a function with inputs - google-apps-script

I know this topic has bee covered before sort of, but none of it really makes sense to me. Basically I wrote a function that will generate a PDF from some API data using user inputs. It looks vaguely like this and works when I run it in the script editor.
function myfunction(InputA,InputB,InputC,InputD,InputE) {
......
var sourceSpreadsheet = SpreadsheetApp.getActive();
var parents = DriveApp.getFileById(sourceSpreadsheet.getId()).getParents();
if (parents.hasNext()) {
var folder = parents.next();
}
var response = UrlFetchApp.fetch(url, options)
var blob = response.getAs('application/pdf').setName(InputA + InputB + InputC)
var newFile = folder.createFile(blob);
return newFile
}
The problem is Google permissions. The classic: "You don't have permission to access the drive".
I have tried publishing the script as a private sheets addon and enabled it on my spreadsheet. But that didn't really do anything. And I don't really know why because I authorized the app for all the required scopes when I approved the add on. I can see it in the extensions menu but I am still getting errors when I try to call the function.
The button method of enabling permissions doesn't work for me because I need to run the code several times based on parameters defined in the Sheet. I tried simple triggers since I want the code to run weekly anyways, but found the same problem.
Can someone give me the step by step of how I'm supposed to do this.
Please don't send links to the google documentation because I have read the related pages and still don't know what I'm doing wrong.

I recommend you use an installable onEdit trigger. I have this approach and see if it works for you.
Sample Data:
Assuming url is from Input D column.
Create a column of checkboxes that will trigger the installed trigger. In my case, ticking it will create the file and unticking it will remove the file created.
Ticking rows where an input (at least 1) is missing, will cancel the creation of the file and then untick the checkbox ticked.
Drive folder:
Script:
function createFileTrigger(e) {
var spreadsheet = e.source;
var sheet = spreadsheet.getActiveSheet();
var range = e.range;
var value = e.value;
var row = range.getRow();
var col = range.getColumn();
// proceed if edited cell is Sheet1!H2:H
if(sheet.getSheetName() == 'Sheet1' && col == 8 && row > 1) {
// if checkbox is ticked
if(value == 'TRUE') {
// inputs = [Input A, Input B, Input C, Input D, Input E]
var inputs = range.offset(0, -6, 1, 5).getValues().flat();
var parents = DriveApp.getFileById(spreadsheet.getId()).getParents();
var folder = parents.next();
// set some conditions here to pre-check the inputs
// e.g. if at least 1 input is blank, cancel file creation (return)
if(inputs.filter(String).length < 5) {
// untick the checkbox ticked
range.setValue('FALSE');
// skip creation of file
return;
}
// assuming url is from Input D (removed options as not needed for presentation)
var response = UrlFetchApp.fetch(inputs[3]);
var newFileName = `${inputs[0]} ${inputs[1]} ${inputs[2]}`;
// if file is existing (which should not happen but just in case)
if(folder.getFilesByName(newFileName).hasNext()) {
// do something else that is needed to be done to avoid duplication of file
// e.g. overwrite or skip creating file
console.log(newFileName + ' is already existing in the parent folder');
}
// if not existing
else {
// create the file
var blob = response.getAs('application/pdf').setName(newFileName)
// for presenation purposes, will write the id of the created file
range.offset(0, -1).setValue(folder.createFile(blob).getId());
}
}
// if checkbox is unticked
else {
// do something else that is needed to be done
// e.g. delete the file using the id returned (using Drive Advanced Services)
var fileIdRange = range.offset(0, -1);
Drive.Files.remove(fileIdRange.getValue());
// remove file id content on the cell
fileIdRange.clearContent();
}
}
}
Ticking checkbox (folder):
Ticking checkbox (sheet):
Note:
This can still be improved, but should already be enough for your case.

Related

Conditionally copy and paste a range within a loop in Google Apps Script

I have created a sales tool to quickly mark companies as qualified or disqualified using check boxes in google apps script. Users can manually check a box for "qualified", "unqualified", or "Not Sure". If an "unqualified", or "not sure" box is checked, the row will be hidden and and the "not sure" rows will be copied and pasted into a new sheet called "Maybes". I have built an auto qualify function that will automatically check the qualify or disqualify box based certain terms that it reads from column E. Sometimes the macro will check both boxes (both qualifying and disqualifying terms present). When that happens, I have written this macro to automatically check the not sure box and then paste that row into the "Maybes" tab. To do this I have created a loop to check that both boxes are checked, check the "not sure" box (this part works perfectly), and then copy and paste the row into the new tab. The problem I am running into is that only the very last row ends up getting copied. When I run the debugger it will copy whatever row I exit at (i.e. when step over i = 5 and then I hit stop, it will copy and paste that row. But when I let the whole function run it will only copy and paste i = 1000). It seems like the loop does not want to paste all the values, instead it only wants to paste the last one run. All help is appreciated!
function auto_maybes(){
var destSheet = SpreadsheetApp.getActive().getSheetByName("Maybes");
var destRange = destSheet.getRange(destSheet.getLastRow()+1,1);
var source = SpreadsheetApp.getActive().getSheetByName("Qualification 1");
var Qual_range = source.getDataRange().getValues();
for( var i = 1; i< Qual_range.length; i++){
if(Qual_range[i][0] == true && Qual_range[i][1] == true){
var maybe_range = source.getRange(1+i,3);
maybe_range.setValue("TRUE");
var maybe = maybe_range.getRow();
var info = source.getRange(maybe,4,1,7);
info.copyTo(destRange, {contentsOnly: true} );
}
}
}
'''
[1]: https://i.stack.imgur.com/Bzpzc.png
Try it this way
Since you used getDataRange() is hard to be sure but normally there is at least one header in a spreadsheet. So I start my data at row 2. But perhaps you can correct me if I'm wrong.
function auto_maybes() {
const ss = SpreadsheetApp.getActive();
const msh = ss.getSheetByName("Maybes");
const qsh = ss.getSheetByName("Qualification 1");
const qvs = qsh.getRange(2,1,qsh.getLastRow() - 1, qsh.getLastColumn()).getValues();
qvs.forEach((r,i) => {
if(r[0] == true && r[1] == true) {
qsh.getRange(i + 2,3).setValue("TRUE");
qsh.getRange(i + 2, 4, 1, 7).copyTo(msh.getRange(msh.getLastRow() + 1 ,1))
}
})
}

onFormSubmit() not working in Google Form?

[UPDATE]
I had a look at add-ons and I am afraid this won't work. So let me take a step back and describe what I am trying to achieve.
I have a spreadsheet A, with a list of individual events. Each event is a line item in the spreadsheet. The spreadsheet is very long for one, and has many fields that I don't need to expose to event owners (different events different owners). Which means if I allow all these different people edit access to the sheet, it becomes really chaotic.
The solution I came up with is to generate unique IDs programmatically for each event, which I've done. Then for each event, I create an individual form and a pre-filled link, with pre-filled answers that is pulled from the cell values. I intend to give the pre-filled links to event owners when they need to make any updates.
The issue is now I have 100+ forms, and I don't want to have 100+ corresponding tabs set as destinations of these forms. These 100+ forms need to submit responses to one same sheet (tab). Instead I wrote a function for submitted responses to find the right event (the event unique ID is the title of the form) and updates the right cell. This is what you see below processSubmission().
I have tried to write the processSubmission() in the spreadsheet where the events are listed. If I don't set this spreadsheet as destination of these 100+ forms then the spreadsheet doesn't know there is a "submission" event. Therefore the setting the trigger onFormSubmit() in the spreadsheet doesn't work.
Then I moved onFormSubmit() -> processSubmission() and it doesn't set the trigger because as you all pointed out, it's an installable trigger.
What I did manage to to write an onOpen() -> create the onFormSubmission() trigger. That means I had to manually open 100 forms and close them to create that trigger. The triggers are created alright. But turned out for the trigger to actually run I need to manually grant permission!
When I looked at add-on triggers, it says "Add-ons can only create triggers for the file in which the add-on is used. That is, an add-on that is used in Google Doc A cannot create a trigger to monitor when Google Doc B is opened." So I think that also rules out the add-on triggers. So now I am out of ideas.
[ORIGINAL]
I made a custom function for the processing of submission responses. I use the form title as a key, and the response answers are written to the corresponding headers in the row with the right key.
My first try was something like this. But it simply didn't execute when the form was submitted:
function onFormSubmit(e){
var form = FormApp.getActiveForm();
var key = form.getTitle();
var responses = e.response;
var ss= SpreadsheetApp.openById(ss_id);
var sheet = spreadsheet.getSheetByName('Launch list');
var frozenRow = sheet.getFrozenRows();
var lastRow = sheet.getLastRow();
var lastColumn = sheet.getLastColumn();
var headers = sheet.getRange(1, 1, 1, lastColumn).getValues()[0];
var keyCol = headers.indexOf(key_header) + 1;
var header1Col = headers.indexOf(header_1) + 1;
var header2Col = headers.indexOf(header_2) + 1;
var header3Col = headers.indexOf(header_3) + 1;
var keysRange = sheet.getRange(frozenRow+1, keyCol , lastRow - frozenRow, 1);
var allKys = keysRange.getValues();
for (i=0; i<allKys.length; i++){
var keyValue = allKys[i][0];
if (keyValue === key){
var rowNum = l + frozenRow + 1;
break;
}
else {
continue;
}
}
var dataRow = sheet.getRange(rowNum, 1, 1, lastColumn).getValues()[0];
var lookUp = {};
lookUp[item_title_1] = header1Col ;
lookUp[item_title_2] = header2Col ;
lookUp[item_title_3] = header3Col ;
var items = form.getItems();
var cnt = 0;
var changes = [];
for (i=0; i< items.length; i++){
var item = items[i];
var title = item.getTitle();
var itemResponse = responses.getResponseForItem(item);
var existingValue = dataRow[lookUp[title] -1];
if ((itemResponse.getResponse() !=='' || itemResponse.getResponse() !== undefined) && itemResponse.getResponse() != existingValue){
cnt++;
var cell = sheet.getRange(rowNum, lookUp[title], 1, 1);
cell.setValue(itemResponse.getResponse());
changes.push(title);
}
else {
continue;
}
}
Logger.log('Made ',cnt,'changes for launch ',featureID,': ',changes);
}
I also tried a slightly different approach but also didn't work:
function onFormSubmit(){
processSubmission();
}
// Processing form submission
function processSubmission() {
var form = FormApp.getActiveForm();
var key = form.getTitle();
var responses = form.getResponses()[form.getResponses().length-1];
// The rest is the same.
}
Manually running the function in the second approach proved my function processSubmission() works. Manually add a onFormSubmit() trigger via the Apps Script Dashboard is not going to be possible because I am generating hundreds of forms (one for each key) programmatically so I chose to have onFormSubmit(e) in the template and every new form is a copy of the template which should also have copies of these functions. But it just doesn't work! Any insight?
The onFormSubmit trigger is an installable trigger which means that it requires to be set up before being able to use it.
It's also important to keep in mind the following, according to the installable triggers documentation:
Script executions and API requests do not cause triggers to run. For example, calling FormResponse.submit() to submit a new form response does not cause the form's submit trigger to run.
What you can do instead is to create the trigger programmatically, something similar to this:
function createTrigger() {
ScriptApp.newTrigger('onFormSubmit')
.forForm('FORM_KEY')
.onFormSubmit()
.create();
}
Reference
Apps Script Installable Triggers;
Apps Script FormTriggerBuilder Class.

I am currently trying to execute an App script in Google Ads that is triggered when there is a specific value set in a cell A1 of a google sheet

I currently have a script that will remove a negative keywords from a shared negative keyword list when run.
I would like to have this script run when a specific value for example "run" is put in a cell A1 of a an external google sheet.
Below is the current code I have with out the trigger.
function main() {
removeAllNegativeKeywordsFromList();
}
function removeAllNegativeKeywordsFromList() {
var NEGATIVE_KEYWORD_LIST_NAME = 'test';
var negativeKeywordListIterator =
AdsApp.negativeKeywordLists()
.withCondition('Name = "' + NEGATIVE_KEYWORD_LIST_NAME + '"')
.get();
if (negativeKeywordListIterator.totalNumEntities() == 1) {
var negativeKeywordList = negativeKeywordListIterator.next();
var sharedNegativeKeywordIterator =
negativeKeywordList.negativeKeywords().get();
var sharedNegativeKeywords = [];
while (sharedNegativeKeywordIterator.hasNext()) {
sharedNegativeKeywords.push(sharedNegativeKeywordIterator.next());
}
for (var i = 0; i < sharedNegativeKeywords.length; i++) {
sharedNegativeKeywords[i].remove();
}
}
}
I have tried connecting the script to a google sheet and having it run when i put the trigger word in, however it does not seem to work
I did not enter the trigger i used as it think it is completely wrong. the code is without the trigger
Run above script when the word "run" is put in cell A1 of an external google sheet.enter code here
So, if I understood right you are only looking for the trigger for this code. To trigger your code when 'run' is inserted into A1 you would use onEdit(e)
function onEdit(e)
{
// Your testword
var theword = 'run';
// Get the value in cell
var value = e.value;
// Check the range. You could use the spreadsheet->cell->getvalue() but
// this should perform better as calling the SheetApp - API is costly
var isA1 = (e.range.columnStart == 1 && e.range.rowStart == 1);
// if the word and the range both match
if( isA1 && (value == theword))
{
// Run your code
removeAllNegativeKeywordsFromList();
}
}
The trigger itself is tested and working, but as for your code I could not confirm that as I do not know if your 'removeAllNegativeKeywordsFromList' works in the first place. In case you need any help with that please advise some more with this AdsApp, please.
Also please note that any user calling this function via onEdit has to authorise it first. This can be done by running it from script editor or creating a specific authorise-function.
Hope this helps at all.

Hide Sheets Based on a Cell Value

I'm pretty new to learning app script and looked over/tried to edit this script, but I'm not getting my desired result. I have a sheet titled "Menu" where I'm wanting a user to select from three different drop down options in Cell A2 (e.g. Blue, Yellow, Green). I then want to hide the different sheets based on the selection. So if a user selects "Blue" I want only the sheets that start with the word "Blue" to be visible + the "Menu" sheet and the rest to be hidden. Same for Yellow and Green. As a note there are 13 sheets for each color.
Any help with this is much appreciated.
This is an alternative implementation of #JSmith's answer, using the Sheets REST API to more efficiently hide & unhide a large number of sheets.
To use the Sheets REST API from Apps Script, you will first need to enable it, as it is an "advanced service."
The Sheets API approach enables you to work with the JavaScript representation of the data, rather than needing to interact with the Spreadsheet Service repeatedly (e.g. to check each sheet's name). Additionally, a batch API call is processed as one operation, so all visibility changes are reflected simultaneously, while the Spreadsheet Service's showSheet() and hideSheet() methods flush to the browser after each invocation.
var MENUSHEET = "Menu";
function onEdit(e) {
if (!e) return; // No running this from the Script Editor.
const edited = e.range,
sheet = edited.getSheet();
if (sheet.getName() === MENUSHEET && edited.getA1Notation() === "A2")
hideUnselected_(e.source, e.value);
}
function hideUnselected_(wb, choice) {
// Get all the sheets' gridids, titles, and hidden state:
const initial = Sheets.Spreadsheets.get(wb.getId(), {
fields: "sheets(properties(hidden,sheetId,title)),spreadsheetId"
});
// Prefixing the choice with `^` ensures "Red" will match "Reddish Balloons" but not "Sacred Texts"
const pattern = new RegExp("^" + choice, "i");
// Construct the batch request.
const rqs = [];
initial.sheets.forEach(function (s) {
// s is a simple object, not an object of type `Sheet` with class methods
// Create the basic request for this sheet, e.g. what to modify and which sheet we are referencing.
var rq = { fields: "hidden", properties: {sheetId: s.properties.sheetId} };
// The menu sheet and any sheet name that matches the pattern should be visible
if (s.properties.title === MENUSHEET || pattern.test(s.properties.title))
rq.properties.hidden = false;
else
rq.properties.hidden = true;
// Only send the request if it would do something.
if ((!!s.properties.hidden) !== (!!rq.properties.hidden))
rqs.push( { updateSheetProperties: rq } );
});
if (rqs.length) {
// Visibility changes will fail if they would hide the last visible sheet, even if a later request in the batch
// would make one visible. Thus, sort the requests such that unhiding comes first.
rqs.sort(function (a, b) { return a.updateSheetProperties.properties.hidden - b.updateSheetProperties.properties.hidden; });
Sheets.Spreadsheets.batchUpdate({requests: rqs}, initial.spreadsheetId);
}
}
There are a fair number of resources to be familiar with when working with Google's various REST APIs:
Google APIs Explorer (interactive request testing)
Google Sheets REST API Reference
Partial Responses (aka the "fields" parameter)
Determining method signatures
google-sheets-api
A little testing in a workbook with 54 sheets, in which I used the Sheets API to apply some changes and #JSmith's code to revert the changes, showed the API approach to be about 15x faster, as measured with console.time & console.timeEnd. API changes took from 0.4 to 1.1s (avg 1s), while the Spreadsheet Service method took between 15 and 42s (avg 20s).
try this code:
function onEdit(e)
{
//filter the range
if (e.range.getA1Notation() == "A2")
{
// get value of cell (yellow||green||...)
onlySheet(e.value)
}
}
function onlySheet(str)
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
//get all sheets
var sheets = ss.getSheets();
for (var i = 0; i < sheets.length; i++)
{
//get the sheet name
var name = sheets[i].getName();
// check if the sheet is not the "Menu" sheet
if (name != "Menu")
{
// check if the name of the sheet contains the value of the cell, here str
//if it does then show sheet if it doesn't hide sheet
if (name.match(new RegExp(str, "gi")))
sheets[i].showSheet();
else
sheets[i].hideSheet();
}
}
}

no item with the given id could be found

I created a script that pdf's and emails Google Form responses to me (based off of TJHouston's script). It works perfectly but errors if the enduser tries to edit their original response.
The original trigger is onFormSubmit, From Spreadsheet, On Form Submit. This works great.
I created a second trigger which is onFormSubmit, From Spreadsheet, On edit thinking that was the trigger to prompt another creation of the PDF - but when I click the link to edit my google form responses and then resubmit the form, I get the error "No item with the given ID could be found, or you do not have permission to access it." and it references the line
var copyId = DriveApp.getFileById(docTemplate)
I also tried the onFormSubmit, From Spreadsheet, On change trigger and got the same results.
// Get template from Google Docs and name it
var docTemplate;
var docName;
if (NYcollection == "YES") {docTemplate = "1JRDVjOYxeyl1dXIC115u5W2SQTVgbBMPUERBQ9xfgjo";docName = "Form M";}
else if (NYcollection == "NO") {docTemplate = "1YGx8wGZyBvfmcfdkTWGQF8XUGlJF7zaYI9ZVO2lYnto";docName = "Form N";}
// Get document template, copy it as a new temp doc, and save the Doc’s id
var copyId = DriveApp.getFileById(docTemplate)
.makeCopy(NYfacility +' - '+docName+' '+NYdate +NQE4)
.getId();
// Open the temporary document
var copyDoc = DocumentApp.openById(copyId);
// Get the document’s body section
var copyBody = copyDoc.getActiveSection();
// Replace place holder keys,in our google doc template
copyBody.replaceText('keyNYname', NYname);
copyBody.replaceText('keyNYdate', NYdate);
The code goes on to replace all my keys and then email the pdf to me. I'm not understanding why it can get the ID on the initial trigger but not on the edit trigger. Am I using the wrong trigger or should there be additional code in there?
Thanks for any help you can give!!
Have the code check for the absence of either YES or NO, and if the values you want are not there, use the getValue() method of the range class to get the value.
Put this code before the line that assigns the SS ID.
if (NYcollection !== "YES" && NYcollection !== "NO") {
//Get the value
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetWhereValueIs = ss.getSheetByName('YourSheetNameHere');
var rowOfMyValue = 3;
var columnOfMyValue = 4;
var NYcollection = sheetWhereValueIs.getRange(rowOfMyValue, columnOfMyValue).getValue();
};