retrieve google forms edit urls and maintain existing data structure - google-apps-script

I am interested in making some alterations to a snippet of the code that allows you to record URLs to edit Google Form's responses.
What I wish to achieve is a repeatable automated process of receiving those URLs upon form submit. However, one condition that I want to follow is to have those URLs pasted in the first column of the spreadsheet, maintaining the integrity of existing data in the following columns.
I think it is the only viable option for me, as the form is still in the development stage, and may receive more variables as the time progresses.
I have attempted:
Manually inserting a column in the Responses' Google Sheet and then try to setup the assignEditUrls{} function, but it yields no results.
Using a one time mini-function to insert the left most column, and then run the code. Also produced no results, unless it was included in the main function, which then inserts the leftmost column every time there is a submission.
Here's that:
function insertLeftMostColumn(){
//insert a column before the first one
sheet.insertColumnBefore(1);
//rename the header for the new column
var cell = sheet.getRange("A1");
cell.setValue("columnName");
}
This is the function.
function assignEditUrls() {
var form = FormApp.openById('1cg7bGRQjsv91sSCjYCwNJyoB3wN_MZ_9raV3tP3v1MA');
//enter form ID here
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Form Responses 1');
var lastColumn = sheet.getLastColumn(); // This logs the value of the very last column of this sheet (without the values)
//Change the sheet name as appropriate
var data = sheet.getDataRange().getValues();
var urlCol = 5;
var responses = form.getResponses();
var timestamps = [], urls = [], resultUrls = [];
for (var i = 0; i < responses.length; i++) {
timestamps.push(responses[i].getTimestamp().setMilliseconds(0));
urls.push(responses[i].getEditResponseUrl());
}
for (var j = 1; j < data.length; j++) {
resultUrls.push([data[j][0]?urls[timestamps.indexOf(data[j][0].setMilliseconds(0))]:'']);
}
sheet.getRange(2, urlCol, resultUrls.length).setValues(resultUrls);
}
current result
expected result

If my understanding is right, you want to know how to insert the edit URLs in the first column instead of the last one.
If so, you need to implement some small modifications in your code:
Insert an empty column into the sheet, e.g. with the function insertLeftMostColumn(), as you intended above. Keep in mind that if you want to run the function only once, it needs to be called separately from assignEditUrls(), so you have to define sheet one more time within the function
urlCol is the column number, where the URLs shall be pasted. To change the column to the first one, modify var urlCol = 5; to var urlCol = 1;
data[j][0]?urls[timestamps.indexOf(data[j][0] tries to find timestamps in the first column.
Given that you inserted an empty column and the timestamps have been shifted to the second column - you need to change the code to data[j][1]?urls[timestamps.indexOf(data[j][1];
If you want the new URLs to be inserted every time upon form submit, you need to incorporate into your assignEditUrls() function a Form Submit trigger. Here you can find information about how to incorporate installable triggers.

Related

How to use a cell value as a sheet name in Google Apps Script

I'm trying to create a script that copies rows from one sheet to another. The core of this is something I've done before using GetSheetByName() to name the source and target sheets, but for this particular project there are a lot of target sheets, so I need the target sheet names to be variable and based on a String that will be in the final cell of each row.
To complicate matters, that final cell will be using a VLOOKUP formula to get the relevant sheet name based on the data in the row.
I know I've been able to use cell references as a sheet reference in a formula natively in Google Sheets using the INDIRECT function, but I haven't found an equivalent for Google Apps Script.
Any help would be greatly appreciated.
Thanks.
EDIT
Assuming that the last cell of each row is in the same column and you want to get the last cell of every row, you could use getRange(rage).getValue() to get all the data within the specified range or getDataRange().getValue() to get all the data within a Sheet. Both will return an array which you can loop through to access the data.
Sample Code:
function getRowLastCell(){
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.getSheetByName("Sheet1");
var data=sheet.getDataRange().getValues(); //get all data from Sheet1
var array = new Array();
for(var i = 0; i < data.length; i++) {
for(var j = 0; j < data[i].length; j++) {
if(j == data[i].length-1){ //check the last cell for each row.
array.push(data[i][j]); //save to array
}
}
}
}
In my example, I looped through the array and added condition that will check the last element of each row then save the content to an array.
From there, you can loop through the newly populated array and access each element to do operations.
Reference
GetDataRange()
GetRange()
If I understand correctly, what you want is to copy the values of each rows of a sheet into their respective destination sheet which can be determined by the last column of each row.
I tried replicating the scenario on my end and came up with this code:
function myFunction(){
let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("test");
let range = sheet.getRange(1,1,sheet.getLastRow(),sheet.getLastColumn())
for(var i=1;i<=range.getLastRow();i++){
let sourceval = sheet.getRange(i,1,1,range.getLastColumn());
let destname = sheet.getRange(i,range.getLastColumn()).getValue();
let destsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(destname);
sourceval.copyTo(destsheet.getRange(destsheet.getLastRow()+1, 1));
}
}
Snippet of sample spreadsheet
This may not be the exact code that you are looking for. If you have questions, feel free to comment.

How to edit an imported Google sheet using ImportRange?

I used =QUERY(IMPORTRANGE..) to import data from Sheet 1 to Sheet 2 for Column A, B & C. I have to take note in Column D of Sheet 2 for each entry imported. However, for any new one added, the note (for the previous ones) in Column D stays in the same cells. For example, the formula is in A2, so the new data will be added to A2,B2 & C2. The note is in D2. When a new one is imported, the previous one moved to A3, B3 & C3. However, the note is still in D2.
Is there any way to make those notes to move to the next row automatically when a new entry is added?
Here are the files the data has to be imported to and from: https://drive.google.com/drive/folders/1wbOfW9PbSfJbTBv_CwXOTiyyN_LBTiFq?usp=sharing
If my understanding is correct, you want to accomplish the following:
Import data from one spreadsheet to another using IMPORTRANGE.
Add notes manually to a column in your destination spreadsheet.
When a new row is imported to the destination spreadsheet and make previously imported data, the notes should move too.
To achieve that, you would need to keep track of which note belongs to which row of imported data. Both sets of data should be somehow attached. Considering that you have a timestamp in column A, and that this timestamp is probably unique for each row, this timestamp could be used to attach both (if that's not possible, I'd propose adding another column that will be used to identify each row without ambiguity, via some kind of id).
At this point, I would consider using Google Apps Script to accomplish your needs. With this tool, you could develop the functionality that =QUERY(IMPORTRANGE(...)) is providing right now, and you could use other Apps Script tools to reach the desired outcome. Two tools could be specially necessary to accomplish this:
onEdit triggers, to keep track of when the different spreadsheets are edited and make the appropriate changes if that's the case (basically, copying data from one spreadsheet to another).
Properties Service, to store the information about which note is attached to which row of data.
You could do something on the following lines:
Install two edit triggers, (1) one that will fire a function when the source spreadsheet is edited, and (2) another one that will fire when the destination spreadsheet is edited (a simple trigger cannot be used because you have to reference files to which your spreadsheet might not be bound). You can do this manually or programmatically.
Create a function that, for each note that is added to the destination sheet (in this code sample, that's in column D, please change according to your preferences), stores a key-value pair where the key is the value in column A (which should uniquely identify a row of data) and value is the note. This will be used later for the script to know where each note belongs to:
function storeNotes(e) {
var scriptProperties = PropertiesService.getScriptProperties();
var cell = e.range;
var sheet = cell.getSheet();
var rowIndex = cell.getRow();
var column = cell.getColumn();
var noteColumn = 4; // The column where notes are written, change accordingly
// Check whether correct sheet, column and row is edited:
if (column == noteColumn && rowIndex > 1 && sheet.getName() == "Destination") {
var row = sheet.getRange(rowIndex, 1, 1, sheet.getLastColumn()).getValues()[0];
scriptProperties.setProperty(row[0], row[noteColumn - 1]); // Store property to script properties
}
}
Create a function that, every time the source spreadsheet is edited, will delete all content in the destination spreadsheet and copy the data from the source. Then, it will look at the script properties that were store and, using this information, it will write the notes to the appropriate rows (because I see you only want to copy/paste some of the columns, in this sample some of the columns - the ones whose index is in columnsToDelete - are not copied/pasted, you can change this easily to your preferences):
function copyData(e) {
var range = e.range;
var origin = range.getSheet();
var row = range.getRow();
if (origin.getName() == "Origin" && row > 1) { // Check if edited sheet is called "Origin" and edited row is not a header.
var dest = SpreadsheetApp.openById("your-destination-spreadsheet-id").getSheetByName("Destination");
var firstRow = 2;
var firstCol = 1;
var numRows = origin.getLastRow() - 1;
var numCols = origin.getLastColumn();
var values = origin.getRange(firstRow, firstCol, numRows, numCols).getValues();
// Removing some of the columns to get copied/pasted (in this case B and D):
var columnsToDelete = [1, 3];
values = values.map(function(row) {
for (var i = row.length; i > 0; i--) {
for (var j = 0; j < columnsToDelete.length; j++) {
if (i == columnsToDelete[j]) {
row.splice(i, 1);
}
}
}
return row;
})
// Copying content from source to destination:
var firstRowDest = 2;
var firstColDest = 1;
var numRowsDest = values.length;
var numColsDest = values[0].length;
var noteColumn = 4;
var currentValues = dest.getDataRange().getValues();
if (currentValues.length > 1) dest.deleteRows(2, dest.getLastRow() - 1);
var importedRange = dest.getRange(firstRowDest, firstColDest, numRowsDest, numColsDest);
importedRange.setValues(values);
// Writing notes stored in Properties in the appropriate rows:
var properties = PropertiesService.getScriptProperties().getProperties();
for (var i = 0; i < values.length; i++) {
for (var key in properties) {
if (key == values[i][0]) {
dest.getRange(i + 2, noteColumn).setValue(properties[key])
}
}
}
}
}
Notes:
All these functions should be in the same script if you want all both functions to use Properties.
In this sample, the sheet with source data is called Origin and the sheet where it is copied is called Destination (from what I understood, they are in different spreadsheets).
In this simplified example, columns A, B, E from source sheet get copied to columns A, B, C of the destination sheet, and notes are added to column D. Please change this to fit your case by modifying the corresponding indexes.
I hope this is of any help.
Thank you everyone for helping me, especially Lamblichus & user11982798. I recently noticed that importrange will import data to the destination in the same order as that of the source file. Before I sorted the data based on the timestamp in descending order so the new entry was always on the first row. If I changed it to ascending order, the new one is added to the last row, so the note/comment order will not be affected.
Is it possible to update the note/comment in the destination file back to the source one?
If the note is string please try to put in D2 like this:
=ARRAYFORMULA(if(row(A2:A) = max(arrayformula(if(ISBLANK(A2:A),0,row(A2:A)))),"Your Note", ""))
This will automatically place your note to last row of data

Sheets function to add content to another sheet

I've been making slow but steady progress on this app that creates the daily bulletin for the school where I teach.
Data is submitted by staff via a form, and is then naturally in a sheet. I already created a script to purge old data from the sheet, thanks in part to help I've gotten here. An additional script orders content on the data sheet by bulletin category, creates a copy of a template sheet, names it by the desired date, puts the date at the top. That's about as far as I've gotten. It also adds the first category heading by default, which is mostly a test.
What I'm attempting to do now is loop through each row of the data sheet to determine if any of the three date columns contains the desired date (entered via a dialog box earlier in the script). If any of them match today's date, we then will check to see if the current category and the category in the row are the same. If they are not, we change the current category and add a new heading to the bulletin sheet. If they are the same, we get the announcement itself and add that to the bulletin sheet. I suspect I'll use embedded functions for these two purposes.
Right now I'm stuck on the loop portion. Again, this should cycle through each row of the data sheet. There are three columns containing the dates (C, D, E). If I can get it to recognize date matches from one of the cells in this range, I can move forward with the rest.
function writeBulletin() {
//get the bulletin date
var bullSheet = todayDay;
//make the bulletin sheet active
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.setActiveSheet(ss.getSheetByName(todayDate));
//set var for needed sheets
var responses = ss.getSheetByName("Form Responses 1")
var bulletin = ss.getSheetByName(todayDate)
//get the date from the sheet title and apply it to the date range
var dateCell = bulletin.getRange(3,1);
var sheetDate = bulletin.getName();
dateCell.setValue(sheetDate);
//works
//Now we start building the bulletin
//currentDataRow is a reference to the Responses sheet. Used in later for loop
var currentDataRow = 2;
var currentBulletinRow = 11;
var catCurrent = "01 Administration";
var catCurrentSS=catCurrent.substring(3,30);
var lastRow = responses.getLastRow(); //get last row of data sheet
var lastBull = bulletin.getLastRow(); //get last row of bulletin sheet
var nextBullRow = lastBull+2;
var testOutput = bulletin.getRange(6,3);
var nextBullItem = bulletin.getRange(nextBullRow,1);
nextBullItem.setValue(catCurrentSS);
//testOutput.setValue("dude"); //this works
if(responses.getRange(2,3).getValue()==todayDate) {
testOutput.setValue("dude");
}
//bulletin.getRange(2,3).setValue("dude"); //test row
for(var i = 2; i<=lastRow; i++) {
if(5>3) {
//if(responses.getRange(i,3).getValue()==sheetDate||responses.getRange(i,4).getValue()==sheetDate||responses.getRange(i,5).getValue()==sheetDate){
//bulletin.getRange(nextBullRow,3).setValue("dude");//works
bulletin.getRange(nextBullRow,1).setValue(responses.getRange(i,7).getValue());
nextBullRow+=2;
}
}
}
I did notice that my loop condition statement had a reversed inequality sign; however, fixing this did not seem to help.
jdv: Good point. fixed it now
Aside from the issue of repeatedly interacting with the Spreadsheet interface (the alternative being to read values from the Spreadsheet once, then work with the resulting javascript Array object), the issue is that you are comparing a Range object with a String:
var sheetDate = bulletin.getName();
...
if(responses.getRange(i, 3) == sheetDate || ..... ) {
This will not work :) You need to access the value of the Range:
if(responses.getRange(i, 3).getValue() == sheetDate || ... ) {
edit: as mentioned in comments, the values in these responses cells are interpreted as Date objects. Date comparisons are fun, because you get to play with time zones and/or format strings. I recommend avoiding needing to use dates in this manner, especially when starting out with scripts.
One possible fix for this new issue is to use the value from dateCell.getValue() after calling SpreadsheetApp.flush() (to ensure the writing of sheetDate is performed first). This will let the spreadsheet do the nasty work making the correct date:
dateCell.setValue(sheetDate);
SpreadsheetApp.flush();
// Construct the proper Date object from the sheetDate value
var compareDate = dateCell.getValue();
...
for(var i = 2; i <= lastRow; ++i) {
// Read into an array [[ 0: elem#(i,3), 1: elem#(i,4), 2: elem#(i,5), 3: elem#(i,6), 4: elem#(i,7) ]]
var row = responses.getRange(i, 3, 1, 5).getValues();
if(row[0][0] == compareDate || row[0][1] == compareDate || row[0][2] == compareDate) {
...

Google App Script Catch Row Number

I have looked everywhere and seen the related questions on this board, but I am really stuck.
I need to capture the actual row value of a Sheet at is iterates through a for loop based on an if statement.
In other parts of the code, I have to create a copy of a Form's response sheet and move it to a folder. I am trying to log the file id of the new file that is being created to a sheet that already exists (and in which the scripts are being called from a menu).
I can get the data and the values and iterate through them and see everything in the Logs. And based on what I saw in other similar questions and such, I tried the getActiveRange().getRow() part - but it only works for the exact row that the cursor is on when the form is open. I need to be able to dynamically set the ActiveRow variable based on the row that the script is currently looking at as it goes through the for loop.
I can not use the for loop variable as my current row (j+1), because the if statement is only looking for a string value "NOT FOUND" in the column - so the "j" variable value would not necessarily match the row value to update.
Thank you so much for any assistance!!
// Get the active spreadsheet and the active sheet
var ssEXD = SpreadsheetApp.getActive().getSheetByName('FormData');
var lastRowEXD = ssEXD.getLastRow();
var lastColumnEXD = ssEXD.getLastColumn();
var formIDValues = ssEXD.getSheetValues(2, 1, lastRowEXD-1, lastColumnEXD);
//Loop through the returned values
for(var j = 0; j < formIDValues.length; j++) {
//if NOT FOUND is true, do stuff
if (formIDValues[j][6] == "NOT_FOUND") {
//need this variable to be the exact row of the sheet value data
var ActiveRow = ssEXD.getActiveRange().getRow();
//I need to use the ActiveRow in the getActiveRange to update the sheet with some information.
ssEXD.getRange(ActiveRow,6,1).setValue("testing");
}
}
If I'm reading your question correctly your data has a column names on the first row and data begins on the second row.
So I think this will accomplish what you want.
var ssEXD = SpreadsheetApp.getActive().getSheetByName('FormData');
var lastRowEXD = ssEXD.getLastRow();
var lastColumnEXD = ssEXD.getLastColumn();
var dataR = ssEXD.getDataRange();
var dataA = dataR.getValues();
for(var j = 1; j < dataA.length; j++)
{
if (dataA[j][6] == "NOT_FOUND")
{
dataA[j][6] = "testing";
}
}
dataR.setValues(dataA);
var ActiveRow = j + 2;
Once an array(formIDValues) has been extracted from sheet, the array has no more relation to that sheet. One must adjust the index of the item being worked on in the array to the row number in the spreadsheet.
j - the array index - starts at zero which is the second row in this spreadsheet, therefore j + 2
Your concern about the j variable is unwarranted. j is incremented for every pass through the loop whether or not any test has been passed.

Custom script returned array overwrite non-empty cells

Is it possible to make a script returning an array of values that will overwrite non-empty cells?
For example: I have a list of tasks with the following fields: 'task-name, estimated time as in '3w'. The 3rd column is calculated with a custom function that translate the '3w' to 3*5=15 (in days). The custom script takes a range and returns an array of results for the given array of estimated-times from the 2nd column.
All works well until I decide to take a row and move it to re-order the list of tasks. Once I move that row, lets say, one row above, the script complains about an error because the moved row contains a non-empty value where it used be automatically computed by the custom-script. To correct it I need to clear the value of that row at the 3rd column and the script returns to calculate as before.
There should be a way to tell the script to overwrite. Is there?
Thanks,
Erez
Use the isBlank() function to check whether the specified cell in empty and the clear() function in order to clear the value in the cell and provide a new one. Here is a small sample:
function myFunction(){
var ss = SpreadsheetApp.getActiveSheet();
var range = ss.getDataRange();
var maxCol = range.getLastColumn();
var maxRow = range.getLastRow();
for (var i=0; i<maxRow; i++)
{
for (var j=0; j<maxCol; j++)
{
if(!ss.getRange(i+1, j+1).isBlank())
{
ss.getRange(i+1, j+1).clear();
}
}
}
}