I have created event with this script:
function createCalendarEvent() {
let communityCalendar = CalendarApp.getCalendarById("calendar_ID");
let sheet = SpreadsheetApp.getActiveSheet();
let schedule = sheet.getDataRange().getValues();
schedule.splice(0, 2);
schedule.forEach(function(entry){
communityCalendar.createEvent(
entry[2],
entry[0],
entry[1],
{
guests: entry[3],
sendInvites: true
}
);
});
}
Now if I have made a mistake and would like to delete those events, how do I do that?
Thought it's easy as changing
communityCalendar.createEvent
to
communityCalendar.deleteEvent
But it won't work (obviously)
TypeError: communityCalendar.deleteEvent is not a function
(anonimowy) # Kod.gs:7
createCalendarEvent # Kod.gs:6
Much thanks,
Bartosz
As mentioned in Cooper's comment, you need to first retrieve an Event object before calling deleteEvent() on it. The safest way would be to store the IDs on another sheet. Luckily, createEvent() returns the Event object after you create it so you can get its ID right away. As an example based on your code:
const createdEvents = [] //blank array that will contain the list of created event IDs
schedule.forEach(function(entry){
//push the event IDs to the array
createdEvents.push(communityCalendar.createEvent(
entry[2],
entry[0],
entry[1],
{
guests: entry[3],
sendInvites: true
}
).getId()
//calling communityCalendar.createEvent(...).getId() will return the event ID
//as a string like "eq4p0j231j3qj412ddrmhmgg34#google.com"
)
});
At this point you can paste the list of IDs in a separate sheet. Then if you need to delete the events you can get the IDs from the sheet and use getEventByID() to process them
function deleteEvents(){
let communityCalendar = CalendarApp.getCalendarById("calendar_ID");
let sheet SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Events List");
let list = sheet.getRange("range where you pasted the IDs").getValues();
list.forEach(function(entry){
communityCalendar.getEventByID(entry).deleteEvent()
});
}
Alternatively, you could try to "reverse" your original function by using the original spreadsheet data to search for the events with getEvents(). This would probably work if you're the only one managing this calendar, but if there are other events that were not created by your script that also match the search criteria they would be deleted as well.
It would look something like this:
function deleteEvents() {
let communityCalendar = CalendarApp.getCalendarById("calendar_ID");
let sheet = SpreadsheetApp.getActiveSheet();
let schedule = sheet.getDataRange().getValues();
schedule.splice(0, 2);
schedule.forEach(function(entry){
communityCalendar.getEvents(
entry[0],//getEvents() takes starttime and endtime as well so we can reuse them
entry[1],
{
search: entry[2],
//from your code we know that entry[2] is the title so we can use it as a search query
}
)[0].deleteEvent(); //getEvents() returns an array so just take the first position and delete it
});
}
There's room for improvement but I hope this gives you a general idea of how it works. I would recommend to just store the IDs to avoid mistakes.
Related
A really simple one I think, but I would appreciate any help you can give.
In Google Sheets I have in A7:A a list of unique users taken from (F7:F) which can vary from 0 to 20+
=unique(filter(F7:F,F7:F<>""))
In B7:B I keep a tally of how many checkboxes are ticked (G7:G) for those unique users
example: =COUNTIFS($G$7:$G,TRUE,$F$7:$F,A7)
I also have a button/script which clears most cells and is used at the end of each day. for arguements sake lets say it clears users (F) and the checkbox (G).
What I need is for the button to also capture the information in A7:B? BEFORE it clears F and G and output it to a new tab, and as this happens each day I would like for it to not overwrite the previous days capture.
Thanks in advance for any assistance you can provide.
Try this
function dailyReport(){
var doc=SpreadsheetApp.getActiveSpreadsheet()
var mainTab = doc.getSheetByName('myMainTab')
var row = mainTab.getRange('A7').getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow()
var newTab = doc.getSheetByName('myNewTab')
var data = mainTab.getRange('A7:B'+row).getValues()
data=transpose(data)
newTab.getRange(newTab.getLastRow()+1,1,data.length,data[0].length).setValues(data)
}
function transpose(a){
return Object.keys(a[0]).map(function (c) { return a.map(function (r) { return r[c]; }); });
}
you will get raw values on 2 lines.
If you want to do statistics, prefer
function dailyReport(){
var doc=SpreadsheetApp.getActiveSpreadsheet()
var mainTab = doc.getSheetByName('myMainTab')
var row = mainTab.getRange('A7').getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow()
var newTab = doc.getSheetByName('myNewTab')
var data = mainTab.getRange('A7:B'+row).getValues()
var d=new Date()
row = newTab.getLastRow()+1
newTab.getRange(row,2,data.length,data[0].length).setValues(data)
newTab.getRange(row,1,data.length,1).setValue(d)
}
I'm trying to add to an array when a dropdown button is pressed but app-script seems to redeclare the array everytime an event is triggered. is there a way to stop this?
the code in question looks like this:
let by = [];
function addToArray (name = 'hello') {
by.push(name.replace(/\s[\s\S]*/, '').toLowerCase());
console.log(by.join(', '))
}
// ...
// function called every dropdown buttonclick
function handelAdd () {
const response = DocumentApp.getUi().prompt('Name:'); // prompt that gets name of item added
if (response.getSelectedButton() == DocumentApp.getUi().Button.OK) {
add(response.getResponseText());
}
}
I am going to assume that the following line in the question, here:
add(response.getResponseText()); // incorrect?
Should actually be this:
addToArray(response.getResponseText()); // correct?
Whenever your script finishes execution, any state it had saved (for example the value of the array in let by = [];) is lost.
This is to be expected. The script has finished. It exits. The document is still open, but the script has completed its work. It will run again the next time there is a relevant button click event.
To save state between multiple runs of the script, you can use Properties Services. This allows you to store a variable associated with the specific document (and user of that document). You can save your list to this storage, and retrieve it when you need to add a new item to it.
But you also need to store your array of data as a string in this storage - so in my example below I will convert the [...] array to a JSON string representation of the array, using JSON.stringify(). And I will convert back from JSON to an array using JSON.parse():
let by = [];
let byList = 'BY_LIST';
function addToArray (name = 'hello') {
let userProps = PropertiesService.getUserProperties();
// retrieve stored list and convert back from JSON to array:
by = JSON.parse(userProps.getProperty(byList));
console.log(by);
by.push(name.replace(/\s[\s\S]*/, '').toLowerCase());
userProps.setProperty(byList, JSON.stringify(by));
console.log(by.join(', '))
}
// ...
// function called every dropdown buttonclick
function handelAdd() {
const response = DocumentApp.getUi().prompt('Name:'); // prompt that gets name of item added
if (response.getSelectedButton() == DocumentApp.getUi().Button.OK) {
addToArray(response.getResponseText());
}
}
// used for my testing:
//function testMe() {
// handelAdd();
//}
function onOpen(e) {
// whenever the doc is re-opened, set the list to be
// empty, and store it in userProperties as a JSON string:
let userProps = PropertiesService.getUserProperties();
userProps.setProperty(byList, JSON.stringify( [] ));
}
The onOpen() trigger ensures that each time the document is opened, we start with a new empty list.
I am currently working on a semester project for my university in which we want to log data from an Arduino to a Google Sheet.
I was following the numerous tutorials and examples that I could find on Google and it worked so far really, really well. My Arduino is able to upload data to said spreadsheet.
Unfortunately all those examples always only deal with one row to be filled. For our project we would like to fill 2 or 3 lines simultaneously.
I will shortly show what I have done so far and maybe you can help me solve my (probably easy) problem.
I created a google spreadsheet in which I want to log my data
I used the script from a tutorial that should fill one row.
By typing the following line in my browserhttps://script.google.com/macros/s/<gscript id>/exec?tempData=datahereI am now able to fill row one with my data in enter in the end of the url.
But how do I progress now, when I want to fill two or three rows of the table? I say that the author of the code already implemented an option to fill the third row, yet I can't find out what to input in my url then to fill it with data.
All my attempts to write something like
https://script.google.com/macros/s/<gscript id>/exec?tempData=datahere&tempData1=value2
just ended in writing
datahere&tempData1=value2
in my first row, not filling datahere into the first and value2 in to the second row.
How can I provide and write multiple rows of data?
The code in this script is:
/*
GET request query:
https://script.google.com/macros/s/<gscript id>/exec?tempData=data_here
*/
/* Using spreadsheet API */
function doGet(e) {
Logger.log( JSON.stringify(e) ); // view parameters
var result = 'Ok'; // assume success
if (e.parameter == undefined) {
result = 'No Parameters';
}
else {
var id = '<ssheet id>'; // Spreadsheet ID
var sheet = SpreadsheetApp.openById(id).getActiveSheet();
var newRow = sheet.getLastRow() + 1;
var rowData = [];
//var waktu = new Date();
rowData[0] = new Date(); // Timestamp in column A
for (var param in e.parameter) {
Logger.log('In for loop, param='+param);
var value = stripQuotes(e.parameter[param]);
//Logger.log(param + ':' + e.parameter[param]);
switch (param) {
case 'tempData': //Parameter
rowData[1] = value; //Value in column B
break;
case 'tempData1':
rowData[2] = value; //Value in column C
break;
default:
result = "unsupported parameter";
}
}
Logger.log(JSON.stringify(rowData));
// Write new row below
var newRange = sheet.getRange(newRow, 1, 1, rowData.length);
newRange.setValues([rowData]);
}
// Return result of operation
return ContentService.createTextOutput(result);
}
/**
* Remove leading and trailing single or double quotes
*/
function stripQuotes( value ) {
return value.replace(/^["']|['"]$/g, "");
}
I would suggest the following:
Create a 2d array of your data you wish to write to the spreadsheet. If your client on Arduino were using JavaScript this might look like :
var data = [
["row1value1", "row1value2"],
["row2value1", "row2value2"]
];
Convert this to JSON, again in JavaScript this might look like:
var json = JSON.stringify(data);
This gives you a string representation of your array.
Now make your request using this data. I would suggest you should look at using doPost instead of doGet, as you are sending data to the spreadsheet that updates state. However, for the purposes of getting something working, your URL would look like:
https://script.google.com/<.....>/exec?myarray=<stringified JSON>
In Apps Script, in your doGet (again, consider using doPost instead), you could then use:
// Get the JSON representation of the array:
var json = e.parameter.myarray;
// Convert back to 2d array
var data = JSON.parse(json);
Now you can write this to a Range in Sheets using setValues, e.g. assuming a rectangular 2d array:
sheet.getRange(1, 1, data.length, data[0].length).setValues(data);
Hope this helps
I'm working on a script that interacts with Google Form' response sheet.
FormApp.getActiveForm().getDestinationId()
give me the spreadsheet id, but I don't find a way to get the sheet itself. User can change its name and position, so I need to get its id, like in
Sheet.getSheetId()
I also have to determine the number of columns the responses uses. It's not equal to the number of questions in the form. I can count the number of items in the form:
Form.getItems().length
and then search for gridItems, add the number of rows in each and add them minus one:
+ gridItem.getRows().length - 1
Finally, I think there's no way to relate each question with each column in the sheet, but by comparing somehow columns names with items title.
Thank you
#tehhowch came very close to the correct answer, but there is a problem with the code: there is no guarantee that form.getPublishedUrl() and sheet.getFormUrl() will return exactly the same string. In my case, form.getPublishedUrl() returned a URL formed as https://docs.google.com/forms/d/e/{id}/viewform and sheet.getFormUrl() returned https://docs.google.com/forms/d/{id}/viewform. Since the form id is part of the URL, a more robust implementation would be:
function get_form_destination_sheet(form) {
const form_id = form.getId();
const destination_id = form.getDestinationId();
if (destination_id) {
const spreadsheet = SpreadsheetApp.openById(destination_id);
const matches = spreadsheet.getSheets().filter(function (sheet) {
const url = sheet.getFormUrl();
return url && url.indexOf(form_id) > -1;
});
return matches.length > 0 ? matches[0] : null;
}
return null;
}
There is now a way to verify which sheet in a Google Sheets file with multiple linked forms corresponds to the current Form - through the use of Sheet#getFormUrl(), which was added to the Sheet class in 2017.
function getFormResponseSheet_(wkbkId, formUrl) {
const matches = SpreadsheetApp.openById(wkbkId).getSheets().filter(
function (sheet) {
return sheet.getFormUrl() === formUrl;
});
return matches[0]; // a `Sheet` or `undefined`
}
function foo() {
const form = FormApp.getActiveForm();
const destSheet = getFormResponseSheet_(form.getDestinationId(), form.getPublishedUrl());
if (!destSheet)
throw new Error("No sheets in destination with form url '" + form.getPublishedUrl() + "'");
// do stuff with the linked response destination sheet.
}
If you have unlinked the Form and the destination spreadsheet, then obviously you won't be able to use getDestinationId or getFormUrl.
I needed this also, and remarkably there is still no apps script method that facilitates it. In the end I set about finding a reliable way to determine the sheet id, and this is what I ended up with by way of programmatic workaround:
Add a temporary form item with a title that's a random string (or something similarly suitable)
Wait for the new corresponding column to be added to the destination sheet (typically takes a few seconds)
Look though each sheet in the destination until you find this new form item title string in a header row
Delete the temporary form item that was added
Wait for the corresponding column in the sheet to unlink from the form and become deletable (typically takes a few seconds)
Delete the column corresponding to the temporary form item
Return the sheet ID
I'm sure some won't like this approach because it modifies the form and spreadsheet, but it does work well.
With the necessary wait times included it takes about 12 seconds to perform all the look up / clean up operations.
Here's my code for this method in case anyone else might like to use it.
// Takes Apps Script 'Form' object as single paramater
// The second parameter 'obj', is for recursion (do not pass a second parameter)
// Return value is either:
// - null (if the form is not linked to any spreadsheet)
// - sheetId [int]
// An error is thrown if the operations are taking too long
function getFormDestinationSheetId(form, obj) {
var obj = obj || {}; // Initialise object to be passed between recursions of this function
obj.attempts = (obj.attempts || 1);
Logger.log('Attempt #' + obj.attempts);
if (obj.attempts > 14) {
throw 'Unable to determine destination sheet id, too many failed attempts, taking too long. Sorry!';
}
obj.spreadsheetId = obj.spreadsheetId || form.getDestinationId();
if (!obj.spreadsheetId) {
return null; // This means there actually is no spreadsheet destination set at all.
} else {
var tempFormItemTitle = '### IF YOU SEE THIS, PLEASE IGNORE! ###';
if (!obj.tempFormItemId && !obj.sheetId) { // If the sheet id exists from a previous recusion, we're just in a clean up phase
// Check that temp item does not already exist in form
form.getItems(FormApp.ItemType.TEXT).map(function(textItem) {
var textItemTitle = textItem.getTitle();
Logger.log('Checking against form text item: ' + textItemTitle);
if (textItemTitle === tempFormItemTitle) {
obj.tempFormItemId = textItem.getId();
Logger.log('Found matching form text item reusing item id: ' + obj.tempFormItemId);
}
return 0;
}); // Note: Just using map as handy iterator, don't need to assign the output to anything
if (!obj.tempFormItemId) {
Logger.log('Adding temporary item to form');
obj.tempFormItemId = form.addTextItem().setTitle(tempFormItemTitle).getId();
}
}
obj.spreadsheet = obj.spreadsheet || SpreadsheetApp.openById(obj.spreadsheetId);
obj.sheets = obj.sheets || obj.spreadsheet.getSheets();
obj.sheetId = obj.sheetId || null;
var sheetHeaderRow = null;
for (var i = 0, x = obj.sheets.length; i < x; i++) {
sheetHeaderRow = obj.sheets[i].getSheetValues(1, 1, 1, -1)[0];
for (var j = 0, y = sheetHeaderRow.length; j < y; j++) {
if (sheetHeaderRow[j] === tempFormItemTitle) {
obj.sheetId = obj.sheets[i].getSheetId();
Logger.log('Temporary item title found in header row of sheet id: ' + obj.sheetId);
break;
}
}
if (obj.sheetId) break;
}
// Time to start cleaning things up a bit!
if (obj.sheetId) {
if (obj.tempFormItemId) {
try {
form.deleteItem(form.getItemById(obj.tempFormItemId));
obj.tempFormItemId = null;
Logger.log('Successfully deleted temporary form item');
} catch (e) {
Logger.log('Tried to delete temporary form item, but it seems it was already deleted');
}
}
if (obj.sheetId && !obj.tempFormItemId && !obj.tempColumnDeleted) {
try {
obj.sheets[i].deleteColumn(j + 1);
obj.tempColumnDeleted = true;
Logger.log('Successfully deleted temporary column');
} catch (e) {
Logger.log('Could not delete temporary column as it was still attached to the form');
}
}
if (!obj.tempFormItemId && obj.tempColumnDeleted) {
Logger.log('Completed!');
return obj.sheetId;
}
}
SpreadsheetApp.flush(); // Just in case this helps!
// Normally this process takes three passes, and a delay of 4.5 secs seems to make it work in only 3 passes most of the time
// Perhaps if many people are submitting forms/editing the spreadsheet, this delay would not be long enough, I don't know.
obj.delay = ((obj.delay || 4500));
// If this point is reached then we're not quite finished, so try again after a little delay
Logger.log('Delay before trying again: ' + obj.delay / 1000 + ' secs');
Utilities.sleep(obj.delay);
obj.attempts++;
return getFormDestinationSheetId(form, obj);
}
}
To get the spreadsheet, once you have the DestinationID, use SpreadsheetApp.openById(). Once you have that, you can retrieve an array of sheets, and get the response sheet by index, regardless of its name.
var destId = FormApp.getActiveForm().getDestinationId();
var ss = SpreadsheetApp.openById(destId);
var respSheet = ss.getSheets()[0]; // Forms typically go into sheet 0.
...
From this point, you can manipulate the data in the spreadsheet using other Spreadsheet Service methods.
I also have to determine the number of columns the responses uses. It's not equal to the number of questions in the form. I can count the number of items in the form... (but that doesn't match the spreadsheet)
You're right - the number of current items does not equal the number of columns in the spreadsheet. The number of columns each response takes up in the destination sheet includes any questions that have been deleted from the form, and excludes items that are not questions. Also, the order of the columns in the spreadsheet is the order that questions were created in - as you re-arrange your form or insert new questions, the spreadsheet column order does not reflect the new order.
Assuming that the only columns in the spreadsheet are from forms, here's how you could make use of them:
...
var data = respSheet.getDataRange().getValues(); // 2d array of form responses
var headers = data[0]; // timestamp and all questions
var numColumns = headers.length; // count headers
var numResponses = data.length - 1; // count responses
And your last point is correct, you need to correlate names.
Finally, I think there's no way to relate each question with each column in the sheet, but by comparing somehow columns names with items title.
I am just beginning to learn Javascript and Google Apps Script. I have looked at and played with a few scripts, and so am attempting to create my own. What I want to do is take a list of students (Name and Email) and run a script to create "dropboxes", or folders that uses their name and is shared to their email address. This way, they can easily submit work to me in an organized manner. I have written this rough script, which I know will not work.
I was wondering if anyone can give me some tips?
function createDropbox () {
// Get current spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var data = sh1.getDataRange().getValues();
// For each email address (row in a spreadsheet), create a folder, name it with the data from the Class and Name column, then share it to the email
for(n=1;n<data.length;++n){
var class = sheet.getSheetValues(n, 1, 1, 1);
var name = sheet.getSheetValues(n, 2, 1, 1);
var email = sheet.getSheetValues(n, 3, 1, 1);
var folder = DocsList.createFolder('Dropbox');
folder.createFolder(class . name);
var share = folder.addEditor(email);
}
}
You've got the basic structure of your script right, it's only the semantics and some error handling that need to be worked out.
You need to determine how you want to access the contents of your spreadsheet, and be consistent with that choice.
In your question, you are first getting all the contents of the spreadsheet into a two-dimensional array by using Range.getValues(), and then later trying to get values directly from the sheet multiple additional times using .getSheetValues().
Since your algorithm is based on working through values in a range, use of an array is going to be your most effective approach. To reference the content of the data array, you just need to use [row][column] indexes.
You should think ahead a bit. What will happen in the future if you need to add more student dropboxes? As your initial algorithm is written, new folders are created blindly. Since Google Drive allows multiple folders with the same name, a second run of the script would duplicate all the existing folders. So, before creating anything, you will want to check if the thing already exists, and handle it appropriately.
General advice: Write apps script code in the editor, and take advantage of auto-completion & code coloring. That will help avoid mistakes like variable name mismatches (ss vs sh1).
If you are going to complete the exercise yourself, stop reading here!
Script
This script has an onOpen() function to create a menu that you can use within the spreadsheet, in addition to your createDropbox() function.
The createDropbox() function will create a top level "Dropbox" folder, if one does not already exist. It will then do the same for any students in the spreadsheet, creating and sharing sub-folders if they don't already exist. If you add more students to the spreadsheet, run the script again to get additional folders.
I've included comments to explain some of the tricky bits, as a free educational service!
/**
* Create menu item for Dropbox
*/
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [{
name : "Create / Update Dropbox",
functionName : "createDropbox"
}];
sheet.addMenu("Dropbox", entries);
};
function createDropbox () {
// Get current spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var data = ss.getDataRange() // Get all non-blank cells
.getValues() // Get array of values
.splice(1); // Remove header line
// Define column numbers for data. Array starts at 0.
var CLASS = 0;
var NAME = 1;
var EMAIL = 2;
// Create Dropbox folder if needed
var DROPBOX = "Dropbox"; // Top level dropbox folder
try {
// getFolder throws an exception if folder not found.
var dropbox = DocsList.getFolder(DROPBOX);
}
catch (e) {
// Did not find folder, so create it
dropbox = DocsList.createFolder(DROPBOX);
}
// For each email address (row in a spreadsheet), create a folder,
// name it with the data from the Class and Name column,
// then share it to the email
for (var i=0; i<data.length; i++){
var class = data[i][CLASS];
var name = data[i][NAME];
var email = data[i][EMAIL];
var folderName = class + ' ' + name;
try {
var folder = DocsList.getFolder(DROPBOX + '/' + folderName);
}
catch (e) {
folder = dropbox.createFolder(folderName)
.addEditor(email);
}
}
}
Even though the question is ~6 years old it popped up when I searched for "create folders from sheets data" .
So , taking the answer as inspiration, I had to update the code to allow for changes in the Google API, i.e. no more DocsList.
So here it is.
function createMembersFolders () {
// Get current spreadsheet
var MembersFolder = DriveApp.getFolderById("1Q2Y7_3dPRFsblj_W6cmeUhhPXw2xhNTQ"); // This is the folder "ADI Members"
var ss = SpreadsheetApp.getActiveSpreadsheet();
var data = ss.getDataRange().getValues() // Get array of values
// Define column numbers for data. Array starts at 0.
var NAME = 1;
// For each name, create a folder,
for (var i=1; i<data.length; i++){ // Skip header row
var name = data[i][NAME];
Logger.log(name);
if(MembersFolder.getFoldersByName(name).hasNext()){
Logger.log("Found the folder, no need to create it");
Logger.log(Object.keys(MembersFolder.getFoldersByName(name)).length);
} else {
Logger.log("Didn't find the folder so I'll create it");
var newFolder = MembersFolder.createFolder(name);
}
}
}
Note that I'm taking the parent folder directly using it's ID (which you get from the URL when viewing the folder).
You could also do this by name using the DriveApp.getFoldersByName("Folder Name").hasNext() condition which checks if the folder exist. If it does exist then you can access that folder you have found via DriveApp.getFoldersByName("Folder Name").next()
I didn't find that use of hasNext and next very intuitive but there it is.