Initiate Appmaker Document Approval from Google Drive - google-app-maker

I've customized Document Management System template in Appmaker as per my needs. Now instead of going to Appmaker every time to initiate an approval I want to provide functionality to initiate the workflow from Google Drive.So users can select file for Approval directly from Google Drive.
My question is is there any Rest call or something via which I can initiate DMS workflow from Third party app?

Well I found a way out to achieve the result.
Steps:
Drive API provides a way to add your App in 'Open With' menu in Google Drive.
So I've created my custom app and deployed it. This app will simply receive params from Google Drive 'Open with' menu and pass it to Appmaker Document Approval System.
In Appmaker Create request page parse if request contains these params, if yes then select files using these params.
This way my users can initiate the Document Approval Workflow from Google Drive.
References :
How to add/list your App in Google Drive
Step by Step video guideline to Create and publish your App
Code:
'Open With' App code for redirecting users from Google Drive to Appmaker.
code.gs:
var AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/auth';
var TOKEN_URL = 'https://accounts.google.com/o/oauth2/token';
var REDIRECT_URL= ScriptApp.getService().getUrl();
var tokenPropertyName = 'GOOGLE_OAUTH_TOKEN';
var CLIENT_ID = 'your client id';
var CLIENT_SECRET = 'your client secrect';
function doGet(e) {
var HTMLToOutput;
if(e.parameters.state){
var state = JSON.parse(e.parameters.state);
if(state.action === 'create'){
HTMLToOutput = "<html><h1>Creation Of Docs Not supported by this App!</h1></html>";
}
else {
var id = state.exportIds;
var file = DriveApp.getFileById(id);
//append params to your appmaker URL
var url = 'yourappmaker published url'+'?param1='+file.getName()+'&param2='+file.getUrl()+'#AddRequest';
HTMLToOutput = HtmlService.createHtmlOutput('<html><script>'
+'window.close = function(){window.setTimeout(function(){google.script.host.close()},9)};'
+'var a = document.createElement("a"); a.href="'+url+'"; a.target="_blank";'
+'if(document.createEvent){'
+' var event=document.createEvent("MouseEvents");'
+' if(navigator.userAgent.toLowerCase().indexOf("firefox")>-1){window.document.body.append(a)}'
+' event.initEvent("click",true,true); a.dispatchEvent(event);'
+'}else{ a.click() }'
+'close();'
+'</script>'
// Offer URL as clickable link in case above code fails.
+'<body style="word-break:break-word;font-family:sans-serif;">Failed to open automatically. Click here to proceed.</body>'
+'<script>google.script.host.setHeight(40);google.script.host.setWidth(410)</script>'
+'</html>')
.setWidth( 90 ).setHeight( 1 );
}
}
else if(e.parameters.code){//if we get "code" as a parameter in, then this is a callback. we can make this more explicit
getAndStoreAccessToken(e.parameters.code);
HTMLToOutput = '<html><h1>App is installed, you can close this window now or navigate to your Google Drive.</h1></html>';
}
else {//we are starting from scratch or resetting
HTMLToOutput = "<html><h1>Install this App into your Google Drive!</h1><a href='"+getURLForAuthorization()+"'>click here to start</a></html>";
}
console.log(getURLForAuthorization());
return HtmlService.createHtmlOutput(HTMLToOutput);
}
function getURLForAuthorization(){
return AUTHORIZE_URL + '?response_type=code&client_id='+CLIENT_ID+'&redirect_uri='+REDIRECT_URL +
'&scope=https://www.googleapis.com/auth/drive.install https://www.googleapis.com/auth/userinfo.email';
}
function getAndStoreAccessToken(code){
var parameters = { method : 'post',
payload : 'client_id='+CLIENT_ID+'&client_secret='+CLIENT_SECRET+'&grant_type=authorization_code&redirect_uri='+REDIRECT_URL+'&code=' + code};
var response = UrlFetchApp.fetch(TOKEN_URL,parameters).getContentText();
var tokenResponse = JSON.parse(response);
UserProperties.setProperty(tokenPropertyName, tokenResponse.access_token);
}
function getUrlFetchOptions() {
return {'contentType' : 'application/json',
'headers' : {'Authorization' : 'Bearer ' + UserProperties.getProperty(tokenPropertyName),
'Accept' : 'application/json'}};
}
//naive check, not using for now, use refresh tokens and add proper checking
function isTokenValid() {
return UserProperties.getProperty(tokenPropertyName);
}
In Document workflow 'Create Request' page, add event to onAttach() method. Write below function,
//client side
function checkIfRedirected(widget)
{
// console.log(location.origin);
google.script.url.getLocation(function(location) {
var params = location.parameter;
var param1 = params.param1;
var param2 = params.param2;
widget.datasource.item.DocumentName = param1;
widget.datasource.item.DocumentUrl = param2;
widget.datasource.item.Owner = app.user.email;
});
}

Related

How to maintain a session with .aspx server while web scraping through pagination?

I am unable to maintain a session with a .aspx server. I am trying to scrape data by paginating, but it keeps telling me "The Results have expired. Please resubmit the search." I have tried maintaining cookies so I don't think that is the problem unless I somehow did it wrong?
I have to navigate through by first making a GET request to the following URL:
https://www.wandsworth.gov.uk/planning-and-building-control/search-planning-applications/
The following is the code I use to make the request.
First these are all my requires
const cheerio = require('cheerio');
const url = require('url');
const rp = require('request-promise');
const ss = require('string-similarity');
const tc = require('tough-cookie');
Here is how I make my request
var options = {
uri: 'https://www.wandsworth.gov.uk/planning-and-building-control/search-planning-applications/',
transform: function(body){ return cheerio.load(body) },
method: 'GET'
}
var $ = await rp(options);
Now I extract the information I need in order to make a successful post request, and I use the 'string-similarity' package to find a select element that closely matches a tag that matches my input.
// Extract selectable elements
var obj_collection = $('#cboStreetReferenceNumber')[0].children;
var collection = []; // array of inner strings for each select element
// Push innerHTML strings to collection
for(let i=0; i<obj_collection.length; i++){
try {
collection.push(obj_collection[i].children[0].data);
} catch(e) {
collection.push('');
}
}
// Find the best match for our given address
var matches = ss.findBestMatch(address, collection);
var cboStreetReferenceNumber=
obj_collection[matches.bestMatchIndex].attribs.value;
// These are used to verify us
var __VIEWSTATE = $('#__VIEWSTATE')[0].attribs.value;
var __VIEWSTATEGENERATOR = $('#__VIEWSTATEGENERATOR')[0].attribs.value;
var __EVENTVALIDATION = $('#__EVENTVALIDATION')[0].attribs.value;
var cboMonths = 1;
var cboDays = 1;
var csbtnSearch = 'Select';
var rbGroup = 'rbNotApplicable';
// Modify options
options.uri = $('#M3Form')[0].attribs.action;
options.method = 'POST';
options.form = {
cboStreetReferenceNumber,
__VIEWSTATE,
__VIEWSTATEGENERATOR,
__EVENTVALIDATION,
cboMonths,
cboDays,
csbtnSearch,
rbGroup
};
options.followAllRedirects = true;
options.resolveWithFullResponse = true;
delete options.transform;
Now with these options, I'm ready to make my request to page 1 of the data I'm looking for.
// method: #POST
// link: "Planning Explorer"
var body = await rp(options);
var $ = cheerio.load(body.body);
console.log(body.request);
var Referer = 'https://planning1.wandsworth.gov.uk' + body.req.path;
var scroll_uri = 'https://planning1.wandsworth.gov.uk/Northgate/PlanningExplorer/Generic/StdResults.aspx?PT=Planning%20Applications%20On-Line&PS=10&XMLLoc=/Northgate/PlanningExplorer/generic/XMLtemp/ekgjugae3ox3emjpzvjtq045/c6b04e65-fb83-474f-b6bb-2c9d4629c578.xml&FT=Planning%20Application%20Search%20Results&XSLTemplate=/Northgate/PlanningExplorer/SiteFiles/Skins/Wandsworth/xslt/PL/PLResults.xslt&p=10';
options.uri = scroll_uri;
delete options.form;
delete options.followAllRedirects;
delete options.resolveWithFullResponse;
options.method = 'GET';
options.headers = {};
options.headers.Referer = Referer;
options.transform = function(body){
return cheerio.load(body);
}
var $ = await rp(options);
Once I get the next page, I am given a table with 10 items and some pagination if there are more than 10 items available based on my POST request.
This all goes fine until I try to paginate to page 2. The resulting HTML body tells me that my search has expired and that I need to resubmit a search. That means going back to step 1 and submitting a POST request again, however that will always bring me to page 1 of the pagination.
Therefore, I need to somehow find a way to maintain a connection with this server while I 'scroll' through its pages.
I am using node.js & request-promise to make my requests.
The following is my code:
I have already tried maintaining cookies between requests.
Also, __VIEWSTATE shouldn't be the problem because the request to page 2 should be a GET request.
I was able to find a workaround by using the headless browser "Puppeteer" in order to maintain a connection with the server. However, I still do not know how to solve this problem by making raw requests.

Google Form make POST request on submission

Is there a way to call an external API Endpoint on Google Forms every time the form is filled out?
First:
you'll need to set up your App script project and you'll do that by:
Visit script.google.com to open the script editor. (You'll need to be signed in to your Google account.) If this is the first time you've been to script.google.com, you'll be redirected to a page that introduces Apps Script. Click Start Scripting to proceed to the script editor.
A welcome screen will ask what kind of script you want to create. Click Blank Project or Close.
Delete any code in the script editor and paste in the code below.
This video and the doc will help
Second
you'll need to create an installable trigger, you can add it to the form directly or to the spreadsheet that has the responses
function setUpTrigger(){
ScriptApp.newTrigger('sendPostRequest') /* this has the name of the function that will have the post request */
.forForm('formkey') // you'll find it in the url
.onFormSubmit()
.create();
}
Check the doc
Third
create the sendPostRequest function and add the UrlFetchApp to it
function sendPostRequest(e){
// Make a POST request with form data.
var resumeBlob = Utilities.newBlob('Hire me!', 'text/plain', 'resume.txt');
var formData = {
'name': 'Bob Smith',
'email': 'bob#example.com',
'resume': resumeBlob
};
// Because payload is a JavaScript object, it is interpreted as
// as form data. (No need to specify contentType; it automatically
// defaults to either 'application/x-www-form-urlencoded'
// or 'multipart/form-data')
var options = {
'method' : 'post',
'payload' : formData
};
UrlFetchApp.fetch('https://httpbin.org/post', options);
}
Check the doc
Try something like this in your app script:
var POST_URL = "enter your webhook URL";
function onSubmit(e) {
var form = FormApp.getActiveForm();
var allResponses = form.getResponses();
var latestResponse = allResponses[allResponses.length - 1];
var response = latestResponse.getItemResponses();
var payload = {};
for (var i = 0; i < response.length; i++) {
var question = response[i].getItem().getTitle();
var answer = response[i].getResponse();
payload[question] = answer;
}
var options = {
"method": "post",
"contentType": "application/json",
"payload": JSON.stringify(payload)
};
UrlFetchApp.fetch(POST_URL, options);
};
Be sure to replace the POST_URL variable with your webhook, you can use requestcatcher.com to test this out.
Add a trigger to the script by clicking "Triggers" in the side menu
Open the menu (top-right dots)
Click in Script Editor
Paste the above code (changing the POST_URL)
Click in the clock icon (left-side menu), which means Triggers.
On the right-bottom corner, click in the blue Add trigger button (a form will show as the image below).
It should show onSubmit under Choose which function to run.
Make sure Select event type is set as On form submit.
Click Save button.
After that, submit your form and watch for the request to come in.
This is pretty straightforward with Google Scripts.
Just create a new project bound to your spreadsheet and create 2 elements:
A function that will contain all relevant data to make the call (see docs for making a HTTP request from Google Apps Script)
A trigger linked to the spreadsheet. You can set it to run each time an edit occurs or form is submitted
VoilĂ , your sheet will call whatever endpoint you wish on submission. You can even parse the spreadsheet to return that data to your endpoint

Trigger a button click from a URL

We need to scrape VEEC Website for the total number once a week.
As an example, for the week of 17/10/2016 - 23/10/2016 the URL returns the number Total 167,356 when the search button is clicked. We want this number to be stored in our database.
I'm using coldfusion to generate the weekly dates as params and have been passing them like the above URL. But I'm unable to find a query param so that the "Search" button click event is triggered.
I've tried like this & this but nothing seems to be working.
Any pointers?
It seems like for every form submission, a CRSF token is added, which prevents malicious activity. To make matters worse for you, the CRSF token is changed for each form submission, not just for each user, which makes it virtually impossible to circumvent.
When I make a CFHTTP POST request to this form, I get HTML FileContent back, but there is no DB data within the results table cell placeholders. It seems to me that the form owner allows form submission from an HTTP request, but if the CRSF token cannot be validated, no DB data is returned.
It maybe worth asking the website owner, if there is any kind of REST API, that you can hook into...
If you want to use a headless browser PhantomJS (https://en.wikipedia.org/wiki/PhantomJS) for this, here is a script that will save the total to a text file.
At command prompt, after you install PhantomJS, run phantomjs.exe main.js.
main.js
"use strict";
var firstLoad = true;
var url = 'https://www.veet.vic.gov.au/Public/PublicRegister/Search.aspx?CreatedFrom=17%2F10%2F2016&CreatedTo=23%2F10%2F2016';
var page = require("webpage").create();
page.viewportSize = {
width: 1280,
height: 800
};
page.onCallback = function (result) {
var fs = require('fs');
fs.write('veet.txt', result, 'w');
};
page.onLoadStarted = function () {
console.log("page.onLoadStarted, firstLoad", firstLoad);
};
page.onLoadFinished = function () {
console.log("page.onLoadFinished, firstLoad", firstLoad);
if (firstLoad) {
firstLoad = false;
page.evaluate(function () {
var event = document.createEvent("MouseEvents");
event.initEvent("click", true, true);
document.querySelectorAll(".dx-vam")[3].dispatchEvent(event);
});
} else {
page.evaluate(function () {
var element = document.querySelectorAll('.dxgv')[130];
window.callPhantom(element.textContent);
});
setTimeout(function () {
page.render('veet.png');
phantom.exit();
}, 3000);
}
};
page.open(url);
The script is not perfect, you can work on it if you're interested, but as is it will save the total to a file veet.txt and also save a screenshot veet.png.

Google App Maker how to create Data Source from Google Contacts

Using GoogleAppMaker how to create a data source from google contacts. There is an employee HR example app but I want to similarly manage contacts (add, modify, delete) and use select criteria.
At this time this task is not trivial in App Maker and it is pretty much generic. We can change question wording to CRUD operations with 3rd party datasources. Let's break it into smaller parts and address them separately.
Read/list contacts
This task is relatively easy. You need to use Calculated Model to proxy Apps Scripts Contacts API response. Once you create model with subset of fields from the Contact response you can create datasource for the model and bind it to List or Table widget. You can also try to find some inspiration in Calculated Model Sample.
// Server side script
function getContacts_() {
var contacts = ContactsApp.getContacts();
var records = contacts.map(function(contact) {
var record = app.models.Contact.newRecord();
record.FirstName = contact.getGivenName();
record.LastName = contact.getFamilyName();
var companies = contact.getCompanies();
if (companies.length > 0) {
var company = companies[0];
record.Organization = company.getCompanyName();
record.Title = company.getJobTitle();
}
var emails = contact.getEmails();
if (emails.length > 0) {
record.Email = emails[0].getAddress();
}
var phones = contact.getPhones();
if (phones.length > 0) {
record.Phone = phones[0].getPhoneNumber();
}
return record;
});
return records;
}
Create/Update/Delete
Since Calculated Models have some limitations, we need to turn on our imagination to create, update and delete records from their datasources. The basic strategy will be calling server side scripts for CUD operations in response to user actions on client side. To get user's input from UI we will need to utilize page's Custom Properties, one property for each Contact field:
Here are some snippets that should explain the idea
Create
// Client script
function onSubmitContactClick(submitButton) {
var props = submitButton.root.properties;
var contact = {
FirstName: props.FirstName,
LastName: props.LastName,
Organization: props.Organization,
...
};
google.script.run
.withSuccessHandler(function() {
// Most likely we'll need to navigate user back to the
// page with contacts list and reload its datasource
// to reflect recent changes, because our `CUD` operations
// are fully detached from the list datasource
app.showPage(app.pages.Contacts);
app.datasources.Contacts.load();
})
.withFailureHandler(function() {
// TODO: Handle error
})
.createContact(contact);
}
// Server script
function createContact(contactDraft) {
var contact = ContactsApp.createContact(contactDraft.FirsName,
contactDraft.LastName,
contactDraft.Email);
contact.addCompany(contactDraft.Organization, contactDraft.Title);
contact.addPhone(ContactsApp.Field.WORK_PHONE, contactDraft.Phone);
}
Update
Idea to update contact records will be very similar to the new contact creation flow, so I skip it for now.
Delete
Assuming that delete button is located inside contacts table row.
// Client script
function onDeleteContactClick(deleteButton) {
var email = deleteButton.datasource.item.Email;
google.script.run
.withSuccessHandler(function() {
// To update contacts list we can either reload the entire
// datasource or explicitly remove deleted item on the client.
// Second option will work way faster.
var contactIndex = deleteButton.parent.childIndex;
app.datasources.Contacts.items.splice(contactIndex, 1);
})
.withFailureHandler(function() {
// TODO: Handle error
})
.deleteContact(contact);
}
// Server script
function deleteContact(email) {
var contact = ContactsApp.getContact(email);
ContactsApp.deleteContact(contact);
}

How can you get a url for every piece of data?

I am building an IM platform based on Firebase and I would like that every user got an address that directed them to the chat room.
http://chatt3r.sitecloud.cytanium.com/
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="https://cdn.firebase.com/v0/firebase.js"></script>
<script>
var hash; // global incase needed elsewhere
$(function() {
hash = window.location.hash.replace("#", "");;
myRootRef = new Firebase("http://xxxx.firebaseio.com");
var postingRef;
if (hash) {
// user has been given a hashed URL from their friend, so sets firebase root to that place
console.log(hash);
postingRef = new Firebase("http://xxxx.firebaseio.com/chatrooms/" + hash);
} else {
// there is no hash, so the user is looking to share a URL for a new room
console.log("no hash");
postingRef = new Firebase("http://xxxx.firebaseio.com/chatrooms/");
// push for a unique ID for the chatroom
postingRef = postingRef.push();
// exploit this unique ID to provide a unique ID host for you app
window.location.hash = postingRef.toString().slice(34);
}
// listener
// will pull all old messages up once bound
postingRef.on("child_added", function(data) {
console.log(data.val().user + ": " + data.val().message);
});
// later:
postingRef.push({"message": "etc", "user": "Jimmybobjimbobjimbobjim"});
});
</script>
</head>
That's working for me locally. You need to change xxxx to whatever URL yours is at, and add on however many characters that first part is at the .slice() bit.
Hashes.
If I understand your question correctly, you want to be able to share a URL that will allow anyone who clicks on the URL to log onto the same chatroom.
I did this for a Firebase application I made once. The first thing you need to be doing is using the .push() method. Push the room to Firebase, then use the toString() method to get the URL of the push. Some quick JS string manipulation - window.location.hash = pushRef.toString().slice(x) - where 'x' is whatever place you want to snip the URL at. window.location.hash will set the hash for you. Add the hash to the sharing URL, and then for the next step:
You will want a hash listener to check if there is already a hash when you open the page, so $(window).bind('hashchange', function() {UpdateHash()}) goes into a doc.ready function, and then...
function UpdateHash() {
global_windowHash = window.hash.replace("#", "");
// to assign the hash to a global hash variable
if (global_windowHash) {
// if there was already a hash
chatRef = new Firebase("[Your Firebase URL here]" + "/chatrooms/" + global_windowHash);
// chatRef is the global that you append the chat data to, and listen from.
} else {
// there wasn't a hash, so you can auto-create a new one here, in which case:
chatRef = new Firebase("[Your Firebase URL here]" + "/chatrooms/");
chatRef = chatRef.push();
window.location.hash = chatRef.toString().slice(x);
}
}
I hope that helps (and works :P ). If there are any questions or problems, then just ask!

Resources