Part of the shared-curriculum sub-modules umbrella (#396), phase C of 4. Depends on #398 (phase B importer). Full design: _docs/planning/shared-curriculum-submodules-design.md, "Constraints and what the renderers must know" section.
Goal
Module pages, the cohort curriculum flow, the homework breadcrumb, and the shared-curriculum inventory correctly render/report sub-module structure and per-anchor homework placement, when present. Flat modules (the common case, zero sub-modules) must render byte-identical to today.
Scope
courses/views/shared_course.py (module page view) and courses/templates/courses/shared_module.html / _shared_module_rail.html: when a module has sub-modules, group its lesson list by sub-module in order (each group gets a heading with a fragment anchor, not its own route/URL — confirmed in the design doc as sufficient for now); after each sub-module's lesson group, render that sub-module's anchored homework link if one exists; after the last group (or immediately, for a flat module), render the module-anchored homework link if one exists. A flat module (no sub-modules) renders exactly as today — verify with a snapshot/diff test.
courses/services/curriculum_flow.py _shared_flow: unchanged loop shape (one ModuleFlowItem per placement, ordered by placement position). ModuleFlowItem gains an optional sub_module, so the cohort page can title an entry "Module 1 · Part 2: Agents" when anchored to a part. A module with two anchored homeworks (a module-level and a part-level, if that ever occurs) correctly yields two flow entries.
courses/views/homework_context.py: the shared-curriculum breadcrumb resolves homework.shared_module_placements to its anchor and links the module page, including the sub-module's fragment anchor when the homework is anchored to a part.
courses/services/shared_curriculum_inventory.py: add sub_module_slug to each placement row in the reconciliation report.
- Confirm (per the design doc's explicit check) that
courses/views/course_page_context.py:349 (family syllabus module count) and content/public_views.py:1296 (sitemap) need NO changes — both are root-SharedModule-only by construction and must stay that way (add a regression test asserting a split module still counts as one module in the family syllabus count and produces one sitemap entry, not one per sub-module).
Non-goals
- No
zoomcamp-ops/course-repository changes (phase D).
- No sub-module standalone pages/routes (explicitly deferred per the design doc's open question 3 — fragment anchors on the existing module page only).
- No change to
courses/views/dashboard_homeworks.py (confirmed in the design doc: it's keyed purely on Homework rows and never reads placements, so it needs no change — verify this holds, don't change it speculatively).
Dependencies
Acceptance criteria
Django and integration
Browser
Part of the shared-curriculum sub-modules umbrella (#396), phase C of 4. Depends on #398 (phase B importer). Full design:
_docs/planning/shared-curriculum-submodules-design.md, "Constraints and what the renderers must know" section.Goal
Module pages, the cohort curriculum flow, the homework breadcrumb, and the shared-curriculum inventory correctly render/report sub-module structure and per-anchor homework placement, when present. Flat modules (the common case, zero sub-modules) must render byte-identical to today.
Scope
courses/views/shared_course.py(module page view) andcourses/templates/courses/shared_module.html/_shared_module_rail.html: when a module has sub-modules, group its lesson list by sub-module in order (each group gets a heading with a fragment anchor, not its own route/URL — confirmed in the design doc as sufficient for now); after each sub-module's lesson group, render that sub-module's anchored homework link if one exists; after the last group (or immediately, for a flat module), render the module-anchored homework link if one exists. A flat module (no sub-modules) renders exactly as today — verify with a snapshot/diff test.courses/services/curriculum_flow.py_shared_flow: unchanged loop shape (oneModuleFlowItemper placement, ordered by placement position).ModuleFlowItemgains an optionalsub_module, so the cohort page can title an entry "Module 1 · Part 2: Agents" when anchored to a part. A module with two anchored homeworks (a module-level and a part-level, if that ever occurs) correctly yields two flow entries.courses/views/homework_context.py: the shared-curriculum breadcrumb resolveshomework.shared_module_placementsto its anchor and links the module page, including the sub-module's fragment anchor when the homework is anchored to a part.courses/services/shared_curriculum_inventory.py: addsub_module_slugto each placement row in the reconciliation report.courses/views/course_page_context.py:349(family syllabus module count) andcontent/public_views.py:1296(sitemap) need NO changes — both are root-SharedModule-only by construction and must stay that way (add a regression test asserting a split module still counts as one module in the family syllabus count and produces one sitemap entry, not one per sub-module).Non-goals
zoomcamp-ops/course-repository changes (phase D).courses/views/dashboard_homeworks.py(confirmed in the design doc: it's keyed purely onHomeworkrows and never reads placements, so it needs no change — verify this holds, don't change it speculatively).Dependencies
Acceptance criteria
Django and integration
make django-checkpasses;make testpasses.ModuleFlowItem.sub_modulerenders correctly in the cohort page's flow list for both anchor types.sub_module_slugfor anchored placements and isnull/absent for module-level placements.Browser
make test-playwright-corepasses.