I've a spreadsheet that uses a function from another external script (https://github.com/Eloise1988/CRYPTOBALANCE) which grabs the balance from a wallet.
I want to snapshot this value daily on another column, so I've created the following script:
function snapshot() {
SpreadsheetApp.flush()
// Assign 'dashboard' the Dashboard sheet.
var Carteiras = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Carteiras');
attempts = 0;
while (
(Carteiras.getRange("C2:C2").getValue() === '#NAME'
|| Carteiras.getRange("C2:C2").getValue() === "#NAME?"
|| Carteiras.getRange("C2:C2").getValue() === 'Loading...'
|| Carteiras.getRange("C2:C2").getValue() === 'Contact for BALANCE : t.me/TheCryptoCurious')
&& attempts < 60
) {
console.log('Formula is not yet ready... re-attempting in 1seg');
console.log('C2 value is ', Carteiras.getRange("C2:C2").getValue());
Utilities.sleep(1000)
attempts++;
}
console.log('C2 value is ', Carteiras.getRange("C2:C2").getValue());
if (attempts < 60) {
Carteiras.getRange("D2:D23").setValues(Carteiras.getRange("C2:C23").getValues());
console.log('Values updated successfully!');
} else {
console.error('Failed to grab the formula values.');
}
}
This script basically attempts to grab the balance from the wallet (Columns C2:C) , i know that once C2 is loaded all the others are loaded too, so I'm checking that C2 is in a valid state (e.g.: Not loading, no #Name or anything)
I've set a time driven trigger to run this snapshot function every day in the morning (10am to 11am) -- The problem is that the column is always on #NAME?
I think at some point google is not allowing this other external script to run, any ideas how can i make sure how to run this other script?
Also any improvements on my code will be welcomed as i never did anything on google spreadsheets.
Appreciated!
Instead of trying to read the result of custom function from the spreadsheet, call the custom function as a "normal" function.
function snapshot(){
const spreadsheet = SpreadsheetApp.getActiveSpreadshet();
var Carteiras = spreadsheet.getSheetByName('Carteiras');
const values = Carteiras.getRange("C2:C23").getValues();
const balance = values.map(row => {
const ticker = 'a-ticker'; // or use row[some-index-1] or other way to get the ticker
const address = 'a-address'; // or use row[some-index-2] or other way to get the address
const refresh_cell = null; // not needed in this context
return [CRYPTOBALANCE(ticker,address, refresh_cell)]
});
Carteiras.getRange("D2:D23").setValues(balance);
}
The above because Google Apps Script officials docs have not disclosed how exactly the formula recalculation works when the spreadsheet is opened by the script when the spreadsheet has not been first opened by the active user as usually occurs when a daily time-driven trigger is executed. I guess that the custom functions are loaded into the active spreadsheet function list when the formula recalculation is triggered by Google Sheets web client-side code.
Related
Custom formula returning #NAME when Google Sheets is published to web
Google Sheet Script Returning #NAME?
Why am I getting #NAME? retrieving google sheets cell values that depend on custom functions via API?
Google sheets custom function data disappears when viewing site as html after 20 minutes
Related
I have a spreadsheet with multiple sheets inside. What I want to achieve is for the editors to not be able to edit the sheets after a certain date.
That I can do with creating a script lock function for a sheet but what about the other sheets? Do I create a lock script for each individual sheet? Then how do I program them to run. Basically, I want for 1st script which locks the sheet1 to run today for example, then the next script which locks the sheet2 to run tomorrow same time, the 3rd script which locks sheet3 to run day after tomorrow and so on.
How do I do that, if that's even possible. Or maybe there's an easier way.
Thanks,
You can use the simple trigger onOpen(), this will run this script every-time a user opens the file:
function onOpen() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheets = ss.getSheets(); //Getting all the sheets from the file.
const lockDates = ss.getSheetByName("LockDates").getDataRange().getValues(); //Getting list of sheets and their lockdates.
const now = new Date(); //Getting today's date.
for (i=0; i < sheets.length; i++){
var currentSheet = sheets[i];
var sheetIndex = (lockDates.flat().indexOf(currentSheet.getName())/2); //This is to get the index where the current sheet name is located.
if(sheetIndex >= 0){ //If the sheet is not on the list we get -1.
var sheetLockDate = lockDates[sheetIndex][1]; //Assiging the lockDate to a variable.
if (now >= sheetLockDate && sheetLockDate > 0){ //Evaluating if today's date is on or after the lockDate.
currentSheet.protect();
console.log('Sheet -' + currentSheet.getName() + '- was protected');
break;
}
else { //The sheet is unprotected if it's still not time to protect it.
currentSheet.protect().remove();
}
}
}
}
Note the following:
This script will determine the lock dates based on a table at "LockDates" sheet, the code might break if you add additional columns.
If the sheet is not included in the list it will not be affected.
If the sheet is included in the list but doesn't have a lockDate it will be unprotected. This will let you modify the lockdate of specific sheets if needed.
You could protect the control sheet "LockDates" and it will not be affected by the script while it is not added to the list.
This is the setup where the code worked:
I think there are 2 ways we can achieve that result:
You can share the file as always but set an access expiration date, you will share access to a file but the access will expire after a specified date https://support.google.com/a/users/answer/9308784.
You can create an Apps Script project, give it a time-driven trigger so a certain function is executed after some period. This function in question should read a list somewhere (perhaps a form or sheet) and remove the access permissions.
#Bryan approach is very similar to mine. Here is my solution:
The code works with a Form with this structure (change the order by modifying the code under the reviewPermissions() function):
And using the Script Editor in the form add the following code:
let deletionSwitch;
function readResponses() {
var responses = FormApp.getActiveForm().getResponses();
responses.forEach(function (response) {
deletionSwitch = false;
reviewPermissions(response);
if (deletionSwitch)
FormApp.getActiveForm().deleteResponse(response.getId());
});
}
function reviewPermissions(response) {
var fileId = response.getItemResponses()[0].getResponse();
var email = response.getItemResponses()[1].getResponse();
var date = response.getItemResponses()[2].getResponse();
var nextPageToken;
if (Date.now() > new Date(date))
do {
var response = getPermissions(fileId, nextPageToken);
var permissions = response.items;
permissions.forEach(function (permission) {
if (permission.emailAddress.toLowerCase() == email.toLowerCase()) {
deletionSwitch = true;
deletePermission(fileId,permission);
}
});
} while (nextPageToken = response.nextPageToken)
}
function getPermissions(fileId, token = null) {
return permissions = Drive.Permissions.list(fileId, {
fields: "nextPageToken,items(id,emailAddress,role)",
pageToken: token
});
}
function deletePermission(fileId,permission){
if (permission.role != "owner")
Drive.Permissions.remove(fileId,permission.id);
}
This code needs Google Drive to be added as an Advanced Google service, add it with the name "Drive". Information about Advanced services is available in this documentation https://developers.google.com/apps-script/guides/services/advanced.
Necessary triggers:
Form onSubmit, execute the readResponses() function.
Time-driven (clock), execute the readResponses() function at the interval you prefer, I recommend every day.
Short code explanation:
The trigger will read all Form entries.
If there is a response that has an older date than today (expired) the code will check all the permissions of the file and will delete all permissions assigned to that email address address in the entry (not case sensitive).
Note:
Entries will be removed once their date expires.
Entries with dates in the future are ignored and checked in future runs.
Permission deletion is retroactive so submitting an entry with a date in the past will cause the permission to be deleted immediately (if exists).
The owner permission can't be removed, the deletion won't be attempted and the entry removed.
This code only works with files you own or have permission editor access to, you can request other people to copy the form with the script and use it with their own files.
Linking the Form responses to a Google Sheet file will allow you to have a historical record of what permissions should expire, this is not necessary for the code to work, just convenient for record purposes. Requesting the email address in the Form should not affect functionality.
In a self-developed add-on for Google Sheets, the functionality has been added that a sound file will be played from a JavaScript audio player in the sidebar, depending on the selection in the table. For the code itself see here.
When a line is selected in the table the corresponding sound file is played in the sidebar. Every time the next line is selected it takes around 2 seconds before the script will start to run and load the sound file into the sidebar. As the basic idea of the script is to quickly listen through long lists of sound files, it is crucial to reduce the waiting time as fare as possible.
A reproducible example is accessible here; Add-ons > 'play audio' (Google account necessary). To reproduce the error, the sheet has to be opened two times (e.g. in two browsers).
In order to reduce the latency you might try to reduce interval on your poll function as suggested by Cooper on a comment to the question and to change the getRecord function.
poll
At this time the interval is 2 seconds. Please bear in mind that reducing the interval too much might cause an error and also might have an important impact on the consume of the daily usage quotas. See https://developers.google.com/apps-script/guides/services/quotas
getRecord
Every time it runs it make multiple calls to Google Apps Script which are slow so you should look for a way to reduce the number of Google Apps Script calls. In order to do this you could store the spreadsheet table data in the client side code and only read it again if the data was changed.
NOTE: The Properties Service has a 50,000 daily usage quota for consumer accounts.
One way to quickly implement the above is to limit the getRecord function to read the current cell and add a button to reload the data from the table.
Function taken from the script bounded to the demo spreadsheet linked in the question.
function getRecord() {
var scriptProperties = PropertiesService.getScriptProperties();
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getDataRange().getValues();
var headers = data[0];
var rowNum = sheet.getActiveCell().getRow(); // Get currently selected row
var oldRowNum = scriptProperties.getProperty("selectedRow"); // Get previously selected row
if(rowNum == oldRowNum) { // Check if the was a row selection change
// Function returns the string "unchanged"
return "unchanged";
}
scriptProperties.setProperty("selectedRow", rowNum); // Update row index
if (rowNum > data.length) return [];
var record = [];
for (var col=0;col<headers.length;col++) {
var cellval = data[rowNum-1][col];
if (typeof cellval == "object") {
cellval = Utilities.formatDate(cellval, Session.getScriptTimeZone() , "M/d/yyyy");
}
record.push({ heading: headers[col],cellval:cellval });
}
return record;
}
Related
Problems when using a Google spreadsheet add-on by multiple users
I'm using IFTTT to update my Google Sheets when I receive a SMS. Now, I would like to take a step ahead and write a Google Apps Script that would do different things with the data updated by IFTTT into my Google Sheet. I tried to achieve the same using the Google Apps Script's onEdit function, but that does not work.
I did a lot of search on multiple forums regarding this problem, and I learnt that onEdit works only when a "user" makes the changes to the Google Sheet and not when the changes are done over an API request (I believe IFTTT uses the same). I could not see even a single post with a working solution.
Any ideas? Thanks!
After a lot of Google search, I found below code to be working for me. It is inspired by this answer by Mogsdad.
function myOnEdit(e) {
if (!e) throw new Error( "Event object required. Test using test_onEdit()" );
// e.value is only available if a single cell was edited
if (e.hasOwnProperty("value")) {
var cells = [[e.value]];
}
else {
cells = e.range.getValues();
}
row = cells[cells.length - 1];
// Do anything with the row data here
}
function test_onEdit() {
var fakeEvent = {};
fakeEvent.authMode = ScriptApp.AuthMode.LIMITED;
fakeEvent.user = "hello#example.com";
fakeEvent.source = SpreadsheetApp.getActiveSpreadsheet();
fakeEvent.range = fakeEvent.source.getActiveSheet().getDataRange();
// e.value is only available if a single cell was edited
if (fakeEvent.range.getNumRows() === 1 && fakeEvent.range.getNumColumns() === 1) {
fakeEvent.value = fakeEvent.range.getValue();
}
onEdit(fakeEvent);
}
// Installable trigger to handle change or timed events
// Something may or may not have changed, but we won't know exactly what
function playCatchUp(e) {
// Build a fake event to pass to myOnEdit()
var fakeEvent = {};
fakeEvent.source = SpreadsheetApp.getActiveSpreadsheet();
fakeEvent.range = fakeEvent.source.getActiveSheet().getDataRange();
myOnEdit(fakeEvent);
}
Hope this helps someone in future. Do note that the functions playCatchUp and myOnEdit must be set as "change" and "edit" action triggers respectively in Google Apps Script.
I have a formula in Google Spreadsheet (Cell G4):
=AVERAGE(INDEX( GoogleFinance(B4 , "all" , WORKDAY( TODAY(), -50 ) , TODAY() ) , , 3))
where, B4=INDEXBOM:SENSEX
I am trying to use following script to send email when cell value changes:
function checkValue()
{
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("Sheet1");
var valueToCheck = sheet.getRange("G4").getValue();
if(valueToCheck > 10000)
{
MailApp.sendEmail("xxxxxx#gmail.com", "Subject", "Context" + valueToCheck+ ".");
}
}
I set up time driven trigger to automatically run the script every hour. However, script does not send email.
After debugging, I found that since above function takes some time to calculate, valueToCheck output is a string ("#REF!") instead of a number.
Is there a way to work with this? I already tried using Utilities.sleep function to allow spreadsheet to finish calculation before script is executed but I am not successful with it.
I also tried using SpreadsheetApp.flush() but it didn't help as well.
I am not a programmer and have got stuck here. Thanks for help!
I suspect following statement given in GoogleFinance function document could be the reason:
Historical data cannot be downloaded or accessed via the Sheets API or Apps Script. If you attempt to do so, you will see a #N/A error in place of the values in the corresponding cells of your spreadsheet.
In writing a 'custom function' Google Script for my particular sheet, I simply want to hide a column:
function hideColumn(index) {
// get active spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
// get first sheet
var sheet = ss.getSheets()[0];
sheet.hideColumns(index);
}
This code works fine when I run it from within the Script editor but if I try to run it from inside a cell "=hideColumn(2)", I get the following error:
"You do not have permission to call hideColumns (line 48)."
From the same sheet/ script I'm able to run other custom functions such as:
function metersToMiles(meters) {
if (typeof meters != 'number') {
return null;
}
return meters / 1000 * 0.621371;
}
This seems to be some issue with the hideColumns function being run from inside a sheet? (ie. custom function?)
Your script 'hideColumn' is not a custom function, but a 'normal script'. Also it does not return anything (whereas the second function does). Only custom functions can be entered like formulas in the spreadsheet. See here for more info. My advice would be to create an extra menu-item using an onOpen trigger so you can run the function from the (spreadsheet)menu.
Hope that helps ?