Apps Script's spreadsheets "copyTo()" writes the range two times? - google-apps-script

Henlo!
I've been stuck on a weird bug in google apps scripts (relative to sheets) and I didn't find any response on internet anywhere.
I have a sheet with filters (~370 lines) and I'm trying to copy its values to another sheet. But when I do that, for some reason, the 370 lines get copied 2 times, and I have absolutely no idea why. Result sheet is 370 lines + one blank line + 370 lines again.
Here's the code I'm using :
var spreadsheet = SpreadsheetApp.openById(spreadsheetID);
var init_sheet = spreadsheet.getSheetByName("default");
var final_sheet = spreadsheet.insertSheet("final");
var soure_range = init_sheet.getRange("A1:K");
var target_range = final_sheet.getRange("N1:X");
soure_range.copyTo(target_range);
if anyone knows why the range gets copied 2 times, I'd be glad to get unstuck :x
EDIT :
As discussed in the comments, this bug seems to appear only when I use a sheets converted from xlsx (the function above works correctly with any other normal sheet).
Here's the convertion function I use :
function convert(xlsxID, name, parentFolder) {
var xlsxBlob = DriveApp.getFileById(xlsxID).getBlob();
var file = {
title: ,
//Which Drive Folder do you want the file to be placed in
parents: [{'id':parentFolder}],
key: 'XXXX',
value: 'XXXX',
visibility: 'PRIVATE'
};
file = Drive.Files.insert(file, xlsxBlob, {
convert: true
});
return file.getId();
}

I was able to replicate the issue. Initially I did not filter the data from the converted xlsx file.
When copying range using copyTo(destination). Use the top-left cell position for your destination range.
Sample Code:
var spreadsheet = SpreadsheetApp.openById(spreadsheetID);
var init_sheet = spreadsheet.getSheetByName("Feuil1");
var final_sheet = spreadsheet.insertSheet("final");
var soure_range = init_sheet.getRange("A1:K");
var target_range = final_sheet.getRange("N1"); //Top-left cell position
soure_range.copyTo(target_range);
Output:

const ss = SpreadsheetApp.openById(spreadsheetID);
const ish = ss.getSheetByName("default");
const fsh = ss.insertSheet("final");
const srg = ish.getRange(1,1,ish.getLastRow(),11);
const vs = srg.getValues();
const trg = fsh.getRange(1,14,vs.length,vs[0].length);
srg.copyTo(trg);

Related

How do I get the image ID of an image IN THE CELL?

I'm currently working on a code in Google Apps Script that allows a user to fill out a spreadsheet and have the spreadsheet generate printouts for a job board. I'm trying to design this in a way where the user can simply insert a logo image into a row of my Google sheet and have it replace a placeholder in my doc template.
I have found lots of answers about how you can take an image and convert it to a blob and insert it from a url or an ID, however, I can't seem to find a way to get the ID or url from the image in the cell.
Here's my code currently:
//Creates menu option on spreadsheet
function onOpen() {
const ui = SpreadsheetApp.getUi();
const menu = ui.createMenu('AutoFill Docs');
menu.addItem('Create New Docs', 'createNewGoogleDocs');
menu.addToUi();
}
//Defines where to get template and info from
function createNewGoogleDocs() {
const googleDocTemplate = DriveApp.getFileById('14MJNd37pn6D-EmNKCQzXXvxJCcOAoB3KS-TlDgZuWMI');
const destinationFolder = DriveApp.getFolderById('120Sb_CJJlmz5NzJW8W3DB4TNuC4kdD3e');
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('JobBoard');
const rows = sheet.getDataRange().getValues();
rows.forEach(function(row, index) {
if (index === 0) return;
if (row[9]) return;
const copy = googleDocTemplate.makeCopy(`${row[1]}, ${row[0]} Printout`, destinationFolder);
const doc = DocumentApp.openById(copy.getId())
const body = doc.getBody();
const friendlyDate = new Date(row[2]).toLocaleDateString();
//Replacing text
body.replaceText('{{Company}}', row[1]);
body.replaceText('{{jobTitle}}', row[0]);
body.replaceText('{{datePosted}}', friendlyDate);
body.replaceText('{{Description}}', row[3]);
body.replaceText('{{Qualifications}}', row[5]);
body.replaceText('{{Wage}}', row[4]);
body.replaceText('{{Apply}}', row[6]);
//A subfunction to handle replacing the image
function textToImage() {
var replaceTextToImage = function(body, searchText, image, width) {
var next = body.findText(searchText);
if (!next) return;
var r = next.getElement();
r.asText().setText("");
var img = r.getParent().asParagraph().insertInlineImage(0, image);
if (width && typeof width == 100) {
var w = img.getWidth();
var h = img.getHeight();
img.setWidth(width);
img.setHeight(width * h / w);
}
return next;
};
var documentId = doc;
var replaceText = "{{Upload Image}}";
var imageFileId = "### File ID of image ###"; //I don't know how to get this variable
var body = DocumentApp.openById(documentId).getBody();
var image = DriveApp.getFileById(imageFileId).getBlob();
do {
var next = replaceTextToImage(body, replaceText, image, 200);
} while (next);
}
//Close and saves new doc
doc.saveAndClose();
const url = doc.getUrl();
sheet.getRange(index + 1, 10).setValue(url)
})
}
I think what might be messing me up is that I have to loop through all my cells right now so that I can create multiple documents at once (meaning each row will have a different doc and different image ID). I'm just not sure how to work around that.
Here's the template and spreadsheet
https://docs.google.com/spreadsheets/d/1cySHogAxcUgzr0hsJoTyPZakKQkM6uIOtmyPzcMoJUM/edit?usp=sharing
https://docs.google.com/document/d/14MJNd37pn6D-EmNKCQzXXvxJCcOAoB3KS-TlDgZuWMI/edit?usp=sharing
There is a bit of an issue trying to get an image in a specific cell. There's even a Feature Request for that. This year Google released a few classes for image management but there seems to be issues when retrieving those using cellImage class.
I found a related answer (workaround) from user #Tanaike where images are retrieved from Google Sheets, converted to a Blob and inserted into a Google Doc.
Sample code provided was:
const spreadsheetId = "###"; // Google Spreadsheet ID
const res = DocsServiceApp.openBySpreadsheetId(spreadsheetId).getSheetByName("Sheet1").getImages();
console.log(res); // You can check the retrieved images at the log.
if (res.length == 0) return;
const blob = res[0].image.blob; // Here, 1st image of Sheet1 is retrieved. Of course, you can choose the image on the sheet.
let doc = DocumentApp.create("newDocName Goes_Here");
var body = doc.getBody();
var imgPDF = body.appendImage(blob);
Take into consideration that to make the above work you need to:
Install Google Apps Script library. (instructions here)
Enable Drive API.
I tested this and indeed, got the images from the given sheet and inserted them into the Google Doc specified. For some reason, running your code did not show me a newly created file from the template but you can tweak the above accordingly to your case.

Using Class CellImageBuilder to import Image into Google Sheets cell

On the 19th January 2022, the CellImageBuilder class was added to the Google Sheets Spreadsheet service.
This class now allows you to import an image into a cell in Google Sheets, previously you could only add an image above the cell.
Ive been trying to use this new class to take a URL link from a Google sheet, and then create an image in the cell next to the URL. This works perfectly fine if I can hard code the URL into the script (example below)
**function insertImageIntoCell()**
{
var sheet = SpreadsheetApp.getActiveSheet();
var url='Google_Docs_Image_URL'
let image = SpreadsheetApp.newCellImage().setSourceUrl(url).setAltTextDescription('TestImage').toBuilder().build();
SpreadsheetApp.getActive().getActiveSheet().getRange('C2').setValue(image);
}
The problem I am having is that once I create an array to iterate through the column the below script creates a valid array and posts it into the correct column and rows, but when it posts it back into the spreadsheet it only returns the URL and does not convert it into an image in a cell
**function insertImageIntoCell()**
{
var sheet = SpreadsheetApp.getActiveSheet();
var myStringArray = sheet.getRange('B2:B10');
var myStringArray = sheet.getRange('B2:B10').getValues();
//Logger.log(myStringArray)
let image = SpreadsheetApp.newCellImage().setSourceUrl(myStringArray).setAltTextDescription('test').toBuilder().build();
SpreadsheetApp.getActive().getActiveSheet().getRange('C2:C10').setValues(myStringArray);
}
Im using the followign code to create the initial table of data, this pull the file name and DownloadURL from a Google Drive location and then saves this into a sheet
/* modified from #hubgit and http://stackoverflow.com/questions/30328636/google-apps-script-count-files-in-folder
for this stackexchange question http://webapps.stackexchange.com/questions/86081/insert-image-from-google-drive-into-google-sheets by #twoodwar
*/
function listFilesInFolder(folderName) {
var sheet = SpreadsheetApp.getActiveSheet();
sheet.appendRow(["Name","URL","Image"]);
//change the folder ID below to reflect your folder's ID (look in the URL when you're in your folder)
var folder = DriveApp.getFolderById("Google_Drive_Folder");
var contents = folder.getFiles();
let image=[];
var cnt = 0;
var file;
while (contents.hasNext()) {
var file = contents.next();
cnt++;
data = [
file.getName(),
file.getDownloadUrl(),
];
sheet.appendRow(data);
};
};
I am looking for the script to refresh the file information from Google Drive into sheets, then to save the image into a cell, it now appears that this functionality exists, but Im not able to get it to take an array of URL's
Suggestion
Perhaps you can try this sample implementation below.
Sample Tweaked Script
function listFilesInFolder(folderName){
var sheet = SpreadsheetApp.getActiveSheet();
sheet.appendRow(["Name","URL","Image"]);
//change the folder ID below to reflect your folder's ID (look in the URL when you're in your folder)
var folder = DriveApp.getFolderById("DRIVE_FOLDER_ID");
var contents = folder.getFiles();
let image=[];
var cnt = 0;
var file;
while (contents.hasNext()) {
var file = contents.next();
cnt++;
data = [
file.getName(),
file.getDownloadUrl(),
];
sheet.appendRow(data);
};
insertImageIntoCell(); //Insert the images on column C
};
function insertImageIntoCell(){
var sheet = SpreadsheetApp.getActiveSheet();
var row = 1;
sheet.getDataRange().getValues().forEach(url =>{
if(url[1] == "URL")return row += 1;
let image = SpreadsheetApp.newCellImage().setSourceUrl(url[1]).setAltTextDescription('TestImage').toBuilder().build();
SpreadsheetApp.getActive().getActiveSheet().getRange('C'+row).setValue(image);
row += 1;
});
}
Sample Drive Folder with sample images
Sample Result:
After running the function listFilesInFolder:
Update:
This issue is filed. Add a star to this issue for Google developers to prioritize fixing this.
Issue:
setValues() is NOT working with CellImage, while setValue() does.
If/when it starts working, You need to convert each value to cellImage using map :
function insertImagesIntoCell() {
const sheet = SpreadsheetApp.getActiveSheet(),
range = sheet.getRange('B2:B10');
range.setValues(
range.getValues().map(url => [
SpreadsheetApp.newCellImage()
.setSourceUrl(url[0])
.build(),
])
);
}
For anyone struggling with importing images from Google Drive you should know that you have to set the "Sharing" setting on every individual file for CellImageBuilder to work properly.
Like this:
const imageFileUrl = imageFolder.getFilesByName(filename).next()
.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW)
.getDownloadUrl();
const cellImage = SpreadsheetApp.newCellImage().setSourceUrl(imageFileUrl).build();
Additionally, there appears to be a rate limit on the drive download URLs, causing the '.build()' function to fail randomly on a valid URL. Retries might be necessary.
Also, the .toBuilder() call on CellImageBuilder is completely redundant.

Use google apps script to extract text from pdf file found at web address and and insert it into a Google Sheet

In the example below I left the folder and ss blank.
Idea is to retrieve the number after the text "Emerging Markets (" found in the file at the url specified in the code and then insert it into cell b2 in the google sheet specified.
Not getting any errors, but code is not working. Would appreciate your help. Novice here.
Thanks!
const FOLDER_ID = ""; //Folder ID of all PDFs
const SS = "";//The spreadsheet ID
const SHEET = "MSCI";//The sheet tab name
function OpenFile() {
var url = "https://www.yardeni.com/pub/mscipe.pdf";
var blob = UrlFetchApp.fetch(url).getBlob();
var resource = {
title: blob.getName(),
mimeType: blob.getContentType()
};
// Enable the Advanced Drive API Service
var file = Drive.Files.insert(resource, blob, {ocr: true, ocrLanguage: "en"});
// Extract Text from PDF file
var doc = DocumentApp.openById(file.id);
var text = doc.getBody().getText();
return text;
const identifier = {
start: `Emerging Markets (`,
start_include: false,
end: `)`,
end_include: false
};
let results = getDocItems(docID, identifier);
return results;
}
function importToSpreadsheet(results){
const sheet = SpreadsheetApp.openById(SS).getSheetByName(SHEET);
var cell = sheet.getRange("B2");
cell.setValue(results);
}
I see two functions: OpenFile() and importToSpreadsheet(results), but I see no lines where the functions are called.
Just a guess. Perhaps you need to add at the end of your code this line:
importToSpreadsheet(OpenFile());
Update
The OpenFile() function gets you all the text. If you need only the part of the text between 'Emerging Markets (' and ')' you can cut it out this way:
var text = OpenFile(); // all text
var part = text.split('Emerging Markets (')[1].split(')')[0]; // a part between 'Emerging Markets (' and ')'
importToSpreadsheet(part); // put the part in the cell
The lines from const identifier = {... to ...return results; are redundant. Probably they were taken from another sample and don't belong this code.

Google Apps Script: Use getFolderById for multiple variables depending on value

I think I have a very simple issue with Google Apps Script, but I already tried to google the solution for 1.5hrs without success. I guess I search for the wrong terms.
Here my code:
function folderLocations(){
var folder = {
Michael: '1bz9wIBRcRN2V-xxxxxxxxxx',
Chris: '1AEKHiI8iZKjHs-xxxxxxxxxx',
Steve: '1TD8iwjcbR7K5dN-xxxxxxxxxx',
};
return folder;
}
function createNewGoogleDocs() {
//ID of Google Docs Template, what sheet to use + save all values as 2D array
const googleDocTemplate = DriveApp.getFileById('xxxxxxxxxx_XznDn-i0WVtIM');
const sheet = SpreadsheetApp
.getActiveSpreadsheet()
.getSheetByName('Current Overview');
const rows = sheet.getDataRange().getValues();
//Start processing each spreadsheet row
rows.forEach(function(row, index){
//Destination folder ID (can differ from each person)
const destinationFolder = DriveApp.getFolderById(folderLocations().Chris);
// Set custom file name and create file
const copy = googleDocTemplate.makeCopy(`${row[15]} - ${row[3]} Quarterly Review` , destinationFolder);
const doc = DocumentApp.openById(copy.getId());
const body = doc.getBody();
// Replace placeholders with real values
body.replaceText('%NAME%', row[3]);
body.replaceText('%QUARTER%', row[15]);
body.replaceText('%ANSWER_1%', row[16]);
body.replaceText('%ANSWER_2%', row[17]);
[...]
doc.saveAndClose();
})
}
All working fine! BUT: What I want is to "dynamically" change the folder, depending on the value of a cell. It's not always "Chris"...:
const destinationFolder = DriveApp.getFolderById(folderLocations().Chris);
E.g.: If row[4] == Michael, then use the folder ID of "Michael". Somehow I can't get it to work to be "dynamically". 😔
I already tried all this, none working:
const destinationFolder = DriveApp.getFolderById(folderLocations().row[4]);
const destinationFolder = DriveApp.getFolderById(folderLocations(row[4]));
const destinationFolder = DriveApp.getFolderById(folderLocations().`${row[4]}`);
const destinationFolder = DriveApp.getFolderById(folderLocations().toString(row[4]));
etc.
👆🏻 I know what I try to do here is embarrassing. But I am normally not a developer and nobody at my company is familiar with Google Apps Script. That's the last bit I am missing, rest I put together myself using Google.
Thank you SOO much! 🙏🏻
You don't even need a function. Just an object is enough:
const folderLocations = {
Michael: '1bz9wIBRcRN2V-xxxxxxxxxx',
Chris: '1AEKHiI8iZKjHs-xxxxxxxxxx',
Steve: '1TD8iwjcbR7K5dN-xxxxxxxxxx',
};
var id = folderLocations['Chris'];
console.log(id); // 1AEKHiI8iZKjHs-xxxxxxxxxx
const destinationFolder = DriveApp.getFolderById(folderLocations[row[4]]);
This did the trick :
function folderLocations(person){
var folder = {
Michael: '1bz9wIBRcRN2V-xxxxxxxxxx',
Chris: '1AEKHiI8iZKjHs-xxxxxxxxxx',
Steve: '1TD8iwjcbR7K5dN-xxxxxxxxxx',
};
return folder[person];
}
...further below:
const destinationFolder = DriveApp.getFolderById(folderLocations(row[4]));

Checking if a filename exists and updating the file it in Google Script

I currently have a script which merges data from a Google Sheet into a Google Doc template. For each row of the worksheet, a new document is created using the title data from the row. The script works fine, but isn't my work. It has been passed onto me and I'm not skilled enough at Google Script to figure out what I'd like to achieve.
Ideally I wanted to know if it was possible to check when the script is run whether the document file already exists. It would do this as each document that is created uses the title data from the worksheet. If the document does exist then the data could be updated in that sheet, rather than creating a new version of it.
The script is the following
function mergeDocSheet() {
const TEMPLATE_ID = '16YfyeDjGDp-88McAtLCQQyZ1xz4QX5z';// Google Doc template ID
const SS_ID = '1C5gtJCSzHMuSz-oVWEItl2EUVRDwF5iH_'; // Google Sheet ID
const SHEET_NAME = "data"; // Google Sheet Tab name
const MAPPED = mappedDocToSheet;
const FILE_NAME = ["Titre de la formation"] // Header IDs from sheet.
docMerge(TEMPLATE_ID,SS_ID,SHEET_NAME,MAPPED, FILE_NAME);
}
function docMerge(templateID,ssID, sheetName, mapped, fileNameData, rowLen = "auto"){
//Get the Spreadsheet and sheet tab
const ss = SpreadsheetApp.openById(ssID);
const sheet = ss.getSheetByName(sheetName);
//Get number of rows to process
rowLen = (rowLen = "auto") ? getRowLen() - 1 : rowLen;
//Gets the range of data in the sheet then grabs the values of the range
const range = sheet.getRange(1,1,rowLen,sheet.getDataRange().getNumColumns());
const matrix = range.getValues();
// Searches the file mapped object and finds the corresponding number returns the column number in an array.
const fileNameRows = getFileNameRows()
//Loops through each row of the sheet grabbing the data from each row and putting it into a new doc.
for(let i = 1; i < rowLen; i++){
let row = matrix[i];
//Get the title for the file.
let fileName = buildFileName(row)
let newDoc = DriveApp.getFileById(templateID).makeCopy(fileName);
updateFileData(row, newDoc.getId());
};
function updateFileData(rowArray, doc){
//Loops through the mapped object.
mapped.forEach(function(element){
let textID = `\{\{${element.doc}\}\}`
DocumentApp.openById(doc).getBody().replaceText(textID,
rowArray[element.col]);
});
};
function buildFileName(rowArry){
let fileNameArray = fileNameRows.map(ele => rowArry[ele]);
return fileNameArray.join("_");
};
function getFileNameRows(){
//Map the column indexes from fileNameData
let fileNameLocs = fileNameData
.flatMap(name => {
return mapped.filter(element => element.sheet === name)
.map(ele => ele.col);
});
return fileNameLocs;
};
function getRowLen(){
return sheet.getDataRange().getNumRows();
};
};
Would it be possible to set up some kind of conditional, perhaps around these lines?
let newDoc = DriveApp.getFileById(templateID).makeCopy(fileName);
updateFileData(row, newDoc.getId());
I'm hoping someone can point me in the right direction with this. Any advice is much appreciated.
You can consider using searchFiles(params) to search for a specific filename with Doc type in your drive based on the search query term guidelines. Once you found all the files having the same filename, you can delete each file using setTrashed(trashed) before creating a new file using the template document
Sample Code:
//Loops through each row of the sheet grabbing the data from each row and putting it into a new doc.
for(let i = 1; i < rowLen; i++){
let row = matrix[i];
//Get the title for the file.
let fileName = buildFileName(row);
//This query parameter will search for an exact match of the filename with Doc file type
let params = "title='"+fileName+"' and mimeType = 'application/vnd.google-apps.document'"
let files = DriveApp.searchFiles(params);
while (files.hasNext()) {
//Filename exist
var file = files.next();
///Delete file
file.setTrashed(true);
}
//Create a new file
let newDoc = DriveApp.getFileById(templateID).makeCopy(fileName);
updateFileData(row, newDoc.getId());
};
In this given sample code, we will loop all files that have the exact filename and delete each file before creating a new one.
Additional References:
Google Drive Mimetypes