GAS - Button in script gadget runs target script, then disappears - html

I'm trying to create a time-tracking tool on Google Sites for my coworkers to log their hours, with the following needs:
- 3 buttons: One to log a timestamp, one to tally up logs, and one to view a logsheet.
- all users can see button 1, only specific email addresses can see boxes 2 & 3.
- When button 1 is pressed, logging script is run and gadget returns to button, so user can press again.
I have created the below Google Appscript to draw the buttons in a Gadget IEFrame, run the timestamp script on click, and ideally return the user's attention back to the button.
My problem is that, after clicking the button in the appscript gadget, the script runs fine, but the gadget area turns blank. I have to refresh the page to get the button back. I've tried ending the script by reloading the HTML document, but the OutputToHTMLFile command doesn't seem to work the last time I run it, even though it runs ok when the page is first loaded. Is there a way to run the script without the buttons disappearing?
Here is how the issue looks on my test site:
Image of button Disappearance
SendtoLog Script:
function doGet() {
var e = Session.getActiveUser().getEmail();
if( /*e == "user1#email.com" ||*/
e == "user2#email.com" ||
e == "user3#email.com")
{
return HtmlService.createHtmlOutputFromFile('adminButtons');
}
else
{
return HtmlService.createHtmlOutputFromFile('callerButtons');
}
}
function sendToLog(e) {
var email = Session.getActiveUser().getEmail(); // Get current user
var now = new Date(); //Pull login time for user
var nowMinute
var currentDate = now;
//open spreadsheet for tracking and open the sheet for the current user
var folderID = openFolderByName("TimeTrack");
var folderToWrite = DriveApp.getFolderById(folderID);
var logSheetID = openFileByName ("Logsheet");
var logSheet = SpreadsheetApp.openById(logSheetID);
try{logSheet.setActiveSheet(logSheet.getSheetByName("Sheet1"));
logSheet.deleteActiveSheet();}
catch (e){Logger.log("Sheet1 is already deleted")};
try{logSheet.setActiveSheet(logSheet.getSheetByName(email));}
catch (e) {logSheet.insertSheet(email);}
try{logSheet.setActiveSheet(logSheet.getSheetByName("Sheet1"));
logSheet.deleteActiveSheet();}
catch (e){Logger.log("Sheet1 is already deleted")};
//get last row of the sheet so we can check what was done on the last click
var lastRow = logSheet.getLastRow();
if (lastRow < 1) // If there is no data on the sheet, this will be our first logon
var logType = "logon"; //We will assume that current logon event is a logon unless noted otherwise on spreadsheet.
else
{
var dataRange = "A1:B"+ lastRow;
var dataWithoutHeaders = logSheet.getRange(dataRange).getValues() // Pull data from the sheet for comparison
var lastLogType = logSheet.getRange("C"+lastRow).getDisplayValue(); // Pull the description of the last log event
//if last row is login, add logoff, otherwise, add logon.
switch(lastLogType){
case "logon":
var logType = "logoff";
break;
case "logoff":
var logType = "logon";
break;
default:
var logType = "Logon"; //We will assume if last entry could not be compared, this is a logon event.
break;
}//End switch
}//End else statement
logSheet.appendRow([email, currentDate, logType]); //Write email, time stamp, and log event to sheet
var e = Session.getActiveUser().getEmail();
if( /*e == "user1#mail.com" ||*/
e == "user2#mail.com" ||
e == "user3#email.com")
{
return HtmlService.createHtmlOutputFromFile('adminButtons');
}
else
{
return HtmlService.createHtmlOutputFromFile('callerButtons');
}
}//end function SendtoLog
function openFileByName (FileNameString)
{
var FileIterator = DriveApp.getFilesByName(FileNameString);
while (FileIterator.hasNext())
{
var file = FileIterator.next();
if (file.getName() == FileNameString)
{
var sheet = SpreadsheetApp.open(file);
var sheetID = sheet.getId();
}
}
return sheetID;
}
function openFolderByName (FolderNameString)
{
var FolderIterator = DriveApp.getFoldersByName(FolderNameString);
var folderFound = false;
while (FolderIterator.hasNext() || !folderFound)
{
var folder = FolderIterator.next();
if (folder.getName() == FolderNameString)
{
folderFound = true;
var folderID = folder.getId();
}
}
return folderID;
}
callerButtons HTML Page:
<div style="display:block;text-align:left"><input type="button" onclick="location.href='https://script.google.com/a/macros/mycompany.com/s/AKfycbwM842S-Y6Y18sDV7p0fZlb9LqdP6juXcMz1QFa4s6-h4w/exec';" value="Time Check-In" />
adminButtons HTML Page:
<div style="display:block;text-align:left"><input type="button" onclick="location.href='https://script.google.com/a/macros/mycompany.com/s/AKfycbwM842S-Y6Y18sDV7p0fZlb9LqdP6jueL7bXcMz1QFa4s6-h4w/exec';" value="Time Check-In" />
<br>
<div style="display:block;text-align:left"><input type="button" onclick="location.href='https://script.google.com/a/macros/mycompany.com/s/AKfycbwM842S-Y67p0fZlb9LqdP6jueL7bXcMz1QFa4s6-h4w/exec';" value="Tabulate Times" />
<br>
<div style="display:block;text-align:left"><input type="button" onclick="location.href='https://script.google.com/a/macros/mycompany.com/s/AKfycbwM842S-Y6Y18sDV7p0fZlb9LqdP6jueL7bXcMz1QFa44w/exec';" value="CSV Folder" />

OK, so this was a question from someone who didn't even know how to open a developer's console to troubleshoot. For others wondering how to check Javascript code in Chrome, press CTRL+SHIFT+I.
It turns out one of my variables was a broken reference in one of the onclick functions. All fixed!

Related

This script does not populate sheet after parsing retrieved data

I hope this is well explained. First of all, sorry because my coding background is zero and I am just trying to "fix" a previously written script.
Problem The script does not populate sheet after parsing retrieved data if the function is triggered by timer and the sheet is not open in my browser .
The script works OK if run it manually while sheet is open.
Problem details:
When I open the sheet the cells are stuck showing "Loading" and after a short time, data is written.
Expected behavior is to get the data written no matter if I don't open the sheet.
Additional info: This is how I manually run the function
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [
{name: "Manual Push Report", functionName: "runTool"}
];
sheet.addMenu("PageSpeed Menu", entries);
}
Additional info: I set the triggers with Google Apps Script GUI See the trigger
Before posting the script code, you can see how the cells look in the sheet:
Script code
function runTool() {
var activeSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Results");
var rows = activeSheet.getLastRow();
for(var i=3; i <= rows; i++){
var workingCell = activeSheet.getRange(i, 2).getValue();
var stuff = "=runCheck"
if(workingCell != ""){
activeSheet.getRange(i, 3).setFormulaR1C1(stuff + "(R[0]C[-1])");
}
}
}
// URL check //
function runCheck(Url) {
var key = "XXXX Google PageSpeed API Key";
var strategy = "desktop"
var serviceUrl = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=" + Url + "&key=" + key + "&strategy=" + strategy +"";
var array = [];
var response = UrlFetchApp.fetch(serviceUrl);
if (response.getResponseCode() == 200) {
var content = JSON.parse(response.getContentText());
if ((content != null) && (content["lighthouseResult"] != null)) {
if (content["captchaResult"]) {
var score = content["lighthouseResult"]["categories"]["performance"]["score"];
} else {
var score = "An error occured";
}
}
array.push([score,"complete"]);
Utilities.sleep(1000);
return array;
}
}
You can try the code using the sheet below with a valid Pagespeed API key.
You only need to add a Trigger and wait for it's execution while the sheet is not open in your browser
https://docs.google.com/spreadsheets/d/1ED2u3bKpS0vaJdlCwsLOrZTp5U0_T8nZkmFHVluNvKY/copy
I suggest you to change your algorithm. Instead of using a custom function to call UrlFetchApp, do that call in the function called by a time-driven trigger.
You could keep your runCheck as is, just replace
activeSheet.getRange(i, 3).setFormulaR1C1(stuff + "(R[0]C[-1])");
by
activeSheet.getRange(i, 3, 1, 2).setValues(runCheck(url));
NOTE
Custom functions are calculated when the spreadsheet is opened and when its arguments changes while the spreadsheet is open.
Related
Cache custom function result between spreadsheet opens

How do i get event.getEventById('id') or event.getEventSeriesById('id') to return anything other than null

Is there a way to make the .getEventById or .getEventSereiesById return anyhting other than null? I get valid ID for the initial event creation and can make that into a full functional URL but cannot use it in its native environment for its native purpose.
I am trying to make a basic google sheets schedule system that can refer to the calendar invite to check for changes and update the sheet or vise versa based on which side is further out in time. The system will be used in an environment where the scheduling has multiple users and meetings can be moved around a lot, generally further out in time. Everything works right up until i try to get information from the calendar even, .getStartTime(), due to the .getEvent calls returning null. not sure how to fix what other sources are telling me is a nonfunctional command that yet still "functions as intended".
function IEPscheduler() {
var spreadsheet = SpreadsheetApp.getActiveSheet(); // call sheet
//var calendarID = spreadsheet.getRange("H1").getValue();
var eventCal = CalendarApp.getCalendarById("mascharterschool.com_0edapns33khde9ig0di31i2mvc#group.calendar.google.com");
var signups = spreadsheet.getRange("A2:C2").getValues();
var lr = spreadsheet.getLastRow();
var lc = spreadsheet.getLastColumn(); //
var count = spreadsheet.getRange(2,1,lr-1,lc-1).getValues();// get meeting data
for (x=0; x<count.length; x++){
var shift = count[x]; // pull row from meeting data
var Start = shift[0];
var End = shift[1];
var Student = shift[2];
var guests = shift[3];
var description = shift[4];
var location = shift[5];
var run=shift[6]; // run following based on status column
// new meeting is scheduled
if(run == null || run == ''){
var event = {
'location': location,
'description':description ,
'guests':guests +',',
'sendInvites': 'True',
}
var invite = eventCal.createEvent(Student, Start, End, event);
invite.setGuestsCanInviteOthers(true); // allow guests to invite others
var eventId = invite.getId();
var date = invite.getDateCreated();
spreadsheet.getRange(x+2,7).setValue('Invite created'); // set status in sheet
spreadsheet.getRange(x+2,8).setValue(date); // inital date for created meeting invite
spreadsheet.getRange(x+2,9).setValue(eventId);
}
// check existing meetings for updates
else {
var id = shift[9];
var invite = eventCal.getEventSeriesById('id');
// if the time or location has changed update calander
if(invite.getStartTime() !== Start || invite.getEndTime() !== End || invite.getLocation() !== location){
// if sheet override flagged
if(shift[lc-1] !== null || Shift[lc-1] !== ''){
invite.setTime(Start,End); // update start/end time
invite.setLocation(location); // update location
}
// if canalder invite is further out than spreadsheet --> update spreadsheet
if(invite.getStartTime() >> Start){
spreadsheet.getRange(x+2,1).setValue();
spreadsheet.getRange(x+2,2).setValue();
}
// if spread sheet time is later than invite --> updater invite
else{
invite.setTime(Start,End); // update start/end time
invite.setLocation(location); // update location
}
var date = invite.getLastUpdate();
spreadsheet.getRange(x+2,7).setValue('Updated'); // set new status in sheet
spreadsheet.getRange(x+2,8).setValue(date); // set date meeting was updated
}
// if guest list has changed ???
if
}
}
}
// set script to be runnable from sheet tool bar
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Sync to Calendar') // tool bar banner
.addItem('Create Events Now', 'IEPscheduler') // sub catageory (title, function to run)
.addToUi();
}
We actually figured it out shortly after posting and I couldn't get back to this. Turns out the ID from .getId is the iCalUID and the .getEventById() takes a iCalID. The difference is that the UID has '#google.com' appended to the end of the ID. Split at the '#' and the get event works perfectly.
It's a stupid quirk that the getId command returns the right data in a useless form that requires editing to be used for its intended purpose.
No nulls returned for me with this script:
function getEvents() {
const cal=CalendarApp.getDefaultCalendar();
const dt=new Date();
const start=new Date(dt.getFullYear(),dt.getMonth()-1,dt.getDate());
const end=new Date(dt.getFullYear(),dt.getMonth(),dt.getDate());
var events=cal.getEvents(start, end);
let eObj={idA:[],tA:[]}
events.forEach(function(ev,i){
eObj.idA.push(ev.getId());
eObj.tA.push(cal.getEventById(ev.getId()).getTitle());
});
Logger.log(JSON.stringify(eObj));
return eObj;
}

Send email when cell contains a specific value

I am working on a Google Form that allows our employees to submit an in-field inspection of their equipment. I have a script that takes the form responses and creates a new sheet based on the date and the specific unit number of the equipment. The user goes through a checklist and selects either "Good" or "Needs Repair" for each item on the list. They can also add comments and upload pictures of any issues.
I am trying to have the script automatically send an email if "Needs Repair" is selected for any of the checks, as well as if the user adds a comment or a picture. This way we do not have to open every submitted sheet to know if any repairs are required. What I have is just not sending emails and I cannot figure out why. Any help is greatly appreciated!
Here is my current script:
function onFormSubmit() {
// onFormSubmit
// get submitted data and set variables
var ss = SpreadsheetApp.openById("*Spreadsheet Link*");
var sheet = ss.getSheetByName("Submissions");
var row = sheet.getLastRow();
var Col = sheet.getLastColumn();
var headings = sheet.getRange(1,1,1,Col).getValues();
var lastRow = sheet.getRange(row, 1, 1, Col);
var UnitNumber = sheet.getRange(row,3).getValue();
var newSheet = sheet.getRange(row,4,Col).getValue();
var fileExist = false;
var drillSheet = null;
var folder = DriveApp.getFoldersByName("Fraser Drill Inspections").next();
var files = folder.getFilesByName(UnitNumber);
var file = null;
var employee = sheet.getRange(row,2);
var checks = sheet.getRange(row, Col, 1, 20);
// check if Drill has sheet
while (files.hasNext())
{
fileExist = true;
file = files.next();
break;
}
if (fileExist) //If spreadsheet exists, insert new sheet
{
drillSheet = SpreadsheetApp.openById(file.getId());
drillSheet.insertSheet("" + newSheet);
}
else //create new spreadsheet if one doesn't exist
{
drillSheet = SpreadsheetApp.create(UnitNumber);
var ssID = drillSheet.getId();
file = DriveApp.getFileById(ssID);
file = file.makeCopy(UnitNumber, folder);
DriveApp.getFileById(ssID).setTrashed(true);
drillSheet = SpreadsheetApp.openById(file.getId());
drillSheet.renameActiveSheet(newSheet);
}
// copy submitted data to Drill sheet
drillSheet.getSheetByName(newSheet).getRange(1,1,1,Col).setValues(headings);
drillSheet.appendRow(lastRow.getValues()[0]);
drillSheet.appendRow(['=CONCATENATE(B6," ",B5)']);
drillSheet.appendRow(['=TRANSPOSE(B1:2)']);
//Hide top rows with raw data
var hiderange = drillSheet.getRange("A1:A3");
drillSheet.hideRow(hiderange);
//Widen columns
drillSheet.setColumnWidth(1,390);
drillSheet.setColumnWidth(2,700);
//Send email if there are any comments or if anything needs repair
if(lastRow.getValues() == "Needs Repair") {
function SendEmail() {
var ui = SpreadsheetApp.getUi();
MailApp.sendEmail("email#domain.com", "Drill Needs Repair", "This drill requires attention according to the most recent inspection report.")
}
}
}
The function to send an email is:
GmailApp.sendEmail(email, subject, body);
Try changing
if(lastRow.getValues() == "Needs Repair") {
function SendEmail() {
var ui = SpreadsheetApp.getUi();
MailApp.sendEmail("email#domain.com", "Drill Needs Repair", "This drill requires attention according to the most recent inspection report.")
}
}
to just the following:
if(lastRow.getValues() == "Needs Repair") {
GmailApp.sendEmail("youremail#domain.com", "Drill Needs Repair", "This drill requires attention according to the most recent inspection report.");
}
It looks like you've still got some additional work to do too, e.g. to make it send to the email address from the form submission instead of a hardcoded one.

GAS: Script authorization via custom UI menu fails with "You do not have access to perform that action."

I have a spreadsheet with a bound script that tries to identify users currently making edits to the sheet, and appends edit info into specified columns of the sheet.
It seems that users always have to undergo the initial script authorization (that Google popup that asks for permissions) by opening the Script Editor and running a function manually via the "play" button. Invoking the function via a custom UI menu doesn't work and throws an error: "You do not have access to perform that action. Please ask the owner of this item to grant access to you."
Is there any way to invoke authorization in a more user-friendly way that doesn't require the users opening the Scripts Editor?
I thought making the menu was the point of that (manual execution vs. onEdit() or onOpened() triggered execution), but as it stands, this workaround is useless - I don't need the menu if the users have to open the Editor to achieve the same thing.
Full code:
function onEdit(e) {
var sheet = e.source.getActiveSheet();
var labels = e.source.getRangeByName("labels");
if (labels == null) {
Logger.log("Error: couldn't find named range 'labels'!");
return;
}
var found = false;
for(var i = 1; i <= labels.getNumColumns(); i++){
if (labels.getCell(1,i).getValue() == "last edit") {
found = true;
break;
}
}
if (!found) {
Logger.log("Error: couldn't find column 'last edit'!");
return;
}
var minrow = labels.getRowIndex();
var range = sheet.getActiveRange();
var row = range.getRowIndex();
// don't track edits above the labels
if (row <= minrow) { return; }
var range_depth = range.getNumRows();
var rangeA1 = range.getA1Notation();
var timestamp = Utilities.formatDate(new Date(), "GMT+2", "YYYY-MM-dd HH:mm");
// the elegant way - requires the sheet owner and users to be on the same domain:
// var user = Session.getActiveUser().getEmail();
// the workaround - requires the user to identify themselves manually:
var user = PropertiesService.getUserProperties().getProperty("ID");
if (user == null) {
Browser.msgBox('Current user unidentified. Run the identification script via the "Edit tracking" menu (next to "Help").');
user = 'unknown';
}
for (var j = 0; j < range_depth; j++) {
sheet.getRange(row+j, i).setValue(timestamp);
sheet.getRange(row+j, i+1).setValue(user);
sheet.getRange(row+j, i+2).setValue(rangeA1);
}
SpreadsheetApp.flush();
}
function onOpen(e) {
SpreadsheetApp.getUi()
.createMenu('Edit tracking')
.addItem('Identify current user', 'showIdentify')
.addItem('Forget current user', 'showForget')
.addToUi();
}
function showIdentify() {
var ui = SpreadsheetApp.getUi();
var result = ui.prompt(
'Edit tracking',
'Please enter your name:',
ui.ButtonSet.OK_CANCEL);
var button = result.getSelectedButton();
var text = result.getResponseText();
if (button == ui.Button.OK) {
PropertiesService.getUserProperties().setProperty("ID", text);
} else {
ui.alert('Look, you\'re not dodging this, I\'ll nag you again on the next edit.');
}
}
function showForget() {
var ui = SpreadsheetApp.getUi();
PropertiesService.getUserProperties().deleteProperty("ID");
ui.alert('You\'re anonymous now.');
}
It seems to me like a bug in google sheets that the script is not shared along with the sheet.
But to make it more user friendly you can
A) publicly share your sheet (Anyone on the internet can find and edit) or
B) (using "anyone with the link can edit" sharing) In the script editor get the share link File->Share... and have the user open that link.
Then it will work.

How to get ID value of Google Drive file for input to Google Apps Script

I am writing a Google Apps script to send a file from Google Drive to a list of email addresses saved in a Google Spreadsheet.
Since I will be sending a different file every time I use the script, I have my script set up to take the file ID as text input from the user. The only way I've seen to get the ID directly from Drive is to right-click on the file, select "Get Link", copy it to the clipboard, paste it into my form and erase the bits that aren't the ID. I'm writing to ask if anyone knows a better way. I'm also open to comments suggesting a better program design.
function sendEmails() {
var id = "gibberish"; //email spreadsheet
SpreadsheetApp.openById(id);
var sheet = SpreadsheetApp.getActiveSheet();
Logger.log(sheet.getName());
var startRow = 2; // First row of data to process
var numRows = 2; // Number of rows to process
// Fetch the range of cells A2:B3
var dataRange = sheet.getRange(startRow, 1, numRows, 2);
// Fetch values for each row in the Range.
var data = dataRange.getValues();
//Get subject line from user
var ui = SpreadsheetApp.getUi();
var response = ui.prompt('Enter subject: ', ui.ButtonSet.OK_CANCEL);
var subject;
// Process the user's response. TODO- error checking
if (response.getSelectedButton() == ui.Button.OK) {
subject = response.getResponseText();
} else {
Logger.log('The user either canceled or clicked the close button in the dialog\'s title bar.');
subject = "No subject";
}
//get id for attachment file
var ui2 = SpreadsheetApp.getUi();
var response2 = ui2.prompt('Enter Drive id for attachment: ', ui.ButtonSet.OK_CANCEL); //TODO- error checking
var attachmentID;
var file = null;
if (response2.getSelectedButton() == ui.Button.OK) {
attachmentID = response2.getResponseText();
file = DriveApp.getFileById(attachmentID);
Logger.log('The user entered %s', response2.getResponseText());
} else {
Logger.log('The user either canceled or clicked the close button in the dialog\'s title bar.');
}
for (i in data) {
var row = data[i];
var emailAddress = row[0]; // First column
var message = "Time Sheet attached. \n\n -Jessica";
if (file != null) { //TODO- or if file is right file
MailApp.sendEmail(emailAddress, subject, message, {attachments: [file]});
} else {
Logger.log("No file was attached. Email not sent.");
}
}
}
This isn't mine, we've all passed it around this forum quite a bit.
this one works because it gets what you want regardless of if the person pastes in the id or the url:
function getIdFromUrl(url) { return url.match(/[-\w]{25,}/); }
so you don't need to be picky about erasing the rest of the url yourself.
I did something similar in my Copy Folder script.
Basically, I parsed the form input in javascript to select only the folder ID. With this code, you can actually pass in the "Sharing ID" (retrieved by the Right-click and "Get Link" method), the folder URL (retrieved by the browser address bar when you are inside the folder in Google Drive), or just the folder ID. The javascript parses the input and replaces the form entry with just the folder ID, so that your Google Script can retrieve this form data normally.
You can view the source code for the project, but here is the relevant part. In my web app, this is located in the JavaScript.html file.
// Regular expression - find string beginning with "id="
// http://www.w3schools.com/jsref/jsref_regexp_source.asp
var regex = /id=/;
// NOTE: pretty sure you could just make a string variable = "id=" instead of regex, but I didn't want to mess up my script
// Set a temporary variable to the value passed into the "folderId" field
var fId = thisForm.folderId.value;
// Get the index of the string at which the folderId starts
var idStart = fId.search(regex);
var foldersStart = fId.search("folders");
if (idStart > 0) {
// Slice the string starting 3 indices after "id=", which means that it takes away "id=" and leaves the rest
fId = fId.slice(idStart+3);
} else if (foldersStart > 0) {
fId = fId.slice(foldersStart + 8);
}
// Find the ampersand in the remaining string, which is the delimiter between the folderId and the sharing privileges
var amp = fId.indexOf("&");
// Slice the string up to the ampersand
if (amp > 0) {
fId = fId.slice(0,amp);
}
// Set the folderId element within thisForm (retrieved from doGet) to the new, sliced fId variable
thisForm.folderId.value = fId;