Examplary
Start for free
Developer docs

Assessment prerequisites

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.

Loading diagram…

Defining a prerequisite

capabilities/prerequisite.ts
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.");
    }
  },
});
PropertyDescription
idUnique within your app.
nameThe label of the switch in the test's settings.
descriptionShown below the switch once it's turned on.
examSettingsSettings per test, shown once it's turned on. Written like settings, without components.
requiredPlansThe Examplary plans the switch is available on, like ["pro", "enterprise"]. All plans when left out.
requestHeadersThe request headers your hooks get to see, like a signature your browser adds.
exitButtonShows a button to close the secure browser once the student has handed in the test.
settingsComponentA component shown in the test's settings, once it's turned on.
playerComponentA component shown to students while the prerequisite isn't met.
isMetWhether a student may start the test. Met when left out.
validateRequestChecks every request a student makes while taking the test.
onExamUpdatedRuns when the test is saved, e.g. to set it up on your side.
functionsHooks your components can call.
filesFiles served at a public URL, like a configuration file for your browser.

Checking students

Every check gets the app's settings and:

PropertyDescription
examThe test: its id, name, whether it's published, startDateTime, endDateTime, spellCheck, publicUrl and playerUrl, and the workspace's orgUrl and orgDomain.
examSettingsThe values of your examSettings for this test.
dataWhat onExamUpdated returned for this test, see below.
requestThe 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.

Only the headers you ask for

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:

PropDescription
examThe test's id, and privateUrl, the link signed-in students use to take it.
userWhether 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 the user: their id, name, firstName, lastName, email and signInUrl, 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.