Error message cites non-existent line and column in code for Google Apps Script - debugging

I get the following error message when running some code in Google Apps Script. I don't understand the Line/Column reference, Code:46:18. It appears to point to either a line with too few columns or a process with too few lines. I assume I am not interpreting the reference correctly.
TypeError: Cannot set property 'format' of undefined
at processInbox(processInbox Code:46:18)
Line 46 of all my code is this and certainly doesn't have 18 columns (and it closes a function that doesn't refer to format):
}
The process referred to by the error message, processInbox, is only 39 lines long.
The script is called by selecting "Run Script" in the menu, "CiviSchedule" in the related Google Sheet, which triggers the doTasks function. This menu and and trigger are created in the onOpen function.
How am I misinterpreting the error message? (Full code follows)
[screenshot of error]
[screenshot of lines 40-46]
The code for reference:
//General Info
//
// As detailed in Managing Scheduled Jobs URL method http://wiki.civicrm.org/confluence/display/CRMDOC/Managing+Scheduled+Jobs#ManagingScheduledJobs-URLmethod :
//
// a valid Username and Password (for a Drupal, Joomla or WordPress user who has adequate permissions
// for the job or jobs being run. The minimal permissions for that user are: “view all contacts”, “access
// CiviCRM”, “access CiviMail”). It also requires you to pass in the CIVICRM_SITE_KEY which is configured
// by editing your copy of civicrm.settings.php
//
// I’d recommend setting up a dedicated account for scheduling reports with only minimal permissions.
// Once you have a username/password setup open File > Project Properties and open the Script Properties
// tab. Click ‘Add row’ link and add your setup account name (username), pass (password), key (site key).
// Save the Script Properties and then in the script editor window enter the BASE_URL below of your Civi
// installation (in Drupal this looks like http://[SITEROOT]/sites/all/modules/civicrm/bin/cron.php?.
// File > Save your script
var BASE_URL = "https://www.fubar.org/sites/all/modules/civicrm/bin/cron.php?";
// To get this script to run automatically open Resources > Current project triggers
// and slect doTasks to run as a day timer (we set reports to run between 7-8am)
// If you want to run earlier or later also adjust the RERUN_HOUR below which sets the next run time
var RERUN_HOUR = 1;
var PS = PropertiesService.getScriptProperties();
var param = PS.getProperties();
param.job = "mail_report";
// helper so we know which value is in which column
var COL = {report_id: 0,
type: 1,
last_run: 2,
next_run: 3,
format: 4,
ss_id: 5,
ss_sht: 6,
total: 7};
function onOpen(){
var ui = SpreadsheetApp.getUi();
ui.createMenu('CiviSchedule')
.addItem('Run Script', 'doTasks')
.addToUi();
}
function doTasks() {
var doc = SpreadsheetApp.getActiveSpreadsheet(); // get spreadsheet
var sheet = doc.getSheetByName("Tasks"); // get sheet
var data = sheet.getRange(3, 1, sheet.getLastRow(), COL.total).getValues(); // get values
var now = new Date(); // time now
// for each row of the sheet interate accross
for (var i = 0; i < data.length; i++){
if (data[i][COL.report_id] != ""){ // if there is instance id do something
// collect row values
var report_id = data[i][COL.report_id]
var type = data[i][COL.type];
var next_run = data[i][COL.next_run] || 0;
// check if it's time to run the report again
if (next_run < now && type != "never"){
// if it is ping the report trigger
var new_next_run = callUrl(report_id, type, {format: data[i][COL.format], ss_id: data[i][COL.ss_id], ss_sht: data[i][COL.ss_sht]} );
// ..and record when to run again
sheet.getRange(parseInt(i)+3, 3, 1, 2).setValues([[now, new_next_run]]);
}
}
}
}
function callUrl(report_id, type, optParam){
// build the url to trigger the report
param.format = optParam.format || "print";
if (optParam.ss_id && optParam.ss_sht){
// if we have a sheet name and id force csv
param.format = 'csv';
// make a search string to find our report
optParam.search_str = 'report/instance/'+report_id+'?reset=1 has:attachment is:unread';
// store our search for later
PS.setProperty('search_str_'+report_id, JSON.stringify(optParam));
// set the script to read the email run 15min later
ScriptApp.newTrigger("processInbox")
.timeBased()
.after(1 * 60 * 1000)
.create();
}
// make url
var qs = BASE_URL
for(var key in param) {
if (key.substring(0, 10) != "search_str"){
var value = param[key];
qs += key + "=" + value + "&";
}
}
qs += "instanceId="+report_id;
try {
//gg var resp = UrlFetchApp.fetch(qs); // hit the url
// now calculate when to run again
var d = new Date();
d.setHours(RERUN_HOUR);
d.setMinutes(0);
switch (type){
case "daily":
d.setDate(d.getDate() + 1);
break;
case "weekly":
d.setDate(d.getDate() + 7);
break;
case "monthly":
// Get the first Monday in the month
d.setDate(1);
d.setMonth(d.getMonth() + 1);
while (d.getDay() !== 1) {
d.setDate(d.getDate() + 1);
}
break;
}
return d;
} catch(e) {
return e.message;
}
}
function processInbox(){
var PS = PropertiesService.getScriptProperties();
var data = PS.getProperties();
for (var key in data) {
try { if (key.substring(0, 10) == "search_str"){
var param_raw = data[key];
var param = JSON.parse(param_raw);
// get last 20 message threads using serach term
var threads = GmailApp.search(param.search_str, 0, 20);
// assume last thread has our latest data
var last_thread = threads.length-1;
if (last_thread > -1){
// get message in the last thread
var msg = threads[last_thread].getMessages()[0];
// get the attachments
var attachments = msg.getAttachments();
for (var k = 0; k < attachments.length; k++) {
// get the attachment as a string
var csv_str = attachments[k].getDataAsString();
// parse string as csv
var csv = Utilities.parseCsv(csv_str);
// create destination object
var doc = SpreadsheetApp.openById(param.ss_id);
var sheet = doc.getSheetByName(param.ss_sht);
// clear any old data
sheet.clear();
// write new data
sheet.getRange(1, 1, csv.length, csv[0].length).setValues(csv);
// mark message are read and archive (you could also label or delete)
threads[last_thread].moveToArchive().markRead();
PS.deleteProperty(key);
}
}
}
} catch(e) {
SpreadsheetApp.getUi().alert('problem: ${e}');
}
}
}

at processInbox(processInbox Code:46:18)
The syntax is
at ${FUNCTION}(${FILE}:${LINE}:${COLUMN})
This would suggest that the code causing the error is elsewhere.
At file
processInbox Code
within function
processInbox
And at line
46
and at column
18
You probably have a same function name processInbox at a different file named processInbox Code. In that file, at line 46, col 18, you'll have your error.

I think that you problem may be that this line:
var data = sheet.getRange(3, 1, sheet.getLastRow(), COL.total).getValues();
should be like this:
var data = sheet.getRange(3, 1, sheet.getLastRow() - 2, COL.total).getValues();

Related

Filter Data and Remove Rows Fast while keeping Header

This is my final task. I am looking to filter data from column 17, that if anything in that column is less than 1, to remove the data in those rows. There is data that gets imported daily and their are a lot of rows that I want to filter fast and then delete those rows specified. I just want to keep the value of 1 in column 17. There is a frozen header at the top that does not move. What I have works but is extremely slow. Thank you for any help!
function filterData() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh1 = ss.getSheetByName('CancelRawData');
var colCdata = sh1.getRange(1,17,sh1.getLastRow(),1).getValues()
var filtered = colCdata.filter(String);
Logger.log(filtered);
for(let i = colCdata.length-1;i >= 0;i--){
if(colCdata[i] <1 === true){
sh1.deleteRow(i+1)
}
}
SpreadsheetApp.getUi().alert("🎉 Congratulations, your data has been filtered!", SpreadsheetApp.getUi().ButtonSet.OK);
}
In your situation, in order to reduce the process cost of your script, how about the following modifications?
Modified script 1:
In this modification, Sheets API is used. Before you use this script, please enable Sheets API at Advanced Google services.
function filterData() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh1 = ss.getSheetByName('CancelRawData');
var colCdata = sh1.getRange(1, 17, sh1.getLastRow(), 1).getValues();
var sheetId = sh1.getSheetId();
var requests = colCdata.reduce((ar, [q], i) => {
if (!isNaN(q) && q < 1) ar.push({ deleteDimension: { range: { sheetId, startIndex: i, endIndex: i + 1, dimension: "ROWS" } } });
return ar;
}, []).reverse();
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
SpreadsheetApp.getUi().alert("🎉 Congratulations, your data has been filtered!", SpreadsheetApp.getUi().ButtonSet.OK);
}
Modified script 2:
In this modification, retrieved all values are filtered, and the filtered values are put on the sheet.
function filterData() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh1 = ss.getSheetByName('CancelRawData');
var range = sh1.getDataRange();
var values = range.getValues().filter(r => isNaN(r[16]) || r[16] >= 1);
range.clearContent();
sh1.getRange(1, 1, values.length, values[0].length).setValues(values);
SpreadsheetApp.getUi().alert("🎉 Congratulations, your data has been filtered!", SpreadsheetApp.getUi().ButtonSet.OK);
}
References:
Method: spreadsheets.batchUpdate
DeleteDimensionRequest

Google Apps Script that loops through a filter and sends an e-mail with a PDF?

I have data from a questionnaire (20K rows) that I need to share with the store managers (report) of our shops (400 shops). I managed to write a script that sends a pdf of my sheet to a list of e-mail addresses. But I'm stuck on writing the loop for the filter, since I can't get the setVisibleValues(values) function to work for FilterCriteriaBuilder. The setHiddenValues(values) function works, but I can't figure out how to combine that with the loop.
Sample of my Google Sheet
See below for my current code:
/**
* Filtersheet by location
*/
function FilterSheet() {
var spreadsheet = SpreadsheetApp.getActive().getSheetByName('Data')
spreadsheet.getRange('F1').activate();
var criteria = SpreadsheetApp.newFilterCriteria()
.setHiddenValues(['Amsterdam, Rotterdam'])
.build();
spreadsheet.getFilter().setColumnFilterCriteria(6, criteria);
};
/**
* Send pdf of currentspreadsheet
*/
function SendPdf() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var spreadsheet = SpreadsheetApp.getActive().getSheetByName('Adres');
var blob = DriveApp.getFileById(ss.getId()).getAs("application/pdf");
blob.setName(ss.getName() + ".pdf");
var startRow = 2; // First row of data to process
var numRows = 2; // Number of rows to process
// Fetch the range of cells A2:B3
var dataRange = spreadsheet.getRange(startRow, 1, numRows, 2);
// Fetch values for each row in the Range.
var data = dataRange.getValues();
for (var i in data) {
var row = data[i];
var emailAddress = row[0]; // First column
var message = 'I hearby send you the overview of your data'
var subject = 'Overview of data';
MailApp.sendEmail(emailAddress, subject, message,{
attachments:[blob]});
}
}
getValues() returns the values of all range's cells no matter if they are shown or hidden.
Use a loop and isRowHiddenByFilter(rowPosition) to reap out all the filtered values. You could use Array.prototype.push to add the values to a new array or use Array.prototype.splice to modify the array holdin the values returned by getValues()
Related
How to use in Google Sheets setValue only for range of filtered rows (getRange for not hidden cells)?
I managemed to solve the problem.
This script takes a google spreadsheet with 2 sheets,one with Data and one with a combination EmailAdresses.
It sends a filtered list (filter column F) of sheet Data to the corresponding salon (location) in sheet Emailadresses (var mode email). Additionally, it has the option to "store" the pdf's in your google drive (var mode store)
*/
function construct() {
// settings:
//var mode = "store";
var mode = "email";
// get list of all salons and email
var salonList = SpreadsheetApp.getActive().getSheetByName('EmailAdressen');
// set endvar for loop
var endRow = salonList.getLastRow();
// loop trough the rows to get the Salon name and the corresponding email
for(i=1;i<=endRow;i++){
var salonName = salonList.getRange(i,2).getValue();
var email = salonList.getRange(i,1).getValue();
// create an array with all salons that should be hidden (we cant pick which one to show, so we have to go the other way around...)
var filterArray = [];
// create array with all salons to hide
for(c=1;c<=endRow;c++){
// get value from email list, check if it is not the current selected one and if so add it to the list to filter out
salonFilterName = salonList.getRange(c,2).getValue();
if(salonFilterName != salonName) {
filterArray.push(salonFilterName);
}
} // end for c
// filter the list with the array we just created
var spreadsheet = filterList(filterArray);
if(mode == "email"){
// export to PDF
var pdf = exportToPdf(spreadsheet);
// email to email address belonging to this salon
emailToAddress(email, pdf);
} // end if
if(mode == "store"){
StorePdf(spreadsheet, salonName);
}
} // end for i
return;
}
function filterList(salonNameArray) {
// select data sheet
var spreadsheet = SpreadsheetApp.getActive().getSheetByName('Data');
// first remove all existing filters to make sure we are on a clean sheet
if(spreadsheet.getFilter()){
spreadsheet.getFilter().remove();
}
// create the filter
spreadsheet.getRange('F:F').createFilter();
// set criteria for filter with array passed from construct
var criteria = SpreadsheetApp.newFilterCriteria().setHiddenValues(salonNameArray).build();
// apply filter
spreadsheet.getFilter().setColumnFilterCriteria(6, criteria);
return spreadsheet;
}
function exportToPdf(ss) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var spreadsheet = SpreadsheetApp.getActive().getSheetByName('Data');
var blob = DriveApp.getFileById(ss.getId()).getAs("application/pdf");
blob.setName(ss.getName() + ".pdf");
return blob;
}
function StorePdf(ss, salonName) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var spreadsheet = SpreadsheetApp.getActive().getSheetByName('Data');
var blob = DriveApp.getFileById(ss.getId()).getBlob();
blob.setName(salonName + "_" + Utilities.formatDate(new Date(), "GMT+1", "ddMMyyyy")+".pdf");
DriveApp.createFile(blob);
return;
}
function emailToAddress(email, pdf) {
MailApp.sendEmail(email, 'Type here the subject', 'Type here the body',{
attachments:[pdf]});
return;
}

Set the sourceRange of Data Validation to an array of values

I'm creating a social media outreach tracker. I want to create a drop-down list of the contact name. The problem is that I have two sources of names on two different sheets.
I wrote a script that pulls the names from the two different sources and combines them to a single array.
I was hoping to set the source range as that array.
Here is my code:
function setDataValid_(range, sourceRange) {
var rule = SpreadsheetApp.newDataValidation()
.requireValueInRange(sourceRange, true)
.build();
range.setDataValidation(rule);
}
function onEdit() {
var auditionsSheet = SpreadsheetApp.getActiveSpreadsheet();
var castingDirectorsTab = auditionsSheet.getSheetByName("Casting Directors");
var contactsTab = auditionsSheet.getSheetByName("Contacts");
var socialMediaOutreachTab = auditionsSheet.getSheetByName("Social Media Outreach");
var lastRowCD = castingDirectorsTab.getLastRow();
var lastRowContacts = contactsTab.getLastRow();
var activeCell = socialMediaOutreachTab.getActiveCell();
var activeColumn = activeCell.getColumn();
// get data
var castingDirectorNameData = castingDirectorsTab.getRange(2, 1, lastRowCD, 1).getValues();
var contactNameData = contactsTab.getRange(2, 1, lastRowContacts, 1).getValues();
//get name data to a single arrays
var castingDirectorName = [];
castingDirectorNameData.forEach(function(yr) {
castingDirectorName.push(yr[0]);
});
var contactName = [];
contactNameData.forEach(function(yr) {
contactName.push(yr[0]);
});
// get rid of the empty bits in the arrays
for (var x = castingDirectorName.length-1; x > 0; x--) {
if ( castingDirectorName[x][0] === undefined ) {
castingDirectorName.splice( x, 1 )
}
}
for (var x = contactName.length-1; x > 0; x--) {
if ( contactName[x][0] === undefined ) {
contactName.splice( x, 1 )
}
}
//combine two data sources for data validation
var combinedNames = [];
combinedNames.push(castingDirectorName + contactName);
Logger.log (combinedNames);
Logger.log( typeof combinedNames);
// data validation set up and build
if (activeColumn == 1 && auditionsSheet.getName() == "Social Media Outreach") {
var range = auditionsSheet.getRange(activeCell.getRow(), activeColumn +1);
var sourceRange = combinedNames;
setDataValid_(range, sourceRange)
}
}
When I enter a date in Col A on Social Media Outreach, nothing happens in Col 2.
I was using an existing working nested data validation script I have but the sourceRange pulls from a sheet based on the value in the active cell. Here is that code:
function setDataValid_(range, sourceRange) {
var rule = SpreadsheetApp.newDataValidation()
.requireValueInRange(sourceRange, true)
.build();
range.setDataValidation(rule);
}
function onEdit() {
var aSheet = SpreadsheetApp.getActiveSheet();
var aCell = aSheet.getActiveCell();
var aColumn = aCell.getColumn();
// data validation for Auditions Tab Projet Type to Project Details
if (aColumn == 9 && aSheet.getName() == 'Auditions') {
var range = aSheet.getRange(aCell.getRow(), aColumn + 1);
var sourceRange = SpreadsheetApp.getActiveSpreadsheet().getRangeByName('RefTables!' + aCell.getValue())
setDataValid_(range, sourceRange)
}
}
For this script when I select from the data validation drop-down, a new data validation comes up in the next col with the appropriate secondary data validation.
So the question is, can the source range be set to an array or do I need to put the names back into my sheet to reference a la the second script.
I've looked through the documentation and searched and can't find an answer. I'm relatively new to GAS and am not sure of all the inner workings of the data validation builder.

