Google Calendar events to Google Sheers automatic refresh with onEdit trigger - google-apps-script

I am trying to grab time events from my Google Calendar into a Google Spreadsheet.
When a new time-event is created in my Google Calendar this event should be automatically synchronized into my Google Spreadsheet. This should be done automatically by an onEdit event trigger.
At the moment it is only running by refreshing the Google Spreadsheet.
Maybe someone has a better solution for my challenge. Here is my code:
function createSpreadsheetEditTrigger() {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger('myCalendar')
.forSpreadsheet(ss)
.onEdit()
.create();
}
function myCalendar(){
var now=new Date();
// Startzeit
var startpoint=new Date(now.getTime()-60*60*24*365*1000);
// Endzeit
var endpoint=new Date(now.getTime()+60*60*24*1000*1000);
var events=CalendarApp.getCalendarById("your-calendar-ID").getEvents(startpoint, endpoint);
var ss=SpreadsheetApp.getActiveSpreadsheet().getSheetByName("TEST");
ss.clear();
for (var i=0;i<events.length;i++) {
ss.getRange(i+1,1 ).setValue(events[i].getTitle());
ss.getRange(i+1,2).setValue(events[i].getDescription());
ss.getRange(i+1,3).setValue(events[i].getStartTime());
ss.getRange(i+1,4).setValue(events[i].getEndTime());
}
}

Problem
Execute a function updating a spreadsheet when an event in Google Calendar is created.
Solution
Use the EventUpdated installable trigger that is fired each time an event is modified in Calendar (e.g. created, updated, or deleted - see reference). From there, you can go the easy way (update all data in the spreadsheet with a built-in CalendarApp class) or the hard way (update data that was changed with incremental sync - see official guide).
Part 0 - install trigger
/**
* Installs Calendar trigger;
*/
function calendarTrigger() {
var trigger = ScriptApp.newTrigger('callback name here')
.forUserCalendar('calendar owners email here')
.onEventUpdated()
.create();
}
Part 1 - callback (Calendar -> Spreadsheet)
/**
* Updates spreadsheet;
* #param {Object} e event object;
*/
function updateSpreadsheet(e) {
//access spreadsheet;
var ss = SpreadsheetApp.openById('target spreadsheet id');
var sh = ss.getSheetByName('target sheet name');
var datarng = sh.getDataRange(); //assumed that data is only calendar data;
//access calendar;
var calendar = CalendarApp.getCalendarById(e.calendarId);
//set timeframes;
var start = new Date();
var end =new Date();
//get year before and three after;
start.setFullYear(start.getFullYear()-1);
end.setFullYear(end.getFullYear()+3);
//get events;
var events = calendar.getEvents(start, end);
//map events Array to a two-dimensional array of values;
events = events.map(function(event){
return [event.getTitle(),event.getDescription(),event.getStartTime(),event.getEndTime()];
});
//clear values;
datarng.clear();
//setup range;
var rng = sh.getRange(1,1, events.length, events[0].length);
//apply changes;
rng.setValues(events);
}
Notes
As per Tanaike's comment - it is important to account for triggers (both simple and installable) to not firing if event is triggered via script or request (see restrictions reference). To enable such feature you will have to introduce polling or bundle with a WebApp that the script will call after creating an event (see below for a bundling sample).
Your solution is better suited for backwards flow: edit in spreadsheet -> edit in Calendar (if you modify it to perform ops on Calendar instead of updating the spreadsheet, ofc).
Make use of Date built-in object's methods like getFullYear() (see reference for other methods) to make your code more flexible and easier to understand. Btw, I would store "ms in a day" data as a constant (86400000).
Never use getRange(), getValue(), setValue() and similar methods in a loop (and in general call them as little as possible) - they are I/O methods and thus are slow (you can see for yourself by trying to write >200 rows in a loop). Get ranges/values needed at the start, perform modifications and write them in bulk (e.g. with setValues() method).
Reference
EventUpdated event reference;
Calendar incremental synchronization guide;
Date built-in object reference;
setValues() method reference;
Using batch operations in Google Apps Script;
Installable and simple triggers restrictions;
WebApp bundling
Part 0 - prerequisites
If you want to create / update / remove calendar events via script executions, you can bundle the target script with a simple WebApp. You'll need to make sure that:
The WebApp is deployed with access set as anyone, even anonymous (it is strongly recommended to introduce some form of request authentication);
WebApp code has one function named doPost accepting event object (conventionally named e, but it's up to you) as a single argument.
Part 1 - build a WebApp
This build assumes that all modifications are made in the WebApp, but you can, for example, return callback name to run on successfull request and handle updates in the calling script. As only the calendarId property of the event object is used in the callback above, we can pass to it a custom object with only this property set:
/**
* Callback for POST requests (always called "doPost");
* #param {Object} e event object;
* #return {Object} TextOutput;
*/
function doPost(e) {
//access request params;
var body = JSON.parse(e.postData.contents);
//access calendar id;
var calendarId = body.calendar;
if(calendarId) {
updateSpreadsheet({calendarId:calendarId}); //callback;
return ContentService.createTextOutput('Success');
}else {
return ContentService.createTextOutput('Invalid request');
}
}
Part 2 - sample calling script
This build assumes that calling script and the WebApp are the same script project (thus its Url can be accessed via ScriptApp.getService().getUrl(), otherwise paste the one provided to you during WebApp deployment). Being familiar with UrlFetchApp (see reference) is required for the build.
/**
* Creates event;
*/
function createEvent() {
var calendar = CalendarApp.getCalendarById('your calendar id here');
//modify whatever you need to (this build creates a simple event);
calendar.createEvent('TEST AUTO', new Date(), new Date());
//construct request parameters;
var params = {
method: 'post',
contentType: 'application/json',
muteHttpExceptions: true,
payload: JSON.stringify({
calendar: calendar.getId()
})
};
//send request and handle result;
var updated = UrlFetchApp.fetch(ScriptApp.getService().getUrl(),params);
Logger.log(updated); //should log "Success";
}

enter code here
// There are can be many calendar in one calendar of user like his main calendar, created calendar, holiday, etc.
// This function will clear all previous trigger from each calendar of user and create new trigger for remaining calendar of the user.
function createTriggers() {
clearAllTriggers();
let calendars = CalendarApp.getAllCalendars();
calendars.forEach(cal => {
ScriptApp.newTrigger("calendarUpdate").forUserCalendar(cal.id).onEventUpdated().create();
});
}
/* This trigger will provide us the calendar ID from which the event was fired, then you can perform your CalendarApp and sheet operation. If you want to synchronize new update more efficiently then use Calendar Advance Service, which will provide you with synchronization token that you can use to retrieve only updated and added events in calendar.
*/
function calendarUpdate(e) {
logSyncedEvents(e.calendarId);
}
function clearAllTriggers() {
let triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => {
if (trigger.getEventType() == ScriptApp.EventType.ON_EVENT_UPDATED) ScriptApp.deleteTrigger(trigger);
});
}

