Repository: PrairieLearn/PrairieLearn
Pull request #11829: Rewrite question configuration docs
Author: login: reteps
Labels: name: documentation
description: Related to user documentation (not code docs)
name: python
description: Requires Python code changes
Created: 2025-04-22T02:57:36Z | Merged: 2025-04-30T00:37:30Z

Split out of #11830.


Diff:
diff --git a/apps/prairielearn/python/prairielearn/question_utils.py b/apps/prairielearn/python/prairielearn/question_utils.py
index 57345fbb09e..5e699b93ddb 100644
--- a/apps/prairielearn/python/prairielearn/question_utils.py
+++ b/apps/prairielearn/python/prairielearn/question_utils.py
@@ -14,7 +14,12 @@
 class PartialScore(TypedDict):
     """A class with type signatures for the partial scores dict.
 
-    For more information see the [element developer guide](../../devElements.md).
+    For more information see the [element developer guide](https://prairielearn.readthedocs.io/en/latest/devElements/).
+
+    Attributes:
+        score: The score for the partial score.
+        weight: The weight of the partial score.
+        feedback: Feedback for the partial score. Typically used by elements.
     """
 
     score: float | None
@@ -30,28 +35,71 @@ class PartialScore(TypedDict):
 # for their answer data, feedback data, etc., but TypedDicts with Generics are
 # not yet supported: https://bugs.python.org/issue44863
 class QuestionData(TypedDict):
-    """A class with type signatures for the data dictionary.
+    """The data dictionary passed to the question element throughout the lifecycle of the question.
+
+    For more information see the [element developer guide](https://prairielearn.readthedocs.io/en/latest/devElements/).
+
+    Attributes:
+        params: Parameters that describe the question variant.
+        correct_answers: The true answer (if any) for the variant.
+        submitted_answers: The answer submitted by the student (after parsing).
+        format_errors: Any errors encountered while parsing the student input.
+        partial_scores: Partial scores for individual variables in the question.
+        score: The total final score for the question.
+        feedback: Any feedback to the student on their submitted answer.
 
-    For more information see the [element developer guide](../../devElements.md).
     """
 
     params: dict[str, Any]
+    """Parameters that describe the question variant."""
+
     correct_answers: dict[str, Any]
+    """The true answer (if any) for the variant."""
+
     submitted_answers: dict[str, Any]
+    """The answer submitted by the student (after parsing)."""
+
     format_errors: dict[str, Any]
+    """A dictionary of format errors encountered while parsing the student input."""
+
     partial_scores: dict[str, PartialScore]
+    """A dictionary of partial scores for each answer in the question."""
+
     score: float
+    """The total final score for the question."""
+
     feedback: dict[str, Any]
+    """Any feedback to the student on their submitted answer. Elements will never read or write to this dictionary, with the exception of the `<pl-external-grader-results>` element."""
+
     variant_seed: str
+    """The random seed for this question variant."""
+
     options: dict[str, Any]
+    """Any options associated with the question."""
+
     raw_submitted_answers: dict[str, Any]
+    """The answer submitted by the student before parsing."""
+
     editable: bool
+    """Whether the question is currently in an editable state."""
+
     panel: Literal["question", "submission", "answer"]
+    """The panel that is being rendered."""
+
     extensions: dict[str, Any]
+    """A list of extensions that are available to be loaded by this element."""
+
     num_valid_submissions: int
+    """The number of valid (not containing format errors) submissions by the student for the current variant."""
+
     manual_grading: bool
+    """Whether the question is being rendered in the manual grading view."""
+
     ai_grading: bool
+    """Whether the question is being rendered in the AI grading view."""
+
     answers_names: dict[str, bool]
+    """A dictionary whose keys list the names of the answers in the question."""
 
 
 class ElementTestData(QuestionData):
diff --git a/docs/assessment/index.md b/docs/assessment/index.md
index dc9ed7b4309..6399039a22b 100644
--- a/docs/assessment/index.md
+++ b/docs/assessment/index.md
@@ -611,7 +611,7 @@ For assessments with type "Exam", each student will only be presented with a sin
 
 For assessments with type "Homework", students will be presented with an unlimited number of attempts for each question. By default, every new attempt corresponds to a different variant of the question, unless:
 
-- the question is set to [`"singleVariant": true` in the question configuration file](../question.md#the-singlevariant-option-for-non-randomized-questions). In this case, students will get unlimited attempts for the same variant.
+- the question is set to [`"singleVariant": true` in the question configuration file](../question/index.md#non-randomized-questions). In this case, students will get unlimited attempts for the same variant.
 
 - the `triesPerVariant` setting is set as below. In this case, the student will have the set number of attempts to correctly answer the question. Once the student answers the question correctly, or the number of tries per variant is exhausted, the student will be given the option to try a new variant.
 
diff --git a/docs/assets/extra.css b/docs/assets/extra.css
index baabe114506..df767827acf 100644
--- a/docs/assets/extra.css
+++ b/docs/assets/extra.css
@@ -156,3 +156,8 @@ code.doc-symbol-json {
 code.doc-symbol-json::after {
   content: 'json';
 }
+
+/* Hide view source button in mkdocstrings tooltips (see mkdocstrings/python/material/*.jinja) */
+.md-tooltip2__inner.md-typeset > .md-content__button.md-icon {
+  display: none;
+}
diff --git a/docs/clientServerFiles.md b/docs/clientServerFiles.md
index 73fd9e9b021..056f2735e6c 100644
--- a/docs/clientServerFiles.md
+++ b/docs/clientServerFiles.md
@@ -47,4 +47,4 @@ From within HTML, `clientFiles` directories can be templated with the following
 
 ## Accessing files from `server.py` question code
 
-See the [accessing files on disk](question.md#accessing-files-on-disk) section for details.
+See the [accessing files on disk](question/server.md#accessing-files-on-disk) section for details.
diff --git a/docs/concepts/index.md b/docs/concepts/index.md
index 879a3acbb78..d1e52aa2d07 100644
--- a/docs/concepts/index.md
+++ b/docs/concepts/index.md
@@ -27,7 +27,7 @@ Each assessment contains:
 
 ### Questions
 
-[**Questions**](../question.md) are all independent, and a given question can be used on many different assessments across many different course instances. They are written in HTML (as a [Mustache template](https://mustache.github.io/mustache.5.html)) and use [elements](../elements.md) to accept student input. Python code can be used to generate random parameters and grade questions. Every time a question is shown to a student with randomly generated parameters, that is called a **variant** of the question.
+[**Questions**](../question/index.md) are all independent, and a given question can be used on many different assessments across many different course instances. They are written in HTML (as a [Mustache template](https://mustache.github.io/mustache.5.html)) and use [elements](../elements.md) to accept student input. Python code can be used to generate random parameters and grade questions. Every time a question is shown to a student with randomly generated parameters, that is called a **variant** of the question.
 
 ![An example homework question](./hw-question.png)
 
@@ -153,7 +153,7 @@ Now that you have an understanding of the key concepts in PrairieLearn, you can
 You can also:
 
 - learn more about [courses](../course/index.md) or [course instances](../courseInstance.md)
-- learn more about [questions](../question.md), [elements](../elements.md), or [custom elements](../devElements.md)
+- learn more about [questions](../question/index.md), [elements](../elements.md), or [custom elements](../devElements.md)
 - learn more about [assessments](../assessment/index.md)
 - learn more about [external grading](../externalGrading.md) or [workspaces](../workspaces/index.md)
 - learn how to [develop locally](../installing.md)
diff --git a/docs/course/index.md b/docs/course/index.md
index 392de1f3b28..d6eac4120f1 100644
--- a/docs/course/index.md
+++ b/docs/course/index.md
@@ -28,7 +28,7 @@ exampleCourse
     `-- secret1.js
 ```
 
-1. See the [questions documentation](../question.md) for more information.
+1. See the [questions documentation](../question/index.md) for more information.
 2. See the [course instance documentation](../courseInstance.md) for more information.
 3. See the [`clientFiles` and `serverFiles` documentation](../clientServerFiles.md) for information.
 4. See the [`clientFiles` and `serverFiles` documentation](../clientServerFiles.md) for information.
@@ -373,7 +373,7 @@ The tag order in `infoCourse.json` is the order in which the tags will be listed
 
 ## Sharing Sets
 
-Questions can be added to sharing sets to enable other courses to use your questions in their assessments. Sharing sets are designed for sharing questions only to specific courses, if you would like to make questions available for anyone to uses, you should make them publicly shared as explained in the [question documentation](../question.md), or the [sharing documentation](../questionSharing.md).
+Questions can be added to sharing sets to enable other courses to use your questions in their assessments. Sharing sets are designed for sharing questions only to specific courses, if you would like to make questions available for anyone to uses, you should make them publicly shared as explained in the [question documentation](../question/index.md), or the [sharing documentation](../questionSharing.md).
 
 | Property      | Description                                                                                                                                                                                          |
 | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
diff --git a/docs/elements.md b/docs/elements.md
index 8690bbdd89b..0054592baf8 100644
--- a/docs/elements.md
+++ b/docs/elements.md
@@ -1755,7 +1755,7 @@ def generate(data):
 
 #### Details
 
-The element supports displaying Python objects via `repr()`, with support for more complex display options similar to the built-in `pprint` library. **Objects to be displayed must be serializable to JSON.** For details about what objects can be serialized and how to do this with the provided `to_json` and `from_json` functions, see the [Question Writing documentation](question.md#question-data-storage). To display objects that cannot be easily JSON serialized, please refer to the `pl-code` example question [element/code].
+The element supports displaying Python objects via `repr()`, with support for more complex display options similar to the built-in `pprint` library. **Objects to be displayed must be serializable to JSON.** For details about what objects can be serialized and how to do this with the provided `to_json` and `from_json` functions, see the [Question Writing documentation](question/server.md#question-data-storage). To display objects that cannot be easily JSON serialized, please refer to the `pl-code` example question [element/code].
 
 Printing Pandas DataFrames with this element is deprecated. Please use the new [`pl-dataframe`](#pl-dataframe-element) element for this purpose.
 
diff --git a/docs/faq.md b/docs/faq.md
index a571b55f1a4..1e2342dc5d2 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -38,7 +38,7 @@ Note that when granting access via `"active": false`, students can still access
 Writing and maintaining a large pool of questions is a lot of work. There are many strategies for managing this process. The approach taken by the TAM 2XX courses (Introductory Mechanics sequence) at Illinois is:
 
 1. Homework questions are always re-used semester-to-semester. It is assumed that solutions to these will be posted by students on the internet, so they are strictly for practice. Students do get credit for homeworks, but it assumed that any student who puts in the effort will get 100%.
-2. Some questions in the pool are [tagged](https://prairielearn.readthedocs.io/en/latest/question/#question-infojson) as "secret". These questions are only used on exams. Exams consist of a few homework questions, as well as secret questions on that topic. Secret questions are re-used for multiple semesters. Exams are only administered until highly secure conditions in a testing center or similar environment.
+2. Some questions in the pool are [tagged](https://prairielearn.readthedocs.io/en/latest/question/#metadata-infojson) as "secret". These questions are only used on exams. Exams consist of a few homework questions, as well as secret questions on that topic. Secret questions are re-used for multiple semesters. Exams are only administered until highly secure conditions in a testing center or similar environment.
 3. Every semester a small number of secret questions are written, and some older secret questions are moved to homeworks. This keeps the secret pool reasonably fresh and grows the homework pool over time. It also ensures that homework and exam questions are truly comparable in topics and difficulty.
 4. For homeworks, the [`maxPoints`](https://prairielearn.readthedocs.io/en/latest/assessment/#question-specification) option is used so that students don't need to complete all homework questions to get 100% on the homework. This allows the homework to be quite long, and to be partially for credit and partially as a set of extra questions that students can practice.
 5. Homeworks can be accessed for 100% credit until the due date, for 80% credit for a week after that, and for 0% credit (but students can still practice the questions) for the rest of the semester.
@@ -200,7 +200,7 @@ To resolve this issue, first check the name of the folder inside `questions/`
 and, then, check the question name listed in `infoAssessments.json`. Either
 rename the folder or change the name listed in assessments to be the same.
 
-See [Directory Structure](question.md#directory-structure) for more details.
+See [Directory Structure](question/index.md#directory-structure) for more details.
 
 ## Why do I have a Syntax Error in my JSON file?
 
@@ -313,17 +313,6 @@ The HTML specification disallows inserting special characters onto the page (i.e
 
 Docker Toolbox is no longer supported. [Docker Community Edition](https://www.docker.com/community-edition) is required to [run PrairieLearn locally](https://prairielearn.readthedocs.io/en/latest/installing/).
 
-## How can I add comments in my `question.html` source that won't be visible to students?
-
-Course staff members may want to write small maintenance comments in the `question.html` source, but HTML or JavaScript comments will remain visible in the rendered page's source (as can be seen in the browser dev tools). To prevent students from seeing staff comments, you can use [Mustache comments](https://mustache.github.io/mustache.5.html#Comments) that will be removed during the rendering process. To be safe, never put sensitive information such as solutions in a comment.
-
-Example:
-
-```html
-<!-- This is an HTML comment. It will not be visible to students in the web page, but *will be included* in the rendered page source, so students may be able to see it by reading the HTML source. -->
-{{! This is a Mustache comment. It will NOT be shown in the rendered page source. }}
-```
-
 ## How can I make a block that can be re-used in many questions?
 
 If you have a block of text that you want to re-use in many questions, possibly with a few parameters substituted into it, you can use the [`<pl-template>` element](./elements.md#pl-template-element). This element allows you to define a template in one place and then use it in many questions.
@@ -334,7 +323,7 @@ If you have a block of text that you want to re-use in many questions, possibly
 
 ## How can I hide the correct answer when students see their grading results?
 
-Questions can specify the `showCorrectAnswer: false` property in `info.json` to hide the correct answer box entirely. For more information on this option, see [the documentation for question info.json files](question.md#question-infojson).
+Questions can specify the `showCorrectAnswer: false` property in `info.json` to hide the correct answer box entirely. For more information on this option, see [the documentation for question info.json files](question/index.md#metadata-infojson).
 
 For more granular control, some elements in PL have their own options for specifying whether to hide individual correct answers (for example, `pl-checkbox` has a `hide-answer-panel` attribute). Not all element types offer this as an attribute (e.g., `pl-multiple-choice` currently does not). However, to hide the correct answer for any kind of element, you can surround the particular graded pl-element with `pl-hide-in-panel` in the `question.html` file.
 
diff --git a/docs/getStarted.md b/docs/getStarted.md
index ffaaea05725..6691377bedf 100644
--- a/docs/getStarted.md
+++ b/docs/getStarted.md
@@ -93,7 +93,7 @@ A course instance corresponds to a single offering of a course, such as "Fall 20
 
 ### Change the content of the question
 
-To provide a simple example, here we first create a question without any randomization, by modifying the file [question.html](question.md#question-questionhtml).
+To provide a simple example, here we first create a question without any randomization, by modifying the file [question.html](question/index.md#html-questionhtml).
 
 - go to the `Files` tab.
 
@@ -143,7 +143,7 @@ Note that this question does not use any server side code, and for that reason,
 
 ### Change the content of the question
 
-We will add randomization to the previous question, using the file [server.py](question.md#question-serverpy)
+We will add randomization to the previous question, using the file [server.py](question/index.md#custom-generation-and-grading-serverpy)
 
 - go to the `Files` tab.
 
diff --git a/docs/index.md b/docs/index.md
index 330da82a801..3be2c40c045 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -9,8 +9,8 @@ Paid hosting and support: [https://www.prairielearn.com/](https://www.prairielea
 ## Why PrairieLearn?
 
 - [x] **Homework and Exam Support** - Compose your questions into homework sets and exams. [Learn more](./assessment/index.md)
-- [x] **Randomized, custom questions** - Questions variants can be generated and autograded using a Python script, with easily customizable partial credit rules. [Learn more](./question.md/#options-for-grading-student-answers)
-- [x] **Custom HTML-based questions** - Create questions using arbitrary HTML, CSS, and JavaScript. [Learn more](question.md/#question-questionhtml)
+- [x] **Randomized, custom questions** - Questions variants can be generated and autograded using a Python script, with easily customizable partial credit rules. [Learn more](./question/index.md/#grading-student-answers)
+- [x] **Custom HTML-based questions** - Create questions using arbitrary HTML, CSS, and JavaScript. [Learn more](question/index.md/#html-questionhtml)
 - [x] **Rich standard library of question elements** - Include graphs, symbolic algebra, common question types, and more. [Learn more](./elements.md)
 - [x] **Flexible access controls** - Granular control over who can access course content, student submissions, etc. [Learn more](./accessControl/index.md)
 - [x] **Support for code autograding** - Run student code in a sandbox, with support for C, C++, Java, Python, etc. [Learn more](./externalGrading.md)
diff --git a/docs/instructor-guide/index.md b/docs/instructor-guide/index.md
index f019cd51698..db556c2ecaa 100644
--- a/docs/instructor-guide/index.md
+++ b/docs/instructor-guide/index.md
@@ -9,7 +9,7 @@ Learn more about configuring the four main aspects of your course:
 - [Courses](../course/index.md)
 - [Course instances](../courseInstance.md)
 - [Assessments](../assessment/index.md)
-- [Questions](../question.md)
+- [Questions](../question/index.md)
 
 !!! tip
 
diff --git a/docs/manualGrading/index.md b/docs/manualGrading/index.md
index d06d21ea7f9..e98117d22c2 100644
--- a/docs/manualGrading/index.md
+++ b/docs/manualGrading/index.md
@@ -20,7 +20,7 @@ For questions that require manual grading, the question can be set up for manual
 
 Questions set to use only manual grading (i.e., with manual points but no auto points) allow students to "Save" answers, but they don't have a "Save & Grade" button. Instead, the student just saves answers as many times as they want, and all of their submitted answers are stored.
 
-It is recommended to also mark manually-graded questions as `"singleVariant": true` so that students are only given a single random variant, even on Homework assessments. By default, manual grading is performed only for the last submission of the last variant, so having a single variant simplifies both the grading process and the student feedback. The `singleVariant` setting can be updated in the [`info.json` file](../question.md#question-infojson) in the question, like this:
+It is recommended to also mark manually-graded questions as `"singleVariant": true` so that students are only given a single random variant, even on Homework assessments. By default, manual grading is performed only for the last submission of the last variant, so having a single variant simplifies both the grading process and the student feedback. The `singleVariant` setting can be updated in the [`info.json` file](../question/index.md#metadata-infojson) in the question, like this:
 
 ```json title="info.json"
 {
@@ -29,7 +29,7 @@ It is recommended to also mark manually-graded questions as `"singleVariant": tr
 }
 ```
 
-Any [elements](../elements.md) can be used in the [`question.html`](../question.md#question-questionhtml) to write manually graded questions. All student input will be saved and available for manual grading, including `pl-string-input`, `pl-file-editor`, `pl-file-upload`, `pl-rich-text-editor`, etc.
+Any [elements](../elements.md) can be used in the [`question.html`](../question/index.md#html-questionhtml) to write manually graded questions. All student input will be saved and available for manual grading, including `pl-string-input`, `pl-file-editor`, `pl-file-upload`, `pl-rich-text-editor`, etc.
 
 ## Manual grading using file uploads
 
diff --git a/docs/oldQuestion.md b/docs/oldQuestion.md
index 4910a061f19..bd85d3664c9 100644
--- a/docs/oldQuestion.md
+++ b/docs/oldQuestion.md
@@ -1,6 +1,8 @@
 # Old v1/v2 Question Format
 
-**WARNING**: This page describes the old PrairieLearn question format, as used in v1 and v2. This question format will be supported indefinitely, but it is strongly recommended that all new questions be written in the [v3 format](question.md).
+!!! warning
+
+    This page describes the old PrairieLearn question format, as used in v1 and v2. This question format will be supported indefinitely, but it is strongly recommended that all new questions be written in the [v3 format](question/index.md).
 
 ## Partial credit
 
diff --git a/docs/python-reference/index.md b/docs/python-reference/index.md
index 8897fca5620..1f23a9fb3f3 100644
--- a/docs/python-reference/index.md
+++ b/docs/python-reference/index.md
@@ -33,7 +33,7 @@ import prairielearn.sympy_utils as psu
 
 !!! warning
 
-    The following imports are now deprecated.
+    The following imports are now deprecated. They will be removed in a future release. Please update your code to use the new imports
 
     === "Old"
 
diff --git a/docs/question.md b/docs/question.md
deleted file mode 100644
index 7570784507a..00000000000
--- a/docs/question.md
+++ /dev/null
@@ -1,524 +0,0 @@
-# Questions
-
-**NOTE:** Any time you edit a question `info.json` file on a local copy of PrairieLearn, you need to click “Load from disk” to reload the changes. Edits to HTML or Python files can be picked up by reloading the page. You might need to generate a new variant of a question to run new Python code.
-
-**NOTE:** New-style PrairieLearn questions are marked with `"type": "v3"`. This documentation only describes new-style questions, although old-style v2 questions are still supported in the code.
-
-## Directory structure
-
-Questions are all stored inside the `questions` directory (or any subfolder) for a course. Each question is a single directory that contains all the files for that question. The name of the full question directory relative to `questions` is the QID (the "question ID") for that question. For example, here are three different questions:
-
-```text
-questions
-|
-|-- fossilFuelsRadio          # first question, id is "fossilFuelsRadio"
-|   |
-|   +-- info.json             # metadata for the fossilFuelsRadio question
-|   +-- server.py             # secret server-side code (optional)
-|   `-- question.html         # HTML template for the question
-|
-|-- addVectors                # second question, id is "addVectors"
-|   |
-|   +-- info.json             # metadata for the addVectors question
-|   +-- server.py
-|   +-- question.html
-|   +-- notes.docx            # more files, like notes on how the question works
-|   +-- solution.docx         # these are secret (can't be seen by students)
-|   |
-|   +-- clientFilesQuestion/  # Files accessible to the client (web browser)
-|   |   `-- fig1.png          # A client file (an image)
-|   |
-|   +-- tests/                # external grading files (see other doc)
-|       `-- ...
-|
-`-- subfolder                 # a subfolder we can put questions in -- this itself can't be a question
-    |
-    `-- nestedQuestion        # third question, id is "subfolder/nestedQuestion"
-        |
-        +-- info.json         # metadata for the "subfolder/nestedQuestion" question
-        `-- question.html
-```
-
-PrairieLearn assumes independent questions; nothing ties them together. However, each question could have multiple parts (inputs that are validated together).
-
-Example questions are in the [`exampleCourse/questions`](https://github.com/PrairieLearn/PrairieLearn/blob/master/exampleCourse/questions) directory inside PrairieLearn.
-
-## Question `info.json`
-
-The `info.json` file for each question defines properties of the question. For example:
-
-```json title="info.json"
-{
-  "uuid": "cbf5cbf2-6458-4f13-a418-aa4d2b1093ff",
-  "title": "Newton's third law",
-  "topic": "Forces",
-  "tags": ["secret", "Fa18"],
-  "type": "v3",
-  "comment": "You can add comments to JSON files using this property."
-}
-```
-
-| Property                 | Type    | Description                                                                                                                                                            |
-| ------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `uuid`                   | string  | [Unique identifier](uuid.md). (Required; no default)                                                                                                                   |
-| `type`                   | enum    | Type of the test. Must be `"v3"` for new-style questions. (Required; no default)                                                                                       |
-| `title`                  | string  | The title of the question (e.g., `"Addition of vectors in Cartesian coordinates"`). (Required; no default)                                                             |
-| `topic`                  | string  | The category of question (e.g., `"Vectors"`, `"Energy"`). Like the chapter in a textbook. (Required; no default)                                                       |
-| `tags`                   | array   | Optional extra tags associated with the question (e.g., `["secret", "concept"]`). (Optional; default: no tags)                                                         |
-| `gradingMethod`          | enum    | The grading method used for auto-grading this question. Valid values: `Internal`, `External`, or `Manual` (for manual-only questions). (Optional; default: `Internal`) |
-| `singleVariant`          | boolean | Whether the question is not randomized and only generates a single variant. (Optional; default: `false`)                                                               |
-| `showCorrectAnswer`      | boolean | Whether the question should display the answer panel. (Optional; default: `true`)                                                                                      |
-| `partialCredit`          | boolean | Whether the question will give partial points for fractional scores. (Optional; default: `true`)                                                                       |
-| `externalGradingOptions` | object  | Options for externally graded questions. See the [external grading docs](externalGrading.md). (Optional; default: none)                                                |
-| `dependencies`           | object  | External JavaScript or CSS dependencies to load. See below. (Optional; default: `{}`)                                                                                  |
-| `sharePublicly`          | boolean | Whether the question should be available for anyone to preview or use in their course                                                                                  |
-| `shareSourcePublicly`    | boolean | Whether the source code of the question should be available                                                                                                            |
-| `sharingSets`            | array   | Sharing sets which the question belongs to                                                                                                                             |
-
-See the [reference for `infoQuestion.json`](./schemas/infoQuestion.md) for more information about what can be added to this file.
-
-### Question Dependencies
-
-Your question can load client-side assets such as scripts or stylesheets from different sources. A full list of dependencies will be compiled based on the question's needs and any dependencies needed by page elements, then they will be deduplicated and loaded onto the page.
-
-These dependencies are specified in the `info.json` file, and can be configured as follows:
-
-```json title="info.json"
-{
-  "dependencies": {
-    "nodeModulesScripts": ["three/build/three.min.js"],
-    "clientFilesQuestionScripts": ["my-question-script.js"],
-    "clientFilesQuestionStyles": ["my-question-style.css"],
-    "clientFilesCourseStyles": ["courseStylesheet1.css", "courseStylesheet2.css"]
-  }
-}
-```
-
-### Question Sharing
-
-Any question that is marked with `"sharePublicly": true` or `"shareSourcePublicly": true` will be considered and displayed as being published for free use under the [CC-BY-NC](https://www.creativecommons.org/licenses/by-nc/4.0/) license. Questions may be privately shared to individual courses using sharing sets, as explained in the [sharing documentation](questionSharing.md). Sharing sets that a question belongs to are specified as a list of strings. These must match sharing sets that are declared in the [course configuration](course/index.md#adding-sharing-sets).
-
-```json title="info.json"
-{
-  "sharingSets": ["python-exercises"]
-}
-```
-
-The different types of dependency properties available are summarized in this table:
-
-| Property                     | Description                                                                                       |
-| ---------------------------- | ------------------------------------------------------------------------------------------------- |
-| `nodeModulesStyles`          | The styles required by this question, relative to `[PrairieLearn directory]/node_modules`.        |
-| `nodeModulesScripts`         | The scripts required by this question, relative to `[PrairieLearn directory]/node_modules`.       |
-| `clientFilesQuestionStyles`  | The scripts required by this question relative to the question's `clientFilesQuestion` directory. |
-| `clientFilesQuestionScripts` | The scripts required by this question relative to the question's `clientFilesQuestion` directory. |
-| `clientFilesCourseStyles`    | The styles required by this question relative to `[course directory]/clientFilesCourse`.          |
-| `clientFilesCourseScripts`   | The scripts required by this question relative to `[course directory]/clientFilesCourse`.         |
-
-## Question `question.html`
-
-The `question.html` is a template used to render the question to the student. A complete `question.html` example looks like:
-
-```html title="question.html"
-<pl-question-panel>
-  <p>
-    A particle of mass $m = {{params.m}}\rm\ kg$ is observed to have acceleration $a =
-    {{params.a}}\rm\ m/s^2$.
-  </p>
-  <p>What is the total force $F$ currently acting on the particle?</p>
-</pl-question-panel>
-
-<p>
-  <pl-number-input
-    answers-name="F"
-    comparison="sigfig"
-    digits="2"
-    label="$F =$"
-    suffix="$\rm m/s^2$"
-  ></pl-number-input>
-</p>
-```
-
-The `question.html` is regular HTML, with four special features:
-
-1. Any text in double-curly-braces (like `{{params.m}}`) is substituted with variable values. If you use triple-braces (like `{{{params.html}}}`) then raw HTML is substituted (don't use this unless you know you need it). This is using [Mustache](https://mustache.github.io/mustache.5.html) templating.
-
-2. Special HTML elements (like `<pl-number-input>`) enable input and formatted output. See the [list of PrairieLearn elements](elements.md). Note that that **all submission elements must have unique `answers-name` attributes.** This is necessary for questions to be graded properly.
-
-3. A special `<markdown>` tag allows you to write Markdown inline in questions.
-
-4. LaTeX equations are available within HTML by using `$x^2$` for inline equations, and `$$x^2$$` or `\[x^2\]` for display equations.
-
-## Question `server.py`
-
-The `server.py` file for each question creates randomized question variants by generating random parameters and the corresponding correct answer. The `server.py` functions are:
-
-| Function     | Return object                            | modifiable `data` keys                                                                                   | unmodifiable `data` keys                                                                                                                                                                                   | Description                                                                                                                                                                                                                                                                   |
-| ------------ | ---------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `generate()` |                                          | `correct_answers`, `params`                                                                              | `options`, `variant_seed`                                                                                                                                                                                  | Generate the parameter and true answers for a new random question variant. Set `data["params"][name]` and `data["correct_answers"][name]` for any variables as needed. Modify the `data` dictionary in-place.                                                                 |
-| `prepare()`  |                                          | `answers_names`, `correct_answers`, `params`                                                             | `options`, `variant_seed`                                                                                                                                                                                  | Final question preparation after element code has run. Can modify data as necessary. Modify the `data` dictionary in-place.                                                                                                                                                   |
-| `render()`   | `html` (string)                          |                                                                                                          | `correct_answers`, `editable`, `feedback`, `format_errors`, `options`, `panel`, `params`, `partial_scores`, `raw_submitted_answers`, `score`, `submitted_answers`, `variant_seed`, `num_valid_submissions` | Render the HTML for one panel and return it as a string.                                                                                                                                                                                                                      |
-| `parse()`    |                                          | `format_errors`, `submitted_answers`, `correct_answers`, `feedback`                                      | `options`, `params`, `raw_submitted_answers`, `variant_seed`                                                                                                                                               | Parse the `data["submitted_answers"][var]` data entered by the student, modifying this variable. Modify the `data` dictionary in-place.                                                                                                                                       |
-| `grade()`    |                                          | `correct_answers`, `feedback`, `format_errors`, `params`, `partial_scores`, `score`, `submitted_answers` | `options`, `raw_submitted_answers`, `variant_seed`                                                                                                                                                         | Grade `data["submitted_answers"][var]` to determine a score. Store the score and any feedback in `data["partial_scores"][var]["score"]` and `data["partial_scores"][var]["feedback"]`. Modify the `data` dictionary in-place.                                                 |
-| `file()`     | `object` (string, bytes-like, file-like) |                                                                                                          | `correct_answers`, `filename`, `options`, `params`, `variant_seed`                                                                                                                                         | Generate a file object dynamically in lieu of a physical file. Trigger via `type="dynamic"` in the question element (e.g., `pl-figure`, `pl-file-download`). Access the requested filename via `data['filename']`. If `file()` returns nothing, an empty string will be used. |
-
-A complete `question.html` and `server.py` example looks like:
-
-```html title="question.html"
-<pl-question-panel>
-  <!-- params.x is defined by data["params"]["x"] in server.py's `generate()`. -->
-  <!-- params.operation defined by in data["params"]["operation"] in server.py's `generate()`. -->
-  If $x = {{params.x}}$ and $y$ is {{params.operation}} $x$, what is $y$?
-</pl-question-panel>
-
-<!-- y is defined by data["correct_answers"]["y"] in server.py's `generate()`. -->
-<pl-number-input answers-name="y" label="$y =$"></pl-number-input>
-<pl-submission-panel> {{feedback.y}} </pl-submission-panel>
-```
-
-```python title="server.py"
-import random
-import math
-
-def generate(data):
-    # Generate random parameters for the question and store them in the data["params"] dict:
-    data["params"]["x"] = random.randint(5, 10)
-    data["params"]["operation"] = random.choice(["double", "triple"])
-
-    # Also compute the correct answer (if there is one) and store in the data["correct_answers"] dict:
-    if data["params"]["operation"] == "double":
-        data["correct_answers"]["y"] = 2 * data["params"]["x"]
-    else:
-        data["correct_answers"]["y"] = 3 * data["params"]["x"]
-
-def prepare(data):
-    # This function will run after all elements have run `generate()`.
-    # We can alter any of the element data here, but this is rarely needed.
-    pass
-
-def parse(data):
-    # data["raw_submitted_answers"][NAME] is the exact raw answer submitted by the student.
-    # data["submitted_answers"][NAME] is the answer parsed by elements (e.g., strings converted to numbers).
-    # data["format_errors"][NAME] is the answer format error (if any) from elements.
-    # We can modify or delete format errors if we have custom logic (rarely needed).
-    # If there are format errors then the submission is "invalid" and is not graded.
-    # To provide feedback but keep the submission "valid", data["feedback"][NAME] can be used.
-
-    # As an example, we will reject negative numbers for "y":
-    # check we don't already have a format error
-    if "y" not in data["format_errors"] and data["submitted_answers"]["y"] < 0:
-        data["format_errors"]["y"] = "Negative numbers are not allowed"
-
-def grade(data):
-    # All elements will have already graded their answers (if any) before this point.
-    # data["partial_scores"][NAME]["score"] is the individual element scores (0 to 1).
-    # data["score"] is the total score for the question (0 to 1).
-    # We can modify or delete any of these if we have a custom grading method.
-    # This function only runs if `parse()` did not produce format errors, so we can assume all data is valid.
-
-    # grade(data) can also set data['format_errors'][NAME] if there is any reason to mark the question
-    # invalid during grading time.  This will cause the question to not use up one of the student's attempts' on exams.
-    # You are encouraged, though, to do any checks for invalid data that can be done in `parse(data)` there instead,
-    # since that method is also called when the student hits "Save only", in manually graded questions, or in
-    # assessments without real-time grading.
-
-    # As an example, we will give half points for incorrect answers larger than "x",
-    # only if not already correct. Use math.isclose to avoid possible floating point errors.
-    if math.isclose(data["score"], 0.0) and data["submitted_answers"]["y"] > data["params"]["x"]:
-        data["partial_scores"]["y"]["score"] = 0.5
-        data["score"] = 0.5
-        data["feedback"]["y"] = "Your value for $y$ is larger than $x$, but incorrect."
-```
-
-## Question Data Storage
-
-All persistent data related to a question variant is stored under different entries in the `data` dictionary. This dictionary is stored in JSON format by PrairieLearn, and as a result, everything in `data` must be JSON serializable. Some types in Python are natively JSON serializable, such as strings, lists, and dicts, while others are not, such as complex numbers, numpy ndarrays, and pandas DataFrames.
-
-To account for this, the `prairielearn` Python library (usually aliased and used as `pl`), provides the functions [`to_json`][prairielearn.conversion_utils.to_json] and [`from_json`][prairielearn.conversion_utils.from_json] (part of [`conversion_utils.py`](https://github.com/PrairieLearn/PrairieLearn/blob/master/apps/prairielearn/python/prairielearn/conversion_utils.py)), which can respectively serialize and deserialize various objects for storage as part of question data. Please refer to the docstrings on those functions for more information. Here is a simple example:
-
-```python title="server.py"
-import numpy as np
-import prairielearn as pl
-
-def generate(data):
-    data["params"]["numpy_array"] = pl.to_json(np.array([1.2, 3.5, 5.1]))
-
-def grade(data):
-    pl.from_json(data["params"]["numpy_array"])
-```
-
-The `pl.to_json` function supports keyword-only options for different types of encodings (e.g. `pl.to_json(var, df_encoding_version=2)`). These options have been added to allow for new encoding behavior while still retaining backwards compatibility with existing usage.
-
-- `df_encoding_version` controls the encoding of Pandas DataFrames. Encoding a DataFrame `df` by setting `pl.to_json(df, df_encoding_version=2)` allows for missing and date time values whereas `pl.to_json(df, df_encoding_version=1)` (default) does not. However, `df_encoding_version=1` has support for complex numbers, while `df_encoding_version=2` does not.
-
-- `np_encoding_version` controls the encoding of Numpy values. When using `np_encoding_version=1`, then only `np.float64` and `np.complex128` can be serialized by `pl.to_json`, and their types will be erased after deserialization (will become native Python `float` and `complex` respectively). It is recommended to set `np_encoding_version=2`, which supports serialization for all numpy scalars and does not result in type erasure on deserialization.
-
-## Accessing files on disk
-
-From within `server.py` functions, directories can be accessed as:
-
-```python
-data["options"]["question_path"]                      # on-disk location of the current question directory
-data["options"]["client_files_question_path"]         # on-disk location of clientFilesQuestion/
-data["options"]["client_files_question_url"]          # URL location of clientFilesQuestion/ (only in render() function)
-data["options"]["client_files_question_dynamic_url"]  # URL location of dynamically-generated question files (only in render() function)
-data["options"]["client_files_course_path"]           # on-disk location of clientFilesCourse/
-data["options"]["client_files_course_url"]            # URL location of clientFilesCourse/ (only in render() function)
-data["options"]["server_files_course_path"]           # on-disk location of serverFilesCourse/
-```
-
-## Generating dynamic files
-
-You can dynamically generate file objects in `server.py`. These files never appear physically on the disk. They are generated in `file()` and returned as strings, bytes-like objects, or file-like objects. A complete `question.html` and `server.py` example using a dynamically generated `fig.png` looks like:
-
-```html title="question.html"
-<p>Here is a dynamically-rendered figure showing a line of slope $a = {{params.a}}$:</p>
-<img src="{{options.client_files_question_dynamic_url}}/fig.png" />
-```
-
-```python title="server.py"
-import random
-import io
-import matplotlib.pyplot as plt
-
-def generate(data):
-    data["params"]["a"] = random.choice([0.25, 0.5, 1, 2, 4])
-
-def file(data):
-    # We should look at data["filename"], generate the corresponding file,
-    # and return the contents of the file as a string, bytes-like, or file-like object.
-    # We can access data["params"].
-    # As an example, we will generate the "fig.png" figure.
-
-    if data["filename"] == "fig.png":                # check for the appropriate filename
-        plt.plot([0, data["params"]["a"]], [0, 1])   # plot a line with slope "a"
-        buf = io.BytesIO()                           # make a bytes object (a buffer)
-        plt.savefig(buf, format="png")               # save the figure data into the buffer
-        return buf
-```
-
-You can also use this functionality in file-based elements (`pl-figure`, `pl-file-download`) by setting `type="dynamic"`.
-
-## The `singleVariant` option for non-randomized questions
-
-While it is recommended that all questions contain random parameters, sometimes it is impractical to do this. For questions that don't have a meaningful amount of randomization in them, the `info.json` file should set `"singleVariant": true`. This has the following effects:
-
-- On `Homework`-type assessments, each student will only ever be given one variant of the question, which they can repeatedly attempt without limit. The correct answer will never be shown to students.
-- On `Exam`-type assessments, all questions are effectively single-variant, so the `singleVariant` option has no effect.
-
-## The `partialCredit` option
-
-By default, all questions award partial credit. For example, if there are two numeric answers in a question and only one of them is correct, the student will be awarded 50% of the available points.
-
-To disable partial credit for a question, set `"partialCredit": false` in the `info.json` file for the question. This will mean that the question will either give 0% or 100%, and it will only give 100% if every element on the page is fully correct. Some [question elements](elements.md) also provide more fine-grained control over partial credit.
-
-In general, it is _strongly_ recommended to leave partial credit enabled for all questions.
-
-## Using Markdown in questions
-
-HTML and custom elements are great for flexibility and expressiveness. However, they're not great for working with large amounts of text, formatting text, and so on. [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) is a lightweight plaintext markup syntax that's ideal for authoring simple but rich text. To enable this, PrairieLearn adds a special `<markdown>` tag to questions. When a `<markdown>` block is encountered, its contents are converted to HTML. Here's an example `question.html` that utilizes this element:
-
-<!-- prettier-ignore -->
-```html title="question.html"
-<markdown>
-# Hello, world!
-
-This is some **Markdown** text.
-</markdown>
-```
-
-That question would be rendered like this:
-
-```html
-<h1>Hello, world!</h1>
-<p>This is some <strong>Markdown</strong> text.</p>
-```
-
-!!! warning
-
-    Note that markdown recognizes indentation as a code block, so text inside these tags should not be indented with the corresponding HTML content.
-
-    === "Good"
-
-        ```html
-        <div>
-          <markdown>
-        # Hello, world!
-          </markdown>
-        </div>
-        ```
-    === "Bad"
-
-        ```html
-        <div>
-          <markdown>
-            # Hello, world!
-          </markdown>
-        </div>
-        ```
-
-A few special behaviors have been added to enable Markdown to work better within the PrairieLearn ecosystem, as described below.
-
-### Markdown code blocks
-
-Fenced code blocks (those using triple-backticks ` ``` `) are rendered as [`<pl-code>` elements](elements.md#pl-code-element), which will then be rendered as usual by PrairieLearn. These blocks support specifying language and highlighted lines, which are then passed to the resulting `<pl-code>` element. Consider the following Markdown:
-
-<!-- prettier-ignore -->
-````html title="question.html"
-<markdown>
-```cpp{1-2,4}
-int i = 1;
-int j = 2;
-int k = 3;
-int m = 4;
-```
-</markdown>
-````
-
-This will be rendered to the following `<pl-code>` element (which itself will eventually be rendered to standard HTML):
-
-<!-- prettier-ignore -->
-```html
-<pl-code language="cpp" highlight-lines="1-2,4">
-int i = 1;
-int j = 2;
-int k = 3;
-int m = 4;
-</pl-code>
-```
-
-### Escaping `<markdown>` tags
-
-Under the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a question to process as Markdown: it finds an opening `<markdown>` tag and processes everything up to the closing `</markdown>` tag. But what if you want to have a literal `<markdown>` or `</markdown>` tag in your question? PrairieLearn defines a special escape syntax to enable this. If you have `<markdown#>` or `</markdown#>` in a Markdown block, they will be rendered as `<markdown>` and `</markdown>` respectively (but will not be used to find regions of text to process as Markdown). You can use more hashes to produce different strings: for instance, to have `<markdown###>` show up in the output, write `<markdown####>` in your question.
-
-## Using LaTeX in questions (math mode)
-
-PrairieLearn supports LaTeX equations in questions. You can view a full list of [supported MathJax commands](https://docs.mathjax.org/en/latest/input/tex/macros/index.html).
-
-Inline equations can be written using `$x^2$` or `\(x^2\)`, and display equations can be written using `$$x^2$$` or `\[x^2\]`. For example:
-
-<!-- prettier-ignore -->
-```html title="question.html"
-<p>Here is some inline math: $x^2$. Here is some display math: $$x^2$$</p>
-<p>What is the total force $F$ currently acting on the particle?</p>
-
-<markdown>
-# LaTeX works in Markdown too!
-
-$$\phi = \frac{1+\sqrt{5}}{2}$$
-</markdown>
-```
-
-### Using a dollar sign ($) without triggering math mode
-
-Dollar signs by default denote either **inline** (`$ x $`) or **display mode** (`$$ x $$`) environments.
-
-To escape either math environment, consider using PrairieLearn's `<markdown>` tag and inline code syntax.
-
-<!-- prettier-ignore -->
-```html
-<markdown>
-What happens if we use a `$` to reference the spreadsheet cell location `$A$1`?
-</markdown>
-```
-
-In scenarios where it does not make sense to use the code environment, consider disabling math entirely by
-adding the `mathjax_ignore` class to an HTML element.
-
-```html
-<div class="mathjax_ignore">
-  Mary has $5 to spend. If each apple costs $2 dollars and a banana costs $1 dollar, then how many
-  pieces of fruit can Mary get?
-</div>
-
-<div>$x = 1$ and I have <span class="mathjax_ignore">$</span>5 dollars.</div>
-```
-
-## Rendering panels from `question.html`
-
-When a question is displayed to a student, there are three "panels" that will be shown at different stages: the "question" panel, the "submission" panel, and the "answer" panel. These display the question prompt, the solution provided by the student, and the correct answer.
-
-All three panels display the same `question.html` template, but elements will render differently in each panel. For example, the `<pl-number-input>` element displays an input box in the "question" panel, the submitted answer in the "submissions" panel, and the correct answer in the "answer" panel.
-
-Text in `question.html` can be set to only display in the "question" panel by wrapping it in the `<pl-question-panel>` element. This is useful for the question prompt, which doesn't need to be repeated in the "submission" and "answer" panels. There are also elements that only render in the other two panels.
-
-## Hiding staff comments in `question.html`
-
-Please note that HTML or JavaScript comments in your `question.html` source may be visible to students in the rendered page source. To leave small maintenance notes to staff in your `question.html` source, you may prefer to use a Mustache comment that will stay hidden. Please refer to [this FAQ item](faq.md#how-can-i-add-comments-in-my-questionhtml-source-that-wont-be-visible-to-students).
-
-## Options for grading student answers
-
-For most elements, there are four different ways of auto-grading the student answer. This applies to elements like [`pl-number-input`](elements.md#pl-number-input-element) and [`pl-string-input`](elements.md#pl-string-input-element) that allow students to input an answer of their choosing, but not [`pl-multiple-choice`](elements.md#pl-multiple-choice-element) or [`pl-checkbox`](elements.md#pl-checkbox-element) that are much more constrained. The four ways are:
-
-1. Set the correct answer using the correct-answer attributes for each element in `question.html`. This will use the built-in grading methods for each element. Given that this option is typically used for answers with a hard-coded value, without randomization, it is not expected to be used frequently.
-
-2. Set `data["correct_answers"][VAR_NAME]` in `server.py`. This is for questions where you can pre-compute a single correct answer based on the (randomized) parameters.
-
-3. Write a [custom `grade(data)`](#question-serverpy) function in server.py that checks `data["submitted_answers"][VAR_NAME]` and sets scores. This can do anything, including having multiple correct answers, testing properties of the submitted answer for correctness, compute correct answers of some elements based on the value of other elements, etc.
-
-4. Write an [external grader](externalGrading.md), though this is typically applied to more complex questions like coding.
-
-If a question has more than one of the above options, each of them overrides the one before it. Even if options 3 (custom grade function) or 4 (external grader) are used, then it can still be helpful to set a correct answer so that it is shown to students as a sample of what would be accepted. If there are multiple correct answers then it's probably a good idea to add a note with [`pl-answer-panel`](elements.md#pl-answer-panel-element) that any correct answer would be accepted, and the displayed answer is only an example. Moreover, if there is no relevant information to display on the correct answer panel (i.e., a question has multiple correct answers and is meant to be attempted until a full score is achieved), then the panel can be hidden by setting `showCorrectAnswer: false` in `info.json`.
-
-### Custom grading best practices
-
-Although questions with custom grading usually don't use the grading functions from individual elements, it is _highly_ recommended that built-in elements are used for student input, as these elements include helpful parsing and feedback by default. Parsed student answers are present in the `data["submitted_answers"]` dictionary.
-
-Any custom grading function for the whole question should set `data["score"]` as a value between 0.0 and 1.0, which will be the final score for the given question. If a custom grading function is only grading a specific part of a question, the grading function should set the corresponding dictionary entry in `data["partial_scores"]` and then recompute the final `data["score"]` value for the whole question. The [`question_utils.py`](https://github.com/PrairieLearn/PrairieLearn/blob/master/apps/prairielearn/python/prairielearn/question_utils.py) file from the `prairielearn` Python library provides the following score recomputation functions:
-
-- [`set_weighted_score_data`][prairielearn.question_utils.set_weighted_score_data] sets `data["score"]` to be the weighted average of entries in `data["partial_scores"]`.
-- [`set_all_or_nothing_score_data`][prairielearn.question_utils.set_all_or_nothing_score_data] sets `data["score"]` to 1.0 if all entries in `data["partial_scores"]` are 1.0, 0.0 otherwise.
-
-This can be used like so:
-
-```python
-from prairielearn import set_weighted_score_data
-
-def grade(data):
-    # update partial_scores as necessary
-    # ...
-
-    # compute total question score
-    set_weighted_score_data(data)
-```
-
-More detailed information can be found in the docstrings for these functions. If you prefer not to show score badges for individual parts, you may unset the dictionary entries in `data["partial_scores"]` once `data["score"]` has been computed.
-
-To set custom feedback, the grading function should set the corresponding entry in the `data["feedback"]` dictionary. These feedback entries are passed in when rendering the `question.html`, which can be accessed by using the mustache prefix `{{feedback.}}`. See the [above question](#question-serverpy) or [this demo question](https://github.com/PrairieLearn/PrairieLearn/tree/master/exampleCourse/questions/demo/custom/gradeFunction) for examples of this. Note that the feedback set in the `data["feedback"]` dictionary is meant for use by custom grader code in a `server.py` file, while the feedback set in `data["partial_scores"]` is meant for use by element grader code.
-
-For generated floating point answers, it's important to use consistent rounding when displaying numbers to students _and_ when computing the correct answer. For example, the following is problematic:
-
-```python
-def generate(data):
-    a = 33.33337
-    b = 33.33333
-    data["params"]["a_for_student"] = f'{a:.2f}'
-    data["params"]["b_for_student"] = f'{a:.2f}'
-    # Note how the correct answer is computed with full precision,
-    # but the parameters displayed to students are rounded.
-    data["correct_answers"]["c"] = a - b
-```
-
-Instead, the numbers should be rounded at the beginning:
-
-```python
-def generate(data):
-  a = np.round(33.33337, 2)
-  b = np.round(33.33333, 2)
-  data["params"]["a_for_student"] = f'{a:.2f}'
-  data["params"]["b_for_student"] = f'{b:.2f}'
-  data["correct_answers"]["c"] = a - b
-```
-
-Similarly, for grading functions involving floating point numbers, _avoid exact comparisons with `==`._ Floating point calculations in Python introduce error, and comparisons with `==` might unexpectedly fail. Instead, the function [`math.isclose`](https://docs.python.org/3/library/math.html#math.isclose) can be used, as it performs comparisons within given tolerance values. The [`grading_utils.py`](https://github.com/PrairieLearn/PrairieLearn/blob/master/apps/prairielearn/python/prairielearn/grading_utils.py) file from the `prairielearn` Python library also offers several functions to perform more specialized comparisons:
-
-- [`is_correct_scalar_ra`][prairielearn.grading_utils.is_correct_scalar_ra] compares floats using relative and absolute tolerances.
-- [`is_correct_scalar_sf`][prairielearn.grading_utils.is_correct_scalar_sf] compares floats up to a specified number of significant figures.
-- [`is_correct_scalar_dd`][prairielearn.grading_utils.is_correct_scalar_dd] compares floats up to a specified number of digits.
-
-More detailed information can be found in the docstrings for these functions.
-
-_Note:_ Data stored under the `"submitted_answers"` key in the data dictionary may be of varying type. Specifically, the `pl-integer-input`
-element sometimes stores very large integers as strings instead of the Python `int` type used in most cases. The best practice for custom grader
-code in this case is to always cast the data to the desired type, for example `int(data["submitted_answers"][name])`. See the
-[PrairieLearn elements documentation](elements.md) for more detailed discussion related to specific elements.
diff --git a/docs/question/example-question.png b/docs/question/example-question.png
new file mode 100644
index 00000000000..a9d92a8a39e
Binary files /dev/null and b/docs/question/example-question.png differ
diff --git a/docs/question/index.md b/docs/question/index.md
new file mode 100644
index 00000000000..6dc46a8809d
--- /dev/null
+++ b/docs/question/index.md
@@ -0,0 +1,226 @@
+# Questions
+
+Questions are the basic building blocks of PrairieLearn. They are the individual problems that students will solve, and they can be combined into assessments (homeworks and exams). Questions can be simple or complex, and they can be written in a variety of ways. This document describes how to create questions, including the directory structure, metadata, HTML templates, and server-side code.
+
+![A sample question](example-question.png)
+
+## Question components
+
+A question is made up of three main components:
+
+1. **`info.json`**: Metadata about the question, including the question title, topic, and tags.
+2. **`question.html`**: The HTML template that defines the question. This is where you write the question text and define the input elements. Reference the [question template documentation](template.md) for more details.
+3. **`server.py`**: This is where you write the logic for generating random values, grading student responses, and any other server-side code. This file is optional, but it is necessary for any question that has non-trivial randomization or custom grading behavior. Reference the [server documentation](server.md) for more details.
+
+## Directory structure
+
+Questions are all stored inside the `questions` directory (or any subfolder) for a course. Each question is a single directory that contains all the files for that question. The name of the full question directory relative to `questions` is the QID (the "question ID") for that question. For example, here are three different questions:
+
+```text
+questions
+|
+|-- fossilFuelsRadio          # first question, id is "fossilFuelsRadio"
+|   |
+|   +-- info.json             # metadata for the fossilFuelsRadio question
+|   +-- server.py             # secret server-side code (optional)
+|   `-- question.html         # HTML template for the question
+|
+|-- addVectors                # second question, id is "addVectors"
+|   |
+|   +-- info.json             # metadata for the addVectors question
+|   +-- server.py
+|   +-- question.html
+|   +-- notes.docx            # more files, like notes on how the question works
+|   +-- solution.docx         # these are secret (can't be seen by students)
+|   |
+|   +-- clientFilesQuestion/  # Files accessible to the client (web browser)
+|   |   `-- fig1.png          # A client file (an image)
+|   |
+|   +-- tests/                # external grading files (see other doc)
+|       `-- ...
+|
+`-- subfolder                 # a subfolder we can put questions in -- this itself can't be a question
+    |
+    `-- nestedQuestion        # third question, id is "subfolder/nestedQuestion"
+        |
+        +-- info.json         # metadata for the "subfolder/nestedQuestion" question
+        `-- question.html
+```
+
+!!! tip
+
+    Avoid using term names (e.g. `Spring20/questionName`) or assessment names (e.g. `exam3/question12`) in your question directory structure, as these can make it harder to find and reuse questions across assessments and terms. One potential folder structure you could consider is `topic/subtopic/question`.
+
+PrairieLearn assumes independent questions; nothing ties them together. However, each question could have multiple parts (inputs that are validated together).
+
+!!! info
+
+    An extensive library of example questions can be found under the [`exampleCourse/questions`](https://github.com/PrairieLearn/PrairieLearn/blob/master/exampleCourse/questions) directory of PrairieLearn. You can also view these questions by [running PrairieLearn locally](../installing.md/#running-instructions).
+
+## Metadata (`info.json`)
+
+The `info.json` file for each question defines properties of the question. For example:
+
+```json title="info.json"
+{
+  "uuid": "cbf5cbf2-6458-4f13-a418-aa4d2b1093ff",
+  "title": "Newton's third law",
+  "topic": "Forces",
+  "tags": ["secret", "Fa18"],
+  "type": "v3",
+  "comment": "You can add comments to JSON files using this property."
+}
+```
+
+| Property                 | Type    | Description                                                                                                                                                            |
+| ------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `uuid`                   | string  | [Unique identifier](../uuid.md). (Required; no default)                                                                                                                |
+| `type`                   | enum    | Type of the test. Must be `"v3"` for new-style questions. (Required; no default)                                                                                       |
+| `title`                  | string  | The title of the question (e.g., `"Addition of vectors in Cartesian coordinates"`). (Required; no default)                                                             |
+| `topic`                  | string  | The category of question (e.g., `"Vectors"`, `"Energy"`). Like the chapter in a textbook. (Required; no default)                                                       |
+| `tags`                   | array   | Optional extra tags associated with the question (e.g., `["secret", "concept"]`). (Optional; default: no tags)                                                         |
+| `gradingMethod`          | enum    | The grading method used for auto-grading this question. Valid values: `Internal`, `External`, or `Manual` (for manual-only questions). (Optional; default: `Internal`) |
+| `singleVariant`          | boolean | Whether the question is not randomized and only generates a single variant. (Optional; default: `false`)                                                               |
+| `showCorrectAnswer`      | boolean | Whether the question should display the answer panel. (Optional; default: `true`)                                                                                      |
+| `partialCredit`          | boolean | Whether the question will give partial points for fractional scores. (Optional; default: `true`)                                                                       |
+| `externalGradingOptions` | object  | Options for externally graded questions. See the [external grading docs](../externalGrading.md). (Optional; default: none)                                             |
+| `dependencies`           | object  | External JavaScript or CSS dependencies to load. See below. (Optional; default: `{}`)                                                                                  |
+| `sharePublicly`          | boolean | Whether the question should be available for anyone to preview or use in their course                                                                                  |
+| `shareSourcePublicly`    | boolean | Whether the source code of the question should be available                                                                                                            |
+| `sharingSets`            | array   | Sharing sets which the question belongs to                                                                                                                             |
+
+!!! note "Local development note"
+
+    Any time you edit a question `info.json` file on a local copy of PrairieLearn, you need to click “Load from disk” to reload the changes. Most edits to HTML or Python files will be picked up by reloading the page. However, if you made changes to question generation or element arguments, you might also need to generate a new variant.
+
+### Question Sharing
+
+Any question that is marked with `"shareSourcePublicly": true` will be considered and displayed as being published for free use under the [CC-BY-NC](https://www.creativecommons.org/licenses/by-nc/4.0/) license. Any question that is marked with `"sharePublicly": true` will be considered and displayed as being published for free use under the [CC-BY-NC-ND](https://creativecommons.org/licenses/by-nc-nd/4.0/) license. Questions may be privately shared to individual courses using sharing sets, as explained in the [sharing documentation](../questionSharing.md). Sharing sets that a question belongs to are specified as a list of strings. These must match sharing sets that are declared in the [course configuration](../course/index.md#adding-sharing-sets).
+
+```json title="info.json"
+{
+  "sharingSets": ["python-exercises"]
+}
+```
+
+### Question Dependencies
+
+Your question can load client-side assets such as scripts or stylesheets from different sources. A full list of dependencies will be compiled based on the question's needs and any dependencies needed by page elements, then they will be deduplicated and loaded onto the page.
+
+These dependencies are specified in the `info.json` file, and can be configured as follows:
+
+```json title="info.json"
+{
+  "dependencies": {
+    "nodeModulesScripts": ["three/build/three.min.js"],
+    "clientFilesQuestionScripts": ["my-question-script.js"],
+    "clientFilesQuestionStyles": ["my-question-style.css"],
+    "clientFilesCourseStyles": ["courseStylesheet1.css", "courseStylesheet2.css"]
+  }
+}
+```
+
+The different types of dependency properties available are summarized in this table:
+
+| Property                     | Description                                                                                       |
+| ---------------------------- | ------------------------------------------------------------------------------------------------- |
+| `nodeModulesStyles`          | The styles required by this question, relative to `[PrairieLearn directory]/node_modules`.        |
+| `nodeModulesScripts`         | The scripts required by this question, relative to `[PrairieLearn directory]/node_modules`.       |
+| `clientFilesQuestionStyles`  | The scripts required by this question relative to the question's `clientFilesQuestion` directory. |
+| `clientFilesQuestionScripts` | The scripts required by this question relative to the question's `clientFilesQuestion` directory. |
+| `clientFilesCourseStyles`    | The styles required by this question relative to `[course directory]/clientFilesCourse`.          |
+| `clientFilesCourseScripts`   | The scripts required by this question relative to `[course directory]/clientFilesCourse`.         |
+
+Additional details about how to access these fields from `server.py` can be found in the [`server.py` documentation](server.md#accessing-files-on-disk).
+
+### Non-randomized questions
+
+While it is recommended that all questions contain random parameters, sometimes it is impractical to do this. For questions that don't have a meaningful amount of randomization in them, the `info.json` file should set `"singleVariant": true`. This has the following effects:
+
+- On `Homework`-type assessments, each student will only ever be given one variant of the question, which they can repeatedly attempt without limit. The correct answer will never be shown to students.
+- On `Exam`-type assessments, all questions are effectively single-variant, so the `singleVariant` option has no effect.
+
+### Partial credit
+
+By default, all questions award partial credit. For example, if there are two numeric answers in a question and only one of them is correct, the student will be awarded 50% of the available points.
+
+To disable partial credit for a question, set `"partialCredit": false` in the `info.json` file for the question. This will mean that the question will either give 0% or 100%, and it will only give 100% if every element on the page is fully correct. Some [question elements](../elements.md) also provide more fine-grained control over partial credit.
+
+In general, it is _strongly_ recommended to leave partial credit enabled for all questions.
+
+!!! info
+
+    See the [reference for `infoQuestion.json`](../schemas/infoQuestion.md) for an exhaustive list of all the properties available, and their schemas.
+
+## HTML (`question.html`)
+
+The `question.html` is a template used to render the question to the student. A complete `question.html` example looks like:
+
+```html title="question.html"
+<pl-question-panel>
+  <p>
+    A particle of mass $m = {{params.m}}\rm\ kg$ is observed to have acceleration $a =
+    {{params.a}}\rm\ m/s^2$.
+  </p>
+  <p>What is the total force $F$ currently acting on the particle?</p>
+</pl-question-panel>
+
+<p>
+  <pl-number-input
+    answers-name="F"
+    comparison="sigfig"
+    digits="2"
+    label="$F =$"
+    suffix="$\rm m/s^2$"
+  ></pl-number-input>
+</p>
+```
+
+The `question.html` is regular HTML, with a few special features:
+
+1. Any text in double-curly-braces (like `{{params.m}}`) is substituted with variable values using [Mustache](https://mustache.github.io/mustache.5.html). These parameters are typically defined by a [question's `server.py`](#custom-generation-and-grading-serverpy).
+2. Special HTML elements (like [`<pl-number-input>`](../elements.md#pl-number-input-element)) enable input and formatted output. A student's submission is composed of the answers they provide to the question elements. See the [list of PrairieLearn elements](../elements.md).
+
+   :warning: **All submission elements must have unique `answers-name` attributes.** This is necessary for questions to be graded properly.
+
+3. A special `<markdown>` tag allows you to write Markdown inline in questions.
+4. LaTeX equations are available within HTML by using `$x^2$` for inline equations, and `$$x^2$$` or `\[x^2\]` for display equations.
+5. Special layout elements like `<pl-question-panel>` and `<pl-answer-panel>` can be used to show content to students in different contexts.
+
+!!! info
+
+    More details about these features and information about `question.html` is in the [question template documentation](template.md).
+
+## Custom generation and grading (`server.py`)
+
+The `server.py` file for each question creates randomized question variants by generating random parameters and the corresponding correct answers. A minimal `server.py` might set random parameters and calculate correct answers based on those parameters, while using the built-in grading ability of the PrairieLearn elements to grade the student's submission. More complex questions may use `server.py` to provide custom parsing and grading functionality, or randomized images and files.
+
+```python title="server.py"
+import random
+
+def generate(data):
+    # Generate random parameters
+    data["params"]["m"] = random.randint(1, 10)
+    data["params"]["a"] = random.randint(1, 10)
+
+    # Compute the correct answer
+    data["correct_answers"]["F"] = data["params"]["m"] * data["params"]["a"]
+```
+
+!!! info
+
+    More information about `server.py`, custom grading, and more can be found in the [server.py documentation](server.md).
+
+## Grading student answers
+
+Elements like [`pl-multiple-choice`](../elements.md#pl-multiple-choice-element) or [`pl-checkbox`](../elements.md#pl-checkbox-element), which aren't freeform answers, are automatically graded based on the element parameters. For other elements, like [`pl-number-input`](../elements.md#pl-number-input-element) and [`pl-string-input`](../elements.md#pl-string-input-element), which have students input an answer of their choosing, there are four different methods of auto-grading the student answer:
+
+1. Set the correct answer using the correct-answer attributes for each element in `question.html`. This will use the built-in grading methods for each element. This option is typically used for answers with a hard-coded value (i.e. if `"singleVariant": true` in `info.json`), and is not expected to be used in most randomized questions.
+
+2. Set `data["correct_answers"][VAR_NAME]` in `server.py`. This is for questions where you can pre-compute a single correct answer based on the (randomized) parameters.
+
+3. Write a [custom grading function](./server.md#step-5-grade) in `server.py` that checks `data["submitted_answers"][VAR_NAME]` and sets scores. This supports a variety of alternative grading options, including having multiple correct answers, testing properties of the submitted answer for correctness, compute correct answers of some elements based on the value of other elements, etc.
+
+4. Write an [external grader](../externalGrading.md), though this is typically applied to more complex questions like coding.
+
+If a question uses more than one method for grading, options 3 and 4 override options 1 and 2. If you grade a question using a custom grading function (option 3) or an external grader (option 4), you are still strongly encouraged to provide a possible correct answer so that students can see it in the answer panel. More details about providing feedback on student answers can be found in the `"answer"` panel section of the [question template documentation](template.md#answer-panel).
diff --git a/docs/question/lifecycle.d2 b/docs/question/lifecycle.d2
new file mode 100644
index 00000000000..cd62370dcd5
--- /dev/null
+++ b/docs/question/lifecycle.d2
@@ -0,0 +1,84 @@
+# Shapes
+generate: "generate()" {
+  tooltip: "Generate random parameters and correct answers"
+}
+prepare: "prepare()" {
+  tooltip: "Post-process element data after generate()"
+}
+render_question: "render() - question panel" {
+  tooltip: "Render question.html for initial view"
+}
+student_interaction: "Student views and submits." {
+  tooltip: " The student can only submit if they have attempts remaining."
+  shape: person
+  width: 100
+  height: 100
+}
+parse: "parse()" {
+  tooltip: "Parse submitted answers, check format"
+}
+check_format: "Has format errors?" {
+  shape: diamond
+  tooltip: "Check data['format_errors']"
+}
+
+grade: "grade()" {
+  tooltip: "Grade the submission, set score and feedback"
+}
+check_correct: "Is answer correct and valid?" {
+  shape: diamond
+  tooltip: "Check data['score']"
+}
+render_submission: "render() - submission panel" {
+  tooltip: "Render question.html showing submission and feedback. Multiple submissions can be shown."
+}
+render_answer: "render() - answer panel" {
+  tooltip: "Render question.html showing correct answer (if enabled)"
+}
+check_attempts: "Attempts remain?" {
+  shape: diamond
+  tooltip: "Decrement the student's attempts. Check if the student has more attempts available after decrementing."
+}
+file: "file()" {
+  tooltip: "Dynamically generated file"
+  shape: page
+}
+# Flow
+generate -> prepare
+prepare -> render_question
+render_question -> student_interaction
+student_interaction -> parse: "If attempts remain" {
+  style: {
+    stroke-dash: 3
+  }
+}
+parse -> check_format
+
+# If format errors, show question panel again with errors
+check_format -> render_submission: Yes
+
+# If no format errors, proceed to grading
+check_format -> grade: No
+
+grade -> check_correct
+
+render_submission -> render_question
+
+# If correct, show answer panel
+check_correct -> render_answer: "Yes (score is 100%, valid)"
+
+# If incorrect, show submission panel
+check_correct -> check_attempts: "No (score < 100%)"
+
+check_correct -> render_submission: "No (invalid)"
+
+# After rendering submission, check if attempts remain
+check_attempts -> render_submission: "Yes"
+
+# If no attempts remain, show the answer panel
+check_attempts -> render_answer: "No"
+
+render_answer -> render_submission
+# Dynamic file generation
+file -> student_interaction: "Generated file"
+student_interaction -> file: "Dynamic file request \n from browser"
diff --git a/docs/question/panels.png b/docs/question/panels.png
new file mode 100644
index 00000000000..f643b47369d
Binary files /dev/null and b/docs/question/panels.png differ
diff --git a/docs/question/server.md b/docs/question/server.md
new file mode 100644
index 00000000000..e335903cf97
--- /dev/null
+++ b/docs/question/server.md
@@ -0,0 +1,407 @@
+# `server.py`
+
+The `server.py` file for each question creates randomized question variants by generating random parameters and the corresponding correct answers.
+
+The following sections build a simple example of a question that asks the student to double a number. The question will look like this:
+
+<!-- prettier-ignore -->
+```html title="question.html"
+<pl-question-panel>
+  If $x = {{params.x}}$, what is $y$ if $y$ is double $x$?
+</pl-question-panel>
+<pl-integer-input answers-name="y" label="$y =$"></pl-integer-input>
+<pl-submission-panel>
+  {{feedback.y}}
+</pl-submission-panel>
+```
+
+!!! tip
+
+    Jump to the [complete `question.html` and `server.py` example](#complete-example).
+
+More details about the `{{params.x}}` and `{{feedback.y}}` Mustache syntax can be found in the [question template documentation](./template.md#mustache-templates).
+
+## Step 1: `generate`
+
+First, the `generate` function is called to generate random parameters for the variant, and the correct answers. It should set `data["params"]` with the parameters for the question, and `data["correct_answers"]` with the correct answers. The parameters can then be used in the `question.html` file by using `{{params.NAME}}`.
+
+```python title="server.py"
+import random
+
+def generate(data):
+    # Generate random parameters for the question and store them in the data["params"] dict:
+    data["params"]["x"] = random.randint(5, 10)
+
+    # Also compute the correct answer (if there is one) and store in the data["correct_answers"] dict:
+    data["correct_answers"]["y"] = 2 * data["params"]["x"]
+```
+
+### Randomization
+
+Question variants are randomized based on the variant seed (`data["variant_seed"]`), and all random generators (`random`, `np.random`, etc.) are seeded with this value.
+
+??? tip "Generating fake data"
+
+    If you need to generate fake data, you can use the [`faker`](https://faker.readthedocs.io/en/master/) library. This is useful for generating random names, addresses, and other data that looks real but is not.
+
+    ```python title="server.py"
+    from faker import Faker
+    fake = Faker()
+
+    fake.name()
+    # 'Lucy Cechtelar'
+    ```
+
+### Parameter generation
+
+For generated floating point answers, it's important to use consistent rounding when displaying numbers to students _and_ when computing the correct answer. Displaying rounded numbers to students but using the unrounded version for grading can lead to unexpected results.
+
+=== "Good"
+
+    ```python title="server.py"
+    def generate(data):
+        # Rounds numbers at the beginning
+        a = np.round(33.33337, 2)
+        b = np.round(33.33333, 2)
+        data["params"]["a_for_student"] = f'{a:.2f}'
+        data["params"]["b_for_student"] = f'{b:.2f}'
+        data["correct_answers"]["c"] = a - b
+    ```
+
+=== "Bad"
+
+    ```python title="server.py"
+    def generate(data):
+        a = 33.33337
+        b = 33.33333
+        data["params"]["a_for_student"] = f'{a:.2f}'
+        data["params"]["b_for_student"] = f'{a:.2f}'
+        # Correct answer is computed with full precision,
+        # but the parameters displayed to students are rounded.
+        data["correct_answers"]["c"] = a - b
+    ```
+
+## Step 2: `prepare`
+
+Next, the `prepare` function is called after all elements (e.g. `<pl-integer-input>`) have run `prepare()`. This may be done to do any sort of final post-processing, but is not commonly used.
+
+## Step 3: `render`
+
+Next, the `render(data, html)` function is called to render the question. You can use this function to override how the question is rendered. The render function expects two parameters, `data` and `html`, and should return a string of HTML. The HTML after the mustache template has been rendered is available through the `html` parameter. This is rarely used except for very advanced questions, and we won't need it for this example.
+
+## Step 4: `parse`
+
+When a student submits their answer, the `parse` function is called to parse the submitted answers after the individual elements have parsed them. This function can be used to display more-specific format errors than the input elements or to parse the input differently. When our parse function runs, `<pl-integer-input>` will have already parsed the submitted value as an integer, and display an error to the student if it was invalid. For this example, we will allow the student to only submit positive integers, so we will check for that and set a format error with `data["format_errors"]` if it is negative.
+
+The values typically read or modified by a `parse` function are:
+
+- `data["raw_submitted_answers"][NAME]` - the exact raw answer submitted by the student.
+- `data["submitted_answers"][NAME]` - the answer parsed by elements (e.g. strings converted to numbers).
+- `data["format_errors"][NAME]` - the format error message for a named answer.
+
+If there are format errors, then the submission is "invalid" and is not graded. To provide feedback but keep the submission "valid", `data["feedback"][NAME]` can be used instead.
+
+```python title="server.py"
+def parse(data):
+    # Reject negative numbers for "y" if we don't already have a format error
+    if "y" not in data["format_errors"] and int(data["submitted_answers"]["y"]) < 0:
+        data["format_errors"]["y"] = "Negative numbers are not allowed"
+```
+
+We could have also modified the submitted answers directly, if we need to process the inputs further. For example, if we wanted to accept negative answers as equivalent to their absolute value, we could do this instead:
+
+```python title="server.py"
+def parse(data):
+    # Ensure that "y" is positive
+    data["submitted_answers"]["y"] = abs(int(data["submitted_answers"]["y"]))
+```
+
+!!! info
+
+    In general, each function of the question generation process runs *after* all elements in the question. For example, the `parse()` function in `server.py` runs after all elements have run their `parse()` functions. This is important to remember when using the `data` dictionary, as it will contain the results *after* the elements have finished processing.
+
+Although questions with custom grading may not rely on the full grading functionality from individual elements, it is _highly_ recommended that built-in elements are used for student input, as these elements include helpful parsing and feedback by default. Parsed student answers are present in the `data["submitted_answers"]` dictionary.
+
+!!! note
+
+    Data stored under the `"submitted_answers"` key in the data dictionary may be of varying type. Specifically, the `pl-integer-input`
+    element sometimes stores very large integers as strings instead of the Python `int` type used in most cases. The best practice for custom grader
+    code in this case is to always cast the data to the desired type, for example `int(data["submitted_answers"][name])`. See the
+    [PrairieLearn elements documentation](../elements.md) for more detailed discussion related to specific elements.
+
+## Step 5: `grade`
+
+Finally, the `grade(data)` function is called to grade the question. The grade function is responsible for:
+
+- Setting the score and feedback for each named answer in `data["partial_scores"]`.
+- Setting the total score for the question in `data["score"]`.
+- Setting the overall feedback for the question in `data["feedback"]`.
+
+This function only runs if `parse()` did not produce format errors, so we can assume all data is valid. All elements will have already graded their answers (if any) before this point.
+
+The values typically read or modified by a `grade` function are:
+
+- `data["partial_scores"][NAME]["score"]` - the individual scores for the named answers (0 to 1).
+- `data["partial_scores"][NAME]["feedback"]` - the feedback for each named answer from the element that graded it.
+- `data["score"]` - the total score for the question (0 to 1).
+- `data["feedback"][NAME]` - the overall question feedback for each named answer.
+
+It is recommended that you give additional feedback to the student as they make progress towards the solution, and reward this progress with partial credit.
+
+If this function is not defined, the question will be graded automatically based on the correct answers set in `data["correct_answers"]`. Each answer the student provides will also be given feedback from the element that graded it. If the `grade` function _is_ defined, the data you receive has already been graded by the elements. You should ensure you only award partial credit if the answer isn't correct, otherwise you might give partial credit for a correct answer. In the snipped below, we update `data["score"]` using the [`set_weighted_score_data`][prairielearn.question_utils.set_weighted_score_data] utility.
+
+You can set `data["format_errors"][NAME]` to mark the submission as invalid. This will cause the question to not use up one of the student's attempts on assessments. However, you are encouraged to do as many checks for invalid data as possible in `parse` instead of `grade`; the `parse` function is called when the student hits "Save only", in manually or externally graded questions, and in assessments without real-time grading.
+
+```python title="server.py"
+import math
+import prairielearn as pl
+
+def grade(data):
+    # Give half points for incorrect answers larger than "x", only if not already correct.
+    # Use math.isclose to avoid possible floating point errors.
+    y_is_correct = math.isclose(data["partial_scores"]["y"]["score"], 1.0)
+    if not y_is_correct and int(data["submitted_answers"]["y"]) > data["params"]["x"]:
+        data["partial_scores"]["y"]["score"] = 0.5
+        pl.set_weighted_score_data(data)
+        data["feedback"]["y"] = "Your value for $y$ is larger than $x$, but incorrect."
+```
+
+### Providing feedback
+
+To set custom feedback, the grading function should set the corresponding entry in the `data["feedback"]` dictionary. These feedback entries are passed in when rendering the `question.html`, which can be accessed by using the mustache prefix `{{feedback.}}`. See the [above example](#complete-example) or [this demo question](https://github.com/PrairieLearn/PrairieLearn/tree/master/exampleCourse/questions/demo/custom/gradeFunction) for examples of this.
+
+Some elements provide feedback through `data["partial_scores"][NAME]["feedback"]`, which you can override in the grading function. This field is often a string, though some elements such as `<pl-drawing>` and `<pl-checkbox>` use different types. The feedback provided here will be attached to the student's answer for that element, which can make it easier for students to interpret the feedback they received in longer questions. Since not elements provide feedback this way, and use different data structures to represent the feedback they give, we recommend using `data["feedback"][NAME]` to provide question-specific feedback instead.
+
+!!! tip "Answer-specific feedback"
+
+    If you want to provide feedback on a specific answer that doesn't use/support `data["partial_scores"][NAME]["feedback"]`,
+    you could render the feedback directly after the element in `question.html`:
+
+    <!-- prettier-ignore -->
+    ```html title="question.html"
+    <pl-string-input ...></pl-string-input>
+    {{#feedback.value1}}<div>Feedback: {{.}}</div>{{/feedback.value1}}
+    ```
+
+Overall question feedback in `data["feedback"][NAME]` needs to be rendered explicity in the `question.html` template using `{{feedback.NAME}}`. Feedback given in `data["partial_scores"][NAME]["feedback"]`, if supported by the element, will be rendered automatically by the elements, assuming the element is visible.
+
+### Grading floating-point answers
+
+For grading functions involving floating point numbers, **avoid exact comparisons with `==`**. Floating point comparisons with `==` might unexpectedly fail since calculations in Python can introduce floating-point error. Instead, use the [`math.isclose`](https://docs.python.org/3/library/math.html#math.isclose) function, as it performs comparisons within given tolerance. The `prairielearn` Python library also offers several functions to perform more specialized comparisons:
+
+- [`is_correct_scalar_ra`][prairielearn.grading_utils.is_correct_scalar_ra] compares floats using relative and absolute tolerances.
+- [`is_correct_scalar_sf`][prairielearn.grading_utils.is_correct_scalar_sf] compares floats up to a specified number of significant figures.
+- [`is_correct_scalar_dd`][prairielearn.grading_utils.is_correct_scalar_dd] compares floats up to a specified number of digits.
+
+### Recalculating scores
+
+Any custom grading function for the whole question should set `data["score"]` as a value between 0.0 and 1.0, which will be the final score for the given question. If a custom grading function is only grading a specific part of a question, the grading function should set the corresponding dictionary entry in `data["partial_scores"]` and then recompute the final `data["score"]` value for the whole question. The `prairielearn` Python library provides the following score recomputation functions:
+
+- [`set_weighted_score_data`][prairielearn.question_utils.set_weighted_score_data] sets `data["score"]` to be the weighted average of entries in `data["partial_scores"]`.
+- [`set_all_or_nothing_score_data`][prairielearn.question_utils.set_all_or_nothing_score_data] sets `data["score"]` to 1.0 if all entries in `data["partial_scores"]` are 1.0, 0.0 otherwise.
+
+This can be used like so:
+
+```python title="server.py"
+import prairielearn as pl
+
+def grade(data):
+    # update partial_scores as necessary
+    data["partial_scores"]["y"]["score"] = 0.5
+
+    # compute total question score
+    pl.set_weighted_score_data(data)
+```
+
+More detailed information can be found in the [grading utilities documentation](../python-reference/prairielearn/grading_utils.md).
+
+If you prefer not to show score badges for individual parts, you can unset the dictionary entries in `data["partial_scores"]` once `data["score"]` has been computed.
+
+??? example "Code snippet"
+
+    ```python title="server.py"
+    import prairielearn as pl
+
+    def grade(data):
+        # update partial_scores as necessary
+        data["partial_scores"]["y"]["score"] = 0.5
+
+        # compute total question score
+        pl.set_weighted_score_data(data)
+
+        # unset partial scores
+        data["partial_scores"]["y"]["score"] = None
+    ```
+
+## Question lifecycle
+
+The diagram below shows the lifecycle of a question, including the server functions called, the different panels that are rendered, and points of interaction with the student.
+
+![Diagram showing the lifecycle of a question](./lifecycle.d2){layout="dagre" scale="0.5" pad="0" }
+
+## Complete example
+
+The finished, complete `question.html` and `server.py` example looks like:
+
+<!-- prettier-ignore -->
+```html title="question.html"
+<pl-question-panel>
+  If $x = {{params.x}}$, what is $y$ if $y$ is double $x$?
+</pl-question-panel>
+<pl-integer-input answers-name="y" label="$y =$"></pl-integer-input>
+<pl-submission-panel>
+  {{feedback.y}}
+</pl-submission-panel>
+```
+
+```python title="server.py"
+import random
+import math
+import prairielearn as pl
+
+def generate(data):
+    # Generate random parameters for the question and store them in the data["params"] dict:
+    data["params"]["x"] = random.randint(5, 10)
+
+    # Also compute the correct answer (if there is one) and store in the data["correct_answers"] dict:
+    data["correct_answers"]["y"] = 2 * data["params"]["x"]
+
+def parse(data):
+    # Reject negative numbers for "y" if we don't already have a format error
+    if "y" not in data["format_errors"] and int(data["submitted_answers"]["y"]) < 0:
+        data["format_errors"]["y"] = "Negative numbers are not allowed"
+
+def grade(data):
+    # Give half points for incorrect answers larger than "x", only if not already correct.
+    # Use math.isclose to avoid possible floating point errors.
+    y_is_correct = math.isclose(data["partial_scores"]["y"]["score"], 1.0)
+    if not y_is_correct and int(data["submitted_answers"]["y"]) > data["params"]["x"]:
+        data["partial_scores"]["y"]["score"] = 0.5
+        pl.set_weighted_score_data(data)
+        data["feedback"]["y"] = "Your value for $y$ is larger than $x$, but incorrect."
+```
+
+## `server.py` functions
+
+This table summarizes the functions that can be defined in `server.py`.
+
+| Function     | Updates `data`?    | Modifiable `data` keys                                                                                   | Description                                                                                                                                                                                                                                                                   |
+| ------------ | ------------------ | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `generate()` | :white_check_mark: | `correct_answers`, `params`                                                                              | Generate the parameter and true answers for a new random question variant. Set `data["params"][name]` and `data["correct_answers"][name]` for any variables as needed. Modify the `data` dictionary in-place.                                                                 |
+| `prepare()`  | :white_check_mark: | `answers_names`, `correct_answers`, `params`                                                             | Final question preparation after element code has run. Can modify data as necessary. Modify the `data` dictionary in-place.                                                                                                                                                   |
+| `render()`   | :x:                | N/A. Returns `html` as a string                                                                          | Render the HTML for one panel and return it as a string.                                                                                                                                                                                                                      |
+| `parse()`    | :white_check_mark: | `correct_answers`, `format_errors`, `feedback`, `submitted_answers`                                      | Parse the `data["submitted_answers"][var]` data entered by the student, modifying this variable. Modify the `data` dictionary in-place.                                                                                                                                       |
+| `grade()`    | :white_check_mark: | `correct_answers`, `feedback`, `format_errors`, `params`, `partial_scores`, `score`, `submitted_answers` | Grade `data["submitted_answers"][var]` to determine a score. Store the score and any feedback in `data["partial_scores"][var]["score"]` and `data["partial_scores"][var]["feedback"]`. Modify the `data` dictionary in-place.                                                 |
+| `file()`     | :x:                | N/A. Returns an `object` (string, bytes-like, file-like)                                                 | Generate a file object dynamically in lieu of a physical file. Trigger via `type="dynamic"` in the question element (e.g., `pl-figure`, `pl-file-download`). Access the requested filename via `data['filename']`. If `file()` returns nothing, an empty string will be used. |
+
+As shown in the table, all functions (except for `render`) accept a single argument, `data` (a dictionary), and modify it in place. The `render` function accepts two arguments: the `data` dictionary and the `html` content computed from the template and elements.
+
+### `data` dictionary
+
+| Attribute               | Type    | Description                                                                                                                          |
+| ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
+| `params`                | `dict`  | Parameters for the question variant. These are set in the `generate()` function and can be used in the `question.html` file.         |
+| `correct_answers`       | `dict`  | Correct answers for the question variant. Each item maps from a named answer to a value.                                             |
+| `submitted_answers`     | `dict`  | Student answers submitted for the question after parsing.                                                                            |
+| `raw_submitted_answers` | `dict`  | Raw student answers submitted for the question.                                                                                      |
+| `format_errors`         | `dict`  | Dictionary of format errors for each answer. Each item maps from a named answer to a error message.                                  |
+| `partial_scores`        | `dict`  | Dictionary of partial scores for each answer. Each entry is a dictionary with the keys `score` (float) and `weight` (int, optional). |
+| `score`                 | `float` | The total score for the question variant.                                                                                            |
+| `feedback`              | `dict`  | Dictionary of [feedback](#providing-feedback) for each answer. Each item maps from a named answer to a feedback message.             |
+| `variant_seed`          | `int`   | The [random seed](#randomization) for this question variant.                                                                         |
+| `options`               | `dict`  | Any options associated with the question, e.g. for [accessing files](#accessing-files-on-disk)                                       |
+| `filename`              | `str`   | The name of the [dynamic file requested](#generating-dynamic-files-with-file) in the `file()` function.                              |
+
+The key `data` fields and their types are described above. You can view a full list of all fields in the [`QuestionData` reference](../python-reference/prairielearn/question_utils.md#prairielearn.question_utils.QuestionData).
+
+## Question data storage
+
+All persistent data related to a question variant is stored under different entries in the `data` dictionary. This dictionary is stored in JSON format by PrairieLearn, and as a result, **everything in `data` must be JSON serializable**. Some types in Python are natively JSON serializable, such as strings, lists, and dicts, while others are not, such as complex numbers, numpy ndarrays, and pandas DataFrames.
+
+The `prairielearn` Python library provides the utility functions [`to_json`][prairielearn.conversion_utils.to_json] and [`from_json`][prairielearn.conversion_utils.from_json] (part of [`conversion_utils.py`](https://github.com/PrairieLearn/PrairieLearn/blob/master/apps/prairielearn/python/prairielearn/conversion_utils.py)), which can serialize and deserialize various objects for storage as part of question data. Please refer to the documentation for those functions for additional information. Here is a simple example of how to use them to store and retrieve a numpy array:
+
+```python title="server.py"
+import numpy as np
+import prairielearn as pl
+
+def generate(data):
+    data["params"]["numpy_array"] = pl.to_json(np.array([1.2, 3.5, 5.1]))
+
+def grade(data):
+    pl.from_json(data["params"]["numpy_array"])
+```
+
+The [`pl.to_json`][prairielearn.conversion_utils.to_json] function supports keyword-only options for different types of encodings (e.g. `pl.to_json(var, df_encoding_version=2)`). These options have been added to allow for new encoding behavior while still retaining backwards compatibility with existing usage.
+
+- `df_encoding_version` controls the encoding of Pandas DataFrames. Encoding a DataFrame `df` by setting `pl.to_json(df, df_encoding_version=2)` allows for missing and date time values whereas `pl.to_json(df, df_encoding_version=1)` (default) does not. However, `df_encoding_version=1` has support for complex numbers, while `df_encoding_version=2` does not.
+
+- `np_encoding_version` controls the encoding of Numpy values. When using `np_encoding_version=1`, then only `np.float64` and `np.complex128` can be serialized by `pl.to_json`, and their types will be erased after deserialization (will become native Python `float` and `complex` respectively). It is recommended to set `np_encoding_version=2`, which supports serialization for all numpy scalars and does not result in type erasure on deserialization.
+
+## Accessing files on disk
+
+From within `server.py` functions, directories can be accessed as:
+
+```python
+# on-disk location of the current question directory
+data["options"]["question_path"]
+
+# on-disk location of clientFilesQuestion/
+data["options"]["client_files_question_path"]
+
+# URL location of clientFilesQuestion/ (only in render() function)
+data["options"]["client_files_question_url"]
+
+# URL location of dynamically-generated question files (only in render() function)
+data["options"]["client_files_question_dynamic_url"]
+
+# on-disk location of clientFilesCourse/
+data["options"]["client_files_course_path"]
+
+# URL location of clientFilesCourse/ (only in render() function)
+data["options"]["client_files_course_url"]
+
+# on-disk location of serverFilesCourse/
+data["options"]["server_files_course_path"]
+```
+
+## Generating dynamic files with `file()`
+
+You can dynamically generate file objects in `server.py`. These files never appear physically on the disk. They are generated in `file()` and returned as strings, bytes-like objects, or file-like objects. `file()` has access to the same `data` object as the one created by the `generate()` function, including `data["params"]` and `data["correct_answers"]`. A complete `question.html` and `server.py` example using a dynamically generated `fig.png` looks like:
+
+```html title="question.html"
+<p>Here is a dynamically-rendered figure showing a line of slope $a = {{params.a}}$:</p>
+<pl-figure file-name="fig.png" type="dynamic"></pl-figure>
+```
+
+```python title="server.py"
+import random
+import io
+import matplotlib.pyplot as plt
+
+def generate(data):
+    data["params"]["a"] = random.choice([0.25, 0.5, 1, 2, 4])
+
+def file(data):
+    # check for the appropriate filename
+    if data["filename"] == "fig.png":
+        # plot a line with slope "a"
+        plt.plot([0, data["params"]["a"]], [0, 1])
+        # make a bytes object (a buffer)
+        buf = io.BytesIO()
+        # save the figure data into the buffer
+        plt.savefig(buf, format="png")
+        return buf
+```
+
+Requests for files to this URL will be routed to the `file()` function in `server.py` since the `type` of `pl-figure` is set to `"dynamic"`. The filename requested is stored in `data["filename"]`, and the file contents should be returned from the `file()` function.
+
+We recommend using the [`pl-figure`](../elements.md#pl-figure-element) and [`pl-file-download`](../elements.md#pl-file-download-element) elements to display or download files. Specifically, you should use `pl-file-download` for files that should be downloaded or shown in a separate tab (e.g. PDFs, source code, etc.), and `pl-figure` for images that should be displayed.
+
+??? info "Advanced dynamic file usage"
+
+    The URL for the dynamically generated files is set to `{{options.client_files_question_dynamic_url}}` in the HTML, which is a special URL that PrairieLearn uses to route requests to the `file()` function. You could instead write `question.html` like so:
+
+    ```html title="question.html"
+    <p>Here is a dynamically-rendered figure showing a line of slope $a = {{params.a}}$:</p>
+    <img src="{{options.client_files_question_dynamic_url}}/fig.png" />
+    ```
diff --git a/docs/question/template.md b/docs/question/template.md
new file mode 100644
index 00000000000..92bc9cbe031
--- /dev/null
+++ b/docs/question/template.md
@@ -0,0 +1,240 @@
+# `question.html`
+
+## Template file
+
+All `question.html` files are processed with the [Mustache template engine](https://mustache.github.io/mustache.5.html), which allows for basic dynamic rendering of HTML. In addition, it supports:
+
+- [**Rendering panels**](#rendering-panels-from-questionhtml): These panels control what is shown when a student is viewing the question, viewing their submission, or viewing the correct answer.
+
+- [**Markdown**](#markdown-in-questions): HTML and custom elements are great for flexibility and expressiveness. However, they're not great for working with large amounts of text, formatting text, and so on. [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) is a lightweight plaintext markup syntax that's ideal for authoring simple but rich text.
+
+- [**LaTeX**](#using-latex-in-questions-math-mode): PrairieLearn supports LaTeX equations in questions.
+
+## Rendering panels from `question.html`
+
+When a question is displayed to a student, there are three "panels" that will be shown at different stages: the `"question"` panel, the `"submission"` panel, and the `"answer"` panel. These display the question prompt, the solution provided by the student, and the correct answer.
+
+![Question panels](./panels.png)
+
+The `"question"` panel is displayed when the question is first shown to the student. A `"submission"` panel is displayed each time the student saves an answer; there may be multiple submission panels if the student submits multiple answers to the same variant. The `"answer"` panel is displayed based on question/assessment configuration; by default, it is shown when the student either submits the correct answer or runs out of attempts. You can see the [lifecycle diagram](server.md#question-lifecycle) for more details on how these panels are displayed.
+
+All three panels are displayed based on the same `question.html` template, but elements will render differently in each panel. For example, the `<pl-number-input>` element displays an input box in the "question" panel, the submitted answer in the "submissions" panel, and the correct answer in the "answer" panel.
+
+### `"question"` panel
+
+Text in `question.html` can be set to only display in the "question" panel by wrapping it in the [`<pl-question-panel>` element](../elements.md#pl-question-panel-element). This is useful for the question prompt, which doesn't need to be repeated in the "submission" and "answer" panels. For example:
+
+=== "Good"
+
+    ```html
+    <pl-question-panel>
+      <p>What is y, if y is 1 + 3?</p>
+    </pl-question-panel>
+    <pl-number-input answers-name="y" label="$y =$"></pl-number-input>
+    ```
+
+=== "Bad"
+
+    ```html
+    <p>What is y, if y is 1 + 3?</p>
+    <pl-number-input answers-name="y" label="$y =$"></pl-number-input>
+    ```
+
+    The question will be repeated in the "submission" and "answer" panels, which is unnecessary and can be confusing for students.
+
+### `"submission"` panel
+
+By default, feedback given by elements (`data["partial_scores"][NAME]["feedback"]`) is rendered when an element renders in the "submission" panel. You can use the [`pl-submission-panel`](../elements.md#pl-submission-panel-element) to show additional text only in the "submission" panel. For example:
+
+=== "Good"
+
+    ```html
+    <pl-submission-panel>
+      <p>
+        Feedback: {{feedback.y}}
+      </p>
+    </pl-submission-panel>
+    ```
+
+=== "Bad"
+
+    ```html
+    <p>Feedback: {{feedback.y}}</p>
+    ```
+
+    This will show the feedback in the "question" and "answer" panels as well, which is unnecessary and can be confusing for students. Since `feedback.y` won't be defined until the student submits an answer, it will show as an empty string in the "question" panel, just showing `"Feedback: "`.
+
+More details about providing feedback can be found on the [`server.py` page](server.md#providing-feedback).
+
+### `"answer"` panel
+
+By default, the correct answers are shown in the "answer" panel. This can be disabled by setting `showCorrectAnswer: false` in the question's `info.json`. The FAQ item ["How can I hide the correct answer when students see their grading results?"](../faq.md#how-can-i-hide-the-correct-answer-when-students-see-their-grading-results) describes some other techniques for hiding the correct answer.
+
+If you grade a question using a [custom grading function](./server.md#step-5-grade), an [external grader](../externalGrading.md), or using [manual grading](../manualGrading/index.md), you are encouraged to display a correct answer so that students can see it in the answer panel. If there are multiple correct answers, you should add a note within the [`pl-answer-panel`](../elements.md#pl-answer-panel-element) element that any correct answer would be accepted, and the displayed answer is only an example. You can also show additional text in the answer panel as well, e.g. extended explanations for the correct answers.
+
+## Mustache templates
+
+Any text in double-curly-braces (like `{{params.m}}`) is substituted with variable values using [Mustache](https://mustache.github.io/mustache.5.html). These parameters are typically defined by a [question's `server.py`](server.md). For example, if `params["m"]` is defined in `server.py`, you can use it in `question.html` like this:
+
+```html title="question.html"
+<p>If m is {{params.m}}, what is {{params.m}} * 3?</p>
+```
+
+This will be rendered as:
+
+```html
+<p>If m is 5, what is 5 * 3?</p>
+```
+
+### Advanced rendering
+
+You can use the following syntax to conditionally render a piece of HTML:
+
+```html title="question.html"
+{{#params.show}}
+<p>This will only show up if params.show is true</p>
+{{/params.show}}
+```
+
+You can use this syntax to render over an array of strings (the `.` represents the current item in the array):
+
+```html title="question.html"
+{{#params.items}}
+<p>{{.}}</p>
+{{/params.items}}
+```
+
+!!! info "HTML rendering"
+
+    You can use triple-braces (e.g. `{{{params.html}}}`) to substitute raw HTML. This should only be used for content that is defined by the instructor. If you want to render student-provided HTML, you can use the [`<pl-xss-safe>` element](../elements.md#pl-xss-safe-element).
+
+### Hiding staff comments in `question.html`
+
+HTML or JavaScript comments in your `question.html` source are visible to students in the rendered page source. To leave small maintenance notes to staff in your `question.html` source, you should use [Mustache comments](https://mustache.github.io/mustache.5.html#Comments) (`{{! ... }}`) that will be removed during the rendering process. Never put sensitive information, such as solutions, in a HTML/JS comment.
+
+Example:
+
+```html
+<!-- This HTML comment will not be visible to students in the web page, 
+ but *will be included* in the rendered page source, so students may be able to
+ see it by reading the HTML source. -->
+{{! This Mustache comment will NOT be included in the rendered page source. }}
+```
+
+## Markdown in questions
+
+You can use the special `<markdown>` tag to automatically convert its contents to HTML. Here's an example `question.html` that utilizes this element:
+
+<!-- prettier-ignore -->
+```html title="question.html"
+<markdown>
+# Hello, world!
+
+This is some **Markdown** text.
+</markdown>
+```
+
+That question would be rendered like this:
+
+```html
+<h1>Hello, world!</h1>
+<p>This is some <strong>Markdown</strong> text.</p>
+```
+
+!!! warning
+
+    Markdown recognizes indentation as a code block, so text inside these tags should **not** be indented with the corresponding HTML content.
+
+    === "Good"
+
+        ```html
+        <div>
+          <markdown>
+        # Hello, world!
+          </markdown>
+        </div>
+        ```
+    === "Bad"
+
+        ```html
+        <div>
+          <markdown>
+            # Hello, world!
+          </markdown>
+        </div>
+        ```
+
+### Markdown code blocks
+
+Fenced code blocks (those using triple-backticks ` ``` `) are rendered as [`<pl-code>` elements](../elements.md#pl-code-element), which will then be rendered as usual by PrairieLearn. These blocks support specifying language and highlighted lines, which are then passed to the resulting `<pl-code>` element. Consider the following Markdown:
+
+<!-- prettier-ignore -->
+````html title="question.html"
+<markdown>
+```cpp{1-2,4}
+int i = 1;
+int j = 2;
+int k = 3;
+int m = 4;
+```
+</markdown>
+````
+
+This will be rendered to the following `<pl-code>` element (which itself will eventually be rendered to standard HTML):
+
+<!-- prettier-ignore -->
+```html
+<pl-code language="cpp" highlight-lines="1-2,4">
+int i = 1;
+int j = 2;
+int k = 3;
+int m = 4;
+</pl-code>
+```
+
+??? note "Escaping literal `<markdown>` tags"
+
+    PrairieLearn defines a special escape syntax to allow a literal `<markdown>` or `</markdown>` tag in your question. If you have `<markdown#>` or `</markdown#>` in a Markdown block, they will be rendered as `<markdown>` and `</markdown>` respectively (but will not be used to find regions of text to process as Markdown).
+
+## Using LaTeX in questions (math mode)
+
+PrairieLearn supports LaTeX equations in questions. You can view a full list of [supported MathJax commands](https://docs.mathjax.org/en/latest/input/tex/macros/index.html).
+
+Inline equations can be written using `$x^2$` or `\(x^2\)`, and display equations can be written using `$$x^2$$` or `\[x^2\]`. For example:
+
+<!-- prettier-ignore -->
+```html title="question.html"
+<p>Here is some inline math: $x^2$. Here is some display math: $$x^2$$</p>
+<p>What is the total force $F$ currently acting on the particle?</p>
+
+<markdown>
+# LaTeX works in Markdown too!
+
+$$\phi = \frac{1+\sqrt{5}}{2}$$
+</markdown>
+```
+
+### Using a dollar sign ($) without triggering math mode
+
+Dollar signs by default denote either **inline** (`$ x $`) or **display mode** (`$$ x $$`) environments.
+
+To escape either math environment, consider using PrairieLearn's `<markdown>` tag and inline code syntax.
+
+<!-- prettier-ignore -->
+```html
+<markdown>
+What happens if we use a `$` to reference the spreadsheet cell location `$A$1`?
+</markdown>
+```
+
+In scenarios where it does not make sense to use the code environment, consider disabling math entirely by
+adding the `mathjax_ignore` class to an HTML element.
+
+```html
+<div class="mathjax_ignore">
+  Mary has $5 to spend. If each apple costs $2 dollars and a banana costs $1 dollar, then how many
+  pieces of fruit can Mary get?
+</div>
+
+<div>$x = 1$ and I have <span class="mathjax_ignore">$</span>5 dollars.</div>
+```
diff --git a/docs/scripts/d2_svg_file.py b/docs/scripts/d2_svg_file.py
index 59293c88cff..51a48f2080c 100644
--- a/docs/scripts/d2_svg_file.py
+++ b/docs/scripts/d2_svg_file.py
@@ -9,6 +9,8 @@
 from mkdocs.structure.pages import Page
 from mkdocs.utils import write_file
 
+IGNORE_HOOKING_FOR = []
+
 
 def on_config(config: MkDocsConfig) -> MkDocsConfig:
     """Hooks into the config to override the d2_img renderer."""
@@ -30,6 +32,12 @@ def new_render(source, opts, alt):  # noqa: ANN001, ANN202 # pyright:ignore[repo
         result, svg, ok = original_render(source, opts, alt)
         if ok:
             is_file = isinstance(source, Path)
+
+            # Check if this diagram should be ignored
+            if is_file and source.name in IGNORE_HOOKING_FOR:
+                # Return original result without adding hook data
+                return result, svg, ok
+
             if is_file:
                 key = f"{source}_{source.stat().st_mtime}"
             else:
diff --git a/docs/workspaces/index.md b/docs/workspaces/index.md
index 98ddd878fa5..9baa928813b 100644
--- a/docs/workspaces/index.md
+++ b/docs/workspaces/index.md
@@ -150,9 +150,9 @@ A minimal `question.html` for an externally graded workspace should look somethi
 
 ## Creating files in the workspace home directory
 
-Workspace questions can optionally include a `workspace/` subdirectory within the regular [PrairieLearn question directory structure](../question.md#directory-structure). If this `workspace/` subdirectory exists, its contents will be copied into the home directory of the student's workspace container, as configured in the `home` setting in `info.json`.
+Workspace questions can optionally include a `workspace/` subdirectory within the regular [PrairieLearn question directory structure](../question/index.md#directory-structure). If this `workspace/` subdirectory exists, its contents will be copied into the home directory of the student's workspace container, as configured in the `home` setting in `info.json`.
 
-Questions using workspaces can also be randomized, i.e., include files that contain random and dynamic content. This may be done in two ways: using mustache-based template files or using [the `server.py` file in the question directory](../question.md#question-serverpy). For template files, a workspace question can optionally include a `workspaceTemplates/` subdirectory within the regular question directory structure. The contents will be copied into the home directory of the student's workspace container, as with the `workspace/` directory. However, files within this directory may include mustache tags (e.g., `{{params.value}}`), which will be replaced with the equivalent values set by `server.py`. File names may optionally include the `.mustache` extension, and the file will be renamed before being presented to the student. For example, if `server.py` sets `data["params"]["starting_value"]` to `17`, then if the file `main.py.mustache` inside `workspaceTemplates` has the following content:
+Questions using workspaces can also be randomized, i.e., include files that contain random and dynamic content. This may be done in two ways: using Mustache-based template files or using [the `server.py` file in the question directory](../question/index.md#custom-generation-and-grading-serverpy). For template files, a workspace question can optionally include a `workspaceTemplates/` subdirectory within the regular question directory structure. The contents will be copied into the home directory of the student's workspace container, as with the `workspace/` directory. However, files within this directory may include Mustache tags (e.g., `{{params.value}}`), which will be replaced with the equivalent values set by `server.py`. File names may optionally include the `.mustache` extension, and the file will be renamed before being presented to the student. For example, if `server.py` sets `data["params"]["starting_value"]` to `17`, then if the file `main.py.mustache` inside `workspaceTemplates` has the following content:
 
 ```txt
 # ...
diff --git a/mkdocs.yml b/mkdocs.yml
index 68ad7af61dc..4e476cd0ee6 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -209,7 +209,10 @@ nav:
       - 'Course configuration': 'course/index.md'
       - 'Course instance configuration': 'courseInstance.md'
       - 'Assessment configuration': 'assessment/index.md'
-      - 'Question configuration': 'question.md'
+      - 'Question configuration':
+          - 'question/index.md'
+          - '<code>question.html</code>': 'question/template.md'
+          - '<code>server.py</code>': 'question/server.md'
       - 'Question runtime environment': 'questionRuntime/index.md'
       - 'Elements for writing questions': 'elements.md'
       - 'Access control': 'accessControl/index.md'


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (9)</summary><blockquote>

<details>
<summary>docs/question/server.md (2)</summary><blockquote>

`1-2`: **Ensure top-level heading for consistency**  
Markdown files should start with a top-level `#` heading. Please change the level-2 heading at line 1 to a level-1 heading (e.g., `# Question server.py`) to satisfy MD041 and improve consistency across the docs.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

1-1: First line in a file should be a top-level heading
null

(MD041, first-line-heading, first-line-h1)

</details>

</details>

---

`5-12`: **Clarify return values in the server functions table**  
The **Return object** column is blank for `generate()`, `prepare()`, and `parse()`. Consider specifying `None` (or `void`) for these entries so readers immediately know these functions modify `data` in-place without returning a value.

</blockquote></details>
<details>
<summary>docs/index.md (1)</summary><blockquote>

`13-13`: **Consistent relative link style.**

Consider prefixing the path with `./` for consistency with other links. For example:
```diff
-[Learn more](question/index.md/#question-questionhtml)
+[Learn more](./question/index.md/#question-questionhtml)
```

</blockquote></details>
<details>
<summary>docs/manualGrading/index.md (1)</summary><blockquote>

`23-23`: **Update link and refine hyphenation style.**

The link has been updated to `../question/index.md#question-infojson`. Additionally, per style guidelines, adverbs ending in “-ly” generally shouldn’t be hyphenated; consider changing “manually-graded questions” to “manually graded questions” for clarity.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~23-~23: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...stored.  It is recommended to also mark manually-graded questions as `"singleVariant": true` so...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

</details>

</details>

</blockquote></details>
<details>
<summary>docs/question/html.md (2)</summary><blockquote>

`1-1`: **Use a top‑level heading.**

Markdownlint (MD041) flags that the first line should be a level‑1 heading. Please change:

```diff
- ## Using Markdown in questions
+ # Using Markdown in questions
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

1-1: First line in a file should be a top-level heading
null

(MD041, first-line-heading, first-line-h1)

</details>

</details>

---

`76-76`: **Avoid weak intensifier.**

Consider removing "very" to make the phrasing more concise:

```diff
- PrairieLearn is doing some very simple parsing
+ PrairieLearn is doing some simple parsing
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~76-~76: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...er the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a q...

(EN_WEAK_ADJECTIVE)

</details>

</details>

</blockquote></details>
<details>
<summary>docs/question/index.md (3)</summary><blockquote>

`146-146`: **Remove duplicated word.**

The phrase "Note that that" should be "Note that".  
```diff
- Note that that **all submission elements must have unique `answers-name` attributes.**
+ Note that **all submission elements must have unique `answers-name` attributes.**
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[duplication] ~146-~146: Possible typo: you repeated a word.
Context: ...ieLearn elements](../elements.md). Note that that **all submission elements must have uni...

(ENGLISH_WORD_REPEAT_RULE)

</details>

</details>

---

`241-243`: **Avoid weak intensifier.**

Replace "very large integers" with "large integers" to remove the over‑used "very":

```diff
- ... the `pl-integer-input` element sometimes stores very large integers as strings ...
+ ... the `pl-integer-input` element sometimes stores large integers as strings ...
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~242-~242: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...integer-input` element sometimes stores very large integers as strings instead of the Pyth...

(EN_WEAK_ADJECTIVE)

</details>

</details>

---

`152-154`: **Fix heading hierarchy under `server.py`.**

Currently the "singleVariant" section is at the same level as the `server.py` header. To maintain a proper hierarchy, change it (and subsequent sub‑sections) to level‑3 headings under the `server.py` section:

```diff
- ## Question `server.py`
+ ## Question `server.py`
- ## The `singleVariant` option for non-randomized questions
+ ### The `singleVariant` option for non-randomized questions
- ## The `partialCredit` option
+ ### The `partialCredit` option
- ## Options for grading student answers
+ ### Options for grading student answers
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4099a2df4f77c1da59e6580ab55c0312de6d0e65 and 78f91aaa4abc0e66932ad10b5a91e8d256e848cf.

</details>

<details>
<summary>📒 Files selected for processing (18)</summary>

* `docs/assessment/index.md` (1 hunks)
* `docs/clientServerFiles.md` (1 hunks)
* `docs/concepts/index.md` (2 hunks)
* `docs/course/index.md` (3 hunks)
* `docs/elements.md` (1 hunks)
* `docs/faq.md` (2 hunks)
* `docs/getStarted.md` (2 hunks)
* `docs/index.md` (1 hunks)
* `docs/instructor-guide/index.md` (1 hunks)
* `docs/manualGrading/index.md` (2 hunks)
* `docs/oldQuestion.md` (1 hunks)
* `docs/python-reference/index.md` (1 hunks)
* `docs/question.md` (0 hunks)
* `docs/question/html.md` (1 hunks)
* `docs/question/index.md` (1 hunks)
* `docs/question/server.md` (1 hunks)
* `docs/workspaces/index.md` (1 hunks)
* `mkdocs.yml` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* docs/question.md

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>docs/question/server.md</summary>

1-1: First line in a file should be a top-level heading
null

(MD041, first-line-heading, first-line-h1)

</details>
<details>
<summary>docs/question/html.md</summary>

1-1: First line in a file should be a top-level heading
null

(MD041, first-line-heading, first-line-h1)

</details>

</details>
<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>docs/manualGrading/index.md</summary>

[uncategorized] ~23-~23: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...stored.  It is recommended to also mark manually-graded questions as `"singleVariant": true` so...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

</details>
<details>
<summary>docs/question/html.md</summary>

[style] ~76-~76: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...er the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a q...

(EN_WEAK_ADJECTIVE)

---

[misspelling] ~186-~186: This word is normally spelled as one.
Context: ...2.png"></pl-figure> </div> ```  We then re-parse this tree and again begin looking for m...

(EN_COMPOUNDS_RE_PARSE)

---

[uncategorized] ~186-~186: Use a comma before ‘and’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...n come across each `<pl-figure>` in turn and they will be rendered, with their marku...

(COMMA_COMPOUND_SENTENCE)

---

[style] ~199-~199: This phrasing can be wordy. For improved clarity, try opting for something more concise.
Context: ... we are making this new renderer opt-in for the time being until we can ensure that everyone's que...

(FOR_THE_TIME_BEING)

</details>
<details>
<summary>docs/question/index.md</summary>

[duplication] ~146-~146: Possible typo: you repeated a word.
Context: ...ieLearn elements](../elements.md). Note that that **all submission elements must have uni...

(ENGLISH_WORD_REPEAT_RULE)

---

[uncategorized] ~177-~177: This verb may not be in the correct form. Consider using a different form for this context.
Context: ...f the submitted answer for correctness, compute correct answers of some elements based ...

(AI_EN_LECTOR_REPLACEMENT_VERB_FORM)

---

[typographical] ~181-~181: Consider adding a comma.
Context: ...d. If there are multiple correct answers then it's probably a good idea to add a note...

(IF_THEN_COMMA)

---

[uncategorized] ~209-~209: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... by element grader code.  For generated floating point answers, it's important to use consiste...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~233-~233: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...ilarly, for grading functions involving floating point numbers, _avoid exact comparisons with ...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~233-~233: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...s, _avoid exact comparisons with `==`._ Floating point calculations in Python introduce error,...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~235-~235: A punctuation mark might be missing here.
Context: ...rform more specialized comparisons:  - [`is_correct_scalar_ra`][prairielearn.grad...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

---

[uncategorized] ~237-~237: A punctuation mark might be missing here.
Context: ...ified number of significant figures. - [`is_correct_scalar_dd`][prairielearn.grad...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

---

[style] ~242-~242: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...integer-input` element sometimes stores very large integers as strings instead of the Pyth...

(EN_WEAK_ADJECTIVE)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: build-core-images / build-images (ubuntu-24.04-arm)
* GitHub Check: build-core-images / build-images (ubuntu-24.04)
* GitHub Check: test-prairielearn
* GitHub Check: native-checks

</details>

<details>
<summary>🔇 Additional comments (19)</summary><blockquote>

<details>
<summary>docs/instructor-guide/index.md (1)</summary>

`12-12`: **Link updated correctly**  
The “Questions” item now points to `../question/index.md`, matching the new multi-page structure under `docs/question/`.

</details>
<details>
<summary>docs/oldQuestion.md (1)</summary>

`3-3`: **Updated warning link**  
The deprecation warning now correctly references `[v3 format](question/index.md)`, aligning with the reorganized question documentation.

</details>
<details>
<summary>docs/python-reference/index.md (1)</summary>

`36-36`: **Deprecation notice added**  
The warning clearly informs users that the old imports are deprecated and will be removed in a future release.

</details>
<details>
<summary>docs/elements.md (1)</summary>

`1757-1757`: **Verify updated link target for Question Writing documentation**  
The link now points to `question/server.md#question-data-storage`. Please confirm that:
1. `docs/question/server.md` exists in the repository.
2. The heading `Question Data Storage` (or corresponding anchor) is present and matches `#question-data-storage`.

</details>
<details>
<summary>docs/clientServerFiles.md (1)</summary>

`50-50`: <details>
<summary>✅ Verification successful</summary>

**Hyperlink updated to new server documentation.**

The link now correctly points to `question/server.md#accessing-files-on-disk`, matching the restructured docs. Please verify that this anchor exists in `docs/question/server.md`.  

Run this script to check the anchor:

---

🏁 Script executed:

```shell
#!/bin/bash
# Verify the "Accessing files on disk" section exists
rg -q '## Accessing files on disk' docs/question/server.md
```

Length of output: 60

---

🏁 Script executed:

```shell
#!/bin/bash
echo "Searching for exact heading variants..."
rg -n -i '^#{2,6}\s*Accessing Files on Disk' docs/question/server.md
rg -n -i '^#{2,6}\s*Accessing files on disk' docs/question/server.md

echo -e "\nSearching for the phrase anywhere..."
rg -n -i 'accessing files on disk' docs/question/server.md

echo -e "\nListing the first 50 headings in docs/question/server.md..."
rg -n -n '^#{1,6}\s*' docs/question/server.md | head -n 50
```

Length of output: 786

---



</details>

**Verified updated hyperlink**  

- The heading `## Accessing files on disk` is present at line 105 in `docs/question/server.md`.  
- The link in `docs/clientServerFiles.md` to `question/server.md#accessing-files-on-disk` is correct.

</details>
<details>
<summary>docs/concepts/index.md (2)</summary>

`30-30`: **Link to question overview updated.**

The "Questions" link now correctly points to `../question/index.md` after the docs split.

---

`156-156`: **Next steps link updated.**

The "questions" link in the Next steps section now references `../question/index.md`.

</details>
<details>
<summary>docs/index.md (1)</summary>

`12-12`: **Randomized questions link updated.**

The "Learn more" link now points to `./question/index.md/#options-for-grading-student-answers`, aligning with the new file structure.

</details>
<details>
<summary>docs/faq.md (2)</summary>

`203-203`: **Link to directory structure updated.**

The reference now points to `question/index.md#directory-structure`, reflecting the new docs layout.

---

`337-337`: **Link to question info.json docs updated.**

The link targets `question/index.md#question-infojson`, consistent with the split.

</details>
<details>
<summary>docs/assessment/index.md (1)</summary>

`614-614`: **`singleVariant` link updated for non-randomized questions.**

The hyperlink now points to `../question/index.md#the-singlevariant-option-for-non-randomized-questions`, matching the reorganized documentation.

</details>
<details>
<summary>docs/course/index.md (2)</summary>

`31-31`: **Approve link update for questions documentation.**

The link has been correctly updated to point to `../question/index.md` instead of the removed `question.md`. This aligns with the new multi-page structure.

---

`139-141`: **Approve updated link for question configuration documentation.**

The table entry now correctly references the [question configuration documentation](../question/html.md#the-new-renderer) in `docs/question/html.md`.

</details>
<details>
<summary>docs/manualGrading/index.md (1)</summary>

`32-32`: **Approve link update for manual grading HTML reference.**

The link correctly points to `../question/index.md#question-questionhtml` following the documentation restructure.

</details>
<details>
<summary>docs/getStarted.md (2)</summary>

`96-96`: **Approve link update for question HTML documentation.**

The link now points to `question/index.md#question-questionhtml` as expected after moving `docs/question.md`. Please verify that the `#question-questionhtml` anchor is present in `docs/question/index.md`.

---

`146-146`: **Approve link update for server-side documentation.**

The link now targets `question/index.md#question-serverpy`. Ensure that the `#question-serverpy` anchor exists in `docs/question/index.md`.

</details>
<details>
<summary>mkdocs.yml (1)</summary>

`212-215`: **Approve navigation restructure for question configuration.**

The “Question configuration” section has been expanded to include entries for `question/index.md`, `question/html.md`, and `question/server.md` with proper code-style labels. This aligns navigation with the new documentation layout.

</details>
<details>
<summary>docs/workspaces/index.md (2)</summary>

`153-153`: **Approve link update for workspace directory structure reference.**

The link correctly points to `../question/index.md#directory-structure` to reflect the updated question documentation layout.

---

`155-155`: **Approve link update for server.py documentation reference.**

The link now references `../question/index.md#question-serverpy` as intended after the documentation reorganization.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T03:00:58Z
user: login: reteps
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T03:03:20Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T03:04:10Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>docs/question/html.md (4)</summary><blockquote>

`3-12`: **Well-structured introduction and example**  
The explanation of the `<markdown>` tag and its purpose is clear, and the example snippet effectively demonstrates its usage. The link to an external Markdown cheat sheet is helpful, though you may optionally link to the official CommonMark specification for added completeness.

---

`21-43`: **Clarify admonition and example formatting**  
The `!!! warning` block combines a general caveat and Good/Bad examples, which can feel dense. Consider:
- Using a `note` or `tip` admon for this informational guidance.
- Indenting code fences by exactly 4 spaces within the admon so they are correctly parsed.
- Splitting the Good/Bad examples into separate content tabs or sections outside the warning for improved clarity.

---

`76-76`: **Remove unnecessary intensifier**  
In the sentence "Under the hood, PrairieLearn is doing some very simple parsing...", consider removing "very" to improve clarity ("doing simple parsing").

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~76-~76: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...er the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a q...

(EN_WEAK_ADJECTIVE)

</details>

</details>

---

`199-199`: **Streamline phrasing**  
The phrase "for the time being" is somewhat wordy. Consider simplifying to "until everyone's questions are validated" to make the instruction more concise.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~199-~199: This phrasing can be wordy. For improved clarity, try opting for something more concise.
Context: ... we are making this new renderer opt-in for the time being until we can ensure that everyone's que...

(FOR_THE_TIME_BEING)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 78f91aaa4abc0e66932ad10b5a91e8d256e848cf and fdc0fb8e1134530cce4603b33c4d7102b0391a5a.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `docs/question/html.md` (1 hunks)
* `docs/question/server.md` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* docs/question/server.md

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>docs/question/html.md</summary>

[style] ~76-~76: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...er the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a q...

(EN_WEAK_ADJECTIVE)

---

[misspelling] ~186-~186: This word is normally spelled as one.
Context: ...2.png"></pl-figure> </div> ```  We then re-parse this tree and again begin looking for m...

(EN_COMPOUNDS_RE_PARSE)

---

[uncategorized] ~186-~186: Use a comma before ‘and’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...n come across each `<pl-figure>` in turn and they will be rendered, with their marku...

(COMMA_COMPOUND_SENTENCE)

---

[style] ~199-~199: This phrasing can be wordy. For improved clarity, try opting for something more concise.
Context: ... we are making this new renderer opt-in for the time being until we can ensure that everyone's que...

(FOR_THE_TIME_BEING)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: test-prairielearn
* GitHub Check: native-checks
* GitHub Check: build-core-images / build-images (ubuntu-24.04-arm)
* GitHub Check: build-core-images / build-images (ubuntu-24.04)

</details>

<details>
<summary>🔇 Additional comments (13)</summary><blockquote>

<details>
<summary>docs/question/html.md (13)</summary>

`1-2`: The heading clearly indicates the focus on Markdown usage within questions. No changes needed here.

---

`15-19`: **Clear rendering output example**  
The resulting HTML snippet accurately shows the transformation from Markdown to HTML, which is valuable for reader comprehension.

---

`44-61`: **Well-documented Markdown code blocks section**  
The `## Markdown code blocks` section thoroughly covers fenced code blocks, and the use of quadruple backticks to nest triple-backtick examples is correct. The link to the `<pl-code>` element is appropriate.

---

`74-77`: **Correct escape tag explanation**  
The description of escaping `<markdown>` tags using the hash syntax is clear and concise, making it easy for authors to include literal tags.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~76-~76: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...er the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a q...

(EN_WEAK_ADJECTIVE)

</details>

</details>

---

`78-94`: **Comprehensive LaTeX support instructions**  
The section on LaTeX math modes is well organized, covering both inline and display equations with multiple delimiter options. The link to the MathJax command list is a valuable reference.

---

`96-119`: **Dollar sign escaping guidance**  
The guidance on escaping dollar signs—either via code fences or the `mathjax_ignore` class—is practical, and the examples illustrate the concept clearly.

---

`121-129`: **Panel rendering overview**  
The explanation of the three question panels and how elements like `<pl-number-input>` render differently is clear and helpful for authors.

---

`129-132`: **Verify FAQ link target**  
Double-check that the anchor `#how-can-i-add-comments-in-my-questionhtml-source-that-wont-be-visible-to-students` in `../faq.md` matches the actual heading in the FAQ so the link resolves correctly.

---

`133-140`: **Clear rendering engine introduction**  
The overview distinguishing the legacy and new renderers gives readers needed context on upcoming changes.

---

`141-156`: **Legacy renderer details**  
The description of how the legacy renderer handles (and sometimes mishandles) nested elements is accurate and highlights performance considerations.

---

`158-172`: **New renderer example and benefits**  
The new renderer’s AST-driven approach is well explained, and the HTML example shows the correct handling of nested `<pl-figure>` elements.

---

`173-198`: **Step-by-step rendering walkthrough**  
The two-pass rendering walkthrough, with intermediate markup, effectively illustrates how elements are processed in sequence. This is an excellent demonstration of the new renderer’s behavior.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[misspelling] ~186-~186: This word is normally spelled as one.
Context: ...2.png"></pl-figure> </div> ```  We then re-parse this tree and again begin looking for m...

(EN_COMPOUNDS_RE_PARSE)

---

[uncategorized] ~186-~186: Use a comma before ‘and’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...n come across each `<pl-figure>` in turn and they will be rendered, with their marku...

(COMMA_COMPOUND_SENTENCE)

</details>

</details>

---

`199-209`: **Opt-in configuration instruction**  
The JSON snippet for enabling the new renderer via `infoCourse.json` is clear and correctly formatted.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~199-~199: This phrasing can be wordy. For improved clarity, try opting for something more concise.
Context: ... we are making this new renderer opt-in for the time being until we can ensure that everyone's que...

(FOR_THE_TIME_BEING)

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T03:06:36Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>docs/question/index.md (1)</summary><blockquote>

`142-146`: **Fix repeated word typo.**  
At line 146 the phrase "Note that that **all submission elements…**" repeats "that". Removing one improves readability.  
  
```diff
-4. Special HTML elements (like `<pl-number-input>`) enable input and formatted output. See the [list of PrairieLearn elements](../elements.md). Note that that **all submission elements must have unique `answers-name` attributes.**
+4. Special HTML elements (like `<pl-number-input>`) enable input and formatted output. See the [list of PrairieLearn elements](../elements.md). Note that **all submission elements must have unique `answers-name` attributes.**
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[duplication] ~146-~146: Possible typo: you repeated a word.
Context: ...ieLearn elements](../elements.md). Note that that **all submission elements must have uni...

(ENGLISH_WORD_REPEAT_RULE)

</details>

</details>

</blockquote></details>
<details>
<summary>docs/question/html.md (1)</summary><blockquote>

`75-78`: **Refine wording by removing unnecessary intensifier.**  
The phrase "very simple parsing" can be tightened to "simple parsing" for clarity.  

```diff
-Under the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a question to process as Markdown: it finds an opening `<markdown>` tag and processes everything up to the closing `</markdown>` tag.
+Under the hood, PrairieLearn does simple parsing to determine what pieces of a question to process as Markdown: it finds an opening `<markdown>` tag and processes everything up to the closing `</markdown>` tag.
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~76-~76: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...er the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a q...

(EN_WEAK_ADJECTIVE)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between fdc0fb8e1134530cce4603b33c4d7102b0391a5a and bbcc3cd4b99644a0a0ae765d60f5ad84352f1a26.

</details>

<details>
<summary>📒 Files selected for processing (18)</summary>

* `docs/assessment/index.md` (1 hunks)
* `docs/clientServerFiles.md` (1 hunks)
* `docs/concepts/index.md` (2 hunks)
* `docs/course/index.md` (2 hunks)
* `docs/elements.md` (1 hunks)
* `docs/faq.md` (2 hunks)
* `docs/getStarted.md` (2 hunks)
* `docs/index.md` (1 hunks)
* `docs/instructor-guide/index.md` (1 hunks)
* `docs/manualGrading/index.md` (2 hunks)
* `docs/oldQuestion.md` (1 hunks)
* `docs/python-reference/index.md` (1 hunks)
* `docs/question.md` (0 hunks)
* `docs/question/html.md` (1 hunks)
* `docs/question/index.md` (1 hunks)
* `docs/question/server.md` (1 hunks)
* `docs/workspaces/index.md` (1 hunks)
* `mkdocs.yml` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* docs/question.md

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (4)</summary>

* docs/clientServerFiles.md
* docs/concepts/index.md
* docs/getStarted.md
* docs/oldQuestion.md

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (10)</summary>

* docs/instructor-guide/index.md
* docs/python-reference/index.md
* mkdocs.yml
* docs/index.md
* docs/faq.md
* docs/workspaces/index.md
* docs/elements.md
* docs/course/index.md
* docs/question/server.md
* docs/assessment/index.md

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>docs/question/index.md</summary>

[duplication] ~146-~146: Possible typo: you repeated a word.
Context: ...ieLearn elements](../elements.md). Note that that **all submission elements must have uni...

(ENGLISH_WORD_REPEAT_RULE)

---

[uncategorized] ~177-~177: This verb may not be in the correct form. Consider using a different form for this context.
Context: ...f the submitted answer for correctness, compute correct answers of some elements based ...

(AI_EN_LECTOR_REPLACEMENT_VERB_FORM)

---

[typographical] ~181-~181: Consider adding a comma.
Context: ...d. If there are multiple correct answers then it's probably a good idea to add a note...

(IF_THEN_COMMA)

---

[uncategorized] ~209-~209: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... by element grader code.  For generated floating point answers, it's important to use consiste...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~233-~233: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...ilarly, for grading functions involving floating point numbers, _avoid exact comparisons with ...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~233-~233: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...s, _avoid exact comparisons with `==`._ Floating point calculations in Python introduce error,...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~235-~235: A punctuation mark might be missing here.
Context: ...rform more specialized comparisons:  - [`is_correct_scalar_ra`][prairielearn.grad...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

---

[uncategorized] ~237-~237: A punctuation mark might be missing here.
Context: ...ified number of significant figures. - [`is_correct_scalar_dd`][prairielearn.grad...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

---

[style] ~242-~242: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...integer-input` element sometimes stores very large integers as strings instead of the Pyth...

(EN_WEAK_ADJECTIVE)

</details>
<details>
<summary>docs/manualGrading/index.md</summary>

[uncategorized] ~23-~23: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...stored.  It is recommended to also mark manually-graded questions as `"singleVariant": true` so...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

</details>
<details>
<summary>docs/question/html.md</summary>

[style] ~76-~76: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...er the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a q...

(EN_WEAK_ADJECTIVE)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: native-checks
* GitHub Check: test-prairielearn
* GitHub Check: build-core-images / build-images (ubuntu-24.04)
* GitHub Check: build-core-images / build-images (ubuntu-24.04-arm)

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>docs/question/index.md (1)</summary>

`1-245`: **New question configuration docs look well‑structured and complete.**  
The breakdown into directory structure, `info.json` schema, templating, grading methods, and best practices is logical and thorough. Internal links, code samples, and tables all align with the new multi‐page layout.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[duplication] ~146-~146: Possible typo: you repeated a word.
Context: ...ieLearn elements](../elements.md). Note that that **all submission elements must have uni...

(ENGLISH_WORD_REPEAT_RULE)

---

[uncategorized] ~177-~177: This verb may not be in the correct form. Consider using a different form for this context.
Context: ...f the submitted answer for correctness, compute correct answers of some elements based ...

(AI_EN_LECTOR_REPLACEMENT_VERB_FORM)

---

[typographical] ~181-~181: Consider adding a comma.
Context: ...d. If there are multiple correct answers then it's probably a good idea to add a note...

(IF_THEN_COMMA)

---

[uncategorized] ~209-~209: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... by element grader code.  For generated floating point answers, it's important to use consiste...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~233-~233: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...ilarly, for grading functions involving floating point numbers, _avoid exact comparisons with ...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~233-~233: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...s, _avoid exact comparisons with `==`._ Floating point calculations in Python introduce error,...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~235-~235: A punctuation mark might be missing here.
Context: ...rform more specialized comparisons:  - [`is_correct_scalar_ra`][prairielearn.grad...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

---

[uncategorized] ~237-~237: A punctuation mark might be missing here.
Context: ...ified number of significant figures. - [`is_correct_scalar_dd`][prairielearn.grad...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

---

[style] ~242-~242: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...integer-input` element sometimes stores very large integers as strings instead of the Pyth...

(EN_WEAK_ADJECTIVE)

</details>

</details>

</details>
<details>
<summary>docs/manualGrading/index.md (2)</summary>

`23-23`: **Corrected link to question `info.json`.**  
The reference now points to `../question/index.md#question-infojson`, matching the new documentation structure.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~23-~23: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...stored.  It is recommended to also mark manually-graded questions as `"singleVariant": true` so...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

</details>

</details>

---

`32-32`: **Updated link to `question.html`.**  
The hyperlink targets `../question/index.md#question-questionhtml`, which corresponds to the renamed section.

</details>
<details>
<summary>docs/question/html.md (1)</summary>

`1-132`: **Markdown/LaTeX guidance is thorough and clear.**  
The new `html.md` covers inline/display math, code blocks, escaping, rendering panels, and staff‐only comments comprehensively. Examples and admonitions are well‑formatted.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~76-~76: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...er the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a q...

(EN_WEAK_ADJECTIVE)

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T22:31:16Z
user: login: jonatanschroeder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T15:50:17Z
user: login: nwalters512
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T17:23:21Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>docs/question/template.md (2)</summary><blockquote>

`76-76`: **Refine wording: remove redundant intensifier.**

To improve conciseness, drop "very" from this sentence. For example:
```diff
- Under the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a question to process as Markdown...
+ Under the hood, PrairieLearn is doing simple parsing to determine what pieces of a question to process as Markdown...
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~76-~76: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...er the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a q...

(EN_WEAK_ADJECTIVE)

</details>

</details>

---

`23-23`: **Capitalize "Markdown" for consistency.**

The term is consistently capitalized elsewhere. Change:
```diff
- Note that markdown recognizes indentation as a code block, so text inside these tags should not be indented with the corresponding HTML content.
+ Note that Markdown recognizes indentation as a code block, so text inside these tags should not be indented with the corresponding HTML content.
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bbcc3cd4b99644a0a0ae765d60f5ad84352f1a26 and 66b3b53b590b071d7a4090e7a4255c7f0afeaf72.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `docs/question/template.md` (1 hunks)
* `mkdocs.yml` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* mkdocs.yml

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>docs/question/template.md</summary>

[style] ~76-~76: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...er the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a q...

(EN_WEAK_ADJECTIVE)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: build-core-images / build-images (ubuntu-24.04-arm)
* GitHub Check: build-core-images / build-images (ubuntu-24.04)
* GitHub Check: test-prairielearn
* GitHub Check: native-checks

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>docs/question/template.md (1)</summary>

`1-132`: **Overall documentation structure is clear and well-organized.**

The new `template.md` comprehensively covers Markdown usage in questions, with clear examples, correct relative links, and consistent heading hierarchy. This fits nicely into the split documentation and aligns with the other `index.md` and `server.md` files.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~76-~76: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...er the hood, PrairieLearn is doing some very simple parsing to determine what pieces of a q...

(EN_WEAK_ADJECTIVE)

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T22:23:13Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (8)</summary><blockquote>

<details>
<summary>docs/question/server.md (4)</summary><blockquote>

`57-57`: **Consider hyphenating "floating point" for consistency**

For technical accuracy, "floating point" should be hyphenated as "floating-point" when used as a compound adjective modifying a noun.

```diff
-For generated floating point answers, it's important to use consistent rounding when displaying numbers to students _and_ when computing the correct answer. Displaying rounded numbers to students but using the unrounded version for grading can lead to unexpected results.
+For generated floating-point answers, it's important to use consistent rounding when displaying numbers to students _and_ when computing the correct answer. Displaying rounded numbers to students but using the unrounded version for grading can lead to unexpected results.
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~57-~57: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...### Parameter generation  For generated floating point answers, it's important to use consiste...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

</details>

</details>

---

`151-151`: **Improve sentence structure**

Consider adding a comma before "otherwise" to improve sentence clarity.

```diff
-If this function is not defined, the question will be graded automatically based on the correct answers set in `data["correct_answers"]`. Each answer the student provides will also be given feedback from the element that graded it. If the `grade` function _is_ defined, the data you receive has already been graded by the elements. You should ensure you only award partial credit if the answer isn't correct, otherwise you might give partial credit for a correct answer. In the snipped below, we update `data["score"]` using the [`set_weighted_score_data`][prairielearn.question_utils.set_weighted_score_data] utility.
+If this function is not defined, the question will be graded automatically based on the correct answers set in `data["correct_answers"]`. Each answer the student provides will also be given feedback from the element that graded it. If the `grade` function _is_ defined, the data you receive has already been graded by the elements. You should ensure you only award partial credit if the answer isn't correct, otherwise, you might give partial credit for a correct answer. In the snipped below, we update `data["score"]` using the [`set_weighted_score_data`][prairielearn.question_utils.set_weighted_score_data] utility.
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~151-~151: Try using a synonym here to strengthen your writing.
Context: ...nswer the student provides will also be given feedback from the element that graded i...

(GIVE_PROVIDE)

---

[typographical] ~151-~151: The word “otherwise” is an adverb that can’t be used like a conjunction, and therefore needs to be separated from the sentence.
Context: ...ward partial credit if the answer isn't correct, otherwise you might give partial credit for a cor...

(THUS_SENTENCE)

---

[uncategorized] ~151-~151: A punctuation mark might be missing here.
Context: ...w, we update `data["score"]` using the [`set_weighted_score_data`][prairielearn.q...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

</details>

</details>

---

`190-190`: **Apply consistent hyphenation in "floating point"**

For technical accuracy and consistency, "floating point" should be hyphenated when used as a compound adjective.

```diff
-For grading functions involving floating point numbers, **avoid exact comparisons with `==`**. Floating point comparisons with `==` might unexpectedly fail since calculations in Python can introduce floating-point error. Instead, use the [`math.isclose`](https://docs.python.org/3/library/math.html#math.isclose) function, as it performs comparisons within given tolerance. The `prairielearn` Python library also offers several functions to perform more specialized comparisons:
+For grading functions involving floating-point numbers, **avoid exact comparisons with `==`**. Floating-point comparisons with `==` might unexpectedly fail since calculations in Python can introduce floating-point error. Instead, use the [`math.isclose`](https://docs.python.org/3/library/math.html#math.isclose) function, as it performs comparisons within a given tolerance. The `prairielearn` Python library also offers several functions to perform more specialized comparisons:
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~190-~190: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...nswers  For grading functions involving floating point numbers, **avoid exact comparisons with...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~190-~190: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... **avoid exact comparisons with `==`**. Floating point comparisons with `==` might unexpectedl...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~190-~190: You might be missing the article “a” here.
Context: ...tion, as it performs comparisons within given tolerance. The `prairielearn` Python li...

(AI_EN_LECTOR_MISSING_DETERMINER_A)

</details>

</details>

---

`307-307`: **Fix article usage**

Use "an" before words starting with a vowel sound.

```diff
-| `format_errors`         | `dict`  | Dictionary of format errors for each answer. Each item maps from a named answer to a error message.                                  |
+| `format_errors`         | `dict`  | Dictionary of format errors for each answer. Each item maps from a named answer to an error message.                                 |
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[misspelling] ~307-~307: Use “an” instead of ‘a’ if the following word starts with a vowel sound, e.g. ‘an article’, ‘an hour’.
Context: .... Each item maps from a named answer to a error message.                         ...

(EN_A_VS_AN)

</details>

</details>

</blockquote></details>
<details>
<summary>docs/question/template.md (1)</summary><blockquote>

`113-113`: **Fix article usage before "HTML"**

Use "an" before words starting with a vowel sound.

```diff
-HTML or JavaScript comments in your `question.html` source are visible to students in the rendered page source. To leave small maintenance notes to staff in your `question.html` source, you should use [Mustache comments](https://mustache.github.io/mustache.5.html#Comments) (`{{! ... }}`) that will be removed during the rendering process. Never put sensitive information, such as solutions, in a HTML/JS comment.
+HTML or JavaScript comments in your `question.html` source are visible to students in the rendered page source. To leave small maintenance notes to staff in your `question.html` source, you should use [Mustache comments](https://mustache.github.io/mustache.5.html#Comments) (`{{! ... }}`) that will be removed during the rendering process. Never put sensitive information, such as solutions, in an HTML/JS comment.
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[misspelling] ~113-~113: Use “an” instead of ‘a’ if the following word starts with a vowel sound, e.g. ‘an article’, ‘an hour’.
Context: ...tive information, such as solutions, in a HTML/JS comment.  Example:  ```html <!-...

(EN_A_VS_AN)

</details>

</details>

</blockquote></details>
<details>
<summary>docs/question/index.md (3)</summary><blockquote>

`1-6`: **Verify image path and refine phrasing**  
Ensure that `example-question.png` is placed under `docs/question/` so the image renders correctly in the site. Also, consider replacing “written in a variety of ways” with “written in various ways” for greater conciseness.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~3-~3: Consider using a synonym to be more concise.
Context: ... or complex, and they can be written in a variety of ways. This document describes how to cr...

(A_VARIETY_OF)

</details>

</details>

---

`155-184`: **Standardize warning style**  
Instead of the inline `:warning:` emoji, consider using an `!!! warning` admonition block for the note on unique `answers-name` attributes, to maintain consistency with other admonitions.

---

`214-227`: **Consider enhancing grading methods section**  
For improved readability, you might present the four auto-grading methods in a summary table and emphasize the override precedence of options 3 & 4 using an admonition.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[style] ~222-~222: Consider using a synonym to be more concise.
Context: ...R_NAME]` and sets scores. This supports a variety of alternative grading options, including ...

(A_VARIETY_OF)

---

[uncategorized] ~222-~222: This verb may not be in the correct form. Consider using a different form for this context.
Context: ...f the submitted answer for correctness, compute correct answers of some elements based ...

(AI_EN_LECTOR_REPLACEMENT_VERB_FORM)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 66b3b53b590b071d7a4090e7a4255c7f0afeaf72 and 6465ba735c0b15e1ad71244ccec4e0bf58e97147.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `docs/question/example-question.png` is excluded by `!**/*.png`
* `docs/question/panels.png` is excluded by `!**/*.png`

</details>

<details>
<summary>📒 Files selected for processing (14)</summary>

* `apps/prairielearn/python/prairielearn/question_utils.py` (2 hunks)
* `docs/assessment/index.md` (1 hunks)
* `docs/assets/extra.css` (1 hunks)
* `docs/faq.md` (3 hunks)
* `docs/getStarted.md` (2 hunks)
* `docs/index.md` (1 hunks)
* `docs/manualGrading/index.md` (2 hunks)
* `docs/oldQuestion.md` (1 hunks)
* `docs/question/index.md` (1 hunks)
* `docs/question/lifecycle.d2` (1 hunks)
* `docs/question/server.md` (1 hunks)
* `docs/question/template.md` (1 hunks)
* `docs/scripts/d2_svg_file.py` (2 hunks)
* `docs/workspaces/index.md` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (6)</summary>

* docs/oldQuestion.md
* docs/assessment/index.md
* docs/assets/extra.css
* docs/workspaces/index.md
* apps/prairielearn/python/prairielearn/question_utils.py
* docs/question/lifecycle.d2

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* docs/faq.md
* docs/getStarted.md
* docs/index.md

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>docs/question/server.md (1)</summary>

```
Learnt from: reteps
PR: PrairieLearn/PrairieLearn#11829
File: docs/question/server.md:86-92
Timestamp: 2025-04-22T03:04:09.254Z
Learning: In PrairieLearn documentation, reference-style links using Python import paths (like `[`to_json`][prairielearn.conversion_utils.to_json]`) are automatically resolved by the mkdocstrings plugin without needing explicit reference definitions.
```

</details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>docs/manualGrading/index.md</summary>

[uncategorized] ~23-~23: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...stored.  It is recommended to also mark manually-graded questions as `"singleVariant": true` so...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

</details>
<details>
<summary>docs/question/index.md</summary>

[style] ~3-~3: Consider using a synonym to be more concise.
Context: ... or complex, and they can be written in a variety of ways. This document describes how to cr...

(A_VARIETY_OF)

---

[uncategorized] ~11-~11: Loose punctuation mark.
Context: ...ree main components:  1. **`info.json`**: Metadata about the question, including ...

(UNLIKELY_OPENING_PUNCTUATION)

---

[uncategorized] ~12-~12: Loose punctuation mark.
Context: ... topic, and tags. 2. **`question.html`**: The HTML template that defines the ques...

(UNLIKELY_OPENING_PUNCTUATION)

---

[uncategorized] ~13-~13: Loose punctuation mark.
Context: ...md) for more details. 3. **`server.py`**: This is where you write the logic for g...

(UNLIKELY_OPENING_PUNCTUATION)

---

[style] ~209-~209: Using many exclamation marks might seem excessive (in this case: 18 exclamation marks for a text that’s 12768 characters long)
Context: ...arams"]["m"] * data["params"]["a"] ```  !!! info      More information about `serve...

(EN_EXCESSIVE_EXCLAMATION)

---

[style] ~222-~222: Consider using a synonym to be more concise.
Context: ...R_NAME]` and sets scores. This supports a variety of alternative grading options, including ...

(A_VARIETY_OF)

---

[uncategorized] ~222-~222: This verb may not be in the correct form. Consider using a different form for this context.
Context: ...f the submitted answer for correctness, compute correct answers of some elements based ...

(AI_EN_LECTOR_REPLACEMENT_VERB_FORM)

</details>
<details>
<summary>docs/question/server.md</summary>

[uncategorized] ~57-~57: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...### Parameter generation  For generated floating point answers, it's important to use consiste...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~111-~111: This verb may not be in the correct tense. Consider changing the tense to fit the context better.
Context: ...d the submitted answers directly, if we need to process the inputs further. For exam...

(AI_EN_LECTOR_REPLACEMENT_VERB_TENSE)

---

[style] ~138-~138: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... for the question in `data["score"]`. - Setting the overall feedback for the question i...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

---

[style] ~151-~151: Try using a synonym here to strengthen your writing.
Context: ...nswer the student provides will also be given feedback from the element that graded i...

(GIVE_PROVIDE)

---

[typographical] ~151-~151: The word “otherwise” is an adverb that can’t be used like a conjunction, and therefore needs to be separated from the sentence.
Context: ...ward partial credit if the answer isn't correct, otherwise you might give partial credit for a cor...

(THUS_SENTENCE)

---

[uncategorized] ~151-~151: A punctuation mark might be missing here.
Context: ...w, we update `data["score"]` using the [`set_weighted_score_data`][prairielearn.q...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

---

[uncategorized] ~190-~190: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...nswers  For grading functions involving floating point numbers, **avoid exact comparisons with...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~190-~190: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... **avoid exact comparisons with `==`**. Floating point comparisons with `==` might unexpectedl...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~190-~190: You might be missing the article “a” here.
Context: ...tion, as it performs comparisons within given tolerance. The `prairielearn` Python li...

(AI_EN_LECTOR_MISSING_DETERMINER_A)

---

[uncategorized] ~192-~192: A punctuation mark might be missing here.
Context: ...rform more specialized comparisons:  - [`is_correct_scalar_ra`][prairielearn.grad...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

---

[uncategorized] ~194-~194: A punctuation mark might be missing here.
Context: ...ified number of significant figures. - [`is_correct_scalar_dd`][prairielearn.grad...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

---

[misspelling] ~307-~307: Use “an” instead of ‘a’ if the following word starts with a vowel sound, e.g. ‘an article’, ‘an hour’.
Context: .... Each item maps from a named answer to a error message.                         ...

(EN_A_VS_AN)

---

[uncategorized] ~334-~334: A punctuation mark might be missing here.
Context: ...ata["params"]["numpy_array"]) ```  The [`pl.to_json`][prairielearn.conversion_uti...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

</details>
<details>
<summary>docs/question/template.md</summary>

[misspelling] ~113-~113: Use “an” instead of ‘a’ if the following word starts with a vowel sound, e.g. ‘an article’, ‘an hour’.
Context: ...tive information, such as solutions, in a HTML/JS comment.  Example:  ```html <!-...

(EN_A_VS_AN)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (17)</summary><blockquote>

<details>
<summary>docs/question/server.md (1)</summary>

`1-400`: **Well-structured documentation with comprehensive examples**

The server.py documentation is thorough and well-organized, providing clear explanations of the question lifecycle and functions. The examples effectively demonstrate best practices for parameter generation, parsing, grading, and file access. The inclusion of both good and bad examples helps clarify implementation details.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~57-~57: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...### Parameter generation  For generated floating point answers, it's important to use consiste...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~111-~111: This verb may not be in the correct tense. Consider changing the tense to fit the context better.
Context: ...d the submitted answers directly, if we need to process the inputs further. For exam...

(AI_EN_LECTOR_REPLACEMENT_VERB_TENSE)

---

[style] ~138-~138: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... for the question in `data["score"]`. - Setting the overall feedback for the question i...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

---

[style] ~151-~151: Try using a synonym here to strengthen your writing.
Context: ...nswer the student provides will also be given feedback from the element that graded i...

(GIVE_PROVIDE)

---

[typographical] ~151-~151: The word “otherwise” is an adverb that can’t be used like a conjunction, and therefore needs to be separated from the sentence.
Context: ...ward partial credit if the answer isn't correct, otherwise you might give partial credit for a cor...

(THUS_SENTENCE)

---

[uncategorized] ~151-~151: A punctuation mark might be missing here.
Context: ...w, we update `data["score"]` using the [`set_weighted_score_data`][prairielearn.q...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

---

[uncategorized] ~190-~190: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...nswers  For grading functions involving floating point numbers, **avoid exact comparisons with...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~190-~190: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... **avoid exact comparisons with `==`**. Floating point comparisons with `==` might unexpectedl...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

---

[uncategorized] ~190-~190: You might be missing the article “a” here.
Context: ...tion, as it performs comparisons within given tolerance. The `prairielearn` Python li...

(AI_EN_LECTOR_MISSING_DETERMINER_A)

---

[uncategorized] ~192-~192: A punctuation mark might be missing here.
Context: ...rform more specialized comparisons:  - [`is_correct_scalar_ra`][prairielearn.grad...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

---

[uncategorized] ~194-~194: A punctuation mark might be missing here.
Context: ...ified number of significant figures. - [`is_correct_scalar_dd`][prairielearn.grad...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

---

[misspelling] ~307-~307: Use “an” instead of ‘a’ if the following word starts with a vowel sound, e.g. ‘an article’, ‘an hour’.
Context: .... Each item maps from a named answer to a error message.                         ...

(EN_A_VS_AN)

---

[uncategorized] ~334-~334: A punctuation mark might be missing here.
Context: ...ata["params"]["numpy_array"]) ```  The [`pl.to_json`][prairielearn.conversion_uti...

(AI_EN_LECTOR_MISSING_PUNCTUATION)

</details>

</details>

</details>
<details>
<summary>docs/manualGrading/index.md (1)</summary>

`23-23`: **Successfully updated documentation references**

The references to question documentation have been correctly updated to point to the new file structure, maintaining documentation consistency across the project.



Also applies to: 32-32

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~23-~23: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: ...stored.  It is recommended to also mark manually-graded questions as `"singleVariant": true` so...

(HYPHENATED_LY_ADVERB_ADJECTIVE)

</details>

</details>

</details>
<details>
<summary>docs/scripts/d2_svg_file.py (1)</summary>

`12-12`: **Well-implemented feature for ignoring diagram hooking**

The addition of `IGNORE_HOOKING_FOR` list and the conditional check effectively allows specific diagram files to bypass the hooking process. This change provides greater flexibility when rendering certain SVG diagrams. The implementation is clean and properly integrated with the existing codebase.



Also applies to: 36-40

</details>
<details>
<summary>docs/question/template.md (1)</summary>

`1-241`: **Clear and comprehensive template documentation**

The documentation provides excellent explanations of question templates with practical examples. The sections on rendering panels, Mustache templates, Markdown usage, and LaTeX support are well-structured and easy to follow. The warnings about common pitfalls (like indentation in Markdown) are particularly helpful for question authors.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[misspelling] ~113-~113: Use “an” instead of ‘a’ if the following word starts with a vowel sound, e.g. ‘an article’, ‘an hour’.
Context: ...tive information, such as solutions, in a HTML/JS comment.  Example:  ```html <!-...

(EN_A_VS_AN)

</details>

</details>

</details>
<details>
<summary>docs/question/index.md (13)</summary>

`7-14`: **Check relative links to new template and server docs**  
The links `[question template documentation](template.md)` and `[server documentation](server.md)` should resolve to `docs/question/template.md` and `docs/question/server.md` respectively. Please verify that navigation works as expected in the rendered site.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~11-~11: Loose punctuation mark.
Context: ...ree main components:  1. **`info.json`**: Metadata about the question, including ...

(UNLIKELY_OPENING_PUNCTUATION)

---

[uncategorized] ~12-~12: Loose punctuation mark.
Context: ... topic, and tags. 2. **`question.html`**: The HTML template that defines the ques...

(UNLIKELY_OPENING_PUNCTUATION)

---

[uncategorized] ~13-~13: Loose punctuation mark.
Context: ...md) for more details. 3. **`server.py`**: This is where you write the logic for g...

(UNLIKELY_OPENING_PUNCTUATION)

</details>

</details>

---

`19-48`: **Directory structure code block is clear**  
The ASCII tree under ```text accurately illustrates the folder layout and nesting for question directories. No formatting issues detected.

---

`50-53`: **Tip admonition is well-formatted**  
The `!!! tip` block correctly highlights best practices for folder naming.

---

`54-59`: **Info admonition linking to example questions**  
The `!!! info` block appropriately references the GitHub example directory and running instructions.

---

`60-91`: **Metadata schema documentation is comprehensive**  
The JSON snippet and the property table thoroughly document `info.json` fields. Verify that links like `../uuid.md` and `../externalGrading.md` resolve correctly from this file’s location.

---

`92-95`: **Local development note is helpful**  
The `!!! note` block clearly explains how to reload question changes during local development.

---

`96-104`: **Question sharing section is clear**  
The explanation and JSON example for the `"shareSourcePublicly"`, `"sharePublicly"`, and `sharingSets` properties are well documented.

---

`106-133`: **Dependency specification section is thorough**  
The JSON example and the following table accurately describe all dependency properties (`nodeModulesScripts`, `clientFilesQuestionStyles`, etc.).

---

`136-142`: **Non-randomized questions section is accurate**  
The behavior of the `singleVariant` flag on Homework- and Exam-type assessments is clearly explained.

---

`143-150`: **Partial credit guidance is effective**  
The instructions for enabling/disabling partial credit and the strong recommendation to leave it enabled are concise and clear.

---

`151-154`: **Reference link to schema documentation**  
Ensure that the link to `../schemas/infoQuestion.md` exists and the schema reference renders correctly.

---

`194-208`: **Server-side example is clear**  
The Python snippet under the `server.py` section effectively demonstrates random parameter generation and correct answer computation.

---

`210-213`: **Post-code snippet info block is helpful**  
The `!!! info` block directs readers to the detailed server documentation appropriately.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T23:40:34Z
user: login: mwest1066
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T00:17:46Z
user: login: mwest1066
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-30T00:20:05Z

Review comments:
diff_hunk: @@ -0,0 +1,149 @@
+## Question `server.py`
+
+The `server.py` file for each question creates randomized question variants by generating random parameters and the corresponding correct answer. The `server.py` functions are:
+
+| Function     | Return object                            | modifiable `data` keys                                                                                   | unmodifiable `data` keys                                                                                                                                                                                   | Description                                                                                                                                                                                                                                                                   |
+| ------------ | ---------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `generate()` |                                          | `correct_answers`, `params`                                                                              | `options`, `variant_seed`                                                                                                                                                                                  | Generate the parameter and true answers for a new random question variant. Set `data["params"][name]` and `data["correct_answers"][name]` for any variables as needed. Modify the `data` dictionary in-place.                                                                 |
+| `prepare()`  |                                          | `answers_names`, `correct_answers`, `params`                                                             | `options`, `variant_seed`                                                                                                                                                                                  | Final question preparation after element code has run. Can modify data as necessary. Modify the `data` dictionary in-place.                                                                                                                                                   |
+| `render()`   | `html` (string)                          |                                                                                                          | `correct_answers`, `editable`, `feedback`, `format_errors`, `options`, `panel`, `params`, `partial_scores`, `raw_submitted_answers`, `score`, `submitted_answers`, `variant_seed`, `num_valid_submissions` | Render the HTML for one panel and return it as a string.                                                                                                                                                                                                                      |
+| `parse()`    |                                          | `format_errors`, `submitted_answers`, `correct_answers`, `feedback`                                      | `options`, `params`, `raw_submitted_answers`, `variant_seed`                                                                                                                                               | Parse the `data["submitted_answers"][var]` data entered by the student, modifying this variable. Modify the `data` dictionary in-place.                                                                                                                                       |
+| `grade()`    |                                          | `correct_answers`, `feedback`, `format_errors`, `params`, `partial_scores`, `score`, `submitted_answers` | `options`, `raw_submitted_answers`, `variant_seed`                                                                                                                                                         | Grade `data["submitted_answers"][var]` to determine a score. Store the score and any feedback in `data["partial_scores"][var]["score"]` and `data["partial_scores"][var]["feedback"]`. Modify the `data` dictionary in-place.                                                 |
+| `file()`     | `object` (string, bytes-like, file-like) |                                                                                                          | `correct_answers`, `filename`, `options`, `params`, `variant_seed`                                                                                                                                         | Generate a file object dynamically in lieu of a physical file. Trigger via `type="dynamic"` in the question element (e.g., `pl-figure`, `pl-file-download`). Access the requested filename via `data['filename']`. If `file()` returns nothing, an empty string will be used. |
+
+A complete `question.html` and `server.py` example looks like:
+
+```html title="question.html"
+<pl-question-panel>
+  <!-- params.x is defined by data["params"]["x"] in server.py's `generate()`. -->
+  <!-- params.operation defined by in data["params"]["operation"] in server.py's `generate()`. -->
+  If $x = {{params.x}}$ and $y$ is {{params.operation}} $x$, what is $y$?
+</pl-question-panel>
+
+<!-- y is defined by data["correct_answers"]["y"] in server.py's `generate()`. -->
+<pl-number-input answers-name="y" label="$y =$"></pl-number-input>
+<pl-submission-panel> {{feedback.y}} </pl-submission-panel>
+```
+
+```python title="server.py"
+import random
+import math
+
+def generate(data):
+    # Generate random parameters for the question and store them in the data["params"] dict:
+    data["params"]["x"] = random.randint(5, 10)
+    data["params"]["operation"] = random.choice(["double", "triple"])
+
+    # Also compute the correct answer (if there is one) and store in the data["correct_answers"] dict:
+    if data["params"]["operation"] == "double":
+        data["correct_answers"]["y"] = 2 * data["params"]["x"]
+    else:
+        data["correct_answers"]["y"] = 3 * data["params"]["x"]
+
+def prepare(data):
+    # This function will run after all elements have run `generate()`.
+    # We can alter any of the element data here, but this is rarely needed.
+    pass
+
+def parse(data):
+    # data["raw_submitted_answers"][NAME] is the exact raw answer submitted by the student.
+    # data["submitted_answers"][NAME] is the answer parsed by elements (e.g., strings converted to numbers).
+    # data["format_errors"][NAME] is the answer format error (if any) from elements.
+    # We can modify or delete format errors if we have custom logic (rarely needed).
+    # If there are format errors then the submission is "invalid" and is not graded.
+    # To provide feedback but keep the submission "valid", data["feedback"][NAME] can be used.
+
+    # As an example, we will reject negative numbers for "y":
+    # check we don't already have a format error
+    if "y" not in data["format_errors"] and data["submitted_answers"]["y"] < 0:
+        data["format_errors"]["y"] = "Negative numbers are not allowed"
+
+def grade(data):
+    # All elements will have already graded their answers (if any) before this point.
+    # data["partial_scores"][NAME]["score"] is the individual element scores (0 to 1).
+    # data["score"] is the total score for the question (0 to 1).
+    # We can modify or delete any of these if we have a custom grading method.
+    # This function only runs if `parse()` did not produce format errors, so we can assume all data is valid.
+
+    # grade(data) can also set data['format_errors'][NAME] if there is any reason to mark the question
+    # invalid during grading time.  This will cause the question to not use up one of the student's attempts' on exams.
+    # You are encouraged, though, to do any checks for invalid data that can be done in `parse(data)` there instead,
+    # since that method is also called when the student hits "Save only", in manually graded questions, or in
+    # assessments without real-time grading.
+
+    # As an example, we will give half points for incorrect answers larger than "x",
+    # only if not already correct. Use math.isclose to avoid possible floating point errors.
+    if math.isclose(data["score"], 0.0) and data["submitted_answers"]["y"] > data["params"]["x"]:
+        data["partial_scores"]["y"]["score"] = 0.5
+        data["score"] = 0.5
+        data["feedback"]["y"] = "Your value for $y$ is larger than $x$, but incorrect."
+```
+
+## Question Data Storage
+
+All persistent data related to a question variant is stored under different entries in the `data` dictionary. This dictionary is stored in JSON format by PrairieLearn, and as a result, everything in `data` must be JSON serializable. Some types in Python are natively JSON serializable, such as strings, lists, and dicts, while others are not, such as complex numbers, numpy ndarrays, and pandas DataFrames.
+
+To account for this, the `prairielearn` Python library (usually aliased and used as `pl`), provides the functions [`to_json`][prairielearn.conversion_utils.to_json] and [`from_json`][prairielearn.conversion_utils.from_json] (part of [`conversion_utils.py`](https://github.com/PrairieLearn/PrairieLearn/blob/master/apps/prairielearn/python/prairielearn/conversion_utils.py)), which can respectively serialize and deserialize various objects for storage as part of question data. Please refer to the docstrings on those functions for more information. Here is a simple example:
+
+```python title="server.py"
+import numpy as np
+import prairielearn as pl
+
+def generate(data):
path: docs/question/server.md
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Fix reference-style links for `to_json` and `from_json`**  
The document uses reference-style links (`[`to_json`][prairielearn.conversion_utils.to_json]`) without defining those references. Convert them to inline links or add the missing reference definitions so the links render correctly.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-22T03:00:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,149 @@
+## Question `server.py`
+
+The `server.py` file for each question creates randomized question variants by generating random parameters and the corresponding correct answer. The `server.py` functions are:
+
+| Function     | Return object                            | modifiable `data` keys                                                                                   | unmodifiable `data` keys                                                                                                                                                                                   | Description                                                                                                                                                                                                                                                                   |
+| ------------ | ---------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `generate()` |                                          | `correct_answers`, `params`                                                                              | `options`, `variant_seed`                                                                                                                                                                                  | Generate the parameter and true answers for a new random question variant. Set `data["params"][name]` and `data["correct_answers"][name]` for any variables as needed. Modify the `data` dictionary in-place.                                                                 |
+| `prepare()`  |                                          | `answers_names`, `correct_answers`, `params`                                                             | `options`, `variant_seed`                                                                                                                                                                                  | Final question preparation after element code has run. Can modify data as necessary. Modify the `data` dictionary in-place.                                                                                                                                                   |
+| `render()`   | `html` (string)                          |                                                                                                          | `correct_answers`, `editable`, `feedback`, `format_errors`, `options`, `panel`, `params`, `partial_scores`, `raw_submitted_answers`, `score`, `submitted_answers`, `variant_seed`, `num_valid_submissions` | Render the HTML for one panel and return it as a string.                                                                                                                                                                                                                      |
+| `parse()`    |                                          | `format_errors`, `submitted_answers`, `correct_answers`, `feedback`                                      | `options`, `params`, `raw_submitted_answers`, `variant_seed`                                                                                                                                               | Parse the `data["submitted_answers"][var]` data entered by the student, modifying this variable. Modify the `data` dictionary in-place.                                                                                                                                       |
+| `grade()`    |                                          | `correct_answers`, `feedback`, `format_errors`, `params`, `partial_scores`, `score`, `submitted_answers` | `options`, `raw_submitted_answers`, `variant_seed`                                                                                                                                                         | Grade `data["submitted_answers"][var]` to determine a score. Store the score and any feedback in `data["partial_scores"][var]["score"]` and `data["partial_scores"][var]["feedback"]`. Modify the `data` dictionary in-place.                                                 |
+| `file()`     | `object` (string, bytes-like, file-like) |                                                                                                          | `correct_answers`, `filename`, `options`, `params`, `variant_seed`                                                                                                                                         | Generate a file object dynamically in lieu of a physical file. Trigger via `type="dynamic"` in the question element (e.g., `pl-figure`, `pl-file-download`). Access the requested filename via `data['filename']`. If `file()` returns nothing, an empty string will be used. |
+
+A complete `question.html` and `server.py` example looks like:
+
+```html title="question.html"
+<pl-question-panel>
+  <!-- params.x is defined by data["params"]["x"] in server.py's `generate()`. -->
+  <!-- params.operation defined by in data["params"]["operation"] in server.py's `generate()`. -->
+  If $x = {{params.x}}$ and $y$ is {{params.operation}} $x$, what is $y$?
+</pl-question-panel>
+
+<!-- y is defined by data["correct_answers"]["y"] in server.py's `generate()`. -->
+<pl-number-input answers-name="y" label="$y =$"></pl-number-input>
+<pl-submission-panel> {{feedback.y}} </pl-submission-panel>
+```
+
+```python title="server.py"
+import random
+import math
+
+def generate(data):
+    # Generate random parameters for the question and store them in the data["params"] dict:
+    data["params"]["x"] = random.randint(5, 10)
+    data["params"]["operation"] = random.choice(["double", "triple"])
+
+    # Also compute the correct answer (if there is one) and store in the data["correct_answers"] dict:
+    if data["params"]["operation"] == "double":
+        data["correct_answers"]["y"] = 2 * data["params"]["x"]
+    else:
+        data["correct_answers"]["y"] = 3 * data["params"]["x"]
+
+def prepare(data):
+    # This function will run after all elements have run `generate()`.
+    # We can alter any of the element data here, but this is rarely needed.
+    pass
+
+def parse(data):
+    # data["raw_submitted_answers"][NAME] is the exact raw answer submitted by the student.
+    # data["submitted_answers"][NAME] is the answer parsed by elements (e.g., strings converted to numbers).
+    # data["format_errors"][NAME] is the answer format error (if any) from elements.
+    # We can modify or delete format errors if we have custom logic (rarely needed).
+    # If there are format errors then the submission is "invalid" and is not graded.
+    # To provide feedback but keep the submission "valid", data["feedback"][NAME] can be used.
+
+    # As an example, we will reject negative numbers for "y":
+    # check we don't already have a format error
+    if "y" not in data["format_errors"] and data["submitted_answers"]["y"] < 0:
+        data["format_errors"]["y"] = "Negative numbers are not allowed"
+
+def grade(data):
+    # All elements will have already graded their answers (if any) before this point.
+    # data["partial_scores"][NAME]["score"] is the individual element scores (0 to 1).
+    # data["score"] is the total score for the question (0 to 1).
+    # We can modify or delete any of these if we have a custom grading method.
+    # This function only runs if `parse()` did not produce format errors, so we can assume all data is valid.
+
+    # grade(data) can also set data['format_errors'][NAME] if there is any reason to mark the question
+    # invalid during grading time.  This will cause the question to not use up one of the student's attempts' on exams.
+    # You are encouraged, though, to do any checks for invalid data that can be done in `parse(data)` there instead,
+    # since that method is also called when the student hits "Save only", in manually graded questions, or in
+    # assessments without real-time grading.
+
+    # As an example, we will give half points for incorrect answers larger than "x",
+    # only if not already correct. Use math.isclose to avoid possible floating point errors.
+    if math.isclose(data["score"], 0.0) and data["submitted_answers"]["y"] > data["params"]["x"]:
+        data["partial_scores"]["y"]["score"] = 0.5
+        data["score"] = 0.5
+        data["feedback"]["y"] = "Your value for $y$ is larger than $x$, but incorrect."
+```
+
+## Question Data Storage
+
+All persistent data related to a question variant is stored under different entries in the `data` dictionary. This dictionary is stored in JSON format by PrairieLearn, and as a result, everything in `data` must be JSON serializable. Some types in Python are natively JSON serializable, such as strings, lists, and dicts, while others are not, such as complex numbers, numpy ndarrays, and pandas DataFrames.
+
+To account for this, the `prairielearn` Python library (usually aliased and used as `pl`), provides the functions [`to_json`][prairielearn.conversion_utils.to_json] and [`from_json`][prairielearn.conversion_utils.from_json] (part of [`conversion_utils.py`](https://github.com/PrairieLearn/PrairieLearn/blob/master/apps/prairielearn/python/prairielearn/conversion_utils.py)), which can respectively serialize and deserialize various objects for storage as part of question data. Please refer to the docstrings on those functions for more information. Here is a simple example:
+
+```python title="server.py"
+import numpy as np
+import prairielearn as pl
+
+def generate(data):
path: docs/question/server.md
user: login: reteps
body: We use autorefs, so this isn't needed.
created_at: 2025-04-22T03:03:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,149 @@
+## Question `server.py`
+
+The `server.py` file for each question creates randomized question variants by generating random parameters and the corresponding correct answer. The `server.py` functions are:
+
+| Function     | Return object                            | modifiable `data` keys                                                                                   | unmodifiable `data` keys                                                                                                                                                                                   | Description                                                                                                                                                                                                                                                                   |
+| ------------ | ---------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `generate()` |                                          | `correct_answers`, `params`                                                                              | `options`, `variant_seed`                                                                                                                                                                                  | Generate the parameter and true answers for a new random question variant. Set `data["params"][name]` and `data["correct_answers"][name]` for any variables as needed. Modify the `data` dictionary in-place.                                                                 |
+| `prepare()`  |                                          | `answers_names`, `correct_answers`, `params`                                                             | `options`, `variant_seed`                                                                                                                                                                                  | Final question preparation after element code has run. Can modify data as necessary. Modify the `data` dictionary in-place.                                                                                                                                                   |
+| `render()`   | `html` (string)                          |                                                                                                          | `correct_answers`, `editable`, `feedback`, `format_errors`, `options`, `panel`, `params`, `partial_scores`, `raw_submitted_answers`, `score`, `submitted_answers`, `variant_seed`, `num_valid_submissions` | Render the HTML for one panel and return it as a string.                                                                                                                                                                                                                      |
+| `parse()`    |                                          | `format_errors`, `submitted_answers`, `correct_answers`, `feedback`                                      | `options`, `params`, `raw_submitted_answers`, `variant_seed`                                                                                                                                               | Parse the `data["submitted_answers"][var]` data entered by the student, modifying this variable. Modify the `data` dictionary in-place.                                                                                                                                       |
+| `grade()`    |                                          | `correct_answers`, `feedback`, `format_errors`, `params`, `partial_scores`, `score`, `submitted_answers` | `options`, `raw_submitted_answers`, `variant_seed`                                                                                                                                                         | Grade `data["submitted_answers"][var]` to determine a score. Store the score and any feedback in `data["partial_scores"][var]["score"]` and `data["partial_scores"][var]["feedback"]`. Modify the `data` dictionary in-place.                                                 |
+| `file()`     | `object` (string, bytes-like, file-like) |                                                                                                          | `correct_answers`, `filename`, `options`, `params`, `variant_seed`                                                                                                                                         | Generate a file object dynamically in lieu of a physical file. Trigger via `type="dynamic"` in the question element (e.g., `pl-figure`, `pl-file-download`). Access the requested filename via `data['filename']`. If `file()` returns nothing, an empty string will be used. |
+
+A complete `question.html` and `server.py` example looks like:
+
+```html title="question.html"
+<pl-question-panel>
+  <!-- params.x is defined by data["params"]["x"] in server.py's `generate()`. -->
+  <!-- params.operation defined by in data["params"]["operation"] in server.py's `generate()`. -->
+  If $x = {{params.x}}$ and $y$ is {{params.operation}} $x$, what is $y$?
+</pl-question-panel>
+
+<!-- y is defined by data["correct_answers"]["y"] in server.py's `generate()`. -->
+<pl-number-input answers-name="y" label="$y =$"></pl-number-input>
+<pl-submission-panel> {{feedback.y}} </pl-submission-panel>
+```
+
+```python title="server.py"
+import random
+import math
+
+def generate(data):
+    # Generate random parameters for the question and store them in the data["params"] dict:
+    data["params"]["x"] = random.randint(5, 10)
+    data["params"]["operation"] = random.choice(["double", "triple"])
+
+    # Also compute the correct answer (if there is one) and store in the data["correct_answers"] dict:
+    if data["params"]["operation"] == "double":
+        data["correct_answers"]["y"] = 2 * data["params"]["x"]
+    else:
+        data["correct_answers"]["y"] = 3 * data["params"]["x"]
+
+def prepare(data):
+    # This function will run after all elements have run `generate()`.
+    # We can alter any of the element data here, but this is rarely needed.
+    pass
+
+def parse(data):
+    # data["raw_submitted_answers"][NAME] is the exact raw answer submitted by the student.
+    # data["submitted_answers"][NAME] is the answer parsed by elements (e.g., strings converted to numbers).
+    # data["format_errors"][NAME] is the answer format error (if any) from elements.
+    # We can modify or delete format errors if we have custom logic (rarely needed).
+    # If there are format errors then the submission is "invalid" and is not graded.
+    # To provide feedback but keep the submission "valid", data["feedback"][NAME] can be used.
+
+    # As an example, we will reject negative numbers for "y":
+    # check we don't already have a format error
+    if "y" not in data["format_errors"] and data["submitted_answers"]["y"] < 0:
+        data["format_errors"]["y"] = "Negative numbers are not allowed"
+
+def grade(data):
+    # All elements will have already graded their answers (if any) before this point.
+    # data["partial_scores"][NAME]["score"] is the individual element scores (0 to 1).
+    # data["score"] is the total score for the question (0 to 1).
+    # We can modify or delete any of these if we have a custom grading method.
+    # This function only runs if `parse()` did not produce format errors, so we can assume all data is valid.
+
+    # grade(data) can also set data['format_errors'][NAME] if there is any reason to mark the question
+    # invalid during grading time.  This will cause the question to not use up one of the student's attempts' on exams.
+    # You are encouraged, though, to do any checks for invalid data that can be done in `parse(data)` there instead,
+    # since that method is also called when the student hits "Save only", in manually graded questions, or in
+    # assessments without real-time grading.
+
+    # As an example, we will give half points for incorrect answers larger than "x",
+    # only if not already correct. Use math.isclose to avoid possible floating point errors.
+    if math.isclose(data["score"], 0.0) and data["submitted_answers"]["y"] > data["params"]["x"]:
+        data["partial_scores"]["y"]["score"] = 0.5
+        data["score"] = 0.5
+        data["feedback"]["y"] = "Your value for $y$ is larger than $x$, but incorrect."
+```
+
+## Question Data Storage
+
+All persistent data related to a question variant is stored under different entries in the `data` dictionary. This dictionary is stored in JSON format by PrairieLearn, and as a result, everything in `data` must be JSON serializable. Some types in Python are natively JSON serializable, such as strings, lists, and dicts, while others are not, such as complex numbers, numpy ndarrays, and pandas DataFrames.
+
+To account for this, the `prairielearn` Python library (usually aliased and used as `pl`), provides the functions [`to_json`][prairielearn.conversion_utils.to_json] and [`from_json`][prairielearn.conversion_utils.from_json] (part of [`conversion_utils.py`](https://github.com/PrairieLearn/PrairieLearn/blob/master/apps/prairielearn/python/prairielearn/conversion_utils.py)), which can respectively serialize and deserialize various objects for storage as part of question data. Please refer to the docstrings on those functions for more information. Here is a simple example:
+
+```python title="server.py"
+import numpy as np
+import prairielearn as pl
+
+def generate(data):
path: docs/question/server.md
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Check the mkdocs configuration for autorefs-related settings
cat mkdocs.yml | grep -i "autoref\|mkdocstrings"

# Also look at the overall structure of mkdocs.yml
cat mkdocs.yml
```

Length of output: 11794

---



</details>

@reteps I understand now - thanks for the clarification! I see that you're using mkdocstrings which automatically resolves those Python reference-style links. The plugin handles references to Python objects like `prairielearn.conversion_utils.to_json` without needing explicit reference definitions. 

My suggestion wasn't applicable here. Thanks for teaching me about your documentation setup!



🐰 *Learning something new every day!*

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: reteps
PR: PrairieLearn/PrairieLearn#11829
File: docs/question/server.md:86-92
Timestamp: 2025-04-22T03:04:09.227Z
Learning: In PrairieLearn documentation, reference-style links using Python import paths (like `[`to_json`][prairielearn.conversion_utils.to_json]`) are automatically resolved by the mkdocstrings plugin without needing explicit reference definitions.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Check the mkdocs configuration for autorefs-related settings
cat mkdocs.yml | grep -i "autoref\|mkdocstrings"

# Also look at the overall structure of mkdocs.yml
cat mkdocs.yml
```

Length of output: 11794

---



</details>

@reteps I understand now - thanks for the clarification! I see that you're using mkdocstrings which automatically resolves those Python reference-style links. The plugin handles references to Python objects like `prairielearn.conversion_utils.to_json` without needing explicit reference definitions. 

My suggestion wasn't applicable here. Thanks for teaching me about your documentation setup!



🐰 *Learning something new every day!*

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: reteps
PR: PrairieLearn/PrairieLearn#11829
File: docs/question/server.md:86-92
Timestamp: 2025-04-22T03:04:09.227Z
Learning: In PrairieLearn documentation, reference-style links using Python import paths (like `[`to_json`][prairielearn.conversion_utils.to_json]`) are automatically resolved by the mkdocstrings plugin without needing explicit reference definitions.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-22T03:04:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -209,7 +209,10 @@ nav:
       - 'Course configuration': 'course/index.md'
       - 'Course instance configuration': 'courseInstance.md'
       - 'Assessment configuration': 'assessment/index.md'
-      - 'Question configuration': 'question.md'
+      - 'Question configuration':
+          - 'question/index.md'
+          - '<code>question.html</code>': 'question/html.md'
+          - '<code>server.py</code>': 'question/server.md'
       - 'Question runtime environment': 'questionRuntime/index.md'
       - 'Elements for writing questions': 'elements.md'
path: mkdocs.yml
user: login: jonatanschroeder
body: I think you could add the elements under "Question configuration" as well, but I'd prefer to hear from @nwalters512 or @mwest1066 about that.
created_at: 2025-04-25T15:50:17Z
author_association: MEMBER
diff_hunk: @@ -209,7 +209,10 @@ nav:
       - 'Course configuration': 'course/index.md'
       - 'Course instance configuration': 'courseInstance.md'
       - 'Assessment configuration': 'assessment/index.md'
-      - 'Question configuration': 'question.md'
+      - 'Question configuration':
+          - 'question/index.md'
+          - '<code>question.html</code>': 'question/html.md'
+          - '<code>server.py</code>': 'question/server.md'
       - 'Question runtime environment': 'questionRuntime/index.md'
       - 'Elements for writing questions': 'elements.md'
path: mkdocs.yml
user: login: nwalters512
body: I somewhat like that idea. If we do that, maybe "Question configuration" should be "Question authoring" instead? Let's get input from others before we make any changes here (and maybe save that for a future PR anyways).
created_at: 2025-04-25T17:23:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -209,7 +209,10 @@ nav:
       - 'Course configuration': 'course/index.md'
       - 'Course instance configuration': 'courseInstance.md'
       - 'Assessment configuration': 'assessment/index.md'
-      - 'Question configuration': 'question.md'
+      - 'Question configuration':
+          - 'question/index.md'
+          - '<code>question.html</code>': 'question/html.md'
+          - '<code>server.py</code>': 'question/server.md'
       - 'Question runtime environment': 'questionRuntime/index.md'
       - 'Elements for writing questions': 'elements.md'
path: mkdocs.yml
user: login: mwest1066
body: Agree on everything that @nwalters512 said!
created_at: 2025-04-30T00:17:46Z
author_association: MEMBER

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-22T02:57:43Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

```markdown
## Walkthrough

This change reorganizes and updates the PrairieLearn question documentation. The old `docs/question.md` file has been deleted and replaced with a new directory structure: `docs/question/index.md` (main question documentation), `docs/question/server.md` (server-side logic), and `docs/question/template.md` (template and Markdown usage). Numerous internal documentation links throughout the project have been updated to reference the new file paths. The documentation navigation in `mkdocs.yml` has been restructured to reflect this multi-file organization. Additionally, the docstrings for `PartialScore` and `QuestionData` TypedDicts in `apps/prairielearn/python/prairielearn/question_utils.py` were enhanced with detailed attribute descriptions and updated URLs. A new CSS rule was added to hide the "view source" button in mkdocstrings tooltips. A new question lifecycle diagram was introduced, and a modification was made to the D2 SVG rendering script to allow ignoring hooking for certain files. No functional code changes were made.

## Changes

| Files/Paths                                                                                     | Change Summary                                                                                                 |
|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| docs/question.md                                                                               | Deleted the old comprehensive question documentation file.                                                     |
| docs/question/index.md                                                                         | Added new main question documentation detailing directory structure, info.json, templating, and grading.       |
| docs/question/server.md                                                                        | Added new documentation describing `server.py` functions, data handling, and dynamic file generation.         |
| docs/question/template.md                                                                      | Added new documentation on Markdown usage, LaTeX, and template best practices in questions.                   |
| docs/assessment/index.md                                                                       | Updated internal link to point to new `question/index.md` anchor.                                             |
| docs/clientServerFiles.md                                                                      | Updated internal link to new `question/server.md` anchor.                                                     |
| docs/concepts/index.md, docs/course/index.md                                                  | Updated internal links to reference `question/index.md` instead of `question.md`.                             |
| docs/faq.md, docs/getStarted.md                                                               | Updated links to point to new question documentation paths and anchors; removed obsolete FAQ section.         |
| docs/instructor-guide/index.md, docs/manualGrading/index.md, docs/oldQuestion.md, docs/workspaces/index.md | Updated various internal links to the new question documentation structure.                                    |
| docs/elements.md                                                                              | Updated link for question data storage to `question/server.md`.                                               |
| docs/index.md                                                                                | Updated "Why PrairieLearn?" section links to `question/index.md` with updated anchors.                         |
| docs/python-reference/index.md                                                               | Expanded deprecation warning for old import paths (content update, not a link change).                        |
| mkdocs.yml                                                                                   | Changed navigation: replaced single `question.md` entry with a section containing `question/index.md`, `question/template.md`, and `question/server.md`. |
| docs/assets/extra.css                                                                        | Added CSS rule to hide the "view source" button within mkdocstrings tooltips.                                 |
| docs/question/lifecycle.d2                                                                   | Added new lifecycle diagram illustrating question processing flow and key functions.                          |
| docs/scripts/d2_svg_file.py                                                                  | Added global list `IGNORE_HOOKING_FOR` and logic to skip hooking for specified diagram files.                 |
| apps/prairielearn/python/prairielearn/question_utils.py                                     | Expanded and updated docstrings for `PartialScore` and `QuestionData` TypedDicts with detailed attribute descriptions and updated URLs. |

## Possibly related PRs

- PrairieLearn/PrairieLearn#11835: Updates question-related documentation by removing internal rendering details from the user-facing `question.md` file, related through documentation restructuring but focusing on different content aspects.

## Suggested reviewers

- nwalters512
```

</details>

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 413c8570eb232a81fe335ed0bff70d680f51b02b and 07a3dbd8c4e31cae93c5ac22b3b7a252831ee4e5.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `apps/prairielearn/python/prairielearn/question_utils.py` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* apps/prairielearn/python/prairielearn/question_utils.py

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: native-checks
* GitHub Check: test-prairielearn
* GitHub Check: build-core-images / build-images (ubuntu-24.04)
* GitHub Check: build-core-images / build-images (ubuntu-24.04-arm)

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=PrairieLearn/PrairieLearn&utm_content=11829):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: reteps
created_at: 2025-04-22T02:57:52Z
body: * **#11830** <a href="https://app.graphite.dev/github/pr/PrairieLearn/PrairieLearn/11830?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#11829** <a href="https://app.graphite.dev/github/pr/PrairieLearn/PrairieLearn/11829?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/PrairieLearn/PrairieLearn/11829?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* `master`


<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-04-22T03:01:29Z
body: ## All images

| Image | Platform | Old Size | New Size | Change |
| --- | --- | --- | --- | --- |
| [prairielearn/executor:07a3dbd8c4e31cae93c5ac22b3b7a252831ee4e5](https://hub.docker.com/layers/prairielearn/executor/07a3dbd8c4e31cae93c5ac22b3b7a252831ee4e5/images/sha256-29d1ee439cecbfd95e4437d7a557bc52419a5836256ff6d5158a7de0ec5a58e4?context=explore) | linux/amd64 | 1216.70 MB | 1216.85 MB | 0.01% |
| [prairielearn/executor:07a3dbd8c4e31cae93c5ac22b3b7a252831ee4e5](https://hub.docker.com/layers/prairielearn/executor/07a3dbd8c4e31cae93c5ac22b3b7a252831ee4e5/images/sha256-6673fa287dcc9d6672e8e23cde0afc93881294f8afee3839693b5996bdbd002c?context=explore) | linux/arm64 | 1230.13 MB | 1230.27 MB | 0.01% |
| [prairielearn/prairielearn:07a3dbd8c4e31cae93c5ac22b3b7a252831ee4e5](https://hub.docker.com/layers/prairielearn/prairielearn/07a3dbd8c4e31cae93c5ac22b3b7a252831ee4e5/images/sha256-da2c1e339ebdfa77b13b546dd2479a16628e617638ce31bf2d7918359caa1763?context=explore) | linux/amd64 | 1216.70 MB | 1216.84 MB | 0.01% |
| [prairielearn/prairielearn:07a3dbd8c4e31cae93c5ac22b3b7a252831ee4e5](https://hub.docker.com/layers/prairielearn/prairielearn/07a3dbd8c4e31cae93c5ac22b3b7a252831ee4e5/images/sha256-d186de0b7cc93d0981bb2c37b18c8ad30349d22c177947d85f8469f9645bb617?context=explore) | linux/arm64 | 1230.12 MB | 1230.27 MB | 0.01% |
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-22T03:05:54Z
body: ## [Codecov](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11829?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 12.20%. Comparing base ([`682ad13`](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/commit/682ad13256339693f0d7f3a64e95bdaf7f416e5d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn)) to head ([`07a3dbd`](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/commit/07a3dbd8c4e31cae93c5ac22b3b7a252831ee4e5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn)).
:warning: Report is 405 commits behind head on master.
> :exclamation:  There is a different number of reports uploaded between BASE (682ad13) and HEAD (07a3dbd). Click for more details.
> 
> <details><summary>HEAD has 2 uploads less than BASE</summary>
>
>| Flag | BASE (682ad13) | HEAD (07a3dbd) |
>|------|------|------|
>|python|2|1|
>|javascript|2|1|
></details>

<details><summary>Additional details and impacted files</summary>



```diff
@@             Coverage Diff             @@
##           master   #11829       +/-   ##
===========================================
- Coverage   71.96%   12.20%   -59.76%     
===========================================
  Files         690      690               
  Lines      105514   105531       +17     
  Branches     8332     1291     -7041     
===========================================
- Hits        75934    12883    -63051     
- Misses      29061    92637    +63576     
+ Partials      519       11      -508     
```

| [Flag](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11829/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) | Coverage Δ | |
|---|---|---|
| [javascript](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11829/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) | `10.70% <ø> (-61.05%)` | :arrow_down: |
| [python](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11829/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) | `81.78% <100.00%> (+0.14%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn#carryforward-flags-in-the-pull-request-comment) to find out more.
</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11829?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn).
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE