Weird problem with appendParagraph method - google-apps-script

I have created a form, used by some users at my school, that uploads a document and creates a new formatted document with all the uploads, which can be images or other docs. At this moment I am having problems with one .docx file that contains some lists, paragraphs and 2 images inside paragraphs. The part of the code I am using is:
...
// Insert Google doc
function insertGdoc(vdoc,IDgdoc,asignat)
{
var Gdoc=DocumentApp.openById(IDgdoc).getBody();
var vdoc_id=vdoc.getId();
var docBody=vdoc.getBody();
docBody.appendPageBreak();
docBody.appendParagraph("Documento de "+asignat).setBold(false);
docBody.appendHorizontalRule();
var insertaBody = DocumentApp.openById(IDgdoc).getActiveSection();
var numElements = insertaBody.getNumChildren();
for( var jj = 0; jj < numElements; ++jj )
{
var element = insertaBody.getChild(jj).copy();
var type = element.getType();
try {
if( type == DocumentApp.ElementType.PARAGRAPH )
{
docBody.appendParagraph(element.asParagraph());
}
else if( type == DocumentApp.ElementType.TABLE )
docBody.appendTable(element);
else if( type == DocumentApp.ElementType.HORIZONTAL_RULE)
docBody.appendHorizontalRule();
else if( type == DocumentApp.ElementType.PAGE_BREAK)
docBody.appendPageBreak();
else if( type == DocumentApp.ElementType.LIST_ITEM )
{
docBody.appendListItem(element);
var glyphType = element.getGlyphType();
element.setGlyphType(glyphType);
}
}
catch (e)
{
Logger.log(e);
}
}
}
// Insert MS Word doc
function insertDoc(vdoc,IDdoc,asignat)
{
var docx = DriveApp.getFileById(IDdoc);
var blob=docx.getBlob();
var newDoc = Drive.newFile();
var file=Drive.Files.insert(newDoc,blob,{convert:true});
insertGdoc(vdoc,file.id,asignat);
}
...
After many attempts, I discovered that the elements that are causing the problems are both pictures. By using the try..catch I can avoid the error by not handling the pictures, but the final document is incomplete. I have also tried to use Utilities.sleep to give the server more time to perform actions and even to close and reopen the document, with no change or worse results.
Another option I discovered here in stackoverflow was to try appending child elements from the paragraph, differentiating if they are pictures, which could copy the first of my pictures (although with size and position changed from the original) but not the second one.
Which is most annoying is that this failure happens if I use the form to do all the process, but if I try the operation manually from the console it works and the whole source document with both pictures is copied to the final doc.
Thank you very much in advance if you can give me any advice.
Rafael

Related

How to deal with if statement when one Form Response has no response in Google Script?

I'm trying to create a script that sends an email when someone submits a google form. The form includes an optional file upload that the script will then attach to the email as a pdf.
The issue I'm facing is how to ignore the process that creates the attachment if the response is empty.
Sample code below
function getIdFrom(url) {
var id = '';
var parts = url.split(
/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/
);
if (url.indexOf('?id=') >= 0) {
id = parts[6].split('=')[1].replace('&usp', '');
return id;
} else {
id = parts[5].split('/');
var sortArr = id.sort(function (a, b) {
return b.length - a.length;
});
id = sortArr[0];
return id; //returns google doc id.
}
}
function onFormSubmit(response) {
var link = response.namedValues['Upload file'];
if (typeof link !== "undefined" && link.length > 0) { // I think it's here that's the issue
var uploadFileId = getIdFrom(link[0]);
var uploadFile = DriveApp.getFileById(uploadFileId);
var uploadFileType = (function () {
if (uploadFile.getMimeType().includes('image')) {
return uploadFile.getMimeType();
} else {
return 'application/pdf';
}
};
var attachArr = [uploadFile.getAs(uploadFileType)];
}
// etc etc send email.
}
Works fine if the user submits a form with an uploaded file.
However if the form is submitted without entering anything in the "Upload File" question, I'm getting a "TypeError: Cannot read property 'split' of undefined" at the getIdFrom(url) function I assume because it's still trying to pass link through getIdFrom() even though it shouldn't because it's undefined.
Weirdly it works perfectly fine when I use the two test inputs I have, one of which 'Upload File' exists but is empty and the other it doesn't exist at all.
I'm not sure what I'm missing here.
Also I have no doubt it's a messy way to do things but I'm getting there.
response.namedValues['Upload file'] is an object
even if it's empty it will have at least the length of >0
Workaround
Modify your if statement to
if (link[0].length > 0) {
...
}

Appending template to Google Docs header/footer not working when testing as Add-on

I have built a tool in google scripts that is currently set up as a bound script to a google docs file. The goal of the tool is to pull in templates from other created docs files from a custom form input. The code works as is as a bound script, but I am trying to port that code over to create an Add-on for users within my organization. The issue right now is that when trying to insert the template elements into the header or footer of the clean test documents, the code throws an error when attempting to append the elements to the saying Cannot call method "appendParagraph" of null.
I have tried appending as a header and footer with DocumentApp.getActiveDocument().getHeader().appendParagraph. This method works when I am using the script as a bound script and it works as expected. When I try to append the paragraph (or other element) to the body instead of the header or footer while testing as an Add-on the elements are appended without issue. The only time I am getting this error is when I try to append the elements to either the header or footer.
function examplefunction_( docLink, locPaste ) {
var srcBody = DocumentApp.openById( docLink )
.getBody();
var srcNumChild = srcBody.getNumChildren();
var activeBody = DocumentApp.getActiveDocument()
.getBody();
var activeHead = DocumentApp.getActiveDocument()
.getHeader();
var activeFoot = DocumentApp.getActiveDocument()
.getFooter();
var a =0;
if ( locPaste == 'Body') {
var activeLoc = activeBody;
}
else if (locPaste == 'Header') {
var activeLoc = activeHead;
}
else if (locPaste == 'Footer') {
var activeLoc = activeFoot;
}
for (var i = 0; i<srcNumChild; i++) {
if (srcBody.getChild(i).getType() == DocumentApp.ElementType.PARAGRAPH) {
var srcText = srcBody.getChild(i).copy();
activeLoc.appendParagraph(srcText);
}
else if (srcBody.getChild(i).getType() == DocumentApp.ElementType.TABLE) {
var srcTable = srcBody.getTables();
var copiedTable = srcTable[a].copy()
a = a + 1;
activeLoc.appendTable(copiedTable);
}
else if (srcBody.getChild(i).getType() == DocumentApp.ElementType.LIST_ITEM) {
var srcList = srcBody.getChild(i).getText();
var listAtt = srcBody.getChild(i).getAttributes();
var nestLvl = srcBody.getChild(i).getNestingLevel();
activeLoc.appendListItem(srcList).setAttributes(listAtt).setNestingLevel(nestLvl);
}
else {
Logger.log("Could not get element: " + i);
}
}
}
I expected the elements to be appended to the headers and footers from the template without error like when run as a bounded script. The actual result while being run kills the process with error "Cannot call method appendParagraph of null." at the line: activeLoc.appendParagraph(srcText);
You cannot append a paragraph to a header or footer, if your document does not have a header or a footer.
Add to the beginning of your function:
if(!DocumentApp.getActiveDocument().getHeader()){
DocumentApp.openById(docLink).addHeader();
}
Same for the footer.

Change link attached to an InlineDrawing object

This question was cross-posted to Web Applications: Help me find my Element in a Google Doc so I can act on it in script?
Here is my Google Doc:
https://docs.google.com/document/d/1TgzOIq0g4DyDefmJIEnqycDITIx_2GNQkpdxMGwtqB8/edit?usp=sharing
You can see a button in it titled ENTER NEW NOTE.
I have been successful at rolling through the elements of the doc to find the table and to replace txt in those areas as needed. But the button here needs to have the URL changed, and I cannot figure out how to do it.
This URL seems to give an idea, but I cannot turn it into my answer since I don't quite understand. Mail merge: can't append images from template
Would someone help me with this to the point of showing the actual code, because I have tried to edit the many examples found about elements, setURL and looking to parent, etc. I just end up with a mess.
I am calling the script from a Google Sheet, to wok on a BUNCH of Google Docs. (I will be running through the spreadsheet to get URL's for the next doc to have it's URL replaced.
Here is as close as I believe I have gotten:
function getDocElements() {
var doc = DocumentApp.openByUrl("https://docs.google.com/document/d/1TgzOIq0g4DyDefmJIEnqycDITIx_2GNQkpdxMGwtqB8/edit?usp=sharing"),
body = doc.getBody(),
numElements = doc.getNumChildren(),
elements = [];
for (var i = 0; i < numElements; ++i){
var element = doc.getChild(i),
type = element.getType();
// daURL = element.getURL();
// Look for child elements within the paragraph. Inline Drawings are children.
// if(element.asParagraph().getNumChildren() !=0 && element.asParagraph().getChild(0).getType() == DocumentApp.ElementType.INLINE_DRAWING) {
var drawingRange = body.findElement(DocumentApp.ElementType.INLINE_DRAWING);
while (drawingRange != null) {
var element = drawingRange.getElement();
var drawingElement = element.asInlineDrawing();
//drawingElement.removeFromParent();
drawingElement.setURL("http://www.google.com");
drawingRange = body.findElement(DocumentApp.ElementType.INLINE_DRAWING);
}
// For whatever reason, drawings don't have their own methods in the InlineDrawing class. This bit copies and adds it to the bottom of the doc.
//var drawing = element.asParagraph().copy();
//body.appendParagraph(drawing);
}
Logger.log(i + " : "+type);
}
Here is my newest iteration that shows in the logs the elements, including the inLineDrawing I want to change...
===========
function getDocElement() {
var doc = DocumentApp.openByUrl("https://docs.google.com/document/d/1TgzOIq0g4DyDefmJIEnqycDITIx_2GNQkpdxMGwtqB8/edit?usp=sharing"),
body = doc.getBody(),
numElements = doc.getNumChildren(),
elements = [];
for (var i = 0; i < numElements; ++i){
var element = doc.getChild(i),
type = element.getType();
// daURL = element.getURL();
Logger.log(i + " : " + numElements + " : "+ type + " " + element);
// Search through the page elements. Paragraphs are top-level, which is why I start with those.
if( type == DocumentApp.ElementType.PARAGRAPH ){
// Look for child elements within the paragraph. Inline Drawings are children.
if(element.asParagraph().getNumChildren() !=0 && element.asParagraph().getChild(0).getType() == DocumentApp.ElementType.INLINE_DRAWING) {
//element.getParent().setLinkUrl("http://www.google.com");
Logger.log(element.asParagraph().getChild(0).getType() + " : " + element.getAttributes());
// For whatever reason, drawings don't have their own methods in the InlineDrawing class. This bit copies and adds it to the bottom of the doc.
var drawing = element.asParagraph().copy();
//body.appendParagraph(drawing);
// body.appendParagraph();
if(element.getParent() !=''){
//element.asParagraph().appendHorizontalRule();
//element.editAsText().appendText("text");
// element.getParent().insertHorizontalRule(0);
}
}
}
}
}
I'm not sure why the setLinkUrl() is not available for InlineDrawing 🤔
If you can replace your drawing with an image (You can download your drawing as png or svg and insert it), you will be able to use setLinkUrl
Here is an example:
function myFunction() {
var body = DocumentApp.getActiveDocument().getBody();
// All inline images as a RangeElement
var images = body.findElement(DocumentApp.ElementType.INLINE_IMAGE);
// select first image, in case your doc has more than one you'll need to loop
var element = images.getElement();
var image = element.asInlineImage();
image.setLinkUrl("www.google.com");
}
Unfortunately the Class InlineDrawing doesn't have methods to access the attached links nor any other to programmatically change it to a InlineImage1. It looks to me that you will have have to make the link changes manually.
Related Feature requests:
Issue 3367: Allow exporting InlineDrawing as an image
Issue 1054: Add ability to create and modify drawings
References
1: Answer by Henrique Abreu to Modifying a drawing using Google Apps Script

How to make a closed search in Google Docs?

I have a document where I need to find a text or word, each time i run a function the selection has to go to next if a word or text is found. If it is at the end it should take me to top in a circular way just like find option in notepad.
Is there a way to do it?
I know about findText(searchPattern, from) but I do not understand how to use it.
There are several wrappers and classes in the DocumentApp. They help to work with the contents of the file.
Class Range
Class RangeElement
Class RangeBuilder
It is necessary to understand carefully what they are responsible. In your case the code below should be work fine:
function myFunctionDoc() {
// sets the search pattern
var searchPattern = '29';
// works with current document
var document = DocumentApp.getActiveDocument();
// detects selection
var selection = document.getSelection();
if (!selection) {
if (!document.getCursor()) return;
selection = document.setSelection(document.newRange().addElement(document.getCursor().getElement()).build()).getSelection();
}
selection = selection.getRangeElements()[0];
// searches
var currentDocument = findNext(document, searchPattern, selection, function(rangeElement) {
// This is the callback body
var doc = this;
var rangeBuilder = doc.newRange();
if (rangeElement) {
rangeBuilder.addElement(rangeElement.getElement());
} else {
rangeBuilder.addElement(doc.getBody().asText(), 0, 0);
}
return doc.setSelection(rangeBuilder.build());
}.bind(document));
}
// the search engine is implemented on body.findText
function findNext(document, searchPattern, from, callback) {
var body = document.getBody();
var rangeElement = body.findText(searchPattern, from);
return callback(rangeElement);
}
It looks for the pattern. If body.findText returns undefined then it sets on top of the document.
I have a gist about the subject https://gist.github.com/oshliaer/d468759b3587cfb424348fa722765187

Google Sheets App Script Mysterious Error

I'm teaching a class and for my class I keep all of my student's marks on a google spreadsheet. On my website I would like to present information to students on an individual basis. I've created an app where it presents them with a password text box. They type in their password and then it retrieves information from the spreadsheet that is unique to them and presents it to them in a label. I've been trying to hack this all together, but it's just not working properly and I'm getting an error that I cannot diagnose. If I print out the information using Browser.msgBox() it outputs the info, but otherwise it generates an error. Why is this happening and what is the fix? Here's the code:
var pointsSheet = SpreadsheetApp.openById('1o8_f063j1jYZjFEnI_P7uAztpnEAvQ6mc3Z1_Owa69Y');
//creates and shows an app with a label and password text box
function doGet() {
var app = UiApp.createApplication().setTitle('Incomplete Challenges');
var mygrid = app.createGrid(1, 2);
mygrid.setWidget(0, 0, app.createLabel('Password:'));
mygrid.setWidget(0, 1, app.createPasswordTextBox().setName("text"));
var mybutton = app.createButton('Submit');
var submitHandler = app.createServerClickHandler('getResults');
submitHandler.addCallbackElement(mygrid);
mybutton.addClickHandler(submitHandler);
var mypanel = app.createVerticalPanel();
mypanel.add(mygrid);
mypanel.add(mybutton);
app.add(mypanel);
SpreadsheetApp.getActiveSpreadsheet().show(app);
//return app; //UNCOMMENT WHEN DEPLOYING APP
}
//obtains data based on password entered by user and outputs their info
function getResults(eventInfo) {
var app = UiApp.createApplication().setTitle('Incomplete Challenges');
var password = eventInfo.parameter.text;
var passwordCheckRange = pointsSheet.getRange("B34:C34").getValues();
if (passwordCheckRange == null) {
Browser.msgBox("Error: Range is null");
return app;
}
var name;
for(var i = 0; i < passwordCheckRange.length; i++) {
if(passwordCheckRange[i][1] == password) {
name = passwordCheckRange[i][0];
break;
}
}
var studentRecordRange = pointsSheet.getRange("B3:AY29").getValues();
var headingRange = pointsSheet.getRange("B1:AY2").getValues();
if (studentRecordRange == null) {
Browser.msgBox("Error: Range is null");
return app;
}
var requestedRecord;
for(var i = 0; i < studentRecordRange.length; i++) {
if(studentRecordRange[i][0] == name)
requestedRecord = studentRecordRange[i];
}
var stringRecord = "";
for(var i = headingRange[1].length-1; i >= 7; i--) {
if (requestedRecord[i] == "")
stringRecord += headingRange[1][i] + ": " + headingRange[0][i] + "XP" + "\\n";
}
var mygrid = app.createGrid(2, 1);
mygrid.setWidget(0, 0, app.createLabel('INCOMPLETE CHALLENGES'));
mygrid.setWidget(1, 0, app.createLabel(stringRecord));
var mypanel = app.createVerticalPanel();
mypanel.add(mygrid);
app.add(mypanel);
//Browser.msgBox(stringRecord);
return app;
}
The error that I experience is: Error encountered: An unexpected error occurred.
As you can see it's very helpful.
Line 28 it should be getActiveApplication() and not createApplication().
You cant create an application on another application. :)
Also I think line 63 it should be "<br>"; instead "\n"; along with line 68 it should be createHTML instead of createLabel
I also think that you have apply few styling css so that your app looks good. check on .setStyleAttributes in UiApp.
There are a few errors in this code, the first one -that generates the error you get - is (as mentioned in the other answer) the UiApp.createApplication() in the handler function.
You can't create an UiApp instance in a handler function, you should instead get the active instance and eventually add elements to it (using UiApp.getActiveApplication()).
You can't neither change the title of this instance. Btw, it doesn't make sense since this title will not appear as a "title" when you will be deploying this app as a webapp. It will simply show up at the top of your browser window (as a page title) as your app will occupy the whole screen and not a modal popup anymore. So if you want a title to appear in your Ui, simply add it as an HTML widget where you can choose the font size and weight (and any other CSS styles).
The other error is in the password check, you are using Browser.msgBox("Error: Range is null"); but Browser class won't work in UiApp. You should only use UiApp elements, not spreadSheetApp elements.
And, as a more general comment, I suggest you test your app directly using the .dev url (last saved version) of the app (after saving a beta version and having deployed it) so that you are in the "real" use condition and have a pertinent pov on the result.