You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Requirements
What problem are we solving?
Studio's backend already understands Courses, Units, and Lessons — le_utils.constants.modalities defines COURSE / UNIT / LESSON, the model layer validates UNIT topics with PRE_POST_TEST completion criteria, and PR #5860 added the lesson_objectives remap on copy. None of that is reachable from the editor today: ContentModalities in shared/constants.js only exposes QUIZ and SURVEY, and there is no UI to mark a folder as part of a Course, edit learning objectives, or pair the two pre/post-test quizzes.
This is the Studio side of learningequality/kolibri-ecosystem#38. Until Studio curators can author and edit Course / Unit / Lesson content, no human-authored Courses can flow to Kolibri for the S2S pilot.
Why this? Why now?
Studio is the only place a human curator can produce the Course content that the ecosystem project hinges on. Without an editing surface, every Course has to be added via content integration scripting using the ricecooker tool, with no curator-review step before it lands in Kolibri. Shipping the editor unblocks human-in-the-loop content for any pilots and gives us a way to test the data model under realistic curator workflows before it becomes a core Kolibri experience.
Outcomes
A Studio curator can toggle any folder as a Course; folders inside a Course automatically act as Units, and their child folders automatically act as Lessons. The curator never picks "this is a Unit" or "this is a Lesson" explicitly — role is positional.
A curator can author a Unit's pre/post-test as two matched quizzes ("A" and "B") drawn from one assessment-item pool, with each item linked to a learning objective. Parity (equal counts, equal objective coverage across A and B) is surfaced as a publish-blocker.
A curator can author a Lesson's learning objectives as an ordered list of free-text strings, with objective references from quiz items maintained across rename / reorder / delete.
A Course publishes through Studio's existing publish flow. Folders that don't conform to their positional role (a Unit with no Lessons, a Lesson with no objectives, etc.) are surfaced as incomplete in the editor and silently dropped from the published artifact, with the publish dialog summarising what will be dropped before the curator confirms.
Constraints
Smallest viable design and implementation diff. Reuse existing backend data shapes (extra_fields.options.modality, lesson_objectives, completion criteria) and existing editing surfaces (details panel, AssessmentItemEditor) — no new data models, no parallel editing experience.
Out of Scope
AI generation surfaces in Studio's editor (regenerate quiz, suggest objectives). Generation is a separate workstream; Studio edits the result.
Course-level metadata beyond what topics already support. No new grade-level / subject-area / duration fields specific to Course.
Learner and coach experiences in Kolibri — rendering Courses, course reports, learner-side A/B distribution, assignment flows. Separate workstream.
Migration of pre-existing topics in production. The editor is additive; existing trees keep working unchanged.
A dedicated "import a Course" affordance. Imported subtrees flow through the same positional auto-coercion as anything else.
Versioning or history for objectives and assessment items beyond Studio's existing draft / publish lifecycle.
Curator review of ricecooker-imported Courses as a distinct workflow. Imported Courses become editable through the same surface as hand-authored ones.
Context
Technical Context
Existing backend support the editor builds on
le_utils.constants.modalities already defines COURSE, UNIT, LESSON, QUIZ, SURVEY, CUSTOM_NAVIGATION.
ContentNode.extra_fields.options.modality stores a topic's modality. The frontend ContentModalities constant in contentcuration/contentcuration/frontend/shared/constants.js currently exposes only QUIZ and SURVEY — COURSE / UNIT / LESSON need to be added.
ContentNode.extra_fields.options.lesson_objectives on a Unit is a map keyed by Lesson child node ID to a list of objective strings. PR Remap Unit extra_fields.options node IDs when copying via copy_node #5860 added remap of these IDs to cloned counterparts on copy_node (see contentcuration/contentcuration/db/models/manager.py).
contentcuration/contentcuration/constants/completion_criteria.py validates that only UNIT-modality topics carry completion criteria, and only with the PRE_POST_TEST mastery model. No curator-facing choice — UNIT implies PRE_POST_TEST.
A UNIT topic publishes as a single exercise archive (see contentcuration/contentcuration/tests/test_exportchannel.py). Assessment items live directly on the Unit, not on child Quiz topics.
Editor model
One explicit curator control: a Course toggle on a folder's details panel. Everything else is positional. A folder inside a Course is a Unit by virtue of being there. A folder inside that Unit is a Lesson by virtue of being there. Moving a folder out of its Course auto-uncoerces it. The curator never picks "this is a Unit / Lesson".
The toggle's UX gives a preview of what the descendant tree will become (which folders auto-become Units, which auto-become Lessons, which fall outside the schema and will be incomplete) before the curator confirms. The preview is the conversion wizard; there is no separate multi-step wizard.
extra_fields.options.modality on Unit and Lesson folders is maintained automatically as a write-through of the positional derivation, so existing backend consumers (completion-criteria validation, lesson_objectives remap, publish path) continue to read the field they read today. Whether the write-through lives in the frontend or the backend is a scoping decision.
Unit editing
A UNIT folder owns a single pool of assessment items (consistent with the one-exercise-archive export). Each item carries a group marker, A or B. The editor surface for these items is one of two visual patterns — choice left to design: a tabbed view (single Quiz surface with an A / B tab toggle) or a matched-pairs view (two-column layout pairing the i-th question of A with the i-th question of B).
Each item belongs to exactly one of A or B. A and B should be matched: same count, same set of objectives covered. The editor surfaces a parity status (e.g., "A has 12 items, B has 11 — add one item to B" / "Objective X is in A only"). Parity is a warning during editing and a hard publish-blocker.
Each assessment-item row gains a single-select learning-objective dropdown sourced from the union of objectives across the Unit's child Lessons, grouped by Lesson. Picking an objective writes through to the Unit's lesson_objectives map. Leaving an item without an objective is allowed during editing but flags the Unit as incomplete and blocks publish.
Completion criteria are fully determined by the modality (UNIT ⇒ PRE_POST_TEST) and require no editor input. No other Unit-specific fields are introduced.
Lesson editing
A LESSON folder gains one new control: a Learning Objectives list editor — an ordered list of free-text strings, with add / remove / reorder. The editor writes to the parent Unit's lesson_objectives map under this Lesson's node ID. The Lesson UI is the curator's mental model; the data lives on the Unit, unchanged from today.
Objectives have stable internal IDs with the string as display label, so editing or reordering an objective string does not break references from quiz items.
Deleting an objective that's referenced by quiz items prompts the curator: either re-point those items to another objective or accept they'll be flagged as missing-objective until repaired. No silent breakage.
Hierarchy semantics
Creation: the tree's "Add" menu doesn't need context-specific Course / Unit / Lesson options because role is positional. It offers "New folder" plus the existing resource-creation actions. The only new affordance is the Course toggle on a folder once it exists.
Copy / move / import — outbound: a folder's role is whatever its new position dictates. Move a Unit out of a Course and it becomes a plain folder (its lesson_objectives map stays as dormant metadata; move it back into any Course and the data is still there).
Copy / move / import — inbound: one special rule, because objectives live on the parent Unit rather than on the Lesson itself. When a Lesson is moved or copied between Units, the objective list keyed by that Lesson's ID migrates — removed from the source Unit's lesson_objectives map and added under the same Lesson ID in the destination Unit's map. PR Remap Unit extra_fields.options node IDs when copying via copy_node #5860's copy-time remap logic handles the cloning case; live moves piggy-back on the same code path. If the destination isn't a Unit, the objectives are discarded — the Lesson is no longer a Lesson, so its objective data has no parent map to live in.
Conformance, not enforcement: Studio does not prevent a curator from creating odd shapes (a Unit with no Lessons, a folder deep inside a Lesson, resources sitting directly on a Unit). Misshapen folders are flagged incomplete in the editor and silently dropped from the published artifact. The omission is not silent from the curator's end — the same incomplete badges that surface during editing call out which children won't be published, and the publish dialog summarises them before confirm.
Validation rules ("incomplete" badge conditions)
Course (folder with the toggle) — incomplete if it has zero Unit-conforming children.
Unit (folder inside a Course) — incomplete if A/B parity fails, OR any assessment item is missing an objective, OR it has zero Lesson-conforming children, OR it has resource-kind content-node children attached directly to it (assessment items themselves live on the Unit and are not children; only video / document / etc. content-node children are forbidden here — those belong inside Lessons).
Lesson (folder inside a Unit) — incomplete if it has zero resource children, OR zero learning objectives, OR it has folder children (topics inside a Lesson have no role).
Areas of the codebase the work will touch
Frontend: contentcuration/contentcuration/frontend/shared/constants.js (extend ContentModalities), channelEdit/components/edit/DetailsTabView.vue (Course toggle + descendant preview), channelEdit/components/edit/ (Lesson Learning Objectives editor), channelEdit/components/AssessmentItemEditor/ (A/B group marker, per-item objective picker, parity status), plus a shared positional-role utility, the tree's incomplete badge, and the modality write-through hook.
Backend: contentcuration/contentcuration/db/models/manager.py (modality field follows ancestry changes via copy / move; existing lesson_objectives remap stays as-is), contentcuration/contentcuration/models.py and constants/completion_criteria.py (add validation for the conformance rules used by publish), and the publish path (gate publication on conformance; silently omit non-conforming children).
Open questions for technical scoping
Visual choice between Quiz A/B tabbed vs matched-pairs editor — design decision.
Exact representation of the A/B group marker on an assessment item.
Where the modality write-through hook lives: pure frontend computation each save, vs a backend signal that maintains modality on every ancestry-changing operation. Either works; the latter is more robust against any consumer that bypasses the editor.
Behaviour when a curator toggles Course off on a folder that already contains a populated Unit / Lesson subtree. Default: the descendants quietly stop being Units / Lessons (modality stripped, completion data goes dormant on the folders themselves). The toggle-off action should preview this before committing.
Assumptions
The three-level Course → Unit → Lesson schema is appropriate for the curriculum that will be authored against it. This has been validated against the structure of the CREE+ channel, previous observations of how the lessons functionality is used, and a need for intermediate assessment points (hence units). Live curriculum development may surface shapes that don't fit cleanly. Engaging with curriculum teams as the editor lands is the way to confirm — either the schema works as-is, or the discipline of the schema usefully constrains curriculum design that has been more free form in the past.
Backend support for UNIT-modality topics and PRE_POST_TEST completion criteria (already merged) is the correct foundation. No migration of existing production content is required; the editor work is additive.
The S2S pilot accepts Courses produced by curators in Studio without a parallel review tool — the publish dialog's incomplete-summary is sufficient gate before content flows to Kolibri.
Studio's existing concurrent-edit semantics handle the Course-toggle and auto-coercion paths without new locking.
Risks
The three-level schema turns out to be wrong for real curriculum. The "freeform tree" curriculum-development workflow may not map cleanly onto Course → Unit → Lesson, requiring a schema rework or escape hatches we haven't designed. Mitigation: engage with Kenneth as he is working on middle grades PBL curriculum.
Implicit positional roles confuse curators who don't realise that moving a folder is what made it a Unit. Mitigation: the details panel always shows a folder's current role ("Unit, because it's inside a Course") and conformance status; the Course toggle's preview shows the auto-coercion across descendants before commit.
Incomplete children dropped at publish without a hard error in the editor relies on the incomplete badges being noticed. Mitigation: the publish dialog summarises every incomplete descendant that will be dropped before the curator confirms.
A/B parity as a publish-blocker frustrates curators if the message isn't actionable. Mitigation: parity surface points at the specific item or objective out of balance.
Timeline
TBD
Acceptance Criteria
A folder's details panel exposes a Course toggle. Toggling it shows a preview of how descendants will auto-coerce (Units, Lessons, or fall outside the schema as incomplete) before commit.
Folders inside a Course render as Units; folders inside those Units render as Lessons, in the details panel and the tree view. No additional curator action required for the derivation.
ContentModalities in contentcuration/contentcuration/frontend/shared/constants.js is extended with COURSE, UNIT, and LESSON, and extra_fields.options.modality is maintained as a write-through of the positional derivation.
Each assessment item on a Unit can be assigned group A or B and linked to a learning objective from any of the Unit's child Lessons via a single-select dropdown.
The Unit's assessment editor surfaces an A/B parity status that names what's out of balance (item count, objective coverage). Parity is a publish-blocker.
Each Lesson exposes an ordered Learning Objectives editor (add / remove / reorder of free-text strings); writes go to the parent Unit's lesson_objectives map under the Lesson's node ID.
Renaming or reordering an objective preserves all quiz-item references; deleting an objective that is referenced prompts the curator to re-point references or accept missing-objective flags.
Moving or copying a Lesson between Units migrates that Lesson's objective list from the source Unit's lesson_objectives map to the destination Unit's map under the same Lesson node ID.
Moving a typed folder out of its enclosing Course strips its derived modality; moving it back into a Course restores positional role.
Misshapen folders surface an incomplete badge in the tree view and details panel, per the conformance rules described in Technical Context.
The publish dialog summarises every incomplete descendant that will be dropped before the curator confirms. Publish proceeds with the conforming subtree only; existing backend consumers (completion-criteria validation, PR Remap Unit extra_fields.options node IDs when copying via copy_node #5860's lesson_objectives remap, publish path) operate unchanged.
Backend tests cover modality auto-derivation across ancestry changes (Course toggle on/off, folder moved into / out of a Course, Lesson moved between Units) and publish-time conformance dropping.
Frontend tests cover the Course toggle and descendant preview, the positional-role utility, the A/B editor + parity status, the per-item objective picker, the Learning Objectives editor + delete prompt, and the tree's incomplete badge.
AI usage
Drafted with Claude Code using the superpowers:brainstorming and le-skills:writing-github-issues skills. The editor model — Course toggle as the single explicit control, positional auto-coercion for Unit and Lesson roles, conformance-not-enforcement for hierarchy — was developed collaboratively through the brainstorming pass; I made the design calls and Claude wrote them up. Each PRD section was built section-by-section through the writing-github-issues workflow and confirmed before moving on. The Technical Context references existing code (modalities, lesson_objectives remap from #5860, completion_criteria validation) directly against the codebase rather than asserting from memory.
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Requirements
What problem are we solving?
Studio's backend already understands Courses, Units, and Lessons —
le_utils.constants.modalitiesdefinesCOURSE/UNIT/LESSON, the model layer validates UNIT topics withPRE_POST_TESTcompletion criteria, and PR #5860 added thelesson_objectivesremap on copy. None of that is reachable from the editor today:ContentModalitiesinshared/constants.jsonly exposesQUIZandSURVEY, and there is no UI to mark a folder as part of a Course, edit learning objectives, or pair the two pre/post-test quizzes.This is the Studio side of learningequality/kolibri-ecosystem#38. Until Studio curators can author and edit Course / Unit / Lesson content, no human-authored Courses can flow to Kolibri for the S2S pilot.
Why this? Why now?
Studio is the only place a human curator can produce the Course content that the ecosystem project hinges on. Without an editing surface, every Course has to be added via content integration scripting using the ricecooker tool, with no curator-review step before it lands in Kolibri. Shipping the editor unblocks human-in-the-loop content for any pilots and gives us a way to test the data model under realistic curator workflows before it becomes a core Kolibri experience.
Outcomes
Constraints
extra_fields.options.modality,lesson_objectives, completion criteria) and existing editing surfaces (details panel, AssessmentItemEditor) — no new data models, no parallel editing experience.Out of Scope
Context
Technical Context
Existing backend support the editor builds on
le_utils.constants.modalitiesalready definesCOURSE,UNIT,LESSON,QUIZ,SURVEY,CUSTOM_NAVIGATION.ContentNode.extra_fields.options.modalitystores a topic's modality. The frontendContentModalitiesconstant incontentcuration/contentcuration/frontend/shared/constants.jscurrently exposes onlyQUIZandSURVEY—COURSE/UNIT/LESSONneed to be added.ContentNode.extra_fields.options.lesson_objectiveson a Unit is a map keyed by Lesson child node ID to a list of objective strings. PR Remap Unit extra_fields.options node IDs when copying via copy_node #5860 added remap of these IDs to cloned counterparts oncopy_node(seecontentcuration/contentcuration/db/models/manager.py).contentcuration/contentcuration/constants/completion_criteria.pyvalidates that onlyUNIT-modality topics carry completion criteria, and only with thePRE_POST_TESTmastery model. No curator-facing choice — UNIT implies PRE_POST_TEST.contentcuration/contentcuration/tests/test_exportchannel.py). Assessment items live directly on the Unit, not on child Quiz topics.Editor model
extra_fields.options.modalityon Unit and Lesson folders is maintained automatically as a write-through of the positional derivation, so existing backend consumers (completion-criteria validation,lesson_objectivesremap, publish path) continue to read the field they read today. Whether the write-through lives in the frontend or the backend is a scoping decision.Unit editing
AorB. The editor surface for these items is one of two visual patterns — choice left to design: a tabbed view (single Quiz surface with an A / B tab toggle) or a matched-pairs view (two-column layout pairing the i-th question of A with the i-th question of B).lesson_objectivesmap. Leaving an item without an objective is allowed during editing but flags the Unit as incomplete and blocks publish.Lesson editing
lesson_objectivesmap under this Lesson's node ID. The Lesson UI is the curator's mental model; the data lives on the Unit, unchanged from today.Hierarchy semantics
lesson_objectivesmap stays as dormant metadata; move it back into any Course and the data is still there).lesson_objectivesmap and added under the same Lesson ID in the destination Unit's map. PR Remap Unit extra_fields.options node IDs when copying via copy_node #5860's copy-time remap logic handles the cloning case; live moves piggy-back on the same code path. If the destination isn't a Unit, the objectives are discarded — the Lesson is no longer a Lesson, so its objective data has no parent map to live in.Validation rules ("incomplete" badge conditions)
Areas of the codebase the work will touch
contentcuration/contentcuration/frontend/shared/constants.js(extendContentModalities),channelEdit/components/edit/DetailsTabView.vue(Course toggle + descendant preview),channelEdit/components/edit/(Lesson Learning Objectives editor),channelEdit/components/AssessmentItemEditor/(A/B group marker, per-item objective picker, parity status), plus a shared positional-role utility, the tree's incomplete badge, and the modality write-through hook.contentcuration/contentcuration/db/models/manager.py(modality field follows ancestry changes via copy / move; existinglesson_objectivesremap stays as-is),contentcuration/contentcuration/models.pyandconstants/completion_criteria.py(add validation for the conformance rules used by publish), and the publish path (gate publication on conformance; silently omit non-conforming children).Open questions for technical scoping
Assumptions
PRE_POST_TESTcompletion criteria (already merged) is the correct foundation. No migration of existing production content is required; the editor work is additive.Risks
Timeline
Acceptance Criteria
ContentModalitiesincontentcuration/contentcuration/frontend/shared/constants.jsis extended withCOURSE,UNIT, andLESSON, andextra_fields.options.modalityis maintained as a write-through of the positional derivation.lesson_objectivesmap under the Lesson's node ID.lesson_objectivesmap to the destination Unit's map under the same Lesson node ID.lesson_objectivesremap, publish path) operate unchanged.AI usage
Drafted with Claude Code using the
superpowers:brainstormingandle-skills:writing-github-issuesskills. The editor model — Course toggle as the single explicit control, positional auto-coercion for Unit and Lesson roles, conformance-not-enforcement for hierarchy — was developed collaboratively through the brainstorming pass; I made the design calls and Claude wrote them up. Each PRD section was built section-by-section through the writing-github-issues workflow and confirmed before moving on. The Technical Context references existing code (modalities,lesson_objectivesremap from #5860,completion_criteriavalidation) directly against the codebase rather than asserting from memory.