Related

Permissions API Call Failing when used in onOpen simple trigger

I've created a Google Sheet with an Apps Script to do issue and task tracking. One of the features I'm trying to implement is permissions based assigning of tasks. As a precursor to that, I have a hidden sheet populated with a list of users and their file permissions, using code similar to that in this StackOverflow question
When I manually run the code, it works fine. However, I want it to load every time the sheet is opened in case of new people entering the group or people leaving the group. So I made a call to my function in my onOpen simple trigger. However, when it is called via onOpen, I get the following:
GoogleJsonResponseException: API call to drive.permissions.list failed with error: Login Required
at getPermissionsList(MetaProject:382:33)
at onOpen(MetaProject:44:3)
Here are my functions:
My onOpen Simple Trigger:
function onOpen() {
//Constant Definitions for this function
const name_Access = 'ACCESS';
const row_header = 1;
const col_user = 1;
const col_level = 2;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sht_Access = ss.getSheetByName(name_Access);
var ui = SpreadsheetApp.getUi();
ui.createMenu('MetaProject')
.addSubMenu(
ui.createMenu('View')
.addItem('Restore Default Sheet View', 'restoreDefaultView')
.addItem('Show All Sheets', 'showAllSheets')
)
.addSeparator()
.addToUi();
//Clear Contents, Leave Formatting
sht_Access.clearContents();
//Set Column Headers
var head_name = sht_Access.getRange(row_header,col_user);
var head_level = sht_Access.getRange(row_header,col_level);
head_name.setValue('User');
head_level.setValue('Level');
//Refresh User List for use in this instance
getPermissionsList();
}
Here is getPermissionsList:
function getPermissionsList() {
const fileId = "<REDACTED>"; // ID of your shared drive
const name_Sheet = 'ACCESS';
const row_start = 2;
const col_user = 1;
const col_access = 2;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var thissheet = ss.getSheetByName(name_Sheet);
// THIS IS IMPORTANT! The default value is false, so the call won't
// work with shared drives unless you change this via optional arguments
const args = {
supportsAllDrives: true
};
// Use advanced service to get the permissions list for the shared drive
let pList = Drive.Permissions.list(fileId, args);
//Put email and role in an array
let editors = pList.items;
for (var i = 0; i < editors.length; i++) {
let email = editors[i].emailAddress;
let role = editors[i].role;
//Populate columns with users and access levels / role
thissheet.getRange(row_start + i,col_user).setValue(email);
thissheet.getRange(row_start + i,col_access).setValue(role);
}
I searched before I posted and I found the answer via some related questions in the comments, but I didn't see one with an actual answer. The answer is, you cannot call for Permissions or anything requiring authorization from a Simple Trigger. You have to do an Installable trigger.
In order to do that do the following:
Create your function or rename it something other than "onOpen".
NOTE: If you name it onOpen (as I originally did and posted in this answer) it WILL work, but you will actually run TWO triggers - both the installable trigger AND the simple trigger. The simple trigger will generate an error log, so a different name is recommended.
Click the clock (Triggers) icon on the left hand side in Apps Script.
Click "+ Add Trigger" in the lower right
Select the name of your function for "which function to run"
Select "onOpen" for "select event type"
Click "Save".
The exact same code I have above now runs fine.

Folders.hasNext() returning false from Google Sheets but true when called directly from Google Script

Overview
Im making a script for my google sheet.
I have a helper method which takes email for parameter (string) and then invites that email to google drives folder.
I also have edit() function which gets called whenever a field in Google Sheets is changed.
Problem:
When helper method is called by itself from Google Scripts it works fine and I get an invite.
When helper method is triggered from edit() while im in google sheets, it doesn't go all the way through to send an invitation. It stops at while function which returns false:
var newFolder = DriveApp.getFoldersByName("New Folder");
Logger.log(newFolder.hasNext());
while(newFolder.hasNext()) { -> returns false while in google sheets
var folder = newFolder.next();
Logger.log("folder -> " + folder);
folder.addViewer(newPersonEmail);
}
What I tried:
Logger.log shows that newFolder.hasNext() returns false while script
gets called from edit() function (when im making a change in google
sheets). But it returns true when I simply debug function in Google
scripts.
You have to register edit() for the specific user. It's not possible for the simple onEdit trigger.
It returns false because there is nothing for the current user. And it's true.
===== Updated =====
As for me, I think It's normal to create a triggers for users every time when we needs their scopes. Suppose we need to force a user to register a trigger. He has to run regTrigger()
/**
* #param {GoogleAppsScript.Events.SheetsOnEdit} e
*/
function edit(e) {
// there is the edit eveng action
}
/**
* Register trigger
* #returns {GoogleAppsScript.Script.Trigger}
*/
function regTrigger() {
var activeSpreadsheet = SpreadsheetApp.getActive();
var triggers = ScriptApp.getUserTriggers(activeSpreadsheet).filter(function(
trigger
) {
return (
trigger.getEventType() === ScriptApp.EventType.ON_EDIT &&
trigger.getHandlerFunction() === 'edit'
);
});
if (triggers.length) return triggers[0];
return ScriptApp.newTrigger('edit')
.forSpreadsheet(activeSpreadsheet)
.onEdit()
.create();
}
Now your task is to give the user the opportunity to perform this function once (regTrigger). For example, from the menu. Or make it happen when you open the Table forcing some events.

Triggering Google Script function with REST API request

I am running into trouble to trigger a function on edit when REST API software called Workato receives data from Quick Base and inputting in Google Spreadsheet.
Following codes auto sort stated tabs in Google Spreadsheet.
function onPost(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ApprovedTab = ss.getSheetByName("APPROVED");
var CollateralPending = ss.getSheetByName("COLLATERAL PENDING");
var InProcessing = ss.getSheetByName("IN PROCESSING");
var InClosing = ss.getSheetByName("IN CLOSING");
var funded = ss.getSheetByName("FUNDED");
var ApprovedTabRange = ApprovedTab.getRange("A2:T99");
var CollateralPendingRange = CollateralPending.getRange("A2:T99");
var InProcessingRange = InProcessing.getRange("A2:T99");
var InClosingRange = InClosing.getRange("A2:T99");
var fundedRange = funded.getRange("A2:T99");
ApprovedTabRange.sort( { column : 1, ascending: true } );
CollateralPendingRange.sort( { column : 1, ascending: true } );
InProcessingRange.sort( { column : 1, ascending: true } );
InClosingRange.sort( { column : 1, ascending: true } );
fundedRange.sort( { column : 1, ascending: true } );
}
When i try using onEdit instead of onPost and manually update a row in spreadsheet, it sorts rows by ID column.
When i try onPost and send a update request from Workato, Google Script function does not run and as result it is not sorting rows.
Any help would be appreciated.
Thank You
If I understand correctly, you want to have the spreadsheet automatically call the sorting function after Workato edits data in the sheet.
Since edits via scripts or add-ons don't generate an OnEdit trigger, you'll need to send a separate POST request to trigger a Google Apps Script function in your spreadsheet after Workato updates the data.
In order to call a function via a POST request, you must name the function "doPost()" rather than "onPost()", and you then must Publish the script as a web-app, from the Publish menu.
When publishing the script you will want to "execute as" you, and be accessible to "anyone, even anonymous".
Publishing the script as a web app allows it to receive an incoming GET or POST request, via functions named doGet() or doPost().
See the documentation here:
https://developers.google.com/apps-script/guides/web

Calling a bound script's method using the Execution API

I'm using PropertiesServices as variables, specifically Document Properties , in order to replace some tokens like "{client name}". Since those properties are scoped to the bound script only, I'm looking for a way to modify their values from my PHP application.
Is it possible to call a bound script's function using the Execution API, or maybe from a standalone script? Otherwise, should I instead use the Script Properties instead (although the docs make me think you can't use them if the script isn't 'standalone).
It looks like if the user that the Execution API is running under has permission to the doc that bound script ran by the execution api can read document properties.
Here is my test:
Create a new spreadsheet. Create a new script. Add some data using the menu from onOpen. Run executeAPI inside the script. The log successfully shows the document properties.
function onOpen() {
var testMenu = SpreadsheetApp.getUi().createMenu("test")
testMenu.addItem("Add some data", "addData").addToUi();
testMenu.addItem("Preview data", "getData").addToUi();
}
function getData(){
var keys = PropertiesService.getDocumentProperties().getKeys();
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().clear().appendRow(keys)
}
function returnData(){
return PropertiesService.getDocumentProperties().getKeys();
}
function addData(){
var DT = new Date().toString()
PropertiesService.getDocumentProperties().setProperty(DT,DT);
}
function executeAPI(){
var url = 'https://script.googleapis.com/v1/scripts/'+ScriptApp.getProjectKey()+':run';
var payload = JSON.stringify({"function": "returnData","parameters":[], "devMode": true});
var params={method:"POST",
headers:{Authorization: "Bearer "+ ScriptApp.getOAuthToken()},
payload:payload,
contentType:"application/json",
muteHttpExceptions:true};
var results = UrlFetchApp.fetch(url, params);
Logger.log(results)
}

How to detect user changing sheet?

How to detect and trigger custom action during opening specific sheet in spreadsheet?
I could not find proper function in https://developers.google.com/apps-script/guides/triggers/events
To do polling as Mogsdad suggested you could use this code in your Sidebar (or any UI element)
$(function() {
/**
* On document load, assign click handlers to each button and try to load the
* user's origin and destination language preferences if previously set.
*/
poll();
}
function poll(interval){
interval = interval || 2000;
setTimeout(function(){
google.script.run.withSuccessHandler(loadSheetName)
.withFailureHandler(showError).getSheetName();
poll();
}, interval);
};
function loadSheetName(sheetName) {
alert(sheetName);
}
function getSheetName() {
google.script.run.withSuccessHandler(loadSheetName)
.withFailureHandler(showError).getSheetName();
}
And in your .gs code
function getSheetName() {
var sheetName = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getName();
return sheetName;
}
There is no API provided to trigger on user events, aside from the ones in the documentation you've linked.
I can think of only one work-around: If your application involves having a custom UI element, specifically a sidebar or dialog, then you could poll for the active cell, and determine the active sheet from that.
See How to poll a Google Doc from an add-on for a start.