Script editor, automatic end time

Full disclosure, I am in no way a programmer of any kind. My library was looking for an easier way for multiple locations to add events to the public calendar.
Eventually I stumbled upon this script which I was able to adapt for our needs. However, the one change they would like is to have the end time default to 2 hours later. For example, if an event starts at 1 then the end time automatically defaults to 3.
Can anyone show me what change in the script I need to make for that to happen? Here is the test form that we use to enter the dates. Right now the end time is entered manually but I'd imagine that would have to be removed, correct?
Any help in figuring this out would be greatly appreciated.
Thanks!
//insert your google calendar ID
var calendarId = "ID-FOR-TEST-CALENDAR";
//index (starting from 1) of each column in the sheet
var titleIndex = 2;
var descriptionIndex = 3;
var startDateIndex = 4;
var endDateIndex = 5;
var googleCalendarIndex = 6;
/*
find the row where the Google Calendar Event ID is blank or null
The data of this row will be used to create a new calendar event
*/
function findRow(sheet) {
var sheet = SpreadsheetApp.getActiveSheet();
var dataRange = sheet.getDataRange();
var values = dataRange.getValues();
for (var i = 0; i < values.length; i++) {
if(values[i][googleCalendarIndex-1]=="" || values[i][googleCalendarIndex-1]==null)
newEvent(i+1);
}
};
/*
get the data of the new row by calling getSheetData() and
create a new Calendar event by calling submitToGoogleCalendar()
*/
function newEvent(row){
var sheet = SpreadsheetApp.getActiveSheet();
var eventId = submitToGoogleCalendar(getSheetData(sheet,row),null)
if(eventId!=null)
sheet.getRange(row,googleCalendarIndex,1,1).setValue(eventId);
};
/*
Store the data of a row in an Array
*/
function getSheetData(sheet,row)
{
var data = new Array();
data.title=sheet.getRange(row,titleIndex,1,1).getValue();
data.description=sheet.getRange(row,descriptionIndex,1,1).getValue();
data.startDate = sheet.getRange(row,startDateIndex,1,1).getValue();
data.endDate = sheet.getRange(row,endDateIndex,1,1).getValue();
return data;
};
/*
if a cell is edited in the sheet, get all the data of the corresponding row and
create a new calendar event (after deleting the old event) by calling submitToGoogleCalendar()
*/
function dataChanged(event){
var sheet = SpreadsheetApp.getActiveSheet();
var row = event.range.getRow();
var eventId = sheet.getRange(row,googleCalendarIndex,1,1).getValue();
var eventId = submitToGoogleCalendar(getSheetData(sheet,row),eventId)
if(eventId!=null)
sheet.getRange(row,googleCalendarIndex,1,1).setValue(eventId);
};
/*
This function creates an event in the Google Calendar and returns the calendar event ID
which is stored in the last column of the sheet
*/
function submitToGoogleCalendar(sheetData,eventId) {
// some simple validations ;-)
if(sheetData.title == "" || sheetData.startDate == "" || sheetData.startDate == null)
return null;
var cal = CalendarApp.getCalendarById(calendarId);
var start = new Date(sheetData.startDate);
var end = new Date(sheetData.endDate);
// some simple date validations
if(start > end)
return null;
var event = null;
//if eventId is null (when called by newEvent()) create a new calendar event
if(eventId==null)
{
event = cal.createEvent(sheetData.title, start, end, {
description : sheetData.description,
});
return event.getId();
}
/*
else if the eventid is not null (when called by dataChanged()), delete the calendar event
and create a new event with the modified data by calling this function again
*/
else
{
event = cal.getEventSeriesById(eventId);
event.deleteEventSeries();
return submitToGoogleCalendar(sheetData,null);
}
return event.getId();
};
Without having tried it myself, I believe you could do it like this:
function submitToGoogleCalendar(sheetData,eventId) {
var cal = CalendarApp.getCalendarById(calendarId);
var start = new Date(sheetData.startDate);
var end = new Date(sheetData.endDate);
end.setHours(start.getHours() + 2);
...where the last line is the new line added to your original script.
To your question wether the end time should be removed, I would say yes, it should be removed.

Google Drive: Xamarin - Android GetInputStream - can only be called once per Content Instance

I have the Google drive opened and the files are selectable. After clicking the confirmation button, I will have a response and call the InputStream and start reading the file. This was my code which read the InputStream.
var pendingResult = driveFile.OpenAsync(GoogleApiClient, DriveFile.ModeReadOnly, this)
.ContinueWith((resultTask) =>
{
var driveContentResults = resultTask.Result;
var driveContent = driveContentResults.DriveContents;
if (_dialog != null && _dialog.IsShowing)
_dialog.Dismiss();
if (driveContentResults.Status.IsSuccess)
{
if (FileReadyCallback != null)
{
var length = driveContent.InputStream.Length;
//var data = new byte[length];
//driveContent.InputStream.Read(data, 0, data.Length);
//FileReadyCallback(this, data);
}
//var absolutePath = FileUtilities.SaveStreamToDownloadDirectory(driveContent.InputStream, Guid.NewGuid() + ".pdf");
//SendResultToCallingActivity(absolutePath);
}
});
When the application reached to driveContent.InputStream.Length, the breakpoint showed me that the InputStream can only be called once per Content Instance. In the code, that line is my first call of the input stream.
As the error says, instead of calling driveContent.InputStream every time, make a variable named input and assign driveContent.InputStream, then use input to read bytes, get length or other methods.
var input = driveContent.InputStream
and then replace with
var length = input.Length;
and
input.Read(data, 0, data.Length);

Resources