Created
August 2, 2020 15:22
-
-
Save northwestcoder/5008717bb8db6cf89bd0d9396763ef8c to your computer and use it in GitHub Desktop.
CreateDocFromFragments
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
This script generates a google doc from multiple 'parent/child' records. | |
It is meant to be triggered from an AppSheet.com app vis a vis a Google Sheet, e.g.: | |
1. AppSheet adds record/event to Google Sheet >> | |
2. Trigger listening to Google Sheet >> | |
3. This script invoked | |
4. your shiny google doc is generated from a parent/child appsheet app | |
*/ | |
/** | |
STEP 1: PROVIDE A FILE NAME | |
this is the final outputed document name, it will appear on the root of My Drive | |
*/ | |
outputDocumentName = "MERGE-EXAMPLE"; | |
/** | |
STEP 2: PROVIDE THIS SCRIPT WITH THE LOCATION OF THE CHILD DATA | |
the child Google Sheet that is to be queried | |
A (child) SHEET ID for document fragments, referenced by the active sheet being called here (the parent) | |
we assume this sheet has only one tab, with four columns: ChildID, header, details, image | |
*/ | |
gsheetID = "1Fcmbbsc8GDmahDiWcIvo78u8DXKP7mGNvt3yRGNMrDA"; | |
/** | |
STEP 3: PROVIDE THIS SCRIPT WITH TWO LOCATIONS FOR TWO SETS OF IMAGES | |
where are the header (parent) image and fragment (child) image folders located? | |
*/ | |
headerImagesFolder = DriveApp.getFolderById("1q5mvfnCXWQH_V1Vsqx68rgDDEW1E30nO"); | |
fragmentsImagesFolder = DriveApp.getFolderById("16528PaARV_T_3wnhjyl1rs6p8Zhck_s6"); | |
/** | |
STEP 4: PROVIDE A GOOGLE DRIVE ID FOR A TEMPLATE FILE TO GET STARTED | |
we have provided a world readable one with a header and a footer | |
this is a hacky workaround to get things like pagination | |
wondering why we didn't implement headers and footers much more than we did? | |
see here: https://issuetracker.google.com/issues/36755897 | |
we were able to "copy from a template" however. Still, it's pretty tedious work.. | |
here is the publicly shared google doc link: | |
https://docs.google.com/document/d/15essUDitFh8Hr5jHN8Hurs-2uUSIEktD6uOlrSg4vwk | |
*/ | |
templateForHeadersAndFooters = "15essUDitFh8Hr5jHN8Hurs-2uUSIEktD6uOlrSg4vwk"; | |
/** | |
STEP 5: PROVIDE COLUMN NUMBERS FOR TITLE, DETAILS, IMAGE, and PARENT/CHILD KEYS | |
this is to abtract this script so that you don't have to go digging down below in the muck | |
column numbers start at zero (0) from the left of the spreadsheet | |
*/ | |
yourParentKeyCol = 0 | |
yourParentTitleCol = 1 | |
yourParentDetailCol = 2 | |
yourParentImagePathCol = 3 | |
yourChildKeyCol = 0 | |
yourChildParentKeyCol = 1 | |
yourChildTitleCol = 2 | |
yourChildDetailCol = 3 | |
yourChildImagePathCol = 4 | |
/** | |
TECHNICALLY, YOU COULD STOP HERE AND ATTEMPT TO RUN THE SCRIPT :) | |
*/ | |
// using the above ID, let's get ranges up to 500 rows for the fragments (children) | |
docfragmentsSheet = SpreadsheetApp.openById(gsheetID).getSheets()[0]; | |
fragments = docfragmentsSheet.getRange('A2:H500'); | |
// begin formatting styles | |
var parentParagraphStyle = {}; | |
parentParagraphStyle[DocumentApp.Attribute.FONT_SIZE] = 11; | |
parentParagraphStyle[DocumentApp.Attribute.FONT_FAMILY] = 'Open Sans'; | |
var fragmentParagraphStyle = {}; | |
fragmentParagraphStyle[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT] = DocumentApp.HorizontalAlignment.LEFT; | |
fragmentParagraphStyle[DocumentApp.Attribute.FONT_FAMILY] = 'Calibri'; | |
fragmentParagraphStyle[DocumentApp.Attribute.FONT_SIZE] = 10; | |
fragmentParagraphStyle[DocumentApp.Attribute.BOLD] = false; | |
fragmentParagraphStyle[DocumentApp.Attribute.INDENT_FIRST_LINE] = 50; | |
fragmentParagraphStyle[DocumentApp.Attribute.INDENT_START] = 50; | |
fragmentParagraphStyle[DocumentApp.Attribute.INDENT_END] = 50; | |
var fragmentHeaderStyle = {}; | |
fragmentHeaderStyle[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT] = DocumentApp.HorizontalAlignment.CENTER; | |
var fragmentImageStyle = {}; | |
fragmentImageStyle[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT] = DocumentApp.HorizontalAlignment.CENTER; | |
fragmentImageStyle[DocumentApp.Attribute.INDENT_FIRST_LINE] = 50; | |
fragmentImageStyle[DocumentApp.Attribute.INDENT_START] = 50; | |
fragmentImageStyle[DocumentApp.Attribute.INDENT_END] = 50; | |
fragmentImageStyle[DocumentApp.Attribute.MARGIN_LEFT] = 50; | |
var pageHeaderStyle = {} | |
pageHeaderStyle[DocumentApp.Attribute.MARGIN_TOP] = 0.0; | |
// end formatting styles | |
// begin main function "onChange" | |
// this is the function you would add to your App Script Trigger | |
function onChange(e) { | |
// the google doc we are going to create, at the top level of your drive | |
newDoc = DocumentApp.create(outputDocumentName); | |
// we use a world readable template to copy the header and footer, this part is pretty janky. | |
finalDoc = addHeaderFooterFromTemplate(newDoc); | |
// in fact does it even work? We are skeptical | |
// the actively called sheet (the parent), we assume it only has one tab, with four columns extracted: ID, header, details, image | |
var mergeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0]; | |
var range = mergeSpreadsheet.getRange('A2:H500'); | |
range.activate(); | |
var rows = range.getDisplayValues(); | |
var docrows = [] | |
for (var i = 0; i < rows.length ; i++){ | |
// if the first column is not empty let's grab that row | |
if(rows[i][0].length > 0) | |
{ | |
docrows.push(rows[i]); | |
} | |
} | |
for (var parentrow = 0; parentrow < docrows.length; ++parentrow ) { | |
body = finalDoc.getActiveSection(); | |
// Append a section header paragraph. | |
var header = body.appendParagraph(docrows[parentrow][yourParentTitleCol]); | |
header.setHeading(DocumentApp.ParagraphHeading.HEADING2); | |
// Get the image specified by column D ("Image") | |
var rowImage = headerImagesFolder.getFilesByName(docrows[parentrow][yourParentImagePathCol].split("/")[1]).next(); | |
var getimageFileID = getFileByName(rowImage); | |
var insertable = DriveApp.getFileById(getimageFileID.id).getBlob(); | |
var finalImage = body.appendImage(insertable); | |
finalImage.setHeight(finalImage.getHeight()/2.0); | |
finalImage.setWidth(finalImage.getWidth()/2.0); | |
// Append a section body paragraph from the parent/header info | |
var thisParagraph = body.appendParagraph("\r\r"); | |
thisParagraph.appendText(docrows[parentrow][yourParentDetailCol]); | |
thisParagraph.appendText("\r\r"); | |
thisParagraph.setAttributes(parentParagraphStyle); | |
// now let's run through the possible children of each parent row | |
fragments.activate(); | |
fragmentRows = fragments.getDisplayValues(); | |
// this is a brute force loop - super wasteful - comparing the parent ID to the child ID | |
// because we didn't have time to learn the sheets query subsystem | |
for (var row = 0; row < fragmentRows.length; ++row) { | |
if (docrows[parentrow][yourParentKeyCol] == fragmentRows[row][yourChildParentKeyCol]) { | |
var fragmentTitle = body.appendParagraph(fragmentRows[row][yourChildTitleCol]); | |
fragmentTitle.setHeading(DocumentApp.ParagraphHeading.HEADING4); | |
fragmentTitle.setAttributes(fragmentHeaderStyle); | |
var fragmentBody = body.appendParagraph(fragmentRows[row][yourChildDetailCol]); | |
fragmentBody.appendText("\r\r"); | |
fragmentBody.setAttributes(fragmentParagraphStyle); | |
// this next line says, "get image column from appsheet data, parse away the "_Document_Files" folder | |
// so that we have just a filename to search with, then call getFilesByName with this filename, | |
// this returns a drive ID so that we can insert the image as a blob into our final Google Doc | |
var rowImage = fragmentsImagesFolder.getFilesByName(fragmentRows[row][yourChildImagePathCol].split("/")[1]).next(); | |
var getimageFileID = getFileByName(rowImage); | |
var insertable = DriveApp.getFileById(getimageFileID.id).getBlob(); | |
// interestingly we need to append the image into the paragraph fragment, not the overall body | |
// else it does not get indented correctly. | |
var imageParagraph = body.appendParagraph(""); | |
var finalImage = imageParagraph.appendInlineImage(insertable); | |
imageParagraph.setAttributes(fragmentImageStyle); | |
// we also decided to cut the size of any child image to 50% of original | |
finalImage.setHeight(finalImage.getHeight()/2.0); | |
finalImage.setWidth(finalImage.getWidth()/2.0); | |
} | |
} | |
// final spaces, spread things out a bit | |
finalSpaces = body.appendParagraph("\r\r\r"); | |
} | |
} | |
// helper function to copy headers and footers from a template | |
function addHeaderFooterFromTemplate(document){ | |
var body = document.getBody(); | |
var headerSection = document.addHeader(); | |
var footerSection = document.addFooter(); | |
var headerFooterTemplateDoc = DocumentApp.openById(templateForHeadersAndFooters); | |
var headerTemplate = headerFooterTemplateDoc.getHeader(); | |
var footerTemplate = headerFooterTemplateDoc.getFooter(); | |
var headerParagraphs = headerTemplate.getParagraphs(); | |
var footerParagraphs = footerTemplate.getParagraphs(); | |
var bodyParagraphsAttributes = headerFooterTemplateDoc.getBody().getAttributes(); | |
var bodyParagraphs = headerFooterTemplateDoc.getBody().getParagraphs(); | |
body.setMarginTop(headerFooterTemplateDoc.getBody().getMarginTop()); | |
body.setMarginBottom(headerFooterTemplateDoc.getBody().getMarginBottom()); | |
body.setMarginLeft(headerFooterTemplateDoc.getBody().getMarginLeft()); | |
body.setMarginRight(headerFooterTemplateDoc.getBody().getMarginRight()); | |
body.setAttributes(bodyParagraphsAttributes); | |
for (i = 0; i < headerParagraphs.length; ++i) { | |
headerSection.appendParagraph(headerParagraphs[i].copy()); | |
headerSection.setAttributes(pageHeaderStyle); | |
} | |
for (i = 0; i < footerParagraphs.length; ++i) { | |
footerSection.appendParagraph(footerParagraphs[i].copy()); | |
footerSection.setAttributes(footerParagraphs[i].getAttributes()); | |
} | |
for (i = 0; i < bodyParagraphs.length; ++i) { | |
body.appendParagraph(bodyParagraphs[i].copy()); | |
} | |
return document; | |
} | |
// helper function called above, you should not have to change this | |
// finding ID's by filename can result in more than one file found, | |
// this function tries to deal with that | |
function getFileByName(fileName, fileInFolder){ | |
var filecount = 0; | |
var dupFileArray = []; | |
var folderID = ""; | |
var files = DriveApp.getFilesByName(fileName); | |
while(files.hasNext()){ | |
var file = files.next(); | |
dupFileArray.push(file.getId()); | |
filecount++; | |
}; | |
if(filecount > 1){ | |
if(typeof fileInFolder === 'undefined'){ | |
folderID = {"id":false,"error":"More than one file with name: "+fileName+". \nTry adding the file's folder name as a reference in Argument 2 of this function."} | |
}else{ | |
//iterate through list of files with the same name | |
for(fl = 0; fl < dupFileArray.length; fl++){ | |
var activeFile = DriveApp.getFileById(dupFileArray[fl]); | |
var folders = activeFile.getParents(); | |
var folder = "" | |
var foldercount = 0; | |
//Get the folder name for each file | |
while(folders.hasNext()){ | |
folder = folders.next().getName(); | |
foldercount++; | |
}; | |
if(folder === fileInFolder && foldercount > 1){ | |
folderID = {"id":false,"error":"There is more than one parent folder: "+fileInFolder+" for file "+fileName} | |
}; | |
if(folder === fileInFolder){ | |
folderID = {"id":dupFileArray[fl],"error":false}; | |
}else{ | |
folderID = {"id":false,"error":"There are multiple files named: "+fileName+". \nBut none of them are in folder, "+fileInFolder} | |
}; | |
}; | |
}; | |
}else if(filecount === 0){ | |
folderID = {"id":false,"error":"No file in your drive exists with name: "+fileName}; | |
}else{ //IF there is only 1 file with fileName | |
folderID = {"id":dupFileArray[0],"error":false}; | |
}; | |
return folderID; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment