Randomize rows when google doc is opened - google-apps-script

I am trying to create a petition with a google form and doc. I've found an app script that will take the responses from the form and input them into a table on the doc. To help reduce the bias early respondents may face, I am trying to develop a script that will randomize the rows of the table every time the document is opened. Trying to scramble the signatures so that the same signatures aren't always at the top (can't be targeted).
Can anyone help me with this?
Below is the code I am using to populate the table from the form
//Since there could be a bunch of people submitting, we lock the script with each execution
//with a 30 second timeout so nothing gets overwritten
const lock = LockService.getScriptLock();
lock.waitLock(30000);
//Here we read the variables from the form submission event
const date = new Date(e.values[0]).toLocaleDateString();
//of you can use toLocaleString method if you want the time in the doc
const name = e.values[1];
const employeeNumber = e.values[2];
var form = FormApp.openById('');
const num = form.getResponses().length;
var num1 = num.toString()
//Next format those values as an array that corresponds to the table row layout
//in your Google Doc
const tableCells = [num1,name,employeeNumber]
//Next we open the letter and get its body
const letter = DocumentApp.openById('')
const body = letter.getBody();
//Next we get the first table in the doc and append an empty table row
const table = body.getTables()[0]
const tableRow = table.appendTableRow()
//Here we loop through our table cells from above and add
// a table cell to the table row for each piece of data
tableCells.forEach(function(cell, index){
let appendedCell = tableRow.appendTableCell(cell)
})
//here we save and close our letter and then release a lock
letter.saveAndClose();
lock.releaseLock();
}
Got this from a helpful vimeo tutorial by Abhishek Narula and Rebekah Modrak.

Randomize row in a google document table
First function Creates a table in a google document from a table in a spreadsheet
function creatTable() {
const style1 = {};
style1[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT]=DocumentApp.HorizontalAlignment.RIGHT;
const ss = SpreadsheetApp.openById("ssid");
const sh = ss.getSheetByName('Sheet0');
const tA = sh.getDataRange().getValues();
const doc = DocumentApp.getActiveDocument();
let body = doc.getBody();
body.clear();
let n = body.getNumChildren();
for(let i =0;i<n-1;i++) {
if(i==0) {
body.getChild(i).asText().setText('');
} else {
body.getChild(i).removeFromParent()
}
}
let table=body.appendTable(tA).setAttributes(style1);
}
Second function reads the table from the document and randomizes the rows it and reloads the table. This function actually searches through all of the children to find a table. If there is more than one table it will randomize all of them so it will need to be modified to integrate with your specific document.
This now removes the first line and then randomizes the array and then adds the first line back
function readandrandomizetable() {
const style1 = {};
style1[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT] = DocumentApp.HorizontalAlignment.RIGHT;
const doc = DocumentApp.getActiveDocument();
let body = doc.getBody();
let vs = [];
let fl = '';
[...Array.from(new Array(body.getNumChildren()).keys())].forEach(idx => {
let ch = body.getChild(idx);
if (ch.getType() == DocumentApp.ElementType.TABLE) {
Logger.log(`This is the index I want: ${idx}`);
let tbl = ch.asTable();
[...Array.from(new Array(tbl.getNumRows()).keys())].forEach(ridx => {
let s = tbl.getRow(ridx).getText().split('\n');
//Logger.log(JSON.stringify(s));
vs.push(s);
});
Logger.log(JSON.stringify(vs));
fl = vs.shift();
vs.sort((a, b) => {
vA = Math.floor(Math.random() * 100);
vB = Math.floor(Math.random() * 100);
return vA - vB;
});
vs.unshift(fl);
}
});
Logger.log(JSON.stringify(vs));
body.clear();
let n = body.getNumChildren();
for (let i = 0; i < n - 1; i++) {
if (i == 0) {
body.getChild(i).asText().setText('');
} else {
body.getChild(i).removeFromParent()
}
}
body.appendTable(vs).setAttributes(style1);
}

Related

How to get daily number of emails under a label in gmail to google sheets?

Editing my question as requested.
I need to get the daily number of emails under a label in gmail to google sheets, in a way that I get the date and the number of emails per day (not including answers on the thread, just the first email received to be counted).
Sample:
Not my code, credit to #Suyash Gandhi
I tried using Suyash's code:
function CountEmail() {
var label = GmailApp.getUserLabelByName("LabelName");
var labelname = label.getName();
var mails = label.getThreads();
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var date = new Date();
sheet.appendRow([labelname,date,mails.length]);
}
But it gives me the error "TypeError: Cannot read properties of null (reading 'getName')
CountEmail # CountEmail.gs:4"
How can I make it work?
I am very new to this and don't fully understand how scripts can be edited, appreciate any help.
Provides Date,Count,list of subjects and sorts the output list by date and displays in on a sheet
function CountEmail() {
const ts = GmailApp.search("label: ");//update label
let obj = {pA:[]}
ts.forEach(t => {
let s = t.getFirstMessageSubject();
let dt = t.getMessages()[0].getDate();
let p = `${dt.getFullYear()}~${dt.getMonth()+1}~${dt.getDate()}`
if(!obj.hasOwnProperty(p)) {
obj[p] = {date:p,subject:[s],count:1};
obj.pA.push(p);
} else {
obj[p].subject.push(s);
obj[p].count = Number(obj[p].count) + 1;
}
});
let o = obj.pA.map(p => {
return [obj[p].date,obj[p].count,obj[p].subject.join('\n')];
});
o.sort((a,b) => {
let ta = a[0].split('~');
let tb = b[0].split('~');
let va = new Date(ta[0],ta[1],ta[2]);
let vb = new Date(tb[0],tb[1],tb[2]);
return va - vb;
})
o.unshift(['Date','Count','Subjects'])
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Sheet0');//update sheet
sh.clearContents();
sh.getRange(1,1,o.length,o[0].length).setValues(o);
}

Function Keeps Looping After Last Row

I am using the following code to fill a Google Docs template with data pulled from a spreadsheet.
function createBulkMembershipCards() {
const template = DriveApp.getFileById("--------");
const docFolder = DriveApp.getFolderById("----------");
const pdfFolder = DriveApp.getFolderById("----------------");
const libroSoci = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("LibroSoci");
const data = libroSoci.getRange(352, 1, libroSoci.getLastRow()-1,19).getDisplayValues();
data.forEach(row => {
createMembershipCard (row[3],row[4],row[0],row[1], row[6],template,docFolder,pdfFolder);
});
}
function createMembershipCard (name,surname,msnumber,timestamp, email,template,docFolder,pdfFolder) {
const file = template.makeCopy(docFolder);
const docFile = DocumentApp.openById(file.getId());
const body = docFile.getBody();
body.replaceText("{name}", name);
body.replaceText("{surname}", surname);
body.replaceText("{msnumber}", msnumber);
body.replaceText("{timestamp}", timestamp);
body.replaceText("{email}", "<<"+email+">>");
docFile.saveAndClose();
docFile.setName(msnumber+" "+name+" "+surname);
const pdfBlob = docFile.getAs(MimeType.PDF);
pdfFolder.createFile(pdfBlob).setName(msnumber+" "+name+" "+surname);
}
I do not understand why, even though I used "getLastRow", the function keeps going on indefinitely after the last populated row.
Please not that I put 352 as the starting row because I want to extract data from that row on.
Is anyone able to help?
Thank you!
Let's say getLastRow() returns 1000. Then you are getting 999 rows. What you want is 1000-351 rows.
Change
const data = libroSoci.getRange(352, 1, libroSoci.getLastRow()-1,19).getDisplayValues();
To
const data = libroSoci.getRange(352, 1, libroSoci.getLastRow()-351,19).getDisplayValues();

Is there a way to list the array values in one cell by adding one onto another

I'm making a google sheets app function that checks if the ID in one sheet can be associated with any of the patients (each patient receives an ID), then add it into their file (a single cell next to their name).
I'm at a point where I can get the info into the cell with .copyValuesToRange, but the problem is that all the values are copied into the cell one after another. The desired effect is that I get all values separated by ", ".
Here's my code:
function newCaseIn() {
let app = SpreadsheetApp;
let dest = app.getActiveSpreadsheet().getSheetByName("Baza Danych");
let form = app.getActiveSpreadsheet().getSheetByName("Zgloszenia");
for (let i = 2; i < 200; i++) {
if (form.getRange(i, 2).getValue()) {
while (true) {
form.getRange(i, 3).copyValuesToRange(0, 9, 9, 2, 2);
}
}
}
}
And here's how the database looks: Database FormSubmissions
NOTE: There is a form that comes down to the second sheet to allow people submit new patient files to a specified ID
It could be something like this:
function main() {
let ss = SpreadsheetApp.getActiveSpreadsheet();
let dest = ss.getSheetByName("Baza Danych");
let form = ss.getSheetByName("Zgloszenia");
// get all data from the form
var source_data = form.getDataRange().getValues();
source_data.shift(); // remove the header
// make the data object
// in: 2d array [[date,id1,doc], [date,id2,doc], ...]
// out: object {id1: [doc, doc, doc], id2: [doc, doc], ...}
var source_obj = {};
while(source_data.length) {
let [date, id, doc] = source_data.shift();
try { source_obj[id].push(doc) } catch(e) { source_obj[id] = [doc] }
}
// get all data from the dest sheet
var dest_data = dest.getDataRange().getValues();
// make a new table from the dest data and the object
var table = [];
while (dest_data.length) {
let row = dest_data.shift();
let id = row[0];
let docs = source_obj[id]; // get docs from the object
if (docs) row[8] = docs.join(', ');
table.push(row);
}
// clear the dest sheet and put the new table
dest.clearContents();
dest.getRange(1,1,table.length,table[0].length).setValues(table);
}
Update
The code from above clears existed docs in the cells of column 9 and fills it with docs from the form sheet (for relevant IDs).
If the dest sheet already has some docs in the column 9 and you want to add new docs you have to change the last loop this way:
// make a new table from the dest data and the object
var table = [];
while (dest_data.length) {
let row = dest_data.shift();
let id = row[0];
let docs = source_obj[id]; // get docs from the object
if (docs) {
let old_docs = row[8];
row[8] = docs.join(', ');
if (old_docs != '') row[8] = old_docs + ', ' + row[8];
}
table.push(row);
}

Search column for text, and use array list to insert text in another cell

Current Problem:
I am trying to have my script look through column A in the example below, and if a cell contains a certain word, to have text inserted into the cell next to it in column B.
I need to have the script find column heads by their name instead of hard references (example, find column called "Ingredient").
I'm unsure how to have my script insert text adjacent to a cell if there is a match
I made my script just with apple so far, as I think I can take it from there.
I was hoping to use a script that would use some form of array list, so that if a cell contained a word from that list it would insert text into an adjacent cell.
However. I didn't quite know how to do that so I think what I was able to research may suffice. I couldn't find documentation on how the whenTextContains filter works, so I think match is the next best answer?
At the moment my code works, but it's not placing the snack in the right place (ex. placing it in B1 instead of B2 for apple).
What I've Tried:
Made a simple code that works but needs hard references to column/row references
Tried a few different for iterations but it doesn't seem to work
Example of current sheet:
Example of desired result:
Simple Code that works but needs hard reference to columns/rows:
function snack() {
const ws = SpreadsheetApp.getActive().getSheetByName('Sheet1');
var indgredientRange = ws.getRange(2,1);
var iValues = indgredientRange.getValues();
for (var i in iValues){
if(iValues[i][0].match("apple")!=null){
ws.getRange(2,2).setValue('apple pie');
}//from the question https://stackoverflow.com/questions/11467892/if-text-contains-quartal-or-year-then-do-something-google-docs and answer of #Kuba Orlik
}
My Code:
function snack() {
const ws = SpreadsheetApp.getActive().getSheetByName('Sheet1');
//search headers (converted to sep. function to return row & column)
const [tfIRow, tfICol] = getLocationOfText(ws,'Ingredient');
const [tfSnackRow,tfSnackCol] = getLocationOfText(ws,'Snack');
const [tfRatingRow,tfRatingCol] = getLocationOfText (ws,'Rating');
//snack arrays below
let applesnack = ['apple pie']
let flowersnack = ['flower seeds']
let orangesnack = ['orange slices']
let baconsnack = ['bacon pie']
let ewsnack = ['ew']
function getLocationOfText(sheet, text) {
const tf = sheet.createTextFinder(text);
tf.matchEntireCell(true).matchCase(false);
const tfNext = tf.findNext();
return [tfNext.getRow(), tfNext.getColumn()]
}//end of getLocationofText function
//action script below:
var indgredientRange = ws.getRange(tfIRow,tfICol,ws.getLastRow(),ws.getLastColumn());
var iValues = indgredientRange.getValues();
for (var i in iValues){
if(iValues[i][0].match("apple")!=null){
ws.getRange(tfSnackRow,tfSnackCol).setValue(applesnack);
}
}
}//end of snack function
Raw Data:
Ingredient
Snack
Rating
apple
flower
orange
bacon
lemon
apple bitters
bacon bits
References:
Filteria Criteria
Using Column Header Variables
Searching column script example
Same functionality no text finder required
function snack() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Sheet0');
const [hA, ...vs] = sh.getDataRange().getValues();
let col = {};
let idx = {};
hA.forEach((h, i) => { col[h] = i + 1; idx[h] = i; });//build col and idx so that headers can move anywhere
const snack = { 'apple': 'apple pie', 'flower': 'flower seeds', 'orange': 'orange slices', 'bacon': 'bacon pie','lemon':'ew'};//relates search keys to snack strings
const keys = Object.keys(snack);
vs.forEach((r, i) => { //loop rows
keys.forEach((k,j) => { //loop through keys
if(~r[idx['Ingredient']].toString().indexOf(k)) {
sh.getRange(i + 2, col['Snack']).setValue(sh.getRange( i + 2, col['Snack']).getValue() + '\n' + snack[k]);//displays appropriate keys for each match and it supports multiple matches on the same string
}
});
});
}
Sheet0 (after):
Reference:
Object
Additional Question:
If you wish to move the below data to the third column then change this:
sh.getRange(i + 2, col['Snack']).setValue(sh.getRange( i + 2, col['Snack']).getValue() + '\n' + snack[k]);
to this:
sh.getRange(i + 2, 3).setValue(sh.getRange( i + 2, col['Snack']).getValue() + '\n' + snack[k]);
The result of createTextFinder....findNext() is a Range that have getRow() and getColumn() method that you can use it to determine the position of a column header. Since you've mentioned here that "if a cell contains a certain word, to have text inserted into the cell next to it", you only need to search for the Ingredient column and add +1 to the result of getColumn().
Try this code below:
function snacks(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName("Sheet1");
var ing = sh.createTextFinder("Ingredient").findNext(); // find Ingredient Column
var ingCol = sh.getRange(ing.getRow()+1, ing.getColumn(), sh.getLastRow()-1, 1); //get the range of the content of Ingredient column
var snacks = ['apple', 'flower', 'orange', 'bacon', 'lemon']; //search strings
for(var i = 0; i < snacks.length; i++){
// loop each search string and use it to search to Ingredient range.
var search = ingCol.createTextFinder(".*"+snacks[i]+".*").useRegularExpression(true).findAll();
if(search){ //if found set the appropriate value
search.forEach(el => {
var val = "";
if(snacks[i] == "apple"){
val = "apple pie";
}else if(snacks[i] == "flower"){
val = "flower seeds";
}else if(snacks[i] == "orange"){
val = "orange slices";
}else if(snacks[i] == "bacon"){
val = "bacon pie";
}else if(snacks[i] == "lemon"){
val = "ew"
}
sh.getRange(el.getRow(), el.getColumn()+1).setValue(val)// set value
})
}
}
}
Before:
After:
Reference:
Class Range
Class TextFinder

exceeded maximum execution time, google sheet, how to improve it?

Good morning, everyone,
I come to see you because after having fought hard to make my google script work, on the execution page I see that my script works, however on my google sheet I have an error: "Exceeded maximum execution time". I've seen on the internet that for custom google script functions only leave 30 sec of execution, I'm not sure how to do that? add code to tweak this feature? I confess that I didn't understand the difference between a custom function and a google app script but I know that there is 6 minutes of execution time for scripts... here is an extract of my code:
// Standard functions to call the spreadsheet sheet and activesheet
function GetPipedriveDeals2() {
let ss = SpreadsheetApp.getActiveSpreadsheet();
let sheets = ss.getSheets();
let sheet = ss.getActiveSheet();
//the way the url is build next step is to iterate between the end because api only allows a fixed number of calls (100) this way i can slowly fill the sheet.
let url = "https://laptop.pipedrive.com/v1/products:(id)?start=";
let limit = "&limit=500";
//let filter = "&filter_id=64";
let pipeline = 1; // put a pipeline id specific to your PipeDrive setup
let start = 1;
//let end = start+50;
let token = "&api_token=XXXXXXXXXXXXXXXXXXXXXXXX"
let response = UrlFetchApp.fetch(url+start+limit+token); //
let dataAll = JSON.parse(response.getContentText());
let dataSet = dataAll;
//let prices = prices;
//create array where the data should be put
let rows = [], data;
for (let i = 0; i < dataSet.data.length; i++) {
data = dataSet.data[i];
rows.push([data.id,
GetPipedriveDeals4(data.id)
]);
}
Logger.log( 'function2' ,JSON.stringify(rows,null,8) ); // Log transformed data
return rows;
}
// Standard functions to call the spreadsheet sheet and activesheet
function GetPipedriveDeals4(idNew) {
let ss = SpreadsheetApp.getActiveSpreadsheet();
let sheets = ss.getSheets();
let sheet = ss.getActiveSheet();
//the way the url is build next step is to iterate between the end because api only allows a fixed number of calls (100) this way i can slowly fill the sheet.
let url = "https://laptop.pipedrive.com/v1/products/"+idNew+"/deals:(id,d93b458adf4bf84fefb6dbce477fe77cdf9de675)?start=";
let limit = "&limit=500";
//let filter = "&filter_id=64";
let pipeline = 1; // put a pipeline id specific to your PipeDrive setup
let start = 1;
//let end = start+50;
let token = "&api_token=XXXXXXXXXXXXXXXXXXXXXX"
let response = UrlFetchApp.fetch(url+start+limit+token); //
let dataAll = JSON.parse(response.getContentText());
let dataSet = dataAll;
//Logger.log(dataSet)
//let prices = prices;
//create array where the data should be put
let rows = [], data;
if(dataSet.data === null )return
else {
for (let i = 0; i < dataSet.data.length; i++) {
data = dataSet.data[i];
let idNew = data.id;
rows.push([data.id, data['d93b458adf4bf84fefb6dbce477fe77cdf9de675']]);
}
Logger.log( 'function4', JSON.stringify(rows,null,2) ); // Log transformed data
return rows;
}
}
Thank you all in advance.
EDIT : --------------------------FOR EACH LOOP----------------------------
function getPipedriveDeals(start = 0,apiRequestLimit = 39) {
console.log("start="+start);
//Make the initial request to get the ids you need for the details.
var idsListRequest = "https://laptop.pipedrive.com/v1/products:(id)?start=";
var limit = "&limit=" + apiRequestLimit;
var token = "&api_token=XXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
var response = UrlFetchApp.fetch(idsListRequest + start + limit + token);
var data = JSON.parse(response.getContentText()).data;
//For every id in the response, construct a url (the detail url) and push to a list of requests
var requests = [];
console.log("data="+data);
data.forEach(function(product) {
var productDetailUrl = "https://laptop.pipedrive.com/v1/products/" + product.id + "/deals:(id,d93b458adf4bf84fefb6dbce477fe77cdf9de675)?start=";
requests.push(productDetailUrl + start + limit + token)
});
//With the list of detail request urls, make one call to UrlFetchApp.fetchAll(requests)
var responses = UrlFetchApp.fetchAll(requests);
return [responses,JSON.parse(responses[0].getContentText()).additional_data.pagination.more_items_in_collection];
}
function getAllDeals(){
var allResponses = [];
for(var i = 0; i<500; ){
var deals = getPipedriveDeals(start=i);
deals[0].forEach((response)=>{allResponses.push(response)});
if(deals[1]){
// If there are more items sleep for 1000 milliseconds
Utilities.sleep(1000);
i+=39;
}
else{
console.log("No more items in collection.");
break;
}
}
console.log("allResponses="+allResponses);
return allResponses;
}
About the weird behavior in the logs :
-------------------------------EDIT Number2----------------------------------
Until now when I executed =getPipedriveDeals()it return False (like I was saying in my comment) and also when I try to remove additional_data.pagination.more_items_in_collection (Because it's not very usefull for me) I don't know why but the script won't work anymore, and I have some difficulties to select the data I want to return.
You can use UrlFetchApp.fetchAll(requests) to save in quotas and likely a lot in script execution duration.
I have removed a few redundancies in your code to make this following example showing the use of fetchAll for your case.
Example:
function getPipedriveDeals(start = 0,apiRequestLimit = 50) {
console.log("start="+start);
//Make the initial request to get the ids you need for the details.
var idsListRequest = "https://laptop.pipedrive.com/v1/products:(id)?start=";
var limit = "&limit=" + apiRequestLimit;
var token = "&api_token=xxxxxxxxxxx";
var response = UrlFetchApp.fetch(idsListRequest + start + limit + token);
var data = JSON.parse(response.getContentText()).data;
//For every id in the response, construct a url (the detail url) and push to a list of requests
var requests = [];
console.log("data="+data);
data.forEach(function(product) {
var productDetailUrl = "https://laptop.pipedrive.com/v1/products/" + product.id + "/deals:(id,d93b458adf4bf84fefb6dbce477fe77cdf9de675)?start=";
requests.push(productDetailUrl + start + limit + token)
});
//With the list of detail request urls, make one call to UrlFetchApp.fetchAll(requests)
var responses = UrlFetchApp.fetchAll(requests);
return [responses,JSON.parse(responses[0].getContentText()).additional_data.pagination.more_items_in_collection];
}
Now suppose you have a plan that limits the quota to 50 requests per 2 seconds.
You could make the limit of requests in the fetchAll call to 50, and introduce a delay/sleep of (X) milliseconds to initiate a second request, starting with id=50 like:
function getAllDeals(){
var allResponses = [];
for(var i = 0; i<500; ){
var deals = getPipedriveDeals(start=i);
deals[0].forEach((response)=>{allResponses.push(response)});
if(deals[1]){
// If there are more items sleep for 1000 milliseconds
Utilities.sleep(1000);
i+=50;
}
else{
console.log("No more items in collection.");
break;
}
}
console.log("allResponses="+allResponses);
return allResponses;
}