An assessment prerequisite is something students have to meet before they can start a test. Secure browsers, like Schoolyear, Muute and Safe Exam Browser, are built this way: once a teacher turns one on for a test, students can only take it from inside that browser.
A prerequisite adds a switch to the test's settings. When a teacher turns it on, Examplary shows its settings and its settings component. When students open the test, its player component explains how to meet it, like how to open the test in the secure browser, until it's met.
Defining a prerequisite
import { AssessmentPrerequisite } from "@examplary/apps";
export default new AssessmentPrerequisite({
id: "acme-browser",
name: { en: "Require the Acme browser" },
description: { en: "Students need to take this test in the Acme browser." },
requestHeaders: ["x-acme-signature"],
exitButton: true,
examSettings: [
{
key: "allowCalculator",
label: { en: "Allow the calculator" },
type: "boolean",
default: false,
},
],
playerComponent: {
path: "./components/player.tsx",
translations: {
title: { en: "Open this test in the Acme browser" },
},
},
isMet: (settings, { request }) => !!request.headers["x-acme-signature"],
validateRequest: async (settings, { request }) => {
const valid = await verifySignature(
settings.apiToken,
request.headers["x-acme-signature"],
);
if (!valid) {
throw new Error("Open this test in the Acme browser.");
}
},
});| Property | Description |
|---|---|
id | Unique within your app. |
name | The label of the switch in the test's settings. |
description | Shown below the switch once it's turned on. |
examSettings | Settings per test, shown once it's turned on. Written like settings, without components. |
requiredPlans | The Examplary plans the switch is available on, like ["pro", "enterprise"]. All plans when left out. |
requestHeaders | The request headers your hooks get to see, like a signature your browser adds. |
exitButton | Shows a button to close the secure browser once the student has handed in the test. |
settingsComponent | A component shown in the test's settings, once it's turned on. |
playerComponent | A component shown to students while the prerequisite isn't met. |
isMet | Whether a student may start the test. Met when left out. |
validateRequest | Checks every request a student makes while taking the test. |
onExamUpdated | Runs when the test is saved, e.g. to set it up on your side. |
functions | Hooks your components can call. |
files | Files served at a public URL, like a configuration file for your browser. |
Checking students
Every check gets the app's settings and:
| Property | Description |
|---|---|
exam | The test: its id, name, whether it's published, startDateTime, endDateTime, spellCheck, publicUrl and playerUrl, and the workspace's orgUrl and orgDomain. |
examSettings | The values of your examSettings for this test. |
data | What onExamUpdated returned for this test, see below. |
request | The student's request: its method, path, host, and the headers you listed in requestHeaders. |
isMet decides whether the student can start the test. It runs when the student opens the test and when they start it. It also gets input, a value your player component passed with setValue(), which is useful when your browser tells the page something that the server can check, like a configuration key.
validateRequest runs on every request a student makes while taking the test, like saving an answer. Throw an error to refuse the request; its message is shown to the student. Since it runs often, keep it quick.
Your hooks only see the request headers you list in requestHeaders, never the student's cookies or authorization headers.
Setting up the test on your side
When the test is saved while your prerequisite is on, Examplary calls onExamUpdated with the same exam, examSettings and data, plus enabled. That's the moment to create or update the test in your own platform. Whatever you return is stored for this test, and passed to every later hook as data:
onExamUpdated: async (settings, { exam, data, enabled }) => {
if (enabled && !data.acmeId) {
return { acmeId: await createTest(settings.apiToken, exam) };
}
if (data.acmeId) {
await updateTest(settings.apiToken, data.acmeId, { name: exam.name, archived: !enabled });
}
},It also runs after the teacher turns the prerequisite off, as long as there's data, so you can archive the test on your side. When a test is duplicated, the copy starts without data. An error thrown here is logged, but doesn't stop the teacher from saving the test.
Components
Both components get settings (the test's values of your examSettings), icon (the app's icon), and callFunction(name, input), besides the usual props.
The settings component also gets the exam: its id, whether it's published, and its startDateTime and endDateTime. Use it for things like a link to the test's dashboard on your platform.
The player component gets:
| Prop | Description |
|---|---|
exam | The test's id, and privateUrl, the link signed-in students use to take it. |
user | Whether the student is signed in (isAuthenticated), and their role. |
setValue(value) | Lets the student continue to start the test, where isMet checks the value as input. |
fileUrl(name) | The public URL of one of your files. |
Functions
Functions are hooks your components can call with callFunction(name, input), which returns the function's result. Each one says who may call it:
editor: teachers who can edit the test, from the settings component.student: signed-in students, from the player component. These also get theuser: theirid,name,firstName,lastName,emailandsignInUrl, a link that signs them in and opens the test, valid for an hour.
functions: {
dashboardUrl: {
for: "editor",
run: async (settings, { data }) => getDashboardLink(settings.apiToken, data.acmeId),
},
openInBrowser: {
for: "student",
run: async (settings, { data, user }) =>
createSession(settings.apiToken, data.acmeId, user!.signInUrl),
},
},Files
Some secure browsers start from a configuration file. A file is a hook that returns its contentType, an optional filename and its body, served at a public URL that the player component gets with fileUrl(name):
files: {
config: (settings, { exam, examSettings }) => ({
contentType: "application/xml",
filename: `${exam.id}.acme`,
body: buildConfig(exam, examSettings),
}),
},Anyone with the URL can download the file, so don't put anything secret in it.