From 00c5850ae0b4c53658e19e296dd75f30c4d66d26 Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Thu, 30 Jul 2026 13:12:51 -0700 Subject: [PATCH 1/3] Do not include chapter numbers in title when processing runestone-manifest --- components/rsptx/build_tools/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/rsptx/build_tools/core.py b/components/rsptx/build_tools/core.py index 077b3bb9b..2cfcd8e65 100644 --- a/components/rsptx/build_tools/core.py +++ b/components/rsptx/build_tools/core.py @@ -734,7 +734,7 @@ def _process_single_chapter(sess, db_context, chapter, chap_counter, course_name db_context["chapters"] .insert() .values( - chapter_name=f"{cnum} {chapter.find('./title').text}", + chapter_name=f"{chapter.find('./title').text}", course_id=course_name, chapter_label=chapter.find("./id").text, chapter_num=cnum, @@ -808,7 +808,7 @@ def _process_single_subchapter( titletext = " ".join( [ET.tostring(y).decode("utf8") for y in subchapter.findall("./title/*")] ) - titletext = f"{scnum} {titletext.strip()}" + titletext = f"{titletext.strip()}" # Insert subchapter ins = ( From 1eb4308c732b7a753e35dbd1bbf7c1221956655d Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Tue, 4 Aug 2026 11:42:34 -0700 Subject: [PATCH 2/3] CRUD: include chapter numbers in fetch_questions --- components/rsptx/db/crud/question.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/components/rsptx/db/crud/question.py b/components/rsptx/db/crud/question.py index b414c04f0..3bc8df729 100644 --- a/components/rsptx/db/crud/question.py +++ b/components/rsptx/db/crud/question.py @@ -823,6 +823,10 @@ def find_page_id(chapter, subchapter): "num": chapters[chapter]["sub_chapters"][subchapter][ "sub_chapter_num" ], + "chapter_num": chapters[chapter]["chapter_num"], + "sub_chapter_num": chapters[chapter]["sub_chapters"][ + subchapter + ]["sub_chapter_num"], "chapter": chapter, "subchapter": subchapter, "id": find_page_id(chapter, subchapter), @@ -835,7 +839,16 @@ def find_page_id(chapter, subchapter): ), }, "children": [ - {"key": q.name, "data": q.dict()} + { + "key": q.name, + "data": { + **q.dict(), + "chapter_num": chapters[chapter]["chapter_num"], + "sub_chapter_num": chapters[chapter][ + "sub_chapters" + ][subchapter]["sub_chapter_num"], + }, + } for q in questions[chapter][subchapter] ], } @@ -846,6 +859,7 @@ def find_page_id(chapter, subchapter): "data": { "title": chapters[chapter]["chapter_name"], "num": chapters[chapter]["chapter_num"], + "chapter_num": chapters[chapter]["chapter_num"], }, "children": subs, } From 2a0508f088f1ec68dd3b6504065fd76adbdc24ed Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Tue, 4 Aug 2026 11:43:20 -0700 Subject: [PATCH 3/3] Assibment builder - add explicit chapter/subchapter numbers to builder pages --- .../AssignmentReadingsTable.module.css | 5 +++ .../reading/AssignmentReadingsTable.spec.tsx | 29 ++++++++++---- .../reading/AssignmentReadingsTable.tsx | 40 ++++++++++++++++--- .../components/ChooseReadingsButton.spec.tsx | 22 +++++----- .../components/ChooseReadingsButton.tsx | 23 ++++++++++- .../assignment_builder/src/types/exercises.ts | 4 ++ .../routers/instructor.py | 4 ++ 7 files changed, 101 insertions(+), 26 deletions(-) diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.module.css b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.module.css index 09a8b2e20..798b49065 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.module.css +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.module.css @@ -2,6 +2,11 @@ white-space: nowrap; } +.chapterCell { + min-width: 0; + max-width: 16rem; +} + .sectionCell { min-width: 0; max-width: 24rem; diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.spec.tsx index 2ea339e4b..5dc855d6f 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.spec.tsx @@ -31,8 +31,12 @@ const READINGS: Exercise[] = [ id: 1, name: "Welcome", title: "Welcome", - chapter: "Intro", + chapter: "intro", + chapter_name: "Intro", + chapter_num: 1, subchapter: "intro-welcome", + sub_chapter_name: "Welcome", + sub_chapter_num: 1, numQuestions: 10, activities_required: 6, points: 5, @@ -42,8 +46,12 @@ const READINGS: Exercise[] = [ id: 2, name: "Variables", title: "Variables", - chapter: "Basics", + chapter: "basics", + chapter_name: "Basics", + chapter_num: 2, subchapter: "basics-variables", + sub_chapter_name: "Variables", + sub_chapter_num: 3, numQuestions: 0, activities_required: 0, points: 3, @@ -66,16 +74,21 @@ const baseProps = { describe("AssignmentReadingsTable", () => { it("renders one row per reading with chapter and section", () => { renderWithMantine(); - expect(screen.getByText("Intro")).toBeInTheDocument(); - expect(screen.getByText("Welcome")).toBeInTheDocument(); - expect(screen.getByText("Basics")).toBeInTheDocument(); - expect(screen.getByText("Variables")).toBeInTheDocument(); + expect(screen.getByText("1 Intro")).toBeInTheDocument(); + expect(screen.getByText("intro")).toBeInTheDocument(); + expect(screen.getByText("1.1 Welcome")).toBeInTheDocument(); + expect(screen.getByText("intro-welcome")).toBeInTheDocument(); + expect(screen.getByText("2 Basics")).toBeInTheDocument(); + expect(screen.getByText("basics")).toBeInTheDocument(); + expect(screen.getByText("2.3 Variables")).toBeInTheDocument(); + expect(screen.getByText("basics-variables")).toBeInTheDocument(); }); it("filters readings by chapter or section text", () => { renderWithMantine(); - expect(screen.queryByText("Welcome")).not.toBeInTheDocument(); - expect(screen.getByText("Variables")).toBeInTheDocument(); + expect(screen.queryByText("1.1 Welcome")).not.toBeInTheDocument(); + expect(screen.getByText("2.3 Variables")).toBeInTheDocument(); + expect(screen.getByText("basics-variables")).toBeInTheDocument(); }); it("defaults the activity count to at least one and required to 80 percent", () => { diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.tsx index 55af61220..981093222 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/AssignmentReadingsTable.tsx @@ -35,13 +35,28 @@ interface AssignmentReadingsTableProps { const getNumQuestionsOrDefault = (numQuestions: Nullable): number => Math.max(numQuestions ?? 0, 1); +const formatNumberedLabel = (label: string, numbers: Array): string => { + const prefix = numbers + .filter((value): value is number => value !== null && value !== undefined) + .join("."); + + return prefix ? [prefix, label].filter(Boolean).join(" ") : label; +}; + const matchesFilter = (reading: Exercise, filter: string): boolean => { const query = filter.trim().toLowerCase(); if (!query) { return true; } - return [reading.name, reading.title, reading.chapter, reading.subchapter] + return [ + reading.name, + reading.title, + reading.chapter_name, + reading.sub_chapter_name, + reading.chapter, + reading.subchapter + ] .filter(Boolean) .some((field) => field!.toLowerCase().includes(query)); }; @@ -75,8 +90,17 @@ export const AssignmentReadingsTable = ({ { key: "chapter", header: "Chapter", - width: "12rem", - render: (row) =>
{row.chapter}
+ width: "16rem", + render: (row) => ( +
+
+ {formatNumberedLabel(row.chapter_name || row.chapter, [row.chapter_num])} +
+
+ {row.chapter} +
+
+ ) }, { key: "subchapter", @@ -84,8 +108,14 @@ export const AssignmentReadingsTable = ({ width: "20rem", render: (row) => (
-
- {row.name || row.title} +
+ {formatNumberedLabel( + row.sub_chapter_name || row.title || row.name || row.subchapter, + [row.chapter_num, row.sub_chapter_num] + )}
{row.subchapter} diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/ChooseReadingsButton.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/ChooseReadingsButton.spec.tsx index c90c66768..13aa9f6dd 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/ChooseReadingsButton.spec.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/ChooseReadingsButton.spec.tsx @@ -12,10 +12,10 @@ const { mockUpdate, mockReadingsSelector, updatingHolder, READINGS_TREE } = vi.h READINGS_TREE: [ { key: "chapter-1", - data: { title: "Chapter 1" }, + data: { title: "Chapter 1", chapter_num: 1 }, children: [ - { key: "sec-1", data: { title: "Section 1", id: 11 } }, - { key: "sec-2", data: { title: "Section 2", id: 12 } } + { key: "sec-1", data: { title: "Section 1", id: 11, chapter_num: 1, sub_chapter_num: 1 } }, + { key: "sec-2", data: { title: "Section 2", id: 12, chapter_num: 1, sub_chapter_num: 2 } } ] } ] as TreeNode[] @@ -46,18 +46,18 @@ describe("ChooseReadingsButton", () => { it("reveals the readings tree when the button is clicked", async () => { renderWithMantine(); - expect(screen.queryByText("Chapter 1")).not.toBeInTheDocument(); + expect(screen.queryByText("1 Chapter 1")).not.toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Choose readings" })); - expect(screen.getByText("Chapter 1")).toBeInTheDocument(); + expect(screen.getByText("1 Chapter 1")).toBeInTheDocument(); }); it("adds the selected leaf readings on select", async () => { renderWithMantine(); await userEvent.click(screen.getByRole("button", { name: "Choose readings" })); - await userEvent.click(screen.getByRole("button", { name: "Expand Chapter 1" })); + await userEvent.click(screen.getByRole("button", { name: "Expand 1 Chapter 1" })); - await userEvent.click(screen.getByRole("checkbox", { name: "Select Section 1" })); + await userEvent.click(screen.getByRole("checkbox", { name: "Select 1.1 Section 1" })); expect(mockUpdate).toHaveBeenCalledWith({ idsToAdd: [11], isReading: true }); }); @@ -70,9 +70,9 @@ describe("ChooseReadingsButton", () => { renderWithMantine(); await userEvent.click(screen.getByRole("button", { name: "Choose readings" })); - await userEvent.click(screen.getByRole("button", { name: "Expand Chapter 1" })); + await userEvent.click(screen.getByRole("button", { name: "Expand 1 Chapter 1" })); - await userEvent.click(screen.getByRole("checkbox", { name: "Select Section 1" })); + await userEvent.click(screen.getByRole("checkbox", { name: "Select 1.1 Section 1" })); expect(mockUpdate).toHaveBeenCalledWith({ idsToRemove: [99], isReading: true }); }); @@ -100,9 +100,9 @@ describe("ChooseReadingsButton", () => { renderWithMantine(); await userEvent.click(screen.getByRole("button", { name: "Choose readings" })); - await userEvent.click(screen.getByRole("button", { name: "Expand Chapter 1" })); + await userEvent.click(screen.getByRole("button", { name: "Expand 1 Chapter 1" })); - await userEvent.click(screen.getByRole("checkbox", { name: "Select Section 1" })); + await userEvent.click(screen.getByRole("checkbox", { name: "Select 1.1 Section 1" })); expect(mockUpdate).not.toHaveBeenCalled(); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/ChooseReadingsButton.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/ChooseReadingsButton.tsx index d739ca10e..5d894a9a1 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/ChooseReadingsButton.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/reading/components/ChooseReadingsButton.tsx @@ -13,10 +13,29 @@ import { getLeafNodes } from "@/utils/exercise"; import styles from "./ChooseReadingsButton.module.css"; +type NumberedNodeData = Partial & { + chapter_num?: number; + sub_chapter_num?: number; +}; + +const formatNumberedNodeLabel = (node: TreeNode): string => { + const data = node.data as NumberedNodeData | undefined; + const title = data?.title ?? String(node.key); + const numbers = + data?.sub_chapter_num !== null && data?.sub_chapter_num !== undefined + ? [data.chapter_num, data.sub_chapter_num] + : [data?.chapter_num ?? data?.num]; + const prefix = numbers + .filter((value): value is number => value !== null && value !== undefined) + .join("."); + + return prefix ? [prefix, title].filter(Boolean).join(" ") : title; +}; + const READINGS_COLUMNS: TreeTableColumn[] = [ { header: "Select readings", - render: (node) => {(node.data as Exercise)?.title} + render: (node) => {formatNumberedNodeLabel(node)} } ]; @@ -83,7 +102,7 @@ export const ChooseReadingsButton = () => { onSelect={onSelect} onUnselect={onUnselect} ariaLabel="Choose readings" - getNodeLabel={(node) => (node.data as Exercise)?.title ?? String(node.key)} + getNodeLabel={formatNumberedNodeLabel} />
diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/types/exercises.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/types/exercises.ts index 0b8bcbbf8..911699cb9 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/types/exercises.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/types/exercises.ts @@ -68,6 +68,10 @@ export type Exercise = { owner: string; tags: string; num: number; + chapter_num?: number; + sub_chapter_num?: number; + chapter_name?: string; + sub_chapter_name?: string; numQuestions: number; required: boolean; title: string; diff --git a/bases/rsptx/assignment_server_api/routers/instructor.py b/bases/rsptx/assignment_server_api/routers/instructor.py index af0d2010f..5d68448d5 100644 --- a/bases/rsptx/assignment_server_api/routers/instructor.py +++ b/bases/rsptx/assignment_server_api/routers/instructor.py @@ -966,6 +966,10 @@ async def get_assignment_questions( # augment the assignment question with additional question data aq["name"] = q["name"] aq["subchapter"] = q["subchapter"] + aq["chapter_num"] = row.Chapter.chapter_num + aq["sub_chapter_num"] = row.SubChapter.sub_chapter_num + aq["chapter_name"] = row.Chapter.chapter_name + aq["sub_chapter_name"] = row.SubChapter.sub_chapter_name aq["chapter"] = q["chapter"] aq["base_course"] = q["base_course"] aq["htmlsrc"] = q["htmlsrc"]