Run server-side code with google chart select - google-apps-script

I have a google visualization table that I'm publishing in a web app.
Background:
I run a script that lists all the documents in a google folder in a spreadsheet. I then push that list into the google table I have published in the web app.
The need:
I want to manage those that same list of documents directly from the web app. I want to be able to to move the document from one folder to another when I select the applicable row on the table.
Two things I have accomplished:
I have a script that will move the document to a specific folder on
the google drive using it's doc id.
I have an event listener on the table so that when you click a row and
then click the delete icon, you get a prompt that asks, "are sure
you want to archive [enter document name]?" When I click ok, I get my
test prompt that says "document archived". When I click no, I get my
test prompt that says "request cancelled". So from this, I know I
have the appropriate code (at least that's how it seems).
What I'm struggling with:
I can't seem to get the codes above to work together. The event listener is providing me the url of the document which I have parsed to give me only the id. This is what I was hoping to use to get the rest of the code to run, but I think because I'm trying to interact with the server-side from the client-side, it's not working. Can anyone help me figure it out? I know that I need to use google.script.run.withSuccessHandler when running a server side script from the client side, but I don't know how it applies to this case the docid I need is being collected on table select. Any help is appreciated and I hope the above makes sense!
// Draw Dashboard
h2dashboard.bind([h2stringFilter, h2typeFilter], [h2chart]);
h2dashboard.draw(h2dataView);
google.visualization.events.addOneTimeListener(h2chart, 'ready', function() {
google.visualization.events.addListener(h2chart.getChart(), 'select', function() {
var selection = h2chart.getChart().getSelection();
var dt = h2chart.getDataTable();
// Get Value of clicked row
if (selection.length) {
var item = selection[0];
var docurl = dt.getValue(item.row, 1);
var docname = dt.getValue(item.row, 0);
var source = dt.getValue(item.row, 3);
// When button is clicked, show confirm box with value
$(document).ready(function() {
$("#hu2archive").on("click", function() {
var answer = confirm("Are you sure you want to archive " + docname + "?");
if (answer === true) {
var archive = DriveApp.getFolderById("FOLDER ID");
var docid = docurl.match(/[-\w]{25,}/); // This is where I'm grabbing the value from the row.
var doc = DriveApp.getFileById(docid);
doc.makeCopy(archive).setName(doc.getName());
source.removeFile(doc);
alert(docname + " has been archived!");
} else {
alert("Request cancelled");
}
});
});
}
});
});

I just got it! What I was having a hard time understanding was how to pass a variable from the client side to code.gs. I have only run a script in code.gs from the client side on button submit but never passed anything back.
So I ended up changing my code to the below which passes the variable I need into a successhandler where archiveDoc is the function in my code.gs and docurl is the name of the variable I need to pass from the eventlistener.
if (answer === true) { google.script.run.withSuccessHandler(onSuccess).withFailureHandler(err).archiveDoc(docurl);
I'm still new to coding so I just learned something new! So thanks Spencer Easton. I did in fact answer my own question.

Related

Automatically execute Google App Script on Creation of Google Doc or shortly thereafter

We previously had success (from help we've received here!) adding an onOpen menu item that would trigger text formatting. The back story here is that we have google doc merge templates that each have this bound app script. Once the template is generated using data from another source, the user can click the menu item to then format the text. However, as we've grown this has become cumbersome with the number of documents that are generated daily. It requires a user to authorize the script and click the buttons to ultimately format the merged text. This also causes a stopping point in business process because the user running the script is not the ultimate user of the document.
I've pasted the working code below, but I was hopeful that someone might have some ideas on how to change this script so that it ran automatically, without user intervention, after the document is created. The document creation and the data merge happen simultaneously or nearly simultaneously.
function onOpen() {
DocumentApp.getUi().createMenu('Butler')
.addItem('Format Headings', 'FormatHeadings')
.addToUi();
}
function FormatHeadings() {
handle_tags(['<req>', '</req>'], "Roboto", 14, "Bold", "#4a5356");
handle_tags(['<cit>', '</cit>'], "Roboto", 12, "Bold", "#4a5356");
handle_tags(['<con>', '</con>'], "Roboto", 12, "Bold", "#B38F00");
}
function handle_tags(tags, family, size, style, color) {
var body = DocumentApp.getActiveDocument().getBody();
var start_tag = tags[0];
var end_tag = tags[1];
var found = body.findText(start_tag);
while (found) {
var elem = found.getElement();
var start = found.getEndOffsetInclusive();
var end = body.findText(end_tag, found).getStartOffset()-1;
elem.setFontFamily(start, end, family);
elem.setFontSize(start, end, size);
elem.setForegroundColor(start, end, color);
switch (style.toLowerCase()) {
case 'bold': elem.setBold(start, end, true); break;
case 'italic': elem.setItalic(start, end, true); break;
case 'underline': elem.setUnderline(start, end, true); break;
}
found = body.findText(start_tag, found);
}
body.replaceText(start_tag, '');
body.replaceText(end_tag, '');
}
Thank you!
Just to set the right expectations, you can make Apps Script code run by triggering an event or manually running a function. We have those 2 options.
Now, you can't automatically run after the creation of a file because a file creation is not a supported event. You could however:
Create a form that requests the fileID of the new file (the file must be ready to be formatted), use the onSubmit event so when the form is submitted an Apps Script function is executed and the file is created. The onSubmit event is an installable trigger, for more details check the documentation at https://developers.google.com/apps-script/guides/triggers/events#google_forms_events.
You are using DocumentApp.getActiveDocument() you can replace that and use DocumentApp.openById() with the ID that was submitted in the form. This will automatically apply that same script to the file ID you submit in the form.

How to create Google Forms using a Google Sheets

I have created a google spreadsheet to use as questions in my google form, it's the first time i'm using the apps script, and with the help of some videos I made a code, but the values of the spreadsheet are not going to the form, so the questions remain without a title and I have no idea how to fix it.
function myForm() {
var app=SpreadsheetApp;
var spreadsheet=app.openById("1iYXYufNQ1NMUibEFfSWVvfD0vVi14cqMQNBbIok_JzM");
var sheet=spreadsheet.getSheetByName("Atividades");
var form=FormApp;
var formFinal=form.openById("1_XYQcnlpg3EssBHI7X5T2QKyhplRj3FKufyBxPrJxQs");
formFinal
.setTitle("Pesquisa de Tempo Estimado - Finalístico - Rodada 3")
.setDescription("Área temática: Finalístico \nRodada: 3")
.setConfirmationMessage("Obrigado pela participação!");
var item=formFinal.addSectionHeaderItem();
item.setTitle("Atividades");
sheet.getRange("A3:A14").getValues().map(function(elem,ind,obj){
var item2=formFinal.addTextItem();
item2.setTitle(elem[0]);
});
formFinal.setProgressBar(true);
}
I've just run your script ( of course, I've changed the file ids ), and it seems to work fine.
One thing you shouldn't forget is reloading the form page after running the script.
Additional information: I've added the below code to clear all of the items of the form so that I can run the script repeatedly.
// clear all items
var items = formFinal.getItems();
while(items.length > 0){
formFinal.deleteItem(items.pop());
}

Keep images when adding choices to Google Forms with Apps script

I'm using google scripts to add new choices to a checkbox item in a form every time the form gets submitted. Some of the existing checkbox-items have images attached that users see when filling in the form.
My problem is that every time my script gets run, it removes the images from the checkbox-items. Is there a way to keep the images attached to the form, while adding new choices to it?
(part of) my code:
// retrieve form-checkbox object
var item = form.getItems(FormApp.ItemType.CHECKBOX)
.filter(function(item){
return item.getTitle() === 'Top 5 films';
})[0].asCheckboxItem();
//make 2 lists, 'choices' with all choices in the checkbox obj
// 'existingChoicesTitles' with all the titles of the choices.
var choices = item.getChoices();
var existingChoicesTitles = choices.map(function(value){
return value.getValue();
})
//check if obj in list 'values' already exists in array 'choices, if not add to
//'choices'
for (i = 0; i < values.length; i++) {
if (existingChoicesTitles.includes(values[i]) == false){
choices.push(item.createChoice(values[i]));
}
}
//set 'choices' list as new list of choices
item.setChoices(choices);
Unfortunately there is currently no way to do that.
There is an active Feature Request for the ability to add these kind of images with the Apps Script FormApp.
Add Images to Form Items
I would suggest that you go and mark the ☆ on the issue to let Google know that you would like this functionality, and also to subscribe to updates. You might also want to add in your experience and use case in a comment.
In your case it seems that to add an item to the checkbox list, Apps Script regenerates all the checkbox items, and since FormApp doesn't support these types of images, it doesn't include them when they are regenerated.
For your use case there doesn't seem to be a practical workaround apart from simply not using images in this way if they are to be modified with Apps Script.
If you are willing to put in some extra work, you might want to implement the form as a simple web app. Then you would have almost infinite flexibility and far more functionality.
Web App example
This is a very simple example of a Web App that shows an input box and a button to send the info to the 'back-end', which in this example is Code.gs
Code.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile("index")
}
function receiveInfo(formResponse) {
Logger.log(formResponse)
}
index.html
<!DOCTYPE html>
<html>
<body>
<input type='text' id='input'></input>
<button id=main>Send Info</button>
<script>
function main(){
const input = document.getElementById('input')
const info = input.value
google.script.run.receiveInfo(info)
}
const mainButton = document.getElementById("main")
mainButton.addEventListener('click', () => main())
</script>
</body>
</html>
References
Feature Request
Web apps
Test a web app deployment
Client-to-server communication

How to create a Link that is a Get Request to a Google Web App

I have a button on a spreadsheet that I want users to click that will run a script. The script modifies some protected ranges so I published it as a web app and set it to run as me.
I found these instructions explaining what I am trying to accomplish:
You can create a function with the doGet() reserved function name in your project, and publish the project as a Web App. A link is a GET request. When you click the link, a GET request will be made to the published URL of the Web App. Then the doGet() function will run. The GET request is the "event" and the doGet() function is triggered by the GET request event. doGet() "listens" for the GET request to be made. You can pass information to the doGet(e) function by adding an "event" parameter in the parenthesis. Typically the letter "e" is used, but any letter can be used
My questions are once I have the web app url, how do I create a link that goes to the web app and runs it? Right now I am using a button on the spreadsheet and assigning a script to it. Screenshot of this: https://imgur.com/OeMlczb But this is not working.
And here is the code I turned into a web app that need to be called and ran when the button is clicked:
function confirm(){
var ui = SpreadsheetApp.getUi();
var response = ui.alert('This will submit the timesheet. Do you want to continue?', ui.ButtonSet.YES_NO);
if(response == ui.Button.NO) return;
emailGoogleSpreadsheetAsPDF();
}
/* Email Google Spreadsheet as PDF */
function emailGoogleSpreadsheetAsPDF() {
// Send the PDF of the spreadsheet to this email address
var email = "email#gmail.com";
var exclA=['Timesheet','Note','Settings','Data'];//and others
var timeS=SpreadsheetApp.getActive().getSheetByName('Timesheet')
var ss=SpreadsheetApp.getActive();
var name=ss.getRange("Timesheet!J6").getValue();//trimmed the range down to match the getValue();
var tname=ss.getRange("Timesheet!J6").getValue();
var agency=ss.getRange("Timesheet!B4").getValue();//same here
var fldr=DriveApp.getFolderById('abc123thu8h7r8888tbgyru');
var fA=[];
var today=Utilities.formatDate(new Date(),Session.getScriptTimeZone(),"MM/dd/yyyy");
var subject=Utilities.formatString('%s has Submitted Their Timesheet and Notes',name);
var body=Utilities.formatString('This was submitted on %s',today);
var shts=ss.getSheets();
SpreadsheetApp.flush();//this may not be necessary...not sure
var file=fldr.createFile(ss.getBlob().getAs('application/pdf')).setName(Utilities.formatString('%s_%s_%s_timesheet_notes.pdf', tname,agency,today));
fA.push(file)
for(var i=0;i<shts.length;i++) {
var sh=shts[i];
var name=sh.getName();
if(exclA.indexOf(name)==-1) {
sh.showSheet();
for(var j=0;j<shts.length;j++) {
if(shts[j].getName()!=name) {
shts[j].hideSheet();
}
}
SpreadsheetApp.flush();//this may not be necessary...not sure
var file=fldr.createFile(ss.getBlob().getAs('application/pdf')).setName(Utilities.formatString('%s_%s_%s_note.pdf', name,agency,today));
fA.push(file);
}
}
for(var i=0;i<shts.length;i++) {
if(exclA.indexOf(shts[i].getName())==-1) {
shts[i].showSheet();
}
}
timeS.showSheet();
GmailApp.sendEmail(email,subject,body, {attachments:fA});
for(var i=0;i<fA.length;i++) {
fA[i].setTrashed(true);
}
//CopyDataToNewFile();
}
function makeCopy() {
var ss =SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Note');
var dSheet = sheet.copyTo(SpreadsheetApp.openById(ss.getId()))
dSheet.showSheet()
};
function CopyDataToNewFile(targetSheetName,targetSsId, sourceSheetName,sourceSsId) {
var ss = SpreadsheetApp.openById('gnu84uw84nwutnst9ntrgbrn').getSheetByName('Timesheet');
var ssd = SpreadsheetApp.openById('h3487g8bg8ybw4gy8wytb').getSheetByName('Sheet1');
var therapist = ss.getRange('J6').getValues();
var thedate = ss.getRange('A10').getValues();
var theagency = ss.getRange('B4:C4').getValues();
var thepayperiod = ss.getRange('B6:C6').getValues();
var thecost = ss.getRange('E24').getValues();
var themileage = ss.getRange('E27').getValues();
ssd.getRange(ssd.getLastRow()+1,1,therapist.length,therapist[0].length).setValues(therapist);
ssd.getRange(ssd.getLastRow()+0,2,thedate.length,thedate[0].length).setValues(thedate);
ssd.getRange(ssd.getLastRow()+0,3,theagency.length,theagency[0].length).setValues(theagency);
ssd.getRange(ssd.getLastRow()+0,4,thepayperiod.length,thepayperiod[0].length).setValues(thepayperiod);
ssd.getRange(ssd.getLastRow()+0,5,thecost.length,thecost[0].length).setValues(thecost);
ssd.getRange(ssd.getLastRow()+0,6,themileage.length,themileage[0].length).setValues(themileage);
}
I am just unsure how to structure this and any help is greatly appreciated, thank you!
A Link to a Webapp is just a bookmark in my browser
Here's a simple example of a webapp that I just put together for you.
html:(This is the user interface) It's all done in html and so there is a wide latitude of techniques that can be deployed here depending upon your knowledge of html, javascript, css etc.....
But this one just has one text box and one button and I didn't use any JQuery or other libraries. It's just basic old school JavaScript and html.
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script>
window.onload=function() {
document.getElementById('hdg1').innerHTML="You are now connected to the server";
};
function runMyFunction() {
var txt=document.getElementById('txt1').value;
if(!txt) {
alert("You have not entered any text");
}else{
google.script.run
.withSuccessHandler(function(msg){
document.getElementById('msg1').innerHTML=msg;
})
.myFunction(txt);
}
}
console.log('aq4.html');
</script>
</head>
<body>
<h1 id="hdg1"></h1>
<input type="text" id="txt1" placeholder="Put some text here" />
<input type="button" value="myfunction" onclick="runMyFunction()" />
<div id="msg1"></div>
</body>
</html>
Google Script:
function myFunction(txt) {
return "<h1>I have received your message and ran myFunction():</h1><strong>" + txt + "</strong>";
}
function doGet() {
return HtmlService.createHtmlOutputFromFile('aq4');
}
The link that I was referring to is the simply the url of the web app and all you have to do is bookmark that url and then click on the bookmark from your browser. I describe that a little in this video
You have a problem because code runs 'as the user' (who has no access rights).
You talk as if deploying as a WEB APP to make code run with developer permissions is the only way to go. First of all, I know another way to go.
Deploy as WEB App may prove unsuitable or complex, for many situations, like yours, and mine.
I am new for a month working and the way I have found to execute code 'as ME' -developer- is having ME fire execution. That may get accomplished with installable triggers.
Please find in: Apps Scrip Guides listed under "Restrictions" this point. It says nothing about advantages. I found this point an advantage, because I don't need to give access rights to users on sensible tables !!!
Per my testings, I can .openById(...) and modify objects on which active user has no access rights. Just login as Permissions holding user to https://script.google.com/home and create the installable triggers needed. Code will then run as the user creating the trigger, with all his permissions.
I'm still looking for a simpler way to inherit access rights from developer (...or maybe an ad-hoc created user) to the code written. Or to stop some 'editors' of a Spreadsheet from viewing & modifying the code. But this is for another question.

How to have One URL which sends to one-of-three Google Forms URLs?

I have a need to get equal populations in each of three surveys. The three surveys are identical except for one change - it contains different pictures.
I would like to distribute a single URL to my survey respondents.
I would like to count the number of previous responses I have, and add one.
I would like to redirect the session to one of three (Google Forms) URLs based upon the calculation
(Responses.Count + 1) MOD 3.
I think I need a Google Apps script to do this?
Here is some pseudocode:
var form0 = FormApp.openByUrl(
'htttps://docs.google.com/forms/d/e/2342f23f1mg/viewform'
);
var form1 = FormApp.openByUrl(
'htttps://docs.google.com/forms/d/e/23422333g/viewform'
);
var form2 = FormApp.openByUrl(
'htttps://docs.google.com/forms/d/e/2342wfeijqeovig/viewform'
);
var form0Responses = form0.getResponses();
var form1Responses = form1.getResponses();
var form2Responses = form2.getResponses();
var whichURL = (
form0Responses.length +
form1Responses.length +
form2Responses.length + 1
) % 3; // modulo three
// var goToForm = switch ( whichURL ) blah blah;
// redirect to goToForm;
// How do I redirect now?
Thanks!
Maybe there's a simpler solution possible but I don't think I know of it :)
The common link that you give out could be a link to a "proxy" page that doesn't contain anything but just redirects users to the correct page. Or it could be a link to the actual page with a necessary form embedded. Let's look at the options.
0) Publish your code as web app
In either case you'll need to have your code published as a web app. In GAS it's way simpler than it sounds, you'll find all the info here: https://developers.google.com/apps-script/guides/web
Make sure you set that Anyone, even anonymous can access the app and it always runs as you (if you choose User accessing the app, they'll have to go through authentication process which is not what you need here).
1) Redirect via GAS web app.
Your code in this case would look like so:
// I changed your function a bit so that it could be easier to work with
// in case the number of your forms changes later on
function getForm() {
var forms = [
{
// I'm using openById instead of openByUrl 'cause I've run into issues with
// the latter
form: FormApp.openById('1')
},
{
form: FormApp.openById('2')
},
{
form: FormApp.openById('3')
}
];
var whichURL = 0;
for (var i in forms) {
forms[i].responses = forms[i].form.getResponses().length;
whichURL += forms[i].responses;
}
whichURL++;
// we're returning the actual URL to which we should redirect the visitors
return forms[whichURL % forms.length].form.getPublishedUrl();
}
// doGet is Google's reserved name for functions that
// take care of http get requests to your web app
function doGet() {
// we're creating an html template from which getForm function is called
// as the template is evaluated, the returned result
// of the function is inserted into it
// window.open function is a client-side function
// that will open the URL passed to it as attribute
return HtmlService.createTemplate('<script>window.open("<?= getForm() ?>", "_top");</script>').evaluate();
}
So, after you've published your app, you'll get the link opening which the doGet function will run — and you're going to be redirected to your form.
The thing here is that the URL that you're getting this way is not rather beautiful, sth like https://script.google.com/macros/s/1234567890abcdefghijklmnopqrstuvwxyz/exec and it will also show a message at the top of the page "The app wasn't developed by Google" during those 1-2 seconds before redirect happens.
2) Embed your form into another webpage
The idea here is different: instead of giving your users a "proxy" link, you'll provide them with a page that'll ask a Google script for a correct form link and will display that form in the page in an iframe.
So, there are a couple of steps:
2.1) Change your doGet function (getForm will stay the same):
function doGet() {
return ContentService.createTextOutput(getForm());
}
In this case doGet will not return an html to render by browser but just a link to your form.
Sidenote: after changing the code you'll need to publish a new version of your code for the changes to take effect.
2.2) Create a Google site at sites.google.com
2.3) Insert an "Embed" block into your page, with the following code:
<script>
function reqListener(response) {
// change height and width as needed
document.body.insertAdjacentHTML('beforeend', '<iframe src="' + response.target.response + '" width="400" height="400"></iframe>');
}
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", reqListener);
oReq.open("GET", "INSERT-YOUR-SCRIPT-URL");
oReq.send();
</script>
What it does: javascript code sends an http request to your script, gets the form URL and passes it on into the callback function, reqListener which in turn inserts it into document body within an iframe element.
The good thing is that your URL which will be much more user-friendly (you could use this approach on your own site, too).
As a result, you'll have sth like this: