I'm getting this error in my handler function but I've no clue what's causing it. I've copied the code and debugged it in a non-handler function and there was no error.
function _responseToNext(e) {
var app = UiApp.getActiveApplication();
app.getElementById('btnPrev').setEnabled(true);
var current = parseInt(CacheService.getPublicCache().get('currentItem'));
var agendaItems = Utilities.jsonParse(CacheService.getPublicCache().get('agenda'));
agendaItems[current]['notes'] = e.parameter.tAreaNotes;
agendaItems[current]['status'] = e.parameter.lboxStatus;
CacheService.getPublicCache().put('agenda', Utilities.jsonStringify(agendaItems));
current = current + 1;
CacheService.getPublicCache().put('currentItem', current);
fillAgendaDetail(app);
// only enabled 'Next' if there are more items in the agenda
if (current < agendaItems.length-1) {
app.getElementById('btnNext').setEnabled(true);
}
return app;
}
I suppose, the error cause is that the Cache get method returns null during the 1st execution when the cache is empty. The Utilities.jsonParse throws an exception and the cache becomes in any case empty. Try to use the following modified code.
function _responseToNext(e) {
var app = UiApp.getActiveApplication();
app.getElementById('btnPrev').setEnabled(true);
var cachedCurrent = CacheService.getPublicCache().get('currentItem');
var current;
if (cachedCurrent == null) {
current = 0;
}
else {
current = parseInt(cachedCurrent);
}
var cachedAgendaItems = CacheService.getPublicCache().get('agenda');
var agendaItems;
if (cachedAgendaItems == null) {
agendaItems = [][];
}
else {
agendaItems = Utilities.jsonParse();
}
agendaItems[current]['notes'] = e.parameter.tAreaNotes;
agendaItems[current]['status'] = e.parameter.lboxStatus;
CacheService.getPublicCache().put('agenda', Utilities.jsonStringify(agendaItems));
current = current + 1;
CacheService.getPublicCache().put('currentItem', current);
fillAgendaDetail(app);
// only enabled 'Next' if there are more items in the agenda
if (current < agendaItems.length-1) {
app.getElementById('btnNext').setEnabled(true);
}
return app;
}
Also please mention that the Public Cache (CacheService.getPublicCache()) is the same for all users of your script. In your case, this means, if two users user1#example.com and user2#example.com use the script they will have the same current and agendaItems variables values, i.e. it can be a situation when the _responseToNext handler is already executed under the user1 authority - the current variable is equal to 1, after the user2 executes the _responseToNext handler - the current variable is equal to 2 and so on. If you do not need such behaviour, use the CacheService.getPrivateCache().
Related
Please see the code herein under:
function binanceOrderBook() {
try {
muteHttpExceptions = true;
var workSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var mySheet = workSpreadsheet.getSheetByName('Order Books');
if(mySheet == 'Sheet'){
mySheet.activate();
} else {
mySheet = workSpreadsheet.insertSheet('Order Books', 1).activate();
}
var ui = SpreadsheetApp.getUi();
var string = 'https://api.binance.com/api/v3/depth?';
var symbolResponse = ui.prompt('Pair Name', 'Please enter the pair symbol.\n\nExamples: BTCUSDT or ETHBTC:', ui.ButtonSet.OK_CANCEL);
var symbolButton = symbolResponse.getSelectedButton();
if(symbolButton == ui.Button.CANCEL){return}
var mySymbol = symbolResponse.getResponseText();
mySymbol = mySymbol.toUpperCase();
string = string + "symbol=" + mySymbol;
var limitResponse = ui.prompt('Limit:', 'Please enter Limit (Period Quantity).\nValid limits are:5, 10, 20, 50, 100, 500, 1000. \n Default limit is 100.\n You can leave it blank and simply click OK.', ui.ButtonSet.OK_CANCEL);
if(limitResponse.getSelectedButton() == ui.Button.CANCEL){return}
var myLimit = Number(limitResponse.getResponseText());
if(myLimit != 5 && myLimit != 10 && myLimit != 20 && myLimit != 50 && myLimit != 100 && myLimit != 500 && myLimit != 1000){myLimit = 100;}
string = string + "&limit=" + myLimit;
var myDate = new Date().toUTCString();
var jsonOrderBookData = JSON.parse(UrlFetchApp.fetch('https://api.binance.com/api/v3/depth?symbol=' + mySymbol + '&limit=' + myLimit));
reporter(jsonOrderBookData);
} catch (e){
exceptionHandler(e)
}
}
The problem I have is to run UrlFetchApp.fetch again when it encounters an error. I need to run it several times to get the result. So, I need to prevent the script from stopping when an error (code -1003) occurs, but how can I do that?
EDIT: There is a function windows.onerror in javascript which can be set to prevent the program from stopping. Is it useable in GAS? if yes, how? if No, is there a similar solution for GAS?
You could call binanceOrderBook() from within your catch statement. E.g.
...
} catch (e){
binanceOrderBook()
exceptionHandler(e)
}
Of course you probably should have some condition that exits the function if a certain error occurs, or if you know that the function needs to run no more than x number of times you could check that it has run less than x times before executing. For example,
const maxValue = 10 // whatever the max number of executions should be
function binanceOrderBook(executions) {
if (executions >= maxValue) return;
try {
...
} catch(e) {
binanceOrderBook((executions || 0) + 1));
exceptionHandler(e); // note that I am including this here because it's in your original example, but as it is written now, exception handler won't be called until binanceOrderBook executes without an error.
}
}
[Edit] To answer your second question, there is no equivalent to window.onerror that I know of in GAS. However, window.onerror is a global event handler and so would affect errors thrown by any functions defined in your project. To address a concern with a single function call like this, you are better off using a try catch statement as you have.
I'm trying to create a list of files stored in my Google Drive and also a list of their current and previous permissions. Specifically, I want to create a list of files in my Google Drive which at any point in the past have had the 'Anyone with a link can view/edit (etc)' permission set.
I have created a Google Apps Script to do this and I can iterate through all the files OK and I can get files which currently have that permission set, but I can't see a way to get the history of the file's permissions.
I have found and activated the revisions list API: https://developers.google.com/drive/api/v2/reference/revisions/list
This gets revisions but I can't see anywhere that it lists the sharing history of a revision.
Is what I'm attempting to do possible?
It's definitely possible using the Drive Activity API. You can use the Quickstart for Google Apps Script to view all the activity of an item (file or folder) or done by a User. In this case I modified the Quickstart to show the Permissions changes of a given Drive Id.
function listDriveActivity() {
var request = {
itemName: "items/1bFQvSJ8pMdss4jInrrg7bxdae3dKgu-tJqC1A2TktMs", //Id of the file
pageSize: 10};
var response = DriveActivity.Activity.query(request);
var activities = response.activities;
if (activities && activities.length > 0) {
Logger.log('Recent activity:');
for (var i = 0; i < activities.length; i++) {
var activity = activities[i];
var time = getTimeInfo(activity);
var action = getActionInfo(activity.primaryActionDetail);
var actors = activity.actors.map(getActorInfo);
var targets = activity.targets.map(getTargetInfo);
if (action == "permissionChange"){ //Only show permissionChange activity
Logger.log(
'%s: %s, %s, %s', time, truncated(actors), action,
truncated(targets));
}
}
} else {
Logger.log('No activity.');
}
}
/** Returns a string representation of the first elements in a list. */
function truncated(array, opt_limit) {
var limit = opt_limit || 2;
var contents = array.slice(0, limit).join(', ');
var more = array.length > limit ? ', ...' : '';
return '[' + contents + more + ']';
}
/** Returns the name of a set property in an object, or else "unknown". */
function getOneOf(object) {
for (var key in object) {
return key;
}
return 'unknown';
}
/** Returns a time associated with an activity. */
function getTimeInfo(activity) {
if ('timestamp' in activity) {
return activity.timestamp;
}
if ('timeRange' in activity) {
return activity.timeRange.endTime;
}
return 'unknown';
}
/** Returns the type of action. */
function getActionInfo(actionDetail) {
return getOneOf(actionDetail);
}
/** Returns user information, or the type of user if not a known user. */
function getUserInfo(user) {
if ('knownUser' in user) {
var knownUser = user.knownUser;
var isMe = knownUser.isCurrentUser || false;
return isMe ? 'people/me' : knownUser.personName;
}
return getOneOf(user);
}
/** Returns actor information, or the type of actor if not a user. */
function getActorInfo(actor) {
if ('user' in actor) {
return getUserInfo(actor.user)
}
return getOneOf(actor);
}
/** Returns the type of a target and an associated title. */
function getTargetInfo(target) {
if ('driveItem' in target) {
var title = target.driveItem.title || 'unknown';
return 'driveItem:"' + title + '"';
}
if ('drive' in target) {
var title = target.drive.title || 'unknown';
return 'drive:"' + title + '"';
}
if ('fileComment' in target) {
var parent = target.fileComment.parent || {};
var title = parent.title || 'unknown';
return 'fileComment:"' + title + '"';
}
return getOneOf(target) + ':unknown';
}
Remember to enable the Drive Activity API in Resources > Advanced Google Services
In my example this returns the logs:
You can also look deeper into the Permissions by using the permissionChange Parameters in the query.
If you have a business/enterprise/edu account the admin audit logs will tell you this for 6 months of data. Or it will at least tell you when a permission was changed from x to y.
Can't think of a method for personal.
I have a filter that adds the "unprocessed" label on all incoming emails.
Then a Google Script searches every minute for any email threads that have the "unprocessed" label, processes the messages, and conditionally apply a label to the corresponding thread.
I don't know what I have done wrong, but only SOME of the processed threads get the label. And it works randomly... For example only 3 out of 6 threads got the label, or 1 out of 3.
I have to re-apply the "unprocessed" label, and just run the script again to fix them.
function processGmail() {
var startTime = new Date().getTime();
var mailerRegex = /X-Mailer:(.*)/g;
var scannerLabel = GmailApp.getUserLabelByName("Scanner");
var unprocessedLabel = GmailApp.getUserLabelByName("unprocessed");
var countMessages = 0;
GmailApp.search("label:unprocessed").forEach(
function(emailThread) {
emailThread.getMessages().forEach(
function(message) {
var raw = message.getRawContent();
var result;
var doReturn = false;
while((matches = mailerRegex.exec(raw)) !== null) {
if (matches.some(function(match){return match.indexOf('Canon MFP') >= 0;})) {
emailThread.addLabel(scannerLabel);
emailThread.moveToArchive();
doReturn = true;
break;
}
}
emailThread.removeLabel(unprocessedLabel);
++countMessages;
if (doReturn) {
return;
}
}
);
}
);
var endTime = new Date().getTime();
Logger.log("Processed " + countMessages + " in " + (endTime-startTime) + "ms.");
}
Turns out the bug was Javascript related.
I had forgotten that the regex.exec needs to be looped until a null is returned, only then it will start a-new for a new input.
The fix was removing break :)
in my program, the user sets a range of numbers for the computer to guess. The user then has to guess which number the computer chose with a limit of guesses starting at 5. There are several problems in my functioning program in which I do not understand how to fix. These errors include:
-The number of guesses left always remains at 0. It won't start at 5 and decrease by 1 each time I click the btnCheck button.
-Whenever I click the btnCheck button for a new guessing number, the statement if you've guessed too high or too low remains the same.
-When I press btnNewGame, the values I insert in my low value and my high value text inputs will not be cleared.
-How can the computer generate a random whole number based on what I set as the number range?
Revising my code down below will be much appreciated.
// This line makes the button, btnCheckGuess wait for a mouse click
// When the button is clicked, the checkGuess function is called
btnCheckGuess.addEventListener(MouseEvent.CLICK, checkGuess);
// This line makes the button, btnNewGame wait for a mouse click
// When the button is clicked, the newGame function is called
btnNewGame.addEventListener(MouseEvent.CLICK, newGame);
// Declare Global Variables
var computerGuess:String; // the computer's guess
var Statement:String; // Statement based on your outcome
// This is the checkGuess function
// e:MouseEvent is the click event experienced by the button
// void indicates that the function does not return a value
function checkGuess(e:MouseEvent):void
{
var LowValue:Number; // the user's low value
var HighValue:Number; // the user's high value
var UserGuess:Number; // the user's guess
var CorrectGuess:int; // the correct number
var FirstGuess:String; //the user's guess
// get the user's range and guess
LowValue = Number(txtinLow.text);
HighValue = Number(txtinHigh.text);
UserGuess = Number(txtinGuess.text);
// determine the number of the user
GuessesLeft = checkCorrectGuess(FirstGuess);
lblNumber.text = GuessesLeft.toString();
lblStatement.text = "You have guessed " + Statement.toString() + "\r";
}
// This is function checkColoursCorrect
// g1– the user's guess
function checkCorrectGuess(g1:String):int
{
var GuessesLeft:int = 5; // How many guesses are left
if (g1 != computerGuess)
{
GuessesLeft - 1;
}
else
{
GuessesLeft = 0;
}
return GuessesLeft;
}
// This is the newGame function
// e:MouseEvent is the click event experienced by the button
// void indicates that the function does not return a value
function newGame(e:MouseEvent):void
{
var Guess1:int; // computer's guess in numbers
var UserGuess1:int; // user's guess in numbers
Guess1 = randomWholeNumber(100,1); //It is not (100,1). How do I change this to the range the user put?
UserGuess1 = randomWholeNumber(100,1); //It is not (100,1). How do I change this to the range the user put?
if (Guess1 > UserGuess1) {
Statement = "TOO HIGH";
} else if (Guess1 < UserGuess1) {
Statement = "TOO LOW";
} else if (Guess1 == UserGuess1) {
Statement = "CORRECTLY";
}
txtinGuess.text = "";
lblStatement.text = "";
}
// This is function randomWholeNumber
// highNumber – the maximum value desired
// lowNumber – the minimum value desired
// returns – a random whole number from highNumber to lowNumber inclusive
function randomWholeNumber(highNumber:int,lowNumber:int):int //How do I make a whole random number based on the range the user made?
{
return Math.floor((highNumber - lowNumber + 1) * Math.random() + lowNumber);
}
To answer your questions...
You've declared GuessesLeft inside checkCorrectGuess() which means its a local variable that's being redefined every time you call the function. Futhermore, because you're passing in var FirstGuess:String; (an uninitialized, non-referenced string variable), (g1 != computerGuess) is returning false, and the answer is always 0.
GuessesLeft - 1; is not saving the result back to the variable. You need to use an assignment operator such as GuessesLeft = GuessesLeft - 1 or simply type GuessesLeft-- if all you want is to decrement. You could also write GuessesLeft -= 1 which subtracts the right from the left, and assigns the value to the variable on the left. See AS3 Operators...
You've already assigned values to these TextFields earlier; simply repeat the process inside of newGame() with a txtinLow.text = "" (same with high)
Use your variables. You defined them earlier in checkGuess() as UserGuess, LowValue, and HighValue
Be mindful that you only need to split out functionality into separate functions if that piece of code is likely to be called elsewhere. Otherwise, every function on the stack incurs more memory and performance hits. checkCorrectGuess() falls into that category and is therefore unnecessary.
Also, you are printing your feedback to the user in the newGame() function instead of checkGuess(). It seemed like an oversight.
btnCheckGuess.addEventListener(MouseEvent.CLICK, checkGuess);
btnNewGame.addEventListener(MouseEvent.CLICK, newGame);
// Global Variables
var computerGuess:int;
var remainingGuesses:int;
newGame();
function newGame(e:MouseEvent):void {
// Reset our guess limit
remainingGuesses = 5;
// Generate a new number
computerGuess = random(int(txtinLow.text), int(txtinHigh.text));
// Reset our readouts.
txtinGuess.text = "";
lblStatement.text = "";
}
function checkGuess(e:MouseEvent):void {
var guess:int = int(txtinGuess.text);
var msg:String;
if (guess == computerGuess) { // Win
remainingGuesses = 0; // Zero our count
msg = "CORRECT";
} else { // Missed
remainingGuesses--; // Decrement our count
if (guess > computerGuess) {
msg = "TOO HIGH";
} else if (guess < computerGuess) {
msg = "TOO LOW";
}
}
lblNumber.text = remainingGuesses.toString();
lblStatement.text = "You have guessed " + msg;
}
function random(low:int, high:int):int {
return Math.floor((high - low + 1) * Math.random() + low);
}
Serge's solution here seemed like the way to go about this, but I'm a bit afraid that my circumstances may be too different...
I have a button where users can add a new set of rows with controls to a FlexTable, in order to allow them to insert a new member into a record set. After designing and building the app to do this (and despite assurances to the contrary), a requirement was then added for the users to be able to edit the record sets at a later date.
I've finally managed to get the data retrieved and correctly displayed on the Ui - for single member record sets. As a final stage, I am now attempting to extend this to accommodate record sets having more than one member. Obviously this requires determining how many members there are in the record set, and then adding the new rows/control group to the FlexTable, before loading the member into each control group.
So within this routine, (depending on how many members there are) I may need to trigger the same callback, which the user normally does with a button. However, the difference with Serge's fine example, is that his code triggers the checkbox callback at the end of his routine once all the Ui components are in place. My situation needs to do this on the fly - and so far I'm getting 'Unexpected error', which suggests to me that the Ui is not able to update with the added FlexTable controls before my code attempts to assign values to them.
Does anyone have any insight into this problem? Is my only recourse to completely re-build a fixed Ui and dispense with the dynamic rowset model?
Code follows -
1. event for adding controls:
var app = UiApp.getActiveApplication();
var oFlexGrid = app.getElementById('ExpenseDetail');
var oRowCount = app.getElementById('rowCount');
var oScriptDBId = app.getElementById('scriptDBId');
var iRows = parseInt(e.parameter.rowCount);
var sVId = e.parameter.scriptDBId;
var vGridDefs = loadArrayById(sVId); //retrieve upload definition array from ScriptDB
var vControlNames = [];
if (isOdd(iRows)){
var sColour = 'AliceBlue';
} else {
var sColour = 'LavenderBlush';
};
oFlexGrid.insertRow(0);
oFlexGrid.insertRow(0);
oFlexGrid.insertRow(0);
oFlexGrid.insertRow(0);
oFlexGrid.setRowStyleAttributes(0,{'backgroundColor':sColour});
oFlexGrid.setRowStyleAttributes(1,{'backgroundColor':sColour});
oFlexGrid.setRowStyleAttributes(2,{'backgroundColor':sColour});
oFlexGrid.setRowStyleAttributes(3,{'backgroundColor':sColour});
var vExpenseDef = Get_NamedRangeValues_(CONST_SSKEY_APP,'UIAPP_GridExpense');
iRows = iRows+1;
vControlNames = CreateGrid_MixedSet_(iRows, vExpenseDef, oFlexGrid, app);
oRowCount.setText(iRows.toString()).setValue(iRows.toString());
//SOME INCONSEQUENTIAL CODE REMOVED HERE, LET ME KNOW IF YOU NEED IT
vGridDefs = vGridDefs.concat(vControlNames); // unify grid definition arrays
var sAryId = saveArray('expenseFieldDef', vGridDefs);
oScriptDBId.setText(sAryId).setValue(sAryId); //store array and save ScriptDB ID
if (e.parameter.source == 'btnExpenseAdd'){
hideDialog(); //IGNORE CHEKCBOX-DRIVEN CALLS
};
return app;
2. routine that calls the event
var app = UiApp.getActiveApplication();
var oPanelExpense = app.getElementById('mainPanelExpense');
var oPanelIncome = app.getElementById('mainPanelIncome');
var oPanelEdit = app.getElementById('mainPanelEdit');
var chkExpenseAdd= app.getElementById('chkExpenseAdd');
var bExpenseTrigger = e.parameter.chkExpenseAdd;
var sVoucherId = nnGenericFuncLib.cacheLoadObject(CACHE_EDIT_VOUCHERID);
var sVoucher = e.parameter.ListSearch1Vouchers;
var aryVoucherInfo = getVoucherEditDetail(sVoucherId);
//SAVE FOR RECORD MARKING CALLBACK
nnGenericFuncLib.cacheSaveObject(CACHE_EDIT_OLDRECORDS, JSON.stringify(aryVoucherInfo), CACHE_TIMEOUT);
sVoucher = nnGenericFuncLib.textPad(sVoucher, '0', 7);
var bExp = (sVoucher.substring(0,2) == '03')
var oRowCount = app.getElementById('rowCount');
var iRowCount = parseInt(e.parameter.rowCount);
var sControlName = '';
var vControlVal = '';
var iExpIdx = 0;
var sControlType = '';
var oControl = '';
var vSummaryTotal = 0;
for (var iVal in aryVoucherInfo){
sControlName = aryVoucherInfo[iVal][2];
vControlVal = aryVoucherInfo[iVal][3];
switch (sControlName){
case 'ESUM60':
vSummaryTotal = vControlVal;
break;
case 'EXUSRN':
continue; //DON'T OVERWRITE CURRENT USERNAME
break;
};
if (sControlName.indexOf('_')!=-1){ //TEST FOR CONTROL SET MEMBER
var aryControlSet = sControlName.split('_');
if (parseInt(aryControlSet[1])>iRowCount){//*** TRIGGER THE EVENT ***
Logger.log(bExpenseTrigger + ' - ' + !bExpenseTrigger);
chkExpenseAdd.setValue(!bExpenseTrigger, true);
iRowCount = iRowCount +1;
};
};
oControl = app.getElementById(sControlName);
var vCache = cacheSaveReturn(CACHE_UIEX_LISTS,sControlName);
if (typeof vCache == 'undefined'){
oControl.setValue(vControlVal);
oControl.setText(vControlVal);
//controlSetTextBox(oControl,vControlVal);
//controlSetDateBox(oControl,vControlVal);
} else {
if (!(nnGenericFuncLib.arrayIsReal(vCache))){
vCache = JSON.parse(vCache);
};
vCache = vCache.indexOf(vControlVal);
if (vCache != -1){
oControl.setSelectedIndex(vCache);
} else {
controlSetListBox(oControl,vControlVal);
};
};
};
//SOME CODE REMOVED HERE
hideDialog();
return app;
Mogsdad to the rescue!
The answer (see above) for those at the back of the class (with me) is to simply pass the app instance parameter (e) to the event function, calling it directly from the main routine, thus keeping the chronology in step for when it returns the app to complete the routine. No need for the checkbox in this situation.
This only took me all day, but thanks Mogsdad! :)
Snippet below taken from 1/2 way down code sample 2 in the OP:
if (sControlName.indexOf('_')!=-1){ //TEST FOR CONTROL SET MEMBER
var aryControlSet = sControlName.split('_');
if (parseInt(aryControlSet[1])>iRowCount){
eventAddExpense(e); //THAT'S ALL IT TAKES
iRowCount = iRowCount +1;
};
};