Public
Edited
Jul 8, 2024
8 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof survey = Inputs.bind(Inputs.select(mySurveys, {label: "survey"}), localStorageView("filler-project", {
defaultValue:
new URLSearchParams(location.search).get("survey") ||
mySurveys.includes('demo') ? 'demo' : undefined ||
mySurveys[0]
}))
Insert cell
viewof account = Inputs.bind(Inputs.select(myAccounts, {label: "account"}), localStorageView("filler-account", {
defaultValue: myAccounts[0]
}))
Insert cell
putFile = async (name, buffer) => {
console.log("Uploading ", name);
await putObject(config.CONFIDENTIAL_BUCKET, `accounts/${account}/surveys/${survey}/files/${name}`,
buffer, {
tags: {
"survey": survey,
"account": account,
}
});
}
Insert cell
getFile = async (name) => {
return await getObject(config.CONFIDENTIAL_BUCKET, `accounts/${account}/surveys/${survey}/files/${name}`);
}
Insert cell
Insert cell
pageLoadAnswers = {
console.log("Initializing pageLoadAnswers")
const projectAnswers = accountSettings?.surveys?.[survey]?.["answers"];
if (projectAnswers) {
const name = projectAnswers[projectAnswers.length - 1];
const prev = JSON.parse(await getObject(config.CONFIDENTIAL_BUCKET, `accounts/${account}/surveys/${survey}/${name}`));
prev.answers = new Map(prev.answers || [])
return prev
} else {
return ({
answers: new Map()
})
}
}
Insert cell
auto_save = {
console.log("Initializing auto_save");
function debounce(func, timeout = 2000){ // 2 seconds
let timer;
let hasRun = true; // Only one of setTimeout OR visibility change should run
let args;
const runTask = async () => {
if (!hasRun) {
console.log("auto_save")
hasRun = true;
await func.apply(this, args);
}
};
window.addEventListener('beforeunload', function (e) {
if (!hasRun) {
// Cancel the event
e.preventDefault(); // If you prevent default behavior in Mozilla Firefox prompt will always be shown
// Chrome requires returnValue to be set
e.returnValue = "Please wait for your latest changes to be saved. Try again in a few seconds";
}
});
return (...latestArgs) => {
args = latestArgs;
hasRun = false;
clearTimeout(timer);
document.removeEventListener("visibilitychange", runTask)
timer = setTimeout(runTask, timeout);
document.addEventListener("visibilitychange", runTask);
invalidation.then(() => document.removeEventListener("visibilitychange", runTask))
};
}
return Generators.observe(next => {
const autosave = debounce(async () => {
console.log("saving")
const answers = await saveState()
viewof lastSave.value = answers;
viewof lastSave.dispatchEvent(new Event('input', {bubbles: true}))
next(answers)
});
viewof responses.addEventListener('input', autosave);
invalidation.then(() => viewof responses.removeEventListener('input', autosave))
next("Autosave initialized")
})
}
Insert cell
Insert cell
surveySettings = {
console.log("Initializing surveySettings")
return JSON.parse(await getObject(config.PRIVATE_BUCKET, `surveys/${survey}/settings.json`));
}
Insert cell
accountSettings = {
console.log("Account settings loaded")
return JSON.parse(await getObject(config.CONFIDENTIAL_BUCKET,
`accounts/${account}/settings.json`));
}
Insert cell
version = {
console.log("Loading persisted version");
return JSON.parse(await getObject(config.PRIVATE_BUCKET, `surveys/${survey}/${surveySettings.versions[surveySettings.versions.length - 1]}`));
}
Insert cell
layout = {
console.log("Initializing layout")
return JSON.parse(await getObject(config.PRIVATE_BUCKET, `surveys/${survey}/${version.layout}`));
}
Insert cell
surveyConfig = {
console.log("Initializing config")
return JSON.parse(await getObject(config.PRIVATE_BUCKET, `surveys/${survey}/${version.config}`));
}
Insert cell
questions = {
console.log("Initializing questions")
return new Map(JSON.parse(await getObject(config.PRIVATE_BUCKET, `surveys/${survey}/${version.questions}`)));
}
Insert cell
urlCreds = {
const deliminator = location.hash.includes('%7C') ? '%7C' : '|';
let hash = location.hash.substring(1).replace("%7C", "|");
if (!hash.split("|")[0]) return invalidation;
const password = hash.split("|")[0];
const username = new URLSearchParams(location.search).get("username")
const url = `https://${config.PUBLIC_BUCKET}.s3.${REGION}.amazonaws.com/credentials/${await new hashes.SHA256().hex(username)}.json`
const response = await fetch(url);
const payload = await response.text();
return {...JSON.parse(await decode(await password, payload)), password: password, deliminator};
}
Insert cell
location.hash
Insert cell
me = getUser()
Insert cell
myTags = listUserTags(me.UserName)
Insert cell
mySurveys = (myTags["filler"] || "").split(" ").filter(v => v !== "")
Insert cell
myAccounts = (myTags["account"] || "").split(" ").filter(v => v !== "")
Insert cell
saveState = {
console.log("Initializing saveState")
return async function saveState() {
console.log("saveState saving...");
const answers = Object.entries(viewof responses.value).reduce(
(map, [cell_name, q]) => {
if (q && q.control && q.control !== "undefined" && (q.control.length > 0 || q.control.length === undefined))
map.set(cell_name, q.control)
return map;
},
new Map()
)


const name = `answers_${Date.now()}.json`
const state = ({
answers: [...answers.entries()],
questions: version.questions,
layout: version.layout
});

await putObject(config.CONFIDENTIAL_BUCKET, `accounts/${account}/surveys/${survey}/${name}`,
JSON.stringify(state), {
tags: {
"surveys": survey,
"account": account,
}
});
const newSettings = accountSettings;
newSettings["surveys"] = newSettings["surveys"] || {};
newSettings["surveys"][survey] = newSettings["surveys"][survey] || {};
newSettings["surveys"][survey]["answers"] = newSettings["surveys"][survey]["answers"] || [];
newSettings["surveys"][survey]["answers"] = [...newSettings["surveys"][survey]["answers"], name];

await putObject(config.CONFIDENTIAL_BUCKET, `accounts/${account}/settings.json`,
JSON.stringify(newSettings), {
tags: {
"survey": survey,
"account": account,
}
});
console.log("saveState done");
return answers;
}
}
Insert cell
Insert cell
Insert cell
testing = {
viewof responses // Delay loading modules until survey is setup
if (!document.location.search.includes("username=demoResponder")) return invalidation
const [{ Runtime }, { default: define }] = await Promise.all([
import(
"https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js"
),
import(`https://api.observablehq.com/@tomlarkworthy/testing.js?v=3`)
]);
const module = new Runtime().module(define);
return Object.fromEntries(
await Promise.all(
["expect", "createSuite"].map((n) => module.value(n).then((v) => [n, v]))
)
);
}
Insert cell
viewof tests = testing.createSuite({
name: "Survey Filler Integration Tests",
timeout_ms: 10000
})
Insert cell
tests.test("text_question is saved to responses", async (done) => {
const nonce = "test " + Math.random().toString(15).substring(3);
const control = document.querySelector("#text_question");
const textarea = control.querySelector("textarea");

// set it to something random
textarea.value = nonce;
// trigger dataflow
textarea.dispatchEvent(new Event('input', {bubbles: true}));
await until(() => viewof lastSave.value?.get('text_question') === nonce);
done()
})
Insert cell
/**
* Thanks sscovil!
* https://gist.github.com/sscovil/6502c72de3e24232f66b5bf86de04680
* Utility that waits for @predicate function to return truthy, testing at @interval until @timeout is reached.
*
* Example: await until(() => spy.called);
*
* @param {Function} predicate
* @param {Number} interval
* @param {Number} timeout
*
* @return {Promise}
*/
async function until(predicate, interval = 500, timeout = 30 * 1000) {
const start = Date.now();

let done = false;

do {
if (predicate()) {
done = true;
} else if (Date.now() > (start + timeout)) {
throw new Error(`Timed out waiting for predicate to return true after ${timeout}ms.`);
}

await new Promise((resolve) => setTimeout(resolve, interval));
} while (done !== true);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more