The Assignment Builder and Grader (React)
==========================================
The assignment builder and the grader are a single React single-page
application that instructors use to create assignments, choose or author
exercises, grade student work, and manage grades. It lives in
``bases/rsptx/assignment_server_api/assignment_builder`` and is served by the
FastAPI **assignment server** (``bases/rsptx/assignment_server_api``). In a
running Runestone instance the app is reached at:
* ``/assignment/instructor/builder`` -- the assignment builder
* ``/assignment/instructor/grader`` -- the grader
Both URLs load the same bundle; client-side routing selects the view. The
app replaces the old web2py instructor pages, and a couple of the very first
React screens it replaced are themselves still around as legacy routes
(``/builderV2`` and ``/graderOld``) -- see `Two generations of code`_ below.
.. figure:: ../images/AssignmentBuilderArch.svg
:width: 100%
:alt: The React SPA and the Runestone interactives bundle run in the
browser and talk to the FastAPI assignment server, which serves the
HTML shell, the JSON API, and the built static assets; a separate
build pipeline produces those assets.
Overview of the assignment builder and grader. The left side is the
runtime picture (browser above, assignment server below); the right
column shows how the pieces are built, tested, and shipped.
Technology stack
----------------
* **React 18** with **TypeScript** (``strict`` mode; some older files are
still plain ``.jsx``).
* **Vite** for the dev server and production build.
* **Redux Toolkit** for state, with **RTK Query** for all server
communication.
* **react-router v7** with URL-driven state (the URL is the source of truth
for which assignment, tab, wizard step, or student is showing).
* **Mantine** as the UI component library (plus a small set of in-house
components in ``src/components/ui``: DataGrid, TreeTable, EditableTable,
notify, ...).
* **TipTap** for rich-text question authoring, **Monaco** for code editing,
**dnd-kit** for drag and drop, **TanStack Table** for grids,
**driver.js** for the built-in feature tours.
* **vitest** + Testing Library for unit tests, **Playwright** for end-to-end
tests, **eslint**/**prettier** for style.
How the app is served
---------------------
Development
~~~~~~~~~~~
In development the app runs on Vite's dev server (port 5173).
``vite.config.ts`` proxies API paths (``/ns`` and ``/assignment``) to
``http://localhost``, where the normal Docker composition (or ``dstart``
servers) must be running. Authentication rides along on the Runestone
session cookie: log in as an instructor at ``http://localhost`` first, then
open ``http://localhost:5173`` -- the cookie is shared because both are on
``localhost``.
Two things that a served book page normally provides are stubbed in
development by ``index.html``:
* ``eBookConfig`` is created as an *empty* object. Anything that reads
``window.eBookConfig`` (for example the course name in the navbar) sees
defaults in dev mode.
* The Runestone interactives bundle (the webpack build of
``bases/rsptx/interactives``) is loaded dynamically by reading
``webpack_static_imports.json`` from ``VITE_LOAD_PREFIX``
(``/runestone`` in dev, see ``.env.development``). Vite serves that
prefix from the checked-in snapshot in ``public/runestone``. If you need
newer component JavaScript in dev, rebuild the interactives and refresh
that snapshot.
The interactives bundle is what lets the builder and grader render real
Runestone questions (previews, student answers): it registers
``window.component_factory`` entries which the React code invokes through
``src/componentFuncs.js``.
Production
~~~~~~~~~~
``npm run build`` type-checks (``tsc -p tsconfig.build.json``) and then has
Vite emit the production bundle **into the sibling directory**
``bases/rsptx/assignment_server_api/react`` together with a
``.vite/manifest.json``. The assignment server:
* mounts that directory as static files at ``/assignment/react``
(``core.py``),
* serves the page itself from the ``get_builder`` endpoint in
``routers/instructor.py``. That endpoint verifies the user is an
instructor for the course, then renders the Jinja template
``components/rsptx/templates/assignment/instructor/builder.html``, which
- injects the hashed JS/CSS names read from the Vite manifest
(``get_react_imports`` in ``rsptx.response_helpers``),
- injects the per-course Runestone interactives imports
(``get_webpack_static_imports``),
- emits a real ``eBookConfig`` (course, user, ``author`` name, LaTeX
preamble, ...),
- provides the ``
`` mount point.
So in production the React app is a set of static assets plus a
server-rendered shell page; there is no server-side React.
The build is wired into the normal service build:
``projects/assignment_server/build.py`` runs ``npm install`` and
``npm run build`` in ``assignment_builder`` (it requires the interactives to
have been built first), and ``uv run build --service assignment full``
rebuilds wheel, image, and container. The wheel deliberately excludes
``react/runestone``, ``public/runestone``, ``node_modules`` and other heavy
frontend artifacts (see ``projects/assignment_server/pyproject.toml``).
Application architecture
------------------------
Entry point and shell
~~~~~~~~~~~~~~~~~~~~~
``src/index.tsx`` creates the Redux store provider and the Mantine
providers (theme, modals, notifications) and renders ``App``.
``src/App.tsx`` builds the router. Every route renders inside
``AppContent``, which provides the application shell: the top navigation
bar (``src/components/shell/AppNavBar``, fed by ``navUtils.ts`` and
``eBookConfig``) and the scrollable content region. Routes matching
``/builder`` or ``/grader`` render "full bleed"; everything else is wrapped
in a centered container.
Routing: the URL is the state
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The route table in ``App.tsx`` is the map of the application::
/ -> AssignmentBuilder (lazy)
/builder -> AssignmentBuilder (lazy)
/builder/create[/:step] -> creation wizard
/builder/:assignmentId[/:tab] -> edit an assignment (basic|readings|exercises)
/builder/:id/exercises/:viewMode/... -> exercise list / browse / search / create / edit
/grader -> Grader (lazy), assignment list
/grader/gradebook -> gradebook
/grader/:assignmentId -> question list for an assignment
/grader/:assignmentId/questions/:questionId[/students/:sid]
-> grade one question / one student
/builderV2, /graderOld -> legacy first-generation screens
/except -> deadline-exception scheduler
The two big route components are loaded lazily so the builder does not pay
for the grader's code and vice versa. Inside the builder, the hook
``useAssignmentRouting`` parses the URL into a typed route state (mode,
wizard step, active tab, exercise view mode, exercise type/step, ...) and
provides navigation helpers; components never keep "which screen am I on"
in local state. Deep links and browser back/forward therefore just work,
and the e2e tests rely on that.
State management
~~~~~~~~~~~~~~~~
All server communication goes through **RTK Query** APIs, one per feature
folder in ``src/store``::
src/store/
assignment/ assignment CRUD (assignment.logic.api.ts + assignment.logic.ts)
assignmentExercise/ exercises attached to an assignment
readings/ readings attached to an assignment
exercises/ question CRUD / search
chooseExercises/ the book-tree exercise chooser (slice only)
searchExercises/ smart search state (slice only)
dataset/, datafile/ supporting data
grader/ everything the grader needs
user/ current user info
The convention within a feature folder is ``
.logic.api.ts`` for the
RTK Query API (endpoints, cache tags) and ``.logic.ts`` for a
companion slice holding client-side state. All endpoints hit the FastAPI
assignment server under ``/assignment/instructor/...``; the grader API uses
``/assignment/instructor/grader/...``. ``src/store/baseQuery.ts`` supplies
the shared ``fetchBaseQuery`` plus an error-handling wrapper that pops a
Mantine notification on failures (with a specific message for expired
sessions).
Two generations of code
~~~~~~~~~~~~~~~~~~~~~~~
There are **two** state trees and two component styles in the repository,
and it is important to know which is which:
* ``src/store`` + ``src/components/routes`` -- the current generation:
TypeScript, RTK Query, Mantine. This is where new work goes.
* ``src/state`` + ``src/renderers`` -- the first generation: plain ``.jsx``
renderers and hand-written slices (``acSlice``, ``assignSlice``,
``ePickerSlice``, ...). These power the legacy ``/builderV2`` and
``/graderOld`` routes and a few shared selectors (for example
``selectIsAuthorized``, which gates the whole app).
The store that actually runs is ``src/state/store.ts``; it combines *both*
generations' reducers. Watch out for one naming trap: the key
``assignment`` in the live store belongs to the **legacy** ``assignSlice``,
while the new ``assignmentSlice`` is registered as ``assignmentTemp``.
(``src/store/store.ts`` builds a store from only the new reducers and is
used by tests.) When the legacy routes die, the intent is for
``src/store/rootReducer.ts`` to become the real root.
The assignment builder
~~~~~~~~~~~~~~~~~~~~~~
``src/components/routes/AssignmentBuilder`` contains:
* ``components/list`` -- the assignment list (entry screen).
* ``components/wizard`` -- the three-step creation wizard
(basic info, assignment type, visibility).
* ``components/edit`` -- the edit view with its ``basic``/``readings``/
``exercises`` tabs.
* ``components/reading`` -- picking book sections to read.
* ``components/exercises`` -- the largest subsystem. The exercises tab can
list what is assigned (``AssignmentExercisesList``), browse the book tree
(``ChooseExercises``), search the question bank (``SearchExercises``), or
author a brand new question (``CreateExercise``).
* ``hooks`` -- ``useAssignmentRouting`` (above), ``useAssignmentForm``,
``useAssignmentState``, name validation.
``CreateExercise`` is a small framework of its own. ``ExerciseFactory.tsx``
switches on the question type and renders one editor component per type
(``MultiChoiceExercise``, ``ActiveCodeExercise``, ``ParsonsExercise``,
``FillInTheBlankExercise``, ``DragAndDropExercise``, ``ClickableAreaExercise``,
``PollExercise``, ``ShortAnswerExercise``, ``MatchingExercise``,
``SelectQuestionExercise``, ``IframeExercise``). Each editor is a stepped
form built on ``BaseExerciseForm`` and shared TipTap-based inputs; shared
step/validation plumbing lives in ``CreateExercise/shared`` and
``CreateExercise/hooks``. Display metadata for the type chooser (family,
label, tag, icon, color) comes from ``src/config/exerciseTypes.ts``.
Question JSON import (see :doc:`question_json_schema`) is handled by
``ImportQuestionJsonModal`` with helpers in ``src/utils/importQuestionJson.ts``.
Previews render the actual Runestone component: helpers in
``src/utils/preview`` generate the component HTML for each question type
and ``renderRunestoneComponent`` in ``src/componentFuncs.js`` asks the
globally loaded interactives bundle (``window.component_factory``) to bring
it to life inside a ref'd div.
The grader
~~~~~~~~~~
``src/components/routes/Grader`` is organized as pages plus supporting
components and hooks:
* ``pages/GraderAssignmentsPage`` -- pick an assignment.
* ``pages/GraderQuestionsPage`` -- per-question stats for the assignment.
* ``pages/GraderQuestionPage`` -- the working view: a student list sidebar,
the student's submission rendered as a live Runestone component
(``SubmissionPane``), and the grading panel (``GradePanel``).
* ``pages/GraderGradebookPage`` -- the gradebook grid.
Notable behaviors, each with its own hook or component: grade autosave
(``useAutoSaveGrade`` + ``SaveStatusPill``), keyboard-driven grading
(``useGraderHotkeys``, ``ShortcutsHelpDialog``), multi-student grading
(``MultiGradeDialog``, ``StudentMultiSelect``), regrading with a preview
step (``RegradeWizard``), releasing grades and pass thresholds
(``ReleaseGradesControl``, ``ThresholdControl``), manual totals, deadline
accommodations (``DeadlineExceptionDialog``), and a driver.js onboarding
tour (``useGraderTour``, ``tour/``). The server side for all of this is
``routers/grader.py`` (endpoints for answers, history, grades, regrade
preview/apply, release, thresholds, gradebook data, rosters,
accommodations).
Getting started for programmers
-------------------------------
Prerequisites
~~~~~~~~~~~~~
Node 18+ and npm 8+ (enforced in ``package.json``), plus a working local
Runestone (see :doc:`docker`). All commands below run from
``bases/rsptx/assignment_server_api/assignment_builder``.
The edit/run loop
~~~~~~~~~~~~~~~~~
1. Start the backend: ``docker compose --profile basic up`` from the repo
root (or the ``dstart`` development servers -- then adjust the proxy
ports in ``vite.config.ts``).
2. Log in at ``http://localhost`` as an instructor (``testuser1`` in the
standard dev database).
3. ``npm install`` (first time), then ``npm start``.
4. Open ``http://localhost:5173`` for the builder,
``http://localhost:5173/grader`` for the grader. Vite hot-reloads your
edits; API calls are proxied to the backend from step 1.
Where things go
~~~~~~~~~~~~~~~
============================================ ====================================================
Path What lives there
============================================ ====================================================
``src/App.tsx`` route table
``src/components/routes/AssignmentBuilder`` builder screens, wizard, exercise editors
``src/components/routes/Grader`` grader pages, grading widgets, hotkeys, tour
``src/components/shell`` navbar and app shell
``src/components/ui`` reusable widgets (DataGrid, TreeTable, notify, ...)
``src/store/`` RTK Query API + slice for one feature
``src/hooks`` cross-feature hooks
``src/config/exerciseTypes.ts`` exercise-type metadata (labels, families, icons)
``src/utils`` / ``src/utils/preview`` pure helpers; per-type preview HTML generators
``src/types`` shared TypeScript types
``src/state``, ``src/renderers`` legacy generation -- avoid for new work
``e2e/`` Playwright suites
============================================ ====================================================
Path aliases ``@/*``, ``@store/*`` and ``@components/*`` are configured in
both ``tsconfig.json`` and ``vite.config.ts``.
Conventions
~~~~~~~~~~~
* New code is TypeScript with ``strict`` on; prefer function components and
hooks.
* Tests are colocated: ``Foo.tsx`` has ``Foo.spec.tsx`` next to it.
* Lint must be clean -- CI style is ``npm run test:eslint``
(``--max-warnings=0``). Prettier settings are in ``.prettierrc``.
* Server data belongs in an RTK Query endpoint, not in a hand-rolled
``fetch``; client-only state belongs in the feature's slice; screen
identity belongs in the URL.
Testing
~~~~~~~
* ``npm test`` -- vitest in watch mode with coverage (jsdom, Testing
Library; global setup in ``vitest.setup.ts``).
* ``npm run test:e2e`` -- Playwright against the Vite dev server (it logs
in via the backend, so steps 1-2 of the edit/run loop must be running).
``npm run test:e2e:smoke`` runs the ``@p0`` subset;
``test:e2e:docker`` targets the app as served by Docker/nginx instead of
Vite (``E2E_TARGET=docker``).
* One-off suites live under ``e2e/`` (assignments, exercises,
grader, wizard, tiptap, ...) with fixtures in ``e2e/fixtures``.
Common tasks
~~~~~~~~~~~~
**Adding a server-backed feature.** Add the endpoint to the FastAPI side
(``routers/instructor.py`` or ``routers/grader.py``), then add an endpoint
to the feature's ``*.logic.api.ts`` with appropriate cache tags so lists
refetch when mutations invalidate them. Components consume the generated
hooks (``useGetXQuery``, ``useUpdateXMutation``).
**Adding a screen or sub-view.** Add the path to the route table in
``App.tsx``; for builder sub-views also extend ``useAssignmentRouting`` so
the new URL shape round-trips to route state. Keep the component lazy if
it is heavy.
**Adding a new exercise type editor.** The checklist:
1. Register the type in ``src/config/exerciseTypes.ts`` (family, label,
tag, description, icon).
2. Create ``CreateExercise/components/Exercise`` following an
existing editor (start from a simple one like ``PollExercise``); build
the steps on ``BaseExerciseForm`` and the shared inputs.
3. Add the case to ``ExerciseFactory.tsx`` and the export to the folder's
``index.ts``.
4. Add a preview generator in ``src/utils/preview`` that emits the
component's HTML so ``ExercisePreview`` can render it (the interactives
bundle must know the type -- see :doc:`javascript_feature`).
5. If the type supports question-JSON authoring, extend
``src/utils/importQuestionJson.ts`` and see :doc:`question_json_schema`.
6. Server side: make sure ``new_question``/``update_question`` in
``routers/instructor.py`` accept the type and that htmlsrc generation is
correct.
**Shipping.** ``npm run build`` writes the production bundle to
``../react``; build and restart the service with
``uv run build --service assignment full`` (or run
``projects/assignment_server/build.py``, which performs the npm build for
you). Remember the interactives must be built first if their dist is
missing.
Gotchas
~~~~~~~
* **Two store generations.** The running store is ``src/state/store.ts``;
the ``assignment`` key is the *legacy* slice, the new one is
``assignmentTemp``. Put new state in ``src/store``.
* **eBookConfig is empty in dev.** Production pages inject a real one via
the ``builder.html`` template; guard reads accordingly.
* **The interactives bundle is global.** Question previews and submission
rendering depend on ``window.component_factory`` from the separately
built ``bases/rsptx/interactives`` webpack bundle (snapshot in
``public/runestone`` for dev). ``public/jQuery.js`` is still loaded for
the remaining jQuery-dependent interactive (codelens/pytutor).
* **URL first.** Resist adding "current screen" component state; extend
the routing instead, or deep links and the e2e suites will break.
* **Instructor-only.** ``get_builder`` redirects non-instructors, and the
app renders a sign-in message when the first assignment fetch is
unauthorized (``selectIsAuthorized``).
See also ``README.md`` and ``DEV_NOTES.rst`` in the ``assignment_builder``
directory for historical notes.