Terminate last execution using script - google-apps-script

I have been testing a script for nearly 24 hours, the idea was that if certain cells were edited within the sheet, then the trigger would wait 5 minutes before resetting the values within those cells.
However when testing it became apparent that with many people using this sheet, the trigger was executing many times and sometimes the data edited by a user would reset within seconds because of a earlier execution.
To combat this I thought of putting a few lines of script in there, to cancel any previous executions of this function that is still running.
However with being a newbie to script and not having the knowledge I hope to gain, finding the information to start that script is proving difficult.
This is what I have so far
function RESET(e) {
const ar = e.range;
const as = ar.getSheet();
const cells = ["B3","E3"]; // add cells you want to be edited to activate the script
// if C3 or E3 is edited, wait for 300 seconds
if (as.getName()=="Gauge" && cells.includes(ar.getA1Notation())){
Utilities.sleep(300000);
as.getRange("B3").setValue("ALL");
as.getRange("E3").setValue("ALL");
}
}

You can use a document lock]1 to guard the RESET function running at the same time for multiple users, but this will probably lead to timeouts.
A better approach would be to create a time-driven trigger with a delay of 5 minutes inside the RESET function. When it fires, clear the values in B3/E3 or do any other work required.
Make sure that you clear previous triggers before you create new one, so that at any time only single trigger is active. If there is no user activity for 5 minutes, it will eventually run.
const SHEET = "Gauge"
// make sure to create as onEdit installable trigger, not a simple trigger
function onInstallableEdit(e) {
const ar = e.range;
const as = ar.getSheet();
const cells = ["B3","E3"]; // add cells you want to be edited to activate the script
// if C3 or E3 is edited, wait for 300 seconds
if (as.getName()==SHEET && cells.includes(ar.getA1Notation())){
const lock = LockService.getDocumentLock()
try{
lock.waitLock(10000)
// delete previous triggers, if any
ScriptApp.getProjectTriggers().filter(t=>t.getHandlerFunction()===myHandler.name).forEach(t=>ScriptApp.deleteTrigger(t))
// create new trigger to be fired after 5 minutes
ScriptApp.newTrigger(myHandler.name).timeBased().after(5*60*1000).create()
} catch(e){
console.warn(e)
}
}
}
function myHandler(){
const as = SpreadsheetApp.getActive().getSheetByName(SHEET)
as.getRange("B3").setValue("ALL");
as.getRange("E3").setValue("ALL");
}

Related

Have an onEdit() function run whenever a cell value is changed in Google Sheets

I have a Sheet that has the following:
ID ROW STATUS ......
3588053 4 NEW
The Data for ID and ROW are coming from an External Spread Sheet called KDCAlerts
The STATUS filled is changed with an onEdit command (or in this case onMyEdit) that executes when there are changes on KDCLog
The ID and ROW changes each time the External spreadsheet changes (ex: adding / deleting rows, etc.)
What I need to do is to fix this so that when ID or ROW changes, the onMyEdit function is ran.
How can I do this? Below is the code currently being used now.
Any help, hints or advice would be greatly appreciated.
TIA
function creatTrigger() {
if(ScriptApp.getProjectTriggers().filter(t => t.getHandlerFunction() == "onMyEdit").length == 0) {
ScriptApp.newTrigger("onMyEdit").forSpreadsheet(SpreadsheetApp.getActive()).onEdit().create();
}
}
function onMyEdit(e) {
var sh = e.range.getSheet();
if (sh.getName() == "KDCLog" ) {
var extSS = SpreadsheetApp.openById("1b5qiNxxxxxxxxxRuLf-8dTBgRU9cHLBbd2A");
var extSH = extSS.getSheetByName("KDCAlerts");
} else { return; }
....
....
UPDATE:
#doubleunary - thanks for the response.
It is unclear how the values in the ID and ROW columns get written to the spreadsheet,
There are 2 Files: KDCLog and KDCAlerts.
KDCLog!ID is populated as follows:
=IF ( ISERROR( INDEX(SORTN(FILTER({KDCAlerts!E:E,KDCAlerts!H:H, KDCAlerts!A:A}, KDCAlerts!D:D=E7),1,,2,FALSE),,3) ), -1, INDEX(SORTN(FILTER({KDCAlerts!E:E,KDCAlerts!H:H, KDCAlerts!A:A}, KDCAlerts!D:D=E7),1,,2,FALSE),,3) )
So, it is "pulling" from KDCAlerts using a function (being executed in the KDCLog worksheet).
Whenever the KDCAlerts change, the Value in KDCLog!ID also changes (without manual intervention - the KDCAlerts changes with the adding of rows).
If you are using another script to write to the spreadsheet, it is
likely that no events get sent.
For the STATUS column, it is populated with an onEdit funciton (seen above)
trigger events that can be monitored with an installable on change
trigger.
Is there a sample on how this could work
You cannot use on edit trigger here, because it fires only when the spreadsheet is manually edited by a user, regardless of whether you are using a simple trigger or an installable trigger.
It is unclear how the values in the ID and ROW columns get written to the spreadsheet, which is what actually determines whether you can catch those updates. If you are using another script to write to the spreadsheet, it is likely that no events get sent.
If you are using another integration tool, it may trigger events that can be monitored with an installable on change trigger.

How to set timer for a row in google sheets

I would like to set a timer for an entire row in google sheets where a user can start entering data in the second row only after a certain time after starting row one.
Example: If a user starts filling cells in row 1 then they should be able to fill the data in the second only after the timer ends.
Could anyone suggest me how to get started or suggest me a chrome extension for this use?
You could also suggest me on how to build the chrome extension I can try it along with my colleagues.
This function uses an onEdit trigger to impose a 20 second delay between editing rows. It may not be exactly what you want but perhaps it's a start. It uses PropertiesService to keep state. I think user properties would be a better choice but script properties are easier to develop with since you can modify them directly in the script editor.
function onEdit(e) {
const sh=e.range.getSheet();
const delay=20000;
let ms=Number(new Date().valueOf()).toFixed();
if(sh.getName()=='Sheet10') {
const ps=PropertiesService.getScriptProperties();
let dObj=ps.getProperties();
if(dObj.hasOwnProperty('row') && dObj.hasOwnProperty('delay')) {
if(dObj.row!=e.range.rowStart && Number(ms-dObj.delay)<delay) {
e.range.setValue(e.oldValue);
e.source.toast('Sorry you have ' + (delay-Number(ms-dObj.delay))/1000 + ' seconds left.');
}else{
ps.setProperties({'row':e.range.rowStart,'delay':ms});
}
}else{
ps.setProperties({'row':e.range.rowStart,'delay':ms});
}
}
}
Issue with Protections:
Class Protection is commonly used to protect ranges from being edited. It is not appropriate for your situation, though, because, as specified here, users who are executing the script cannot remove themselves from the list of editors:
Neither the owner of the spreadsheet nor the current user can be removed.
Using oldValue:
Because of this, the best way to go would be to use the parameter oldValue from the onEdit event object.
An onEdit trigger runs every time a user edits a cell. In it, you can use:
PropertiesService to store useful information: (1) whether it is the first time row 1 is edited (isNotFirstTime), and (2) when was last time first row was edited (startTime).
Event object to get information on the edited cell (its row, its old value, etc.).
You can do something along the following lines (check comments):
function onEdit(e) {
var current = new Date(); // Current date
var range = e.range;
var editedRow = range.getRow();
var sheet = range.getSheet();
var props = PropertiesService.getScriptProperties();
var waitingTime = 20 * 1000; // 20 seconds
var isNotFirstTime = props.getProperty("isNotFirstTime"); // Check if first row was previously edited
var startTime = new Date(props.getProperty("startTime")); // Time when first row was first edited
if (editedRow === 1 && !isNotFirstTime) { // Check that (1) edited row is first one, (2) it was not edited before
props.setProperty("startTime", current.toString()); // If it's first time first row was edited, store current time
Utilities.sleep(waitingTime); // Wait for 20 seconds
props.setProperty("isNotFirstTime", true); // Store: first row was previously edited
}
// Check that (1) second row edited, (2) Less than 20 seconds passed since first time first row was edited:
if (editedRow === 2 && (current - startTime) < waitingTime) {
range.setValue(e.oldValue || ""); // Set previous value to edited cell (this avoids editing cells)
}
}
Reference:
onEdit(e)
onEdit Event object
Class PropertiesService

How do I set a time controlled trigger starting at a certain time whitout user interaction?

I would like to have a code (see below) executed daily at 22:00:00. I have already tried to solve this via the G Suite Developer Hub by setting an hour interval trigger. Unfortunately I could not set a time there (in this case 22:00:00). Next I discovered the function "ScriptApp.newTrigger" and created the following code, but I'm not sure if this is the right solution. What do you think? Can it work like this?
The following is important to me.
The code must be reliably executed daily at 22:00:00.
One execution per day is sufficient
At best, even if no user has opened the table.
OnEdit or OnOpen are less suitable for this, since it is not guaranteed that the table a.) will be opened at the right time and b.) If users are online, it is not certain that they will edit it.
function TimeTrigger() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Tickerprüfung');
sheet.getRange('C2').setValue(new Date());
var date = new Date();
date.setHours(22);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
sheet.getRange('C3').setValue(date);
}
function createDayTrigger() {
ScriptApp.newTrigger("TimeTrigger")
.timeBased().everyHours(24).atHour(22).create();
}
You want to run the function of TimeTrigger() only at 22:00:00 every day.
The time zone uses your time zone.
If my understanding is correct, how about this modification? Please think of this as just one of several answers.
Modification points:
When you run the script only at 22:00, how about the following
ScriptApp.newTrigger('TimeTrigger').timeBased().at(setTime).create()
After the trigger was fired, it removes the finished trigger. Because the finished trigger is left.
The flow of this modified script is as follows.
Flow:
At first, please run the function of startScript(). By this, the trigger for running createDayTrigger() is set.
This trigger is run near 21:00 every day.
This function is required to run only one time.
By the trigger set with startScript(), createDayTrigger() is run at 21:00 (Specifies the hour the trigger will run (plus or minus 15 minutes). Ref).
createDayTrigger() set the trigger for running TimeTrigger() at 22:00.
At 22:00, TimeTrigger() is run. At this time, the trigger set by createDayTrigger() is cleared by deleteTrigger().
Because the trigger set with startScript() is existing, createDayTrigger() is run at 21:00. By this cycle, TimeTrigger() can be run at 22:00 every day.
Modified script:
Please copy and paste the following script to the script editor. And please run the function of startScript(). Please run this function only one time. By this, the base trigger is set.
// Your script.
function TimeTrigger() {
deleteTrigger();
// Below script is your script.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Tickerprüfung');
sheet.getRange('C2').setValue(new Date());
var date = new Date();
date.setHours(22);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
sheet.getRange('C3').setValue(date);
}
// Delete finished trigger.
function deleteTrigger() {
var triggers = ScriptApp.getProjectTriggers();
triggers.forEach(function(t) {
if (t.getHandlerFunction() == "TimeTrigger") ScriptApp.deleteTrigger(t);
});
}
function createDayTrigger() {
var d = new Date();
d.setHours(22);
d.setMinutes(0);
d.setSeconds(0);
ScriptApp.newTrigger('TimeTrigger').timeBased().at(d).create();
}
// Please run this function.
function startScript() {
ScriptApp.newTrigger('createDayTrigger').timeBased().everyDays(1).atHour(21).create();
}
References:
Class ClockTriggerBuilder
atHour(hour)
If I misunderstood your question and this was not the direction you want, I apologize.

Script running multiple times onFormSubmit

I have the following code set to run based on a onFormSubmit trigger but it will sometimes run multiple times with the same submission. I want to verify if it already copied the row and if so to stop the script.
function toDo(){
var responses = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Form Responses 1");
var jobs = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Jobs");
var lastrow = responses.getLastRow();
var col = responses.getLastColumn();
var row = responses.getRange(lastrow, 1, 1, 19).getValues();
jobs.appendRow(row[0]);
//copyValuesOnly(copyFromRange, copyToRangeStart);
var si = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Jobs');
var range = si.getRange("A2:R");
range.sort({column: 5,ascending: true}),({column: 1, ascending:true});
}
this is a known problem with GAS + Forms. The way that you solve it is by creating a script lock that rejects (causing them to return early) all other attempts within a period of time.
function toDo(){
SpreadsheetApp.flush();
var lock = LockService.getScriptLock();
try {
lock.waitLock(5000);
} catch (e) {
Logger.log('Could not obtain lock after 5seconds.');
return HtmlService.createHtmlOutput("<b> Server Busy please try after some time <p>")
// In case this a server side code called asynchronously you return a error code and display the appropriate message on the client side
return "Error: Server busy try again later... Sorry :("
}
var responses = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Form Responses 1");
var jobs = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Jobs");
var lastrow = responses.getLastRow();
var col = responses.getLastColumn();
var row = responses.getRange(lastrow, 1, 1, 19).getValues();
jobs.appendRow(row[0]);
//copyValuesOnly(copyFromRange, copyToRangeStart);
var si = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Jobs');
var range = si.getRange("A2:R");
range.sort({column: 5,ascending: true}),({column: 1, ascending:true});
Utilities.sleep(5000);
lock.releaseLock)();
}
I've had scripts that do this up to 8 times, and usually do it every 2-3 seconds. With this solution you are making a lock at the beginning and then sleeping at the end to make sure that the process time is greater than the wait time. (Here I used 5 seconds, that should prevent the double entry).
I have noticed that if you just make another copy of the sheet with the script, this error goes away. Seems to reset whatever was the issue in the original copy. Also try dis-abling the response receipts on your google forms.
How to check:
Go to script editor and check under execution logs. If you see multiple instances of on form submit, then you probably have multiple triggers somehow and the trigger is running multiple times legitimately.
To fix:
Go to triggers tab and delete all unwanted triggers.
Check code if you are creating new trigger through code. And comment that out.
Possible Reason:
I would have expected the code such as below to overwrite existing trigger. I had 19 triggers created. This was because every time I generated the link, it called Initialize and I got a new trigger added. Thus I could see script running 19 times.
const initialize = () => { const form = FormApp.getActiveForm(); //ScriptApp.newTrigger('onFormSubmit').forForm(form).onFormSubmit().create(); };
I noticed the problem got solved if I renamed the function called by onFormSubmit and saved the script.
If I click "run" in the script editor I get 2 emails per form submit. If I click "run" again I get 3 emails per form submit. I reset to only 1 email per form submit if I change the function name again.
Somehow when I run the script it duplicates the triggers.

Google Spreadsheets: Script to check for completion of ImportHTML

I am trying to scrape data of a website once day automatically. In Google Spreadsheets, i use the =ImportHTML() function to import data tables, and then I extract the relevant data with a =query(). These functions take between 10 and 30 seconds to complete calculation, every time I open the spreadsheet.
I use a scheduled Google Apps Script, to copy the data into a different sheet (where it is stored, so i can run statistics) every day.
My problem is that I am having trouble to make the script wait for the calculations to be finished, before the data is copied. The Result is that my script just copies the error Message "N/A".
I tried just adding a Utilities.sleep(60000);, but it didn't work.
Is it possible to create a loop, that checks for the calculation to finish? I tried this without success:
function checkForError() {
var spreadsheet = SpreadsheetApp.getActive();
var source = spreadsheet.getRange ("Today!A1");
if (source = "N/A") {
Utilities.sleep(6000);
checkForError();
} else {
moveValuesOnly();
}
}
Locks are for this. Look up lock services in the docs. Use a public lock.
Here's how I used Zig's suggestion (combined with my own check loop) to solve my similar problem:
// Get lock for public shared resourse
var lock = LockService.getPublicLock();
// Wait for up to 120 seconds for other processes to finish.
lock.waitLock(120000);
// Load my values below
// something like sheet.getRange("A1").setFormula('= etc...
// Now force script to wait until cell D55 set to false (0) before
// performing copy / pastes
var current = SpreadsheetApp.setActiveSheet(sheet.getSheets()[1]);
var ready = 1;
var count = 0;
while (true) {
// break out of function if D55 value has changed to zero or counter
// has hit 250
if (count >= 250) break;
// otherwise keep counting...
ready = current.getRange("D55").getValue();
if (ready == 0) {count = 400;}
Utilities.sleep(100);
++count;
}
// wait for spreadsheet to finish... sigh...
Utilities.sleep(200);
// Do my copy and pastes stuff here
// for example sheet.getRange("a1:b1").copyTo(sheet.getRange("a3"), {contentsOnly:true});
// Key cells are updated so release the lock so that other processes can continue.
lock.releaseLock();
// end script
return;
}
This has worked fantastic for me, stopped Google's sporadic service from ruining my work!
Thanks goes to Zig's suggestion!