diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/AssignmentBuilder.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/AssignmentBuilder.tsx
index ce76e96d0..3c8e59a5f 100644
--- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/AssignmentBuilder.tsx
+++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/AssignmentBuilder.tsx
@@ -108,17 +108,6 @@ export const AssignmentBuilder = () => {
await duplicateAssignment(assignment.id);
};
- const handleReleasedChange = async (assignment: Assignment, released: boolean) => {
- try {
- await updateAssignment({
- ...assignment,
- released
- });
- toast.success(`Assignment ${released ? "released" : "not released"} for students`);
- } catch (error) {
- toast.error("Failed to update assignment release status");
- }
- };
const handleEnforceDueChange = async (assignment: Assignment, enforce_due: boolean) => {
try {
@@ -197,7 +186,6 @@ export const AssignmentBuilder = () => {
onCreateNew={handleCreateNew}
onEdit={handleEdit}
onDuplicate={handleDuplicate}
- onReleasedChange={handleReleasedChange}
onEnforceDueChange={handleEnforceDueChange}
onVisibilityChange={handleVisibilityChange}
onRemove={onRemove}
diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/list/AssignmentList.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/list/AssignmentList.tsx
index 8bfb84609..065f24a65 100644
--- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/list/AssignmentList.tsx
+++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/list/AssignmentList.tsx
@@ -23,7 +23,6 @@ interface AssignmentListProps {
onCreateNew: () => void;
onEdit: (assignment: Assignment) => void;
onDuplicate: (assignment: Assignment) => void;
- onReleasedChange: (assignment: Assignment, released: boolean) => void;
onEnforceDueChange: (assignment: Assignment, enforce_due: boolean) => void;
onVisibilityChange: (
assignment: Assignment,
@@ -39,7 +38,6 @@ export const AssignmentList = ({
onCreateNew,
onEdit,
onDuplicate,
- onReleasedChange,
onEnforceDueChange,
onVisibilityChange,
onRemove
@@ -70,19 +68,6 @@ export const AssignmentList = ({
);
- const releasedBodyTemplate = (rowData: Assignment) => (
-
- onReleasedChange(rowData, e.value)}
- tooltip={rowData.released ? "Released to students" : "Not released to students"}
- tooltipOptions={{
- position: "top"
- }}
- className={styles.smallSwitch}
- />
-
- );
const enforceDueBodyTemplate = (rowData: Assignment) => (
@@ -316,13 +301,6 @@ export const AssignmentList = ({
body={visibilityBodyTemplate}
className={styles.visibilityColumn}
/>
-
diff --git a/docs/source/question_json_schema.md b/docs/source/question_json_schema.md
new file mode 100644
index 000000000..8a6fd8e6c
--- /dev/null
+++ b/docs/source/question_json_schema.md
@@ -0,0 +1,495 @@
+# Question JSON Schema
+
+## Purpose
+
+This document defines the standardized JSON schema for the `question_json` field stored on every question/exercise record in the Runestone system. The `question_json` field is a **JSON-encoded string** that contains the type-specific content and configuration for a question - the fields that vary depending on the `question_type`.
+
+### Source of Truth
+
+| File | Role |
+|---|---|
+| `bases/rsptx/assignment_server_api/assignment_builder/src/types/exercises.ts` | TypeScript `QuestionJSON` type - canonical field-level definition |
+| `bases/rsptx/assignment_server_api/assignment_builder/src/utils/questionJson.ts` | Builder (`buildQuestionJson`), defaults (`getDefaultQuestionJson`), and merge logic (`mergeQuestionJsonWithDefaults`) |
+
+---
+
+## Supported Question Types
+
+Defined in `QuestionType` (Python) and `supportedExerciseTypes` (TypeScript):
+
+| `question_type` | Display Name | Description |
+|---|---|---|
+| `activecode` | Active Code | Interactive coding exercise with real-time execution |
+| `mchoice` | Multiple Choice | Single or multiple correct answers |
+| `shortanswer` | Short Answer | Free-text response |
+| `poll` | Poll | Survey / feedback question |
+| `dragndrop` | Drag and Drop | Match or order items by dragging |
+| `matching` | Matching | Match items from two different sets |
+| `parsonsprob` | Parsons Problem | Arrange code blocks in correct order |
+| `fillintheblank` | Fill in the Blank | Text with blanks to complete |
+| `selectquestion` | Select Question | Meta-question that selects from a pool |
+| `clickablearea` | Clickable Area | Identify areas in text or images |
+| `iframe` | iFrame | Embed external content via an iframe |
+
+> **Note:** Additional types exist in the backend enum (`video`, `codelens`, `youtube`, `hparsons`, `actex`, `page`, `webwork`) but are **not** currently supported in the assignment builder UI and do not have a `question_json` builder path.
+
+---
+
+## Field Reference
+
+All fields within `question_json` are **optional** at the top level (`Partial<{...}>`). Only the fields relevant to a particular `question_type` are populated when the JSON is built.
+
+| Field | Type | Used By Question Type(s) | Description |
+|---|---|---|---|
+| `statement` | `string` | `mchoice`, `poll`, `shortanswer`, `dragndrop`, `matching`, `clickablearea` | The main question/prompt text displayed to the student |
+| `instructions` | `string` | `activecode`, `parsonsprob` | Instructions shown above the coding/ordering area |
+| `questionText` | `string` | `fillintheblank`, `clickablearea` | Question body text (may contain `___` blank placeholders for FITB) |
+| `language` | `string` | `activecode`, `parsonsprob` | Programming language (e.g. `"python"`, `"java"`, `"cpp"`) |
+| `prefix_code` | `string` | `activecode` | Hidden code prepended before the student's code during execution |
+| `starter_code` | `string` | `activecode` | Initial code shown in the editor for the student to modify |
+| `suffix_code` | `string` | `activecode` | Hidden code appended after the student's code (typically unit tests) |
+| `stdin` | `string` | `activecode` | Standard input provided to the program at runtime |
+| `selectedExistingDataFiles` | `string[]` | `activecode` | List of data file names available to the code exercise |
+| `enableCodeTailor` | `boolean` | `activecode` | Enable CodeTailor personalized help feature |
+| `parsonspersonalize` | `"" \| "movable" \| "partial"` | `activecode` | CodeTailor Parsons personalization mode |
+| `parsonsexample` | `string` | `activecode` | Example question ID used by CodeTailor |
+| `enableCodelens` | `boolean` | `activecode` | Enable the Codelens step-through debugger |
+| `attachment` | `boolean` | `shortanswer` | Whether file attachment upload is allowed |
+| `optionList` | [`Option[]`](#option) | `mchoice`, `poll` | List of answer options |
+| `forceCheckboxes` | `boolean` | `mchoice` | Force checkbox UI (for multiple-select) |
+| `poll_type` | `string` | `poll` | Poll display type |
+| `scale_min` | `number` | `poll` | Minimum value for scale-type polls |
+| `scale_max` | `number` | `poll` | Maximum value for scale-type polls |
+| `left` | [`MatchItem[]`](#matchitem) | `dragndrop`, `matching` | Left-side items for matching/drag-and-drop |
+| `right` | [`MatchItem[]`](#matchitem) | `dragndrop`, `matching` | Right-side items for matching/drag-and-drop |
+| `correctAnswers` | `string[][]` | `dragndrop`, `matching` | Array of `[leftId, rightId]` pairs defining correct matches |
+| `feedback` | `string` | `dragndrop`, `matching`, `clickablearea` | Feedback text shown on incorrect answer |
+| `blocks` | [`ParsonsBlock[]`](#parsonsblock) | `parsonsprob` | Code blocks for Parsons problems |
+| `adaptive` | `boolean` | `parsonsprob` | Enable adaptive Parsons mode (provides hints) |
+| `numbered` | `"left" \| "right" \| "none"` | `parsonsprob` | Show line numbers and their position |
+| `noindent` | `boolean` | `parsonsprob` | Disable indentation in the Parsons problem |
+| `grader` | `"line" \| "dag"` | `parsonsprob` | Grading strategy: line-by-line or DAG-based |
+| `orderMode` | `"random" \| "custom"` | `parsonsprob` | Block presentation order |
+| `customOrder` | `number[]` | `parsonsprob` | Custom block ordering indices (when `orderMode` = `"custom"`) |
+| `blanks` | [`BlankWithFeedback[]`](#blankwithfeedback) | `fillintheblank` | Definitions for each blank |
+| `questionList` | `string[]` | `selectquestion` | Pool of question IDs to select from |
+| `questionLabels` | `Record
` | `selectquestion` | Mapping of question IDs to human-readable labels |
+| `abExperimentName` | `string` | `selectquestion` | A/B experiment name for randomized question selection |
+| `toggleOptions` | `string[]` | `selectquestion` | Toggle configuration options |
+| `dataLimitBasecourse` | `boolean` | `selectquestion` | Limit question pool to the base course |
+| `iframeSrc` | `string` | `iframe` | URL of the external content to embed |
+
+---
+
+## Sub-Object Schemas
+
+### `Option`
+
+Represents a single answer option for multiple-choice or poll questions.
+
+**Source:** `types/exercises.ts` - `interface Option`
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `choice` | `string` | ✅ | The text of the answer option |
+| `feedback` | `string` | ❌ | Feedback shown when this option is selected |
+| `correct` | `boolean` | ❌ | Whether this option is a correct answer |
+
+```json
+{ "choice": "Paris", "feedback": "Correct!", "correct": true }
+```
+
+---
+
+### `MatchItem`
+
+Represents a single item on the left or right side of a drag-and-drop or matching exercise.
+
+**Source:** `types/exercises.ts` - inline type on `left` and `right` fields
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `id` | `string` | ✅ | Unique identifier for the item (e.g. `"a"`, `"x"`) |
+| `label` | `string` | ✅ | Display text for the item |
+
+```json
+{ "id": "a", "label": "Python" }
+```
+
+---
+
+### `ParsonsBlock`
+
+Represents a single code block in a Parsons problem.
+
+**Source:** `utils/preview/parsonsPreview.tsx` - `interface ParsonsBlock`
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `id` | `string` | ✅ | Unique identifier for the block |
+| `content` | `string` | ✅ | The code/text content of the block |
+| `indent` | `number` | ✅ | Indentation level (0-based) |
+| `isDistractor` | `boolean` | ❌ | Whether this block is a distractor (should not be used in the solution) |
+| `isPaired` | `boolean` | ❌ | Whether this block is part of a paired distractor |
+| `groupId` | `string` | ❌ | Group identifier for paired distractors |
+| `isCorrect` | `boolean` | ❌ | Whether this is a correct block |
+| `tag` | `string` | ❌ | Tag for DAG grading dependencies |
+| `depends` | `string[]` | ❌ | Tags of blocks this block depends on (DAG grading) |
+| `explanation` | `string` | ❌ | Explanation shown during adaptive feedback |
+| `displayOrder` | `number` | ❌ | Custom display ordering index |
+| `pairedWithBlockAbove` | `boolean` | ❌ | Whether this block is paired with the block immediately above |
+
+```json
+{
+ "id": "block-1",
+ "content": "def greet():",
+ "indent": 0,
+ "isDistractor": false,
+ "tag": "def"
+}
+```
+
+---
+
+### `BlankWithFeedback`
+
+Represents a single blank in a fill-in-the-blank question, including its grading configuration.
+
+**Source:** `components/.../FillInTheBlankExercise/types.ts` - `interface BlankWithFeedback`
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `id` | `string` | ✅ | Unique identifier for the blank |
+| `graderType` | `"string" \| "regex" \| "number"` | ✅ | How this blank is graded |
+| `exactMatch` | `string` | ❌ | Expected exact string (when `graderType` = `"string"`) |
+| `regexPattern` | `string` | ❌ | Regex pattern to match (when `graderType` = `"regex"`) |
+| `regexFlags` | `string` | ❌ | Regex flags, e.g. `"i"` for case-insensitive (when `graderType` = `"regex"`) |
+| `numberMin` | `string` | ❌ | Minimum acceptable number as string (when `graderType` = `"number"`) |
+| `numberMax` | `string` | ❌ | Maximum acceptable number as string (when `graderType` = `"number"`) |
+| `correctFeedback` | `string` | ❌ | Feedback shown on correct answer |
+| `incorrectFeedback` | `string` | ❌ | Feedback shown on incorrect answer |
+
+```json
+{
+ "id": "blank-1",
+ "graderType": "regex",
+ "regexPattern": "sorted|ordered",
+ "regexFlags": "i",
+ "correctFeedback": "Correct!",
+ "incorrectFeedback": "Binary search requires a precondition on the input."
+}
+```
+
+---
+
+## Per-Question-Type Field Mapping
+
+This section shows exactly which fields are serialized into `question_json` for each `question_type`, as implemented by `buildQuestionJson()`.
+
+### `activecode`
+
+| Field | Type | Default |
+|---|---|---|
+| `prefix_code` | `string` | `""` |
+| `starter_code` | `string` | `""` |
+| `suffix_code` | `string` | `""` |
+| `instructions` | `string` | `""` |
+| `language` | `string` | First available language option |
+| `stdin` | `string` | `""` |
+| `selectedExistingDataFiles` | `string[]` | - |
+| `enableCodeTailor` | `boolean` | `false` |
+| `parsonspersonalize` | `"" \| "movable" \| "partial"` | `""` |
+| `parsonsexample` | `string` | `""` |
+| `enableCodelens` | `boolean` | `true` |
+
+### `mchoice`
+
+| Field | Type | Default |
+|---|---|---|
+| `statement` | `string` | `""` |
+| `optionList` | `Option[]` | Two empty options |
+
+### `shortanswer`
+
+| Field | Type | Default |
+|---|---|---|
+| `attachment` | `boolean` | `false` |
+| `statement` | `string` | `""` |
+
+### `poll`
+
+| Field | Type | Default |
+|---|---|---|
+| `statement` | `string` | `""` |
+| `optionList` | `Option[]` | Two empty options |
+
+### `dragndrop`
+
+| Field | Type | Default |
+|---|---|---|
+| `statement` | `string` | `""` |
+| `left` | `MatchItem[]` | `[{ id: "a", label: "" }]` |
+| `right` | `MatchItem[]` | `[{ id: "x", label: "" }]` |
+| `correctAnswers` | `string[][]` | `[["a", "x"]]` |
+| `feedback` | `string` | `"Incorrect. Please try again."` |
+
+### `matching`
+
+| Field | Type | Default |
+|---|---|---|
+| `statement` | `string` | `""` |
+| `left` | `MatchItem[]` | `[{ id: "a", label: "" }]` |
+| `right` | `MatchItem[]` | `[{ id: "x", label: "" }]` |
+| `correctAnswers` | `string[][]` | `[["a", "x"]]` |
+| `feedback` | `string` | `"Incorrect. Please try again."` |
+
+### `parsonsprob`
+
+| Field | Type | Default |
+|---|---|---|
+| `blocks` | `ParsonsBlock[]` | One empty block |
+| `language` | `string` | First available language option |
+| `instructions` | `string` | `""` |
+| `adaptive` | `boolean` | `true` |
+| `numbered` | `"left" \| "right" \| "none"` | `"left"` |
+| `noindent` | `boolean` | `false` |
+
+### `fillintheblank`
+
+| Field | Type | Default |
+|---|---|---|
+| `questionText` | `string` | - |
+| `blanks` | `BlankWithFeedback[]` | - |
+
+### `selectquestion`
+
+| Field | Type | Default |
+|---|---|---|
+| `questionList` | `string[]` | - |
+| `questionLabels` | `Record` | `{}` |
+| `abExperimentName` | `string` | - |
+| `toggleOptions` | `string[]` | - |
+| `dataLimitBasecourse` | `boolean` | - |
+
+### `clickablearea`
+
+| Field | Type | Default |
+|---|---|---|
+| `questionText` | `string` | - |
+| `statement` | `string` | `""` |
+| `feedback` | `string` | `"Incorrect. Please try again."` |
+
+### `iframe`
+
+| Field | Type | Default |
+|---|---|---|
+| `iframeSrc` | `string` | - |
+
+---
+
+## Full Example JSON Objects
+
+### Active Code
+
+```json
+{
+ "prefix_code": "import math\n",
+ "starter_code": "def solve(n):\n # your code here\n pass",
+ "suffix_code": "assert solve(5) == 120",
+ "instructions": "Write a function that computes the factorial of n.",
+ "language": "python",
+ "stdin": "",
+ "selectedExistingDataFiles": [],
+ "enableCodeTailor": false,
+ "parsonspersonalize": "",
+ "parsonsexample": "",
+ "enableCodelens": true
+}
+```
+
+### Multiple Choice
+
+```json
+{
+ "statement": "What is the capital of France?",
+ "optionList": [
+ { "choice": "London", "feedback": "London is the capital of the UK.", "correct": false },
+ { "choice": "Berlin", "feedback": "Berlin is the capital of Germany.", "correct": false },
+ { "choice": "Paris", "feedback": "Correct!", "correct": true },
+ { "choice": "Madrid", "feedback": "Madrid is the capital of Spain.", "correct": false }
+ ]
+}
+```
+
+### Short Answer
+
+```json
+{
+ "statement": "Explain the concept of polymorphism in object-oriented programming.",
+ "attachment": false
+}
+```
+
+### Poll
+
+```json
+{
+ "statement": "How confident are you with recursion?",
+ "optionList": [
+ { "choice": "Very confident" },
+ { "choice": "Somewhat confident" },
+ { "choice": "Not confident" }
+ ]
+}
+```
+
+### Drag and Drop
+
+```json
+{
+ "statement": "Match each language to its type system.",
+ "left": [
+ { "id": "a", "label": "Python" },
+ { "id": "b", "label": "Java" }
+ ],
+ "right": [
+ { "id": "x", "label": "Dynamically typed" },
+ { "id": "y", "label": "Statically typed" }
+ ],
+ "correctAnswers": [["a", "x"], ["b", "y"]],
+ "feedback": "Incorrect. Please try again."
+}
+```
+
+### Matching
+
+```json
+{
+ "statement": "Match the data structure to its time complexity for search.",
+ "left": [
+ { "id": "a", "label": "Hash Table" },
+ { "id": "b", "label": "Linked List" }
+ ],
+ "right": [
+ { "id": "x", "label": "O(1) average" },
+ { "id": "y", "label": "O(n)" }
+ ],
+ "correctAnswers": [["a", "x"], ["b", "y"]],
+ "feedback": "Incorrect. Review the data structures chapter."
+}
+```
+
+### Parsons Problem
+
+```json
+{
+ "instructions": "Arrange the code blocks to create a function that prints 'Hello World'.",
+ "language": "python",
+ "blocks": [
+ { "id": "block-1", "content": "def greet():", "indent": 0 },
+ { "id": "block-2", "content": "print('Hello World')", "indent": 1 },
+ { "id": "block-3", "content": "greet()", "indent": 0 },
+ { "id": "block-4", "content": "print('Goodbye')", "indent": 1, "isDistractor": true }
+ ],
+ "adaptive": true,
+ "numbered": "left",
+ "noindent": false
+}
+```
+
+### Fill in the Blank
+
+```json
+{
+ "questionText": "The time complexity of binary search is O(___) and it requires a ___ array.",
+ "blanks": [
+ {
+ "id": "blank-1",
+ "graderType": "string",
+ "exactMatch": "log n",
+ "correctFeedback": "Correct!",
+ "incorrectFeedback": "Think about how the search space is halved each time."
+ },
+ {
+ "id": "blank-2",
+ "graderType": "regex",
+ "regexPattern": "sorted|ordered",
+ "regexFlags": "i",
+ "correctFeedback": "Correct!",
+ "incorrectFeedback": "Binary search has a precondition on the input."
+ }
+ ]
+}
+```
+
+### Select Question
+
+```json
+{
+ "questionList": ["q-101", "q-102", "q-103"],
+ "questionLabels": {
+ "q-101": "Easy recursion",
+ "q-102": "Medium recursion",
+ "q-103": "Hard recursion"
+ },
+ "abExperimentName": "",
+ "toggleOptions": [],
+ "dataLimitBasecourse": true
+}
+```
+
+### Clickable Area
+
+```json
+{
+ "statement": "Click on the lines that contain a syntax error.",
+ "questionText": "x = 10\nif x = 10:\n print(x)\n
",
+ "feedback": "Look for assignment vs. comparison operators."
+}
+```
+
+### iFrame
+
+```json
+{
+ "iframeSrc": "https://example.com/interactive-simulation"
+}
+```
+
+---
+
+## Default Values
+
+When a new question is created, `getDefaultQuestionJson()` (in `questionJson.ts`) provides the following defaults:
+
+| Field | Default Value |
+|---|---|
+| `statement` | `""` |
+| `language` | First value from available language options |
+| `instructions` | `""` |
+| `prefix_code` | `""` |
+| `starter_code` | `""` |
+| `suffix_code` | `""` |
+| `stdin` | `""` |
+| `attachment` | `false` |
+| `optionList` | `[{ choice: "", feedback: "", correct: false }, { choice: "", feedback: "", correct: false }]` |
+| `left` | `[{ id: "a", label: "" }]` |
+| `right` | `[{ id: "x", label: "" }]` |
+| `correctAnswers` | `[["a", "x"]]` |
+| `feedback` | `"Incorrect. Please try again."` |
+| `blocks` | `[{ id: "block-", content: "", indent: 0 }]` |
+| `adaptive` | `true` |
+| `numbered` | `"left"` |
+| `noindent` | `false` |
+| `enableCodeTailor` | `false` |
+| `parsonspersonalize` | `""` |
+| `parsonsexample` | `""` |
+| `enableCodelens` | `true` |
+
+---
+
+## Implementation Notes
+
+1. **Storage format:** `question_json` is stored as a **JSON-encoded string** on the `Exercise` record. The `buildQuestionJson()` function serializes via `JSON.stringify()`, and only includes the fields relevant to the given `question_type`.
+
+2. **Type-conditional inclusion:** `buildQuestionJson()` uses spread-with-conditional patterns (`...(type === "x" && { ... })`) so that irrelevant fields for a question type are **never** included in the serialized JSON.