/**
* Convert sheetrows to FireStore documents
* @param {string} "collectionpath" collection path
* @param {number} count, number of last records to update if 0=all
* @return {array} values of sheet.
* header row becomes properties
* if score in 3the column, than 2de is email
* docname propery + document name = sheetname+recordnr
*
*/
function QuizsheetToFS_score(collectionpath, count) {
try {
// Script run interactive-> Alerts will work, otherwise not
var DisplayAlerts = true;
try {
var ui = SpreadsheetApp.getUi();
}
catch (err) {
DisplayAlerts = false;
}
//Lock access to vars/sheet from triggers
LockService.getScriptLock().waitLock(300000) //max 6min
// get last know record count
let docProperties = PropertiesService.getDocumentProperties()
//previous versions did not have RecCount
let reccount = (docProperties.getProperty('RecCount') == null) ? 0 : docProperties.getProperty('RecCount')
reccount = parseInt(reccount)
Logger.log(`Previous RecCount=${reccount}`)
// get Firestore db
var firestore = getFireStore()
// Get the table values
var ss = SpreadsheetApp.getActiveSpreadsheet();
// extract name without (response)
var s = ss.getName().split(" (")
var sheetname = s[0]
var sheets = ss.getSheets();
let name = sheets[0].getName();
let formURL = sheets[0].getFormUrl()
// if sheet is not connected to a form, it is no quiz
if (formURL == "" || formURL == null) {
SpreadsheetApp.flush();
LockService.getScriptLock().releaseLock()
if (DisplayAlerts) {
ui.alert(`This sheet is not connected to a form!`);
Logger.log(`This sheet is not connected to a form!`)
}
return false
}
// create sheet to calc % score
if (ss.getSheetByName("calc") == null) { ss.insertSheet("calc") }
// Insert formulas
let Sheet0 = sheets[0].getSheetName()
// formula copy Score column from form respose sheet(sheet0)
let formula0 = `={'${Sheet0}'!C1:C}`
// Put array formula in sheet, Different formula for NL or US versions, based on country codes
var Locale = ss.getSpreadsheetLocale()
let formula1 = ''
switch (Locale) {
case "en_GB":
case "en_US":
formula1 = `=ARRAYFORMULA(IF(A2:A="","",INT(left(A2:A,len(A2:A)-find("/",A2:A)-1)/right(A2:A,len(A2:A)-find("/",A2:A)-1)*100)))`
break;
default: //Default list ";" translation by google to ","
case "nl_NL":
formula1 = `=ARRAYFORMULA(IF(A2:A="";"";INT(left(A2:A;len(A2:A)-find("/";A2:A)-1)/right(A2:A;len(A2:A)-find("/";A2:A)-1)*100)))`
}
// set formulas in new sheet
ss.getSheetByName("calc").getRange(1, 1).setValue(formula0)
ss.getSheetByName("calc").getRange(1, 2).setValue("Score")
ss.getSheetByName("calc").getRange(2, 2).setValue(formula1)
SpreadsheetApp.flush()
// get form response URL
let responsesURL = `https://docs.google.com/forms/d/${getFileIdFromDriveUrl(formURL)}/edit#responses`
var values = ss.getSheetByName(name).getDataRange().getValues();
var valuescalc = ss.getSheetByName("calc").getDataRange().getValues();
// Extract headers
var headers = values[0]
// Extract the body
if (headers[1] != "Score" && headers[2] != "Score") {
SpreadsheetApp.flush();
LockService.getScriptLock().releaseLock()
if (DisplayAlerts) {
ui.alert(`The connected form is no quiz!`);
Logger.log(`The connected form is no quiz!`)
}
return false
}
var body = values.slice(1);
var bodycalc = valuescalc.slice(1)
// number of colums to save 3 if emails are collected
var l = (headers[2] == "Score") ? 3 : 2
// number of questions to calculate %
var max = headers.length - l
//Create ISO date string
var date = new Date().toISOString()
// You need an index for looping through the body rows
//Start record to write next from previous saved pointer
var recnr = (count == 0 || count >= body.length) ? 0 : reccount + 1
Logger.log(`recnr=${recnr}`)
//when multiple triggers within a few seconds, multipe record have to be written.
//save reccount for next write
docProperties.setProperty('RecCount', body.length - 1)
Logger.log(`recCountsave=${body.length - 1}`)
//record pointer
let r = recnr
while (r <= body.length - 1) {
// Initialize document obj, put sheet name as field in record (extra action)
let document = {}
document.docname = `${sheetname}_${r + 1}_${body[r][0]}` //docname = sheetname+date+recordnr
document.quizname = `${sheetname}`
document.formURL = formURL
document.responsesURL = responsesURL
document.changed = date
for (c = 0; c < l; c++) {
document[headers[c]] = ``
if (headers[c] == "Score") {
document[headers[c]] = parseInt(bodycalc[r][1])
//document[headers[c]] = parseInt((body[r][c] / max) * 100)
} else {
document[headers[c]] = body[r][c] //Assign cell value to property
}
}
let updatedoc = firestore.updateDocument(`${collectionpath}/${document.docname}`, document, mask = true);
r++
Logger.log(`Updatedoc=${updatedoc}`)
}
SpreadsheetApp.flush();
LockService.getScriptLock().releaseLock()
return true;
} catch (e) {
LockService.getScriptLock().releaseLock()
if (DisplayAlerts) { ui.alert(e); }
Logger.log(e)
}
}
Het gebruik van de Lock service verhindert dat bij het simultaan invullen van een form, de code ook simultaan wordt uitgevoerd waardoor read/modify/write acities in de knoei komen of verloren gaan.
Omdat het sequentieel afhandelen van aanvragen (onFormSubmit), toch kan leiden tot het "missen" van een reeds overgedragen waarde in de sheet, is het noodzakelijk om buiten het script om, de reeds afgehandelde teller van records te bewaren.
Dit gebeurt in een documentproperty.
//Lock access to vars/sheet from triggers
LockService.getScriptLock().waitLock(300000) //max 6min
// get last know record count
let docProperties = PropertiesService.getDocumentProperties()
//previous versions did not have RecCount
let reccount = (docProperties.getProperty('RecCount') == null) ? 0 : docProperties.getProperty('RecCount')
reccount = parseInt(reccount)
Logger.log(`Previous RecCount=${reccount}`)
// get Firestore db
Omdat de % berekening niet kan nadat de waarde is binnen gehaald in Apps Script (de totaal punten is blijkbaar niet beschikbaar in de API), is het noodzakelijk om deze waarde te herschalen tot % in de sheet zelf.
Hiervoor wordt een extra sheet aangemaakt "calc" die een kopie krijgt van de waardes in het x/y formaat.
Via een arrayformule wordt hieruit de % waarde berekend.
Maar, binnen google wordt er een onderscheid gemaakt naar landkeuze voor de gebruikte separator in de formules.
Hierdoor is het noodzakelijk om de "Locale" code te kennen en afhankelijk ervan de juiste formule in te vullen.
Opmerking: Google vertaald automatische formules die "niet GB" zijn naar het GB formaat, dus als je in deze code enke de ";" formule zou zetten, zal deze eveneens werken in een GB/US omgeving.
Het activeren van de Flush zorgt ervoor dat de waarde berekend is, alvorens de volgende read plaats zal vinden.
// create sheet to calc % score
if (ss.getSheetByName("calc") == null) { ss.insertSheet("calc") }
// Insert formulas
let Sheet0 = sheets[0].getSheetName()
// formula copy Score column from form respose sheet(sheet0)
let formula0 = `={'${Sheet0}'!C1:C}`
// Put array formula in sheet, Different formula for NL or US versions, based on country codes
var Locale = ss.getSpreadsheetLocale()
let formula1 = ''
switch (Locale) {
case "en_GB":
case "en_US":
formula1 = `=ARRAYFORMULA(IF(A2:A="","",INT(left(A2:A,len(A2:A)-find("/",A2:A)-1)/right(A2:A,len(A2:A)-find("/",A2:A)-1)*100)))`
break;
default: //Default list ";" translation by google to ","
case "nl_NL":
formula1 = `=ARRAYFORMULA(IF(A2:A="";"";INT(left(A2:A;len(A2:A)-find("/";A2:A)-1)/right(A2:A;len(A2:A)-find("/";A2:A)-1)*100)))`
}
// set formulas in new sheet
ss.getSheetByName("calc").getRange(1, 1).setValue(formula0)
ss.getSheetByName("calc").getRange(1, 2).setValue("Score")
ss.getSheetByName("calc").getRange(2, 2).setValue(formula1)
SpreadsheetApp.flush()
We voorzien de optie om alle records opnieuw te schrijven of enkel de nieuwe, zodat we bij een manuele aanpassing van de sheet (onEdit) toch alle veranderingen meenemen in de db.
Tevens wordt de bewaarde reccount waarde gebruikt om "verder te gaan" met de volgende write actie en wordt de nieuwe waarde bewaard in de doc properties.
var recnr = (count == 0 || count >= body.length) ? 0 : reccount + 1
Logger.log(`recnr=${recnr}`)
//when multiple triggers within a few seconds, multipe record have to be written.
//save reccount for next write
docProperties.setProperty('RecCount', body.length - 1)
Logger.log(`recCountsave=${body.length - 1}`)
Afhankelijk van de count parameter worden alle records of slechts de laatst toegevoegde behandeld. (r=recnr)
De naam van het FS document bestaat uit de sheet naam, die tevens de naam is van de quiz, de record nummer en de timestamp.
Hiervoor is gekozen, omdat wanneer de from terug aan een nieuwe sheet zou gekoppeld worden, de kans bestaan dat het oude document zou worden overschreven met nieuwe waardes (zelfde naam, zelfde rec nummer, maar toch andere entry)
We kiezen eveneens voor een FS write waarbij de aangeboden velden worden geupadte, bestaande velden niet worden gewist.
Dit om te vermijden dat wanneer we in de toekomst velden zouden toevoegen, de "oude" velden, die desnoods in een andere toepassing worden gebruikt, niet worden verwijdert bij een update (onEdit trigger)
let r = recnr
while (r <= body.length - 1) {
// Initialize document obj, put sheet name as field in record (extra action)
let document = {}
document.docname = `${sheetname}_${r + 1}_${body[r][0]}` //docname = sheetname+date+recordnr
document.quizname = `${sheetname}`
document.formURL = formURL
document.responsesURL = responsesURL
document.changed = date
for (c = 0; c < l; c++) {
document[headers[c]] = ``
if (headers[c] == "Score") {
document[headers[c]] = parseInt(bodycalc[r][1])
//document[headers[c]] = parseInt((body[r][c] / max) * 100)
} else {
document[headers[c]] = body[r][c] //Assign cell value to property
}
}
let updatedoc = firestore.updateDocument(`${collectionpath}/${document.docname}`, document, mask = true);
r++
Logger.log(`Updatedoc=${updatedoc}`)
}
Beide functies worden aangeroepen vanuit een trigger: OnEdit en OnFormSubmit, waarbij de parametes verschillen.
De Collection is gedefinieerd in setKeyFile, waardoor alle documenten steeds onder dezelfde collectie worden bewaard.
Ondanks dat de keuze gelaten wordt om bij iedere sheet setting een andere collection te gebruiken, wordt dit sterk afgeraden.
Reden: queries kunnen enkel binnen eenzelfde collectie lopen.
Het gebruik van een andere collectie is daarom enkel aangewezen indien het over totaal niet gerelateerde gegevens gaat (vb technische opleiding ikv competentieversterking, tevredenheidsonderzoeken dienstverlening)
function WriteToFireStore() {
try {
let docProperties = PropertiesService.getDocumentProperties()
let Collection = docProperties.getProperty('Collection')
if (QuizsheetToFS_score(Collection, 1)) {
Logger.log(`end main onFormSubmit`)
} else {
let ui = SpreadsheetApp.getUi()
ui.alert("form-sheet error")
}
} catch (e) {
Logger.log(e)
}
}
function WriteToFireStoreAll() {
try {
let docProperties = PropertiesService.getDocumentProperties()
let Collection = docProperties.getProperty('Collection')
if (QuizsheetToFS_score(Collection, 0)) {
Logger.log("end main onEdit")
} else {
let ui = SpreadsheetApp.getUi()
ui.alert("form-sheet error")
}
} catch (e) {
Logger.log(e)
}
}