=== Plugixa Exam ===
Contributors: plugixa, freemius
Tags: exam, quiz, test, assessment, elearning
Requires at least: 6.5
Requires PHP: 8.1
Tested up to: 7.1
Stable tag: 1.0.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

A question bank, an exam builder with sections and random pools, a timed runner with autosave, and automatic grading.

== Description ==

Plugixa Exam turns a WordPress site into somewhere people can actually sit an exam.

You write questions once into a bank, organised by a subject tree and by tags. You build
exams out of that bank — either by picking questions one at a time, or by describing a pool
("three algebra questions, two easy ones, two true/false") and letting each candidate draw
their own paper. Then people sit them, and the plugin marks what it can and hands you the
rest.

= What the free version does =

**A question bank.** Seven question types: multiple choice with one answer, multiple choice
with several, true/false, short answer, essay, fill in the blanks, and picture choice, where
the options are images. (Premium adds seven more, for fourteen.) Short answers can be matched
case-insensitively, numerically with a tolerance, or against a regular expression, and each
blank in a passage carries its own accepted answers. Every question carries an explanation
that can be shown after marking, and essays carry a rubric.

**Pictures in questions.** Upload an image straight into a question, or onto each option of
a picture-choice question, from inside the question editor. Uploads are limited to PNG, JPG,
GIF and WebP and are checked by decoding the file, not by trusting its name — so a site that
allows SVG elsewhere still cannot put one in an exam. Each image asks for a description for
people who cannot see it, and a site can make that required. The images are ordinary media
library attachments.

**Exams built from sections.** A section is either a fixed list of questions or a pool with
rules — subject, tag, difficulty, type, how many to draw. Questions can be shuffled, and so
can the options within them.

**A runner that behaves like an exam.** The timer is server-side, so changing the clock on
the candidate's machine does nothing. The paper is FROZEN when they start: editing the exam
half-way through does not change what somebody is already answering. Answers autosave as
they go, the paper can be paused and resumed if you allow it, and an abandoned attempt is
closed by a sweep rather than sitting open forever.

**It works without JavaScript.** The runner degrades to an ordinary form post.

**Marking.** Everything that can be marked automatically is marked on submission. What needs
a person — essays, and anything else you have chosen to mark by hand — goes into a queue
with the rubric beside it. Results can be released immediately, when the exam closes, on a
date you choose, or only when you say so.

**Access.** An exam can be open to everybody, restricted to signed-in users by role, opened
with an access code, or taken by a guest who gives a name and an email. Attempt caps and
cooldowns are enforced server-side.

**Getting questions in and out.** Import from CSV, Aiken, GIFT, Moodle XML and QTI. Export
to CSV, Moodle XML and QTI.

**A place for learners to look.** A dashboard at /my-exams/ listing what has been assigned
to them, what they have scored and what they have earned, plus blocks and shortcodes for the
same three lists anywhere on the site. It is server-rendered, so it works with JavaScript
switched off, and it is never cached — one person's results must not be served to the next
visitor.

**Learners can see what they have already done.** A signed-in learner sees where they stand
on every exam in the catalogue, in their assigned list and on the exam page itself — passed,
not passed, still in progress, or submitted and awaiting a result. An exam they have passed
moves out of "Ready to sit". A score that the exam's release policy is still withholding is
never shown, only that the paper was submitted. Note for sites with a page cache: the
catalogue and an exam's page carry one person's own history once they are signed in or have
started an exam as a guest, so for them those pages are served uncached. For a first-time
visitor they are unchanged and still cacheable, and looking at an exam sets no cookie. The
exam runner and result pages are always uncached. Private pages send no-cache headers and
the DONOTCACHEPAGE constant that WP Super Cache, W3 Total Cache, WP Rocket and LiteSpeed
Cache honour.

**Certificates people can actually use.** A certificate is awarded automatically when
somebody passes and their marking is finished, numbered to a scheme you choose, and
rendered as a real PDF rather than a web page they have to print. It carries a QR code
to a public verification address that anyone can check without an account, and it says
whether the certificate is valid, withdrawn or expired without disclosing the mark. A
learner finds everything they have earned at /my-certificates/. Arabic and Hebrew names
are shaped and ordered properly, which is not something a browser's print button does.
The free version gives you one certificate design and lets you edit all of it: the
wording, the paper, the orientation, how long it stays valid, the numbering, and the
layout itself. Premium adds a drag-and-drop designer, as many designs as you like, and
a different certificate per exam.

**Everything else.** Result emails with eleven editable templates, a role system with five
seeded roles, a schedule calendar, a dashboard, a catalogue block and shortcodes, and full
GDPR export and erasure through WordPress's own privacy tools.

**All of the source is in the plugin.** The admin screens and the exam runner are compiled
JavaScript, and their human-readable TypeScript source ships beside them in
assets/backend/app/src/ and assets/frontend/app/src/, with the build configuration and a
lockfile. Every compiled file names that source on its first line of code. See "Source Code"
below.

= What the premium version adds =

Seven more question types — ordering, matching, extended matching, hotspot (mark a point
or an area on a picture), image matching, range, and file upload for handing in
coursework — a gradebook with letter grades and weighted
transcripts, AI question drafting with your own API key, bridges into Tutor LMS, LearnDash
and LifterLMS, a drag-and-drop certificate designer with as many designs as you like and a
different one per exam, live
invigilation with a real-time board, analytics with item analysis and scheduled reports,
cohorts, prerequisites (an exam can require that another one has been passed, taken, or
scored above a mark), custom fields, version history, practice mode with per-question
feedback, LaTeX rendering, integrity signals, opt-in public leaderboards,
webhooks, LTI 1.3, API tokens, multilingual content (one exam served in several languages, with one result set and one item analysis), and a demo dataset.

= Privacy =

The plugin stores what an exam plugin has to store: who sat what, when, what they answered
and what they scored. Files handed in as answers are kept OUT of the media library — they go
in a private uploads directory under an unguessable name, with a deny rule beside them, and
are served only through a route that checks who is asking. An attachment would have sat at a
public URL that no permission check ever runs on. It ships GDPR exporters and erasers, so a personal-data request made
through WordPress's own tools includes and removes exam data too. It sets no cookies for
tracking; the only cookie it sets is a signed identifier for a guest who starts an exam
without an account, so their paper survives a page reload. It is set when they press Start,
never for looking at an exam.

== Installation ==

1. Upload the plugin folder to `/wp-content/plugins/`, or install the ZIP through
   Plugins → Add New → Upload Plugin.
2. Activate it.
3. Open **Exams** in the admin menu. Create a subject, write a question, build an exam.

The plugin creates its own database tables on activation and removes them on uninstall.

== Frequently Asked Questions ==

= Can somebody cheat by changing their computer's clock? =

No. The deadline is computed and stored on the server when the attempt starts, and every
write checks it. The countdown in the browser is decoration.

= What happens if I edit an exam while somebody is sitting it? =

Nothing, for them. The set of questions, their order, their points and the runner's settings
are frozen into the attempt when it starts. Your edit applies to the next person who begins.

= Does a pool draw a different paper for each candidate? =

Yes. A pool section draws when the attempt starts, and that draw is then frozen for that
candidate.

= Can people take an exam without an account? =

Yes, if you set the exam's access to guest. They give a name and an email, and the plugin
identifies them with a signed cookie. No PHP session is used.

= Is the free version time-limited or feature-locked? =

Neither. The free version is a complete exam plugin. The premium version is a separate
download that adds features; nothing in the free build is disabled, and the free build
contains no premium code to unlock.

One number is worth stating plainly rather than leaving you to find it: the free version
keeps one certificate design. That is not a locked second design — the designer that draws
them is simply not in the free download — and the one you have is fully editable, layout
included. Everything a certificate does is free: it is awarded, numbered, rendered and
verified exactly as it is in premium.

= Is it translation-ready? =

Yes, and that includes the parts that are easy to miss. The admin screens, the exam runner
your candidates actually sit, the block editor controls, the front-end templates and the
notification emails all read from the same text domain (plugixa-exam), so one translation
covers the whole plugin rather than the half a visitor never sees.

Translations can come from three places, and the plugin looks in all of them. Anything
translate.wordpress.org has produced is picked up automatically. A catalogue you place in
wp-content/languages/plugins/ overrides it. Anything shipped inside the plugin's own
languages/ folder is used when neither of the first two has a file. Tools such as Loco
Translate work normally.

Two details worth knowing. Dates and numbers follow the site's locale and timezone, not the
reader's browser — so an exam that opens at 09:00 says 09:00 to everybody. And notification
emails are composed in the LANGUAGE OF THE PERSON RECEIVING THEM where the plugin can tell
who that is: a learner with a French account gets French dates and a French pass or fail,
even when the marker who released the result reads the site in English. The subject and body
wording itself comes from the template you edit under Exams -> Notifications, so that is in
whichever language you write it in.

= Can one exam be offered in several languages? =

Not yet. Translating the PLUGIN and translating your QUESTIONS are different jobs: the first
is a gettext catalogue, the second is a second version of your content. Today a bilingual
site needs one exam per language. Support for one exam carrying several language versions —
so both sit the same paper and land in the same results and item analysis — is planned.

== External services ==

This plugin connects to no external service of its own choosing. It does not load fonts from
a CDN — the KaTeX fonts used by the premium maths rendering are bundled and served from your
own site — and it does not contact plugixa.com; our address appears in the plugin header and
in copyright notices, and no code calls it.

Four services are described below. Freemius handles licensing and updates. The other three
are premium features that contact nothing until an administrator has entered an address or a
key: they have no default endpoint, and on a site where nobody has configured them no request
is ever made.

The plugin is published by Plugixa. Our policies are here:
Terms of Service: https://www.plugixa.com/terms-of-service
Privacy Policy: https://www.plugixa.com/privacy-policy

= Freemius (free and premium) =

What it is and why: Freemius, Inc. provides licensing, plugin updates and checkout, plus —
only if you opt in — anonymous usage diagnostics.

What is sent and when: on activation you are shown an opt-in screen. If you skip it, no
identifiable data is sent. If you opt in, or when you activate or manage a licence or make a
purchase, data such as your site URL, WordPress and PHP versions, active theme and plugins,
and the email address used at opt-in may be transmitted. This happens on activation, on
update checks, and on licence actions.

Terms of Service: https://freemius.com/terms/
Privacy Policy: https://freemius.com/privacy/

= AI question writing (premium only, and only with an API key you supply) =

What it is and why: the plugin can draft questions from source material you paste — a
syllabus, a page of notes, a chapter — so you have something to edit instead of a blank
form. To do that it sends your material to a language-model provider that you choose and
pay for directly.

What is sent and when: at the moment somebody presses Generate, and never otherwise, the
plugin sends the text you pasted together with the instruction built from your choices on
that screen (how many questions, which types, what difficulty, which language). Nothing
else is sent: not your site address, not who you are, not any exam, attempt, mark or
learner data. The reply is shown to you for review and nothing is written to your question
bank until you accept it.

There is no default key and no key is bundled. Until an administrator enters one under
Settings, AI, this feature makes no request at all and the screen says so. The key is
stored on your own site, is never included in any response the plugin sends to a browser,
and is transmitted only to the endpoint you configured — over https, unless that endpoint
is on your own machine or private network.

Whose service it is: whichever provider you choose and hold an account with. The plugin
ships two ways to connect and no relationship with either company.

OpenAI, and any service that speaks its chat-completions API — which includes Azure OpenAI,
OpenRouter, Groq, Together, and local runtimes such as Ollama and LM Studio, reached by
changing the endpoint:
Terms of Use: https://openai.com/policies/terms-of-use
Privacy Policy: https://openai.com/policies/privacy-policy

Anthropic (Claude):
Commercial Terms of Service: https://www.anthropic.com/legal/commercial-terms
Privacy Policy: https://www.anthropic.com/legal/privacy

If you point the endpoint at a different service, its terms and privacy policy are the ones
that apply, and this plugin has no relationship with it.

= Webhooks (premium only, and only to an address you enter) =

What it is and why: if you register a webhook endpoint, the plugin sends an HTTP POST to it
when an attempt is submitted or graded, or when a certificate is issued, so another system
of yours can react.

What is sent and when: the event name and the relevant attempt or certificate data, signed
with a secret the plugin generates, at the moment the event happens. It is sent only to
addresses an administrator has entered. There is no default endpoint, private and reserved
network addresses are refused unless you deliberately allow them, and no data leaves the
site until you add one.

Whose service it is: yours, or whichever third party you point it at. This plugin has no
relationship with it and no endpoint of its own.

= LTI 1.3 (premium only, and only with a platform you register) =

What it is and why: if you register a learning platform (an LMS such as Moodle or Canvas),
the plugin lets its users launch into an exam here and sends their score back.

What is sent and when: the plugin fetches the platform's public signing keys from the JWKS
URL you supply, in order to verify that a launch really came from it. When an attempt is
graded, the score and the attempt's timestamp are posted back to the grade endpoint the
launch supplied. Both happen only for platforms an administrator has registered.

Whose service it is: yours, or your institution's. The URLs are the ones you enter; the
plugin ships no default platform and contacts nothing until you register one.

== Source Code ==

No compiled file in this plugin ships without its source. The complete, human-readable
source for every generated JavaScript and CSS file is included in the plugin itself —
nothing has to be downloaded from anywhere to read, modify or rebuild it. Each app
directory contains its build configuration and a lockfile pinning every dependency to an
exact version, so the build is reproducible.

Every compiled file says so itself. It opens with a comment naming its source directory,
the rebuild command and the licence, and the same pointer sits at the start of its first
line of code — and, where it is safe to put one there, at the start of its longest line.
Each build directory also carries a SOURCE.txt, and each bundle a .js.map whose
sourcesContent holds the original source of that bundle, third-party packages included.

Compiled output -> the source it is built from:

* assets/backend/build/js/*.js and assets/backend/build/css/main.css
  <- assets/backend/app/src/  (React 19, Material UI, TanStack Query, TypeScript)
     build config: assets/backend/app/vite.config.js, tsconfig.json
     dependencies: assets/backend/app/package.json + package-lock.json

* assets/frontend/build/js/*.js and assets/frontend/build/css/frontend.css
  <- assets/frontend/app/src/  (the exam runner and the learner portal)
     build config: assets/frontend/app/vite.config.js, tsconfig.json
     dependencies: assets/frontend/app/package.json + package-lock.json

The hashed filenames under assets/backend/build/js/ are produced by Vite's code splitting;
each corresponds to the module of the same name under assets/backend/app/src/ (for example
ExamForm-*.js is built from assets/backend/app/src/modules/exams/ExamForm.tsx). Files named
after third-party packages come from those packages as published on npm at the exact
versions recorded in package-lock.json, unmodified.

THE GUTENBERG BLOCKS ARE NOT COMPILED AT ALL. blocks/*/index.js and pro/blocks/*/index.js
are plain JavaScript written against the `wp.*` globals WordPress already provides — no JSX,
no bundler, no minification. What ships is what was written.

To rebuild, with Node.js 20.19 or newer (or 22.12 or newer — the versions Vite 8 requires),
run in each of assets/backend/app and assets/frontend/app:

    npm ci
    npm run build

The build writes to assets/backend/build and assets/frontend/build, and then stamps the
files it wrote (assets/build-tools/stamp-source-banner.mjs, plain JavaScript). The admin
build also stamps the Freemius SDK's shipped assets, so run `composer install` in the plugin
root first.

PHP dependencies are managed with Composer. The only third-party PHP in the free plugin is
the Freemius SDK (licensing and updates), declared in the root composer.json and pinned in
composer.lock; the premium build declares its own libraries in pro/composer.json. Its minified JavaScript and CSS under vendor/freemius/wordpress-sdk/assets/
are shipped unmodified from the upstream project, https://github.com/Freemius/wordpress-sdk
(GPL-3.0), and each names that project on its first line of code.

== Changelog ==

= 1.0.0 =
* First public release.
* Fill in the blanks, picture choice and image uploads into questions are part of the free version. Premium keeps its other seven question types.
* Certificates are part of the free version: awarded on a pass, numbered, rendered to PDF, verifiable by QR code, and listed in the learner's portal. A free site gets one certificate design and can edit every part of it, including its layout. Premium adds the drag-and-drop designer, more than one design, and a different certificate per exam.
* An exam can now be given its own certificate design. The setting had been in the database since the first premium release with no control anywhere that set it, so every exam used whichever design was published first.
* Certificates are set in DejaVu Sans, and the PDF engine's other bundled fonts are no longer shipped. Nothing could ever ask for them — there is no font selector — and they were three megabytes of the download.
* Fixed: uploading an image from the question editor always failed with "No image arrived". The editor sent the file in a format that lost it on the way; the server had been accepting uploads correctly all along.
* Fixed: picture-choice and image-matching questions could not be saved from the question editor. The option list had no way to mark the correct picture or say what an image pairs with, and the save was then refused for exactly that.
* Fixed: with JavaScript switched off, picture questions showed no pictures and the hotspot grid lost its layout.
* Fixed: switching Images off in the App Manager also switched off the picture question types, so their papers would have scored zero with no warning. It now only removes the upload button.
* Fixed: result emails were only ever sent while an administrator had the notifications screen open. The queue is drained on a schedule now, as it was always meant to be — the plugin was declaring its five-minute interval one line after asking WordPress to use it, so every request was refused and silently retried for ever.
* Fixed: on a premium site running WooCommerce, every page of the front end was blank. The API-token listener asked WordPress for its own REST address during plugin load, which is before WordPress has built the rewrite rules that answer depends on.
* Fixed: on a site using Polylang with language directories, an exam had no address in any language but the default — /fr/exams/ and everything under it said the page did not exist. All six of the plugin's front-end routes answer under a language prefix now, in the free version as well as premium.
* Fixed: exam pages could report themselves missing on a site with no blog posts at all, with no translation plugin anywhere in it.
* Fixed: the scheduled-report email template was still frozen into whichever language the site was in when the plugin was installed, which is the one template the 0.7.0 fix could not reach.

= 0.7.0 =
* The whole plugin is translatable, and French, Arabic, Spanish and German are bundled. That includes the screens a candidate sees: the exam runner and the learner portal had no translation mechanism at all, so they reverted to English on every translated site.
* Right-to-left support. On an Arabic or Hebrew site the admin and the front end are mirrored, not just the text.
* Certificates are rendered by a PDF engine that shapes Arabic and reorders bidirectional text. The previous one produced disconnected letterforms in reverse order, and the verification page had always been correct — only the download was wrong.
* Result emails go out in the language of the person receiving them, not the language of whoever triggered the queue.
* Role names and email templates are no longer frozen into whichever language the site happened to be in when it was installed.
* Premium: multilingual content. One exam served in several languages, with one question bank, one result set and one item analysis — a French sitting and an Arabic sitting of the same question are the same question, not two exams about the same thing. Accepted answers for short-answer and fill-in-the-blank questions can be given per language, so a candidate answering in French is not marked against an English answer key.
* An attempt records the language it was started in, so switching the site language mid-exam cannot reword a question underneath somebody.

= 0.6.0 =
* Draft questions with AI, using your own API key. Paste a syllabus or a page of notes and get multiple-choice, true/false, short-answer and essay questions to review. Works with OpenAI, with Anthropic's Claude, and with any OpenAI-compatible endpoint — including a model running on your own machine.
* Nothing is generated until you enter a key, and nothing reaches your question bank until you read it and accept it. Questions are added unpublished, through the same route and the same validation as one you typed.
* The review screen marks the correct answer on every draft and warns about the failure that matters: a question with nothing marked correct, which saves and renders perfectly and marks every candidate zero.
* Your API key is stored on your site and is never sent to a browser by any route.

= 0.5.0 =
* LMS bridges for LearnDash and LifterLMS, alongside the existing Tutor LMS one. An exam can be restricted to learners enrolled on a course in any of them, and passing it can mark that course finished.
* The three bridges are one feature rather than three: an exam names one course in one system, so it cannot be linked into two at once and complete both.
* The course picker now reads from this plugin rather than from WordPress's REST API, which listed courses only for LMSs that opt their course type into it — LifterLMS does not, so its courses never appeared.
* Fixed: eight features showed as raw slugs in the App Manager (`advancedtypes`, `api_tokens`, `customfields`, `fileanswers`, `importexport`, `tutorbridge`, `versioning`, `visualtypes`) because that screen labels a switch with its own id.

= 0.4.0 =
* A Tutor LMS bridge. An exam can be restricted to learners enrolled on a course, and passing it can mark that course finished — so the exam engine works inside courses instead of competing with them. Inert when Tutor LMS is not installed.
* Fixed: the per-exam `settings` bag was never written by the API, so anything stored there was lost on the next load.
* File upload questions: candidates hand in coursework, portfolios, spreadsheets or code, marked by a person alongside the rest of the paper. Submissions are kept out of the media library, in a directory that is sealed and served only through a permission-checked route.
* A gradebook. Define letter grades with their own boundaries, points and colours; every result shows its band, and a learner's portal shows a transcript across everything they have sat, weighted per exam.
* Only released marks reach a transcript — a withheld score is absent rather than hidden, because an overall that moves would disclose it anyway.

= 0.3.0 =
* The learner portal is now part of the free version. Anyone who sits an exam can see what they have been assigned, what they scored and what they have earned, at /my-exams/ or through blocks and shortcodes. Public leaderboards remain premium.
* Eight premium question types instead of four: image choice, hotspot, image matching and range join ordering, matching, extended matching and fill-in-the-blanks.
* Hotspot questions ask the examinee to mark a point or drag out an area on a picture, and are graded on where they marked — with a fallback grid so they still work without JavaScript.
* Exam prerequisites: an exam can require that another has been sat, or passed to a given standard, first.
* Roles can now be scoped to an author's own work. "Edit own" was offered on the Roles screen but never enforced; it is now, for both questions and exams.
* Fixed: the free download contained the compiled premium learner-portal control, and the premium front-end source was not declared premium-only.

= 0.2.0 =
* Question stems, explanations, exam instructions, pass and fail messages and subject descriptions are now written in WordPress's own rich text editor instead of a plain textarea.
* Email templates can be written and sent as HTML. Existing templates are untouched and keep sending as plain text until you switch them.
* Fixed: scheduled analytics reports arrived as raw HTML markup instead of a table.
* Added an in-app notifications feed — a bell in the admin bar, and a `[plugixa_exam_alerts]` shortcode for learners.

= 0.1.0 =
* First release.

== Upgrade Notice ==

= 1.0.0 =
First public release. Nothing to upgrade from.

= 0.7.0 =
French, Arabic, Spanish and German translations, right-to-left layouts, and certificates that render Arabic correctly. Nothing to do after updating: a site already in one of those languages stops being in English. Premium adds multilingual content.

= 0.3.0 =
The learner portal moves from premium to free — no action needed, and premium sites keep their leaderboards. Adds four picture-based question types and exam prerequisites. If you had granted a role "Edit own" expecting it to limit that role to its own work, it now does.

= 0.2.0 =
Adds a rich text editor, HTML email templates and an in-app notifications feed. Existing email templates keep sending exactly as they do today.

= 0.1.0 =
First release.
