Part of the shared-curriculum sub-modules umbrella (#396), phase B of 4. Depends on #397 (phase A models). Full design: _docs/planning/shared-curriculum-submodules-design.md, "Course-repository representation" section 1/3/5 for the exact YAML shape, and section 4's "Website parser parity" paragraph.
Goal
The website's content-source parser and curriculum importer understand the optional sub_modules: key in module.yaml and the optional sub_module: key in cohort.yaml homework bindings, and populate the phase-A models correctly. Absent keys must mean exactly today's behavior — this phase must not require any real course repository to change.
Scope
content_sync/course_repository_v2.py _parse_module: accept an optional sub_modules: list, exclusive with a top-level units: (a module has one or the other, never both, never neither — mirror the design doc's exact validation wording as parser error messages). Flatten sub-module units into the same units tuple the parser builds today (part 1's units then part 2's, in list order) so every existing downstream consumer of ModuleSource.units is unaffected. Add a new SubModuleSource tuple (content id, slug, title, summary, unit index range) on ModuleSource. UnitSource gains sub_module_slug: str | None.
_parse_homework_bindings: accept an optional sub_module: <slug> on a binding, validated against module_by_slug[module].sub_modules. HomeworkBindingSource gains sub_module: str | None. duplicate_homework_module becomes a duplicate-anchor check: (module, None) mapped twice, or (module, sub_module) mapped twice — a module anchor and a part anchor on the same module are NOT a duplicate.
courses/services/curriculum_source.py: update ModuleSource/UnitSource/HomeworkBindingSource dataclasses to match.
courses/curriculum_source_validators.py: new all-or-nothing rule (within one module, either every lesson has a sub-module or none does) and contiguity rule (each sub-module's lessons are contiguous in module position order, matching sub-module list order) — both import-time checks, not DB constraints, per the design doc's reasoning (they're what make "after the anchor's last lesson" unambiguous for phase C rendering).
courses/services/curriculum_import.py _import_shared_modules/_upsert_shared_lessons/_import_shared_placements: write SharedSubModule rows, set SharedLesson.sub_module, set CohortSharedModule.sub_module from the binding's anchor.
- Add a mirrored test fixture:
content_sync/tests/fixtures/course_repository/llm_zoomcamp_shared/ gets a split module (a new one, don't rewrite the existing single-part fixture module that other tests depend on) with a sub-module-anchored homework binding in a fixture cohort — matching whatever fixture the zoomcamp-ops checker side uses (coordinate naming/shape, though that repo's own fixtures are out of scope for this issue).
Non-goals
- No
zoomcamp-ops checker changes (a separate repository, phase D coordinates with it but doesn't implement it here).
- No rendering changes (phase C) — this phase only gets data into the database correctly; module/lesson pages continue rendering exactly as before (they simply don't yet read the new fields).
- No real content repository changes — llm-zoomcamp's actual
module.yaml/cohort.yaml are untouched by this phase.
Dependencies
Acceptance criteria
Part of the shared-curriculum sub-modules umbrella (#396), phase B of 4. Depends on #397 (phase A models). Full design:
_docs/planning/shared-curriculum-submodules-design.md, "Course-repository representation" section 1/3/5 for the exact YAML shape, and section 4's "Website parser parity" paragraph.Goal
The website's content-source parser and curriculum importer understand the optional
sub_modules:key inmodule.yamland the optionalsub_module:key incohort.yamlhomework bindings, and populate the phase-A models correctly. Absent keys must mean exactly today's behavior — this phase must not require any real course repository to change.Scope
content_sync/course_repository_v2.py_parse_module: accept an optionalsub_modules:list, exclusive with a top-levelunits:(a module has one or the other, never both, never neither — mirror the design doc's exact validation wording as parser error messages). Flatten sub-module units into the sameunitstuple the parser builds today (part 1's units then part 2's, in list order) so every existing downstream consumer ofModuleSource.unitsis unaffected. Add a newSubModuleSourcetuple (content id, slug, title, summary, unit index range) onModuleSource.UnitSourcegainssub_module_slug: str | None._parse_homework_bindings: accept an optionalsub_module: <slug>on a binding, validated againstmodule_by_slug[module].sub_modules.HomeworkBindingSourcegainssub_module: str | None.duplicate_homework_modulebecomes a duplicate-anchor check:(module, None)mapped twice, or(module, sub_module)mapped twice — a module anchor and a part anchor on the same module are NOT a duplicate.courses/services/curriculum_source.py: updateModuleSource/UnitSource/HomeworkBindingSourcedataclasses to match.courses/curriculum_source_validators.py: new all-or-nothing rule (within one module, either every lesson has a sub-module or none does) and contiguity rule (each sub-module's lessons are contiguous in module position order, matching sub-module list order) — both import-time checks, not DB constraints, per the design doc's reasoning (they're what make "after the anchor's last lesson" unambiguous for phase C rendering).courses/services/curriculum_import.py_import_shared_modules/_upsert_shared_lessons/_import_shared_placements: writeSharedSubModulerows, setSharedLesson.sub_module, setCohortSharedModule.sub_modulefrom the binding's anchor.content_sync/tests/fixtures/course_repository/llm_zoomcamp_shared/gets a split module (a new one, don't rewrite the existing single-part fixture module that other tests depend on) with a sub-module-anchored homework binding in a fixture cohort — matching whatever fixture thezoomcamp-opschecker side uses (coordinate naming/shape, though that repo's own fixtures are out of scope for this issue).Non-goals
zoomcamp-opschecker changes (a separate repository, phase D coordinates with it but doesn't implement it here).module.yaml/cohort.yamlare untouched by this phase.Dependencies
Acceptance criteria
make django-check/make migrations-checkpass.make testpasses, including new parser/importer tests: exclusivity rejected (bothunitsandsub_modulespresent, or neither), a valid split module imports correctly (sub-module rows created, lessons correctly assigned, flattened unit order matches today's ordering rules), a sub-module-anchored homework binding imports correctly, an invalidsub_modulereference in a binding is rejected with a clear error, all-or-nothing and contiguity validators reject malformed fixtures.sub_modules:.