Google Forms as Multiple Choice Quiz - How to Provide Results - google-app-engine

I've built a google form for a multiple choice quiz, with a linked spreadsheet for results, which works very well. I have a specific problem, which is that I'd like to present the user's results to them (i.e. how many answers they got right/wrong). The approach I've taken so far is:
create an extra sheet on the spreadsheet with a formula to calculate the number of correct answers for each response. This gives me two columns "Full Name" and "Scores"
embed the form into a google site
create a google apps script to read the results sheet and display output
embed the above into the same site below the form as an Apps Script Gadget
Currently I am able to display all of the results recorded so far. See here:
https://sites.google.com/site/mcqtest123/home
The script looks like:
// Script-as-app template.
function doGet() {
var app = UiApp.createApplication();
var title = app.createLabel("Survey Results").setStyleAttribute("fontSize","16px");
app.add(title);
//readRows(app);
calculateScores(app);
return app;
};
function calculateScores(app) {
var sheet = SpreadsheetApp.openById("0AlNR-ou0QtandFFzX1JCU1VRdTl0NVBRNTFjOUFhd1E");
var responseSheet = sheet.getSheetByName("Form Responses");
var allData = responseSheet.getDataRange().getValues();
var correct = allData[1];
var responses = allData.slice(2);
//Logger.log("Timestamp, name, score");
Logger.log("Name, Score");
for (var i = 0; i < responses.length; i++) {
var timestamp = responses[i][0];
var name = responses[i][1];
var score = 0;
for (var j = 2; j < correct.length; j++) {
if(responses[i][j] == correct[j]) {
score += 1;
}
}
//var output = timestamp + ", " + name + ", " + score + "/" + correct.length
var output = name + ", " + score + "/" + correct.length
print(app, output);
}
};
function print(app, line) {
Logger.log(line);
app.add(app.createLabel(line));
};
So this leaves two problems:
When the page loads, it loads the scores for all the respondents. I'd like to be able to present only the score for the person who filled out the form.
The scores don't get updated when the form is completed - only when the page is refreshed.
For problem 1), I wondered if there was some way to access the data in the form iframe (e.g. using document.getElementById('targetFrame'), except that google scripts don't seem to have access to the document model) to only display results of the person whose full name matches the name in the form (of course you could then view someone else's results if you know what they'd put as their full name, but without using the timestamp I don't see away round this).
For problem 2), I wondered if there was some way to trigger the script when the responses sheet was updated. However when I go to the spreadsheet and Tools->Script Manager I get the message "No scripts found", so I don't know how to add this trigger.

If you make your own form using HtmlService or UiApp and then that POSTing to your script to populate the spreadsheet, then you can generate a UID in a hidden field and use this to determine the results someone needs to see.
This will be the results as instant feedback to their answers to the quiz. To see these at a later date, you could then also add a bookmarkable link that also included that UID as a parameter. So your doGet() would look for a e.parameters.uid for example.
From Google Forms as they stand I am not so sure. you could potentially, with the new form styles, offer a pre-filled field with such a UID, but the route from form submission to your webapp is again unclear.

Related

Google Sheets Apps Script: How do I loop through replaceText and pull values from one cell to replace in Google Docs template?

I followed a few tutorials to set up a Google Docs template for a job offer letter. I made mail merge fields in the Google Doc set up as "{{FirstName}}", "{{StartDate}}", etc. I have a Google Sheet and a Google Form. I made a sheet called "Fields" where in column D, I have the "merge fields" and then in Column E, I have the values. I have been pasting lines of code to replace the merge fields with values, one-by-one. I saw a solution in another forum about how to do a loop. But after hours of practice, I couldn't get it working.
Ideally, I would like maintain the merge fields in a Google Sheet, so that I can add more. It would be lovely if I could figure out how to write an Apps Script to read any merge fields on my Google Sheet and then do the replaceText function automatically.
Right now, I have the code working one-by-one, but every time I add merge fields, I have to edit the code to find it in my Google Sheet.
I saw some code here:
https://stackoverflow.com/questions/69545550/how-do-you-loop-through-an-object-and-replace-text#:~:text=Loop%20through%20values%20object%20by%20replacing%20the%20body.replaceText%20entries%20in%20your%20code%20with%20this%3A
that I was trying to replicate for a loop but I couldn't get it working :(
enter image description here
Google Sheet
Google Docs Template
const sheet = SpreadsheetApp
.getActiveSpreadsheet()
.getSheetByName('Form')
//Copy template documents in our destinationFolder
const copy = googleDocTemplate.makeCopy(sheet.getRange('Fields!E3').getValues()+' '+sheet.getRange('Fields!E4').getValues()+` - Offer Letter` , destinationFolder)
//Once we have the copy, we then open it using the DocumentApp
const doc = DocumentApp.openById(copy.getId())
//Create constants for the Google Doc Body and Header so we can use replaceText
const body = doc.getBody();
const docHeader = doc.getHeader().getParent();
//Replace text in Headers of Google Doc
docHeader.replaceText(sheet.getRange('Fields!D3').getValues(), sheet.getRange('Fields!E3').getValues()); //First Name
docHeader.replaceText(sheet.getRange('Fields!D4').getValues(), sheet.getRange('Fields!E4').getValues()); //Last Name
//Replace body text
body.replaceText(sheet.getRange('Fields!D2').getValues(), sheet.getRange('Fields!E2').getValues());
body.replaceText(sheet.getRange('Fields!D3').getValues(), sheet.getRange('Fields!E3').getValues());
body.replaceText(sheet.getRange('Fields!D4').getValues(), sheet.getRange('Fields!E4').getValues());
body.replaceText(sheet.getRange('Fields!D5').getValues(), sheet.getRange('Fields!E5').getValues());
body.replaceText(sheet.getRange('Fields!D6').getValues(), sheet.getRange('Fields!E6').getValues());
body.replaceText(sheet.getRange('Fields!D7').getValues(), sheet.getRange('Fields!E7').getValues());
body.replaceText(sheet.getRange('Fields!D8').getValues(), sheet.getRange('Fields!E8').getValues());
I created a sheet and template as shown.
Now using the following script replace all the placeholders with the text. Notice I deliberatly leave one out.
function replaceText() {
try {
let spread = SpreadsheetApp.getActiveSpreadsheet();
let sheet = spread.getSheetByName("Sheet1");
let values = sheet.getDataRange().getDisplayValues();
values.shift(); // remove header
let file = DriveApp.getFileById("1up..........").makeCopy(values[1][4]+" "+values[2][4]+" - Letter");
let doc = DocumentApp.openById(file.getId());
let body = doc.getBody();
values.forEach( row => {
body.replaceText(row[3],row[4])
}
);
}
catch(err) {
console.log(err);
}
}
With the following results
Reference:
Array.forEach()
Body.replaceText()

Google Sheet Coinmarketcap requesting 1 single importxml instead of 1 request for each coin

I am developing a sheet in google sheet that pulls data from coinmarketcap with a script i've been trying to write.
I am a f. noob # coding.
I use the function importxml (i need to refresh the latest price for each coin, like 100 coins) in this script:
function CryptoRefresher() {
var spreadsheet = SpreadsheetApp.getActive();
var queryString = Math.random();
var link1 = "C";
var xpath = "D";
var destination = "E";
var Direction=SpreadsheetApp.Direction;
var NumeroRighe =spreadsheet.getRange("B"+(spreadsheet.getLastRow()+1)).getNextDataCell(Direction.UP).getRow();
for (var i = 2; i <= NumeroRighe; i++) {
var cellFunction1 = '=IMPORTXML("' + SpreadsheetApp.getActiveSheet().getRange(link1+i).getValue() + '?' + queryString + '", "'+ SpreadsheetApp.getActiveSheet().getRange(xpath+i).getValue() + '")';
SpreadsheetApp.getActiveSheet().getRange(destination+i).setValue(cellFunction1);
}
};
Example Data:
Cell B2 = "bitcoin"
cell C2 = "https://coinmarketcap.com/currencies/Bitcoin"
Cell D2 = "//div[#class='priceValue___11gHJ']"
Cell E2 = is the destination and will receive the bitcoin price
The problem is that it's really slow because it calls 1 coin per time.
Question: Is there a way to send ALL THE COINS REQUESTS in 1 single importxml call?
Like I'd like to collect all the coin names in column C (1 cell of column C has 1 different and unique Coin Name) to collect all the coin names that i am watching and ask for 1 single call to speed up the process?
(Is there a way to create an array, a list of the coin names and do 1 single call to coinmarketcap?)
I really can't figure that out and i hope what i'm asking is clear!
Thank you!
Alessandro
Given the structure of the webpage, it is currently not possible to have a single IMPORTXML call to pull multiple arbitrary currencies from the CoinMarketCap site.
However, they have a convenient API that can do that exactly, please see references below:
CoinMarketCap API / Cryptocurrency
And this should get you started in pulling information from the API:
Pulling Currency Data to Google Sheets
I'd suggest using a dedicated service to retrieve the data, for instance this request will give you the data you need without any parsing or signing up to 3rd party services
=IMPORTDATA("https://cryptoprices.cc/BTC")
Trying to parse a complex web page under active development is just prone to fail at some point.
As alternative, go straight to the source, by signing up to the CoinMarketCap api to get more up to date data. (Already mentioned above) You can sign up for the free tier API (333 req/day) at https://pro.coinmarketcap.com/signup/
Wow! Сryptoprices.cc it’s a great service.
But some cryptocurrencies don’t read clearly.
If you change the formula, add / the price is updated and it is correct.

SOQL Query for getting the Application name of a Particular Tab

I would like to get the Application name of a specific sObject. For example: I have a custom object called Candidate__c. How to get the Application name of Candidate__c programmatically?
I am open to any approach like using Schema Namespace as well.
I am answering my question. I used Schema.describeTabs() and It works perfectly, but the Doc says the DescribeTabs method returns the minimum required metadata that can be used to render apps ...
Basically, The All Tabs are not included in the list of described tabs. The results are dependent upon the apps that are available to the running user.
// Get tab set describes for each app
List<Schema.DescribeTabSetResult> allApps = Schema.describeTabs();
// Iterate through each tab set describe for each app
for (Schema.DescribeTabSetResult oneApp : allApps) {
System.debug('The tabs/objects associated with the' + oneApp.getLabel() + ' app are:');
List<Schema.DescribeTabResult> appTabs = oneApp.getTabs();
for (Integer i = 0; i < appTabs.size(); i++) {
System.debug((i + 1) + '. Tab Name: ' + appTabs[i].getLabel());
}
}

Best practices to execute faster a CasperJS script that scrapes thousands of pages

I've written a CasperJS script that works very well except that it takes a (very very) long time to scrape pages.
In a nutshell, here's the pseudo code:
my functions to scrape the elements
my casper.start() to start the navigation and log in
casper.then() where I loop through an array and store my links
casper.thenOpen() to open each link and call my functions to scrap.
It works perfectly (and fast enough) for scraping a bunch of links. But when it comes to thousands (right now I'm running the script with an array of 100K links), the execution time is endless: the first 10K links have been scrapped in 3h54m10s and the following 10K in 2h18m27s.
I can explain a little bit the difference between the two 10K batches : the first includes the looping & storage of the array with the 100K links. From this point, the scripts only open pages to scrap them. However, I noticed the array was ready to go after roughly 30 minutes so it doesn't explain exactly the time gap.
I've placed my casper.thenOpen() in the for loop hoping that after each new link built and stored in the array, the scrapping will happen. Now, I'm sure I've failed this but will it change anything in terms of performance ?
That's the only lead I have in mind right now and I'd be very thankful if anyone is willing to share his/her best practices to reduce significantly the running time of the script's execution (shouldn't be hard!).
EDIT #1
Here's my code below:
var casper = require('casper').create();
var fs = require('fs');
// This array maintains a list of links to each HOL profile
// Example of a valid URL: https://myurl.com/list/74832
var root = 'https://myurl.com/list/';
var end = 0;
var limit = 100000;
var scrapedRows = [];
// Returns the selector element property if the selector exists but otherwise returns defaultValue
function querySelectorGet(selector, property, defaultValue) {
var item = document.querySelector(selector);
item = item ? item[property] : defaultValue;
return item;
}
// Scraping function
function scrapDetails(querySelectorGet) {
var info1 = querySelectorGet("div.classA h1", 'innerHTML', 'N/A').trim()
var info2 = querySelectorGet("a.classB span", 'innerHTML', 'N/A').trim()
var info3 = querySelectorGet("a.classC span", 'innerHTML', 'N/A').trim()
//For scraping different texts of the same kind (i.e: comments from users)
var commentsTags = document.querySelectorAll('div.classComments');
var comments = Array.prototype.map.call(commentsTags, function(e) {
return e.innerText;
})
// Return all the rest of the information as a JSON string
return {
info1: info1,
info2: info2,
info3: info3,
// There is no fixed number of comments & answers so we join them with a semicolon
comments : comments.join(' ; ')
};
}
casper.start('http://myurl.com/login', function() {
this.sendKeys('#username', 'username', {keepFocus: true});
this.sendKeys('#password', 'password', {keepFocus: true});
this.sendKeys('#password', casper.page.event.key.Enter, {keepFocus: true});
// Logged In
this.wait(3000,function(){
//Verify connection by printing welcome page's title
this.echo( 'Opened main site titled: ' + this.getTitle());
});
});
casper.then( function() {
//Quick summary
this.echo('# of links : ' + limit);
this.echo('scraping links ...')
for (var i = 0; i < limit; i++) {
// Building the urls to visit
var link = root + end;
// Visiting pages...
casper.thenOpen(link).then(function() {
// We pass the querySelectorGet method to use it within the webpage context
var row = this.evaluate(scrapDetails, querySelectorGet);
scrapedRows.push(row);
// Stats display
this.echo('Scraped row ' + scrapedRows.length + ' of ' + limit);
});
end++;
}
});
casper.then(function() {
fs.write('infos.json', JSON.stringify(scrapedRows), 'w')
});
casper.run( function() {
casper.exit();
});
At this point I probably have more questions than answers but let's try.
Is there a particular reason why you're using CasperJS and not Curl for example ? I can understand the need for CasperJS if you are going to scrape a site that uses Javascript for example. Or you want to take screenshots. Otherwise I would probably use Curl along with a scripting language like PHP or Python and take advantage of the built-in DOM parsing functions.
And you can of course use dedicated scraping tools like Scrapy. There are quite a few tools available.
Then the 'obvious' question: do you really need to have arrays that large ? What you are trying to achieve is not clear, I am assuming you will want to store the extracted links to a database or something. Isn't it possible to split the process in small batches ?
One thing that should help is to allocate sufficient memory by declaring a fixed-size array ie:
var theArray = new Array(1000);
Resizing the array constantly is bound to cause performance issues. Every time new items are added to the array, expensive memory allocation operations must take place in the background, and are repeated as the loop is being run.
Since you are not showing any code, so we cannot suggest meaningful improvements, just generalities.

Copy range in google docs to another sheet

I have Google form that gets filled in by a few users. Works great but has it's limitations.
I'd like to copy the data entered from the active sheet to a new sheet called "work", all information except the first row that is. In the first row I have a few array formulas that populate some cells as new data is entered on the active sheet.
The second sheet (work) has a header row with all the formatting, data validation, some formulas etc (row 1). This information can not be applied when a new record is added via the form.. so I am told..
Thus, once the data has been copied from the active sheet (called active) I'd like the new data to be formatted as per the heading row (row 1) of the "work" sheet with all the formatting, validation, formulas etc being applied to the new data.
Is this doable? I am a noob when it comes to scripting so a complete solution would be highly appreciated.
here is a sample form you can play with
https://docs.google.com/spreadsheet/ccc?key=0AplugTacg-08dFNRUHROSW82bDhESkxBdjVTV0NOLUE
First thing i noticed one can not just copy/paste as the array formulas will bong things up so it has to be a paste special - values only
Any help greatly appreciated.
I'm struggling a bit with the logic behind what you're doing, but I've attempted a solution in sheet 'work2' in this copy of your spreadsheet. Perhaps have a play and report back what's not right or missing.
The script is this:
function onFormSubmit(e) {
var sheet = SpreadsheetApp.getActive().getSheetByName('work2');
var nextRow = sheet.getRange(sheet.getLastRow() + 1, 1, 1, 9);
sheet.getRange(2, 1, 1, 9).copyTo(nextRow);
var sLength = e.values[2].length;
var huuh = e.values[3] * sLength;
var pending = e.values[3] * e.values[3] / huuh;
var nextMonth = e.values[3] + pending;
nextRow.setValues([[e.values[0], e.values[1], huuh, e.values[2], sLength, e.values[3], 'Please select action', pending, nextMonth]]);
}
and has an "on form submit" trigger attached to it.

Resources