Backend (encoach_ai_course):
- workbook_attempt model + scoring + REST endpoints for student attempts
- dialogue_parser splits scripts by speaker, classifies gender, strips labels
- media_service: multi-voice TTS via Polly/ElevenLabs, ffmpeg concatenation,
manual media upload endpoint (audio/image/video) with size validation
- source_indexer: OCR fallback (pytesseract + pdf2image) for scanned PDFs,
page-streaming to stay under memory limit
- exercise_extractor + rag_context for RAG-grounded interactive workbooks
- course_plan_pipeline: v2 generator that grounds week material on indexed
sources and persists grounded_on_json metadata
- security: access rules for new models
Backend (encoach_lms_api):
- branches model + controller (entity-scoped LMS branches)
- classroom_ext + course_ext (assignment + section workflow)
- classrooms controller: students/teachers/assign-course endpoints
Frontend:
- StudentDashboard: surface assigned AI course plans alongside enrollments;
enrolled-courses stat now counts plans+enrollments
- InteractiveWorkbook + PlanReader components
- AdminCoursePlanDetail: media drawer with upload buttons (audio/image/video),
hidden file inputs, upload mutation
- AdminBranches page + sidebar entry
- coursePlan/lms/classrooms services + types updated for new endpoints
- i18n: studentDash.myCoursePlans/noCoursePlans (en + ar)
Infra & docs:
- odoo.conf: bump memory limits to 4G/5G for OCR + sentence-transformers
- .gitignore: ignore *.tsbuildinfo
- docs/ASSIGNMENT_WORKFLOW.{md,pdf}
- smoke_*.py end-to-end tests for assignment workflow, entity isolation,
course-plan RAG pipeline
Made-with: Cursor
10 KiB
EnCoach LMS — Assignment Workflow
Audience: Entity admins managing students, teachers, classrooms, sections, and batches. Scope: Single-entity workflow. Cross-entity isolation is enforced by the API and is documented at the end. Goal: Get every student into the right batch (course × section × term) with the right teachers, in the fewest clicks.
1. The mental model
EnCoach uses four canonical entities that build on each other. Read this once — it makes every screen self-explanatory afterwards.
| Entity | What it represents | Owns |
|---|---|---|
| Classroom | A homeroom: a physical room repurposed as a class group (e.g. "Grade 7-A") | Roster of students, homeroom teachers, the courses studied here |
| Course | A subject taught (e.g. "Physics 102") | Course sections (A/B/C…), syllabus, learning objectives |
| Course Section | A timetabled instance of a course (e.g. "Physics 102 — Section A") | Optional capacity, sequence, branch |
| Batch | The intersection of a classroom × course × section × term | The actual enrolled students and the teachers who teach this slice |
Rule of thumb: the classroom is the anchor. You set up the classroom once (roster + homeroom teachers), then you bind one or more (course, section, term) tuples to it. Each binding becomes a batch which automatically inherits the roster and homeroom teachers. Per-batch overrides are available when you need them.
Entity (school / academy)
└── Classroom (homeroom) ← step 1 & 2
├── Roster (students) ← step 1
├── Homeroom teachers ← step 2
└── Courses ← step 3 (assign a course → cascade)
└── Batch (course × section × term) ← step 4 (per-batch tuning)
├── Enrolled students (= roster, overridable)
└── Teachers (= homeroom teachers, overridable)
2. The 4-step workflow
The Classrooms page is the single entry point. Each tab is a numbered step. The workflow guide at the top of the detail pane shows green check marks as you complete each step.
Step 1 — Roster
Where: Classrooms → pick a classroom → tab 1. Roster
Action: Click Manage Roster and pick the students who belong to this homeroom.
Why it matters: The roster cascades into every batch you create later. Set this once; downstream steps populate themselves.
API: POST /api/classrooms/{id}/students with { student_ids: [...], mode: "set" | "add" }
Step 2 — Faculty
Where: tab 2. Faculty
Action: Click Manage Faculty and pick the homeroom teachers.
Why it matters: When you assign a course in step 3, these teachers are automatically attached to the new batch. You can override per batch later.
API: POST /api/classrooms/{id}/teachers with { teacher_ids: [...], mode: "set" | "add" }
Step 3 — Courses (cascade)
Where: tab 3. Courses
Action: Click Assign Course, then:
- Pick a course from the dropdown.
- (Optional) Pick a section. Sections are loaded dynamically from
/api/courses/{course_id}/sections. Leave on Auto to use the whole course. - (Optional) Enter a term key (e.g.
2026-T1). - Click Assign + Cascade.
What the server does in one transaction:
- Creates a canonical batch
(classroom × course × section × term)(or reuses an existing one). - Auto-enrolls every student from the classroom roster into the batch.
- Attaches the homeroom teachers to the batch.
API: POST /api/classrooms/{id}/assign-course with { course_id, section_id?, term_key?, teacher_ids? }. Cross-entity course/section/teacher IDs are rejected with HTTP 403.
Step 4 — Batches (per-batch tuning)
Where: tab 4. Batches
Action: Each batch row has a Manage button. The dialog has two tabs:
4a. Students
Pre-checked with the batch's actual enrollments. Use this when you need a subset of the homeroom in this batch (e.g. half the homeroom is in section A, the other half is in section B). One click on reset to classroom roster re-syncs back to the cascade.
APIs:
POST /api/batches/{id}/students— add studentsPOST /api/batches/{id}/students/remove— remove students
4b. Teachers
Pre-checked with the batch's current teachers. Use this when sections are taught by different teachers, or when a substitute takes over for one batch. One click on reset to homeroom teachers re-syncs.
APIs:
GET /api/batches/{id}/teachersPOST /api/batches/{id}/teacherswith{ teacher_ids: [...], mode: "set" | "add" }POST /api/batches/{id}/teachers/remove
3. Worked example
Goal: enroll 24 Grade-7 students into Physics 102 (sections A and B), with Ms. Salem teaching A and Mr. Khaled teaching B.
| # | Step | What you do | Result |
|---|---|---|---|
| 1 | Create classroom | Classrooms → New Classroom → "Grade 7" | Empty classroom shell. |
| 2 | Roster | Tab 1 → Manage Roster → pick all 24 students | Classroom now has 24 students. |
| 3 | Homeroom teachers | Tab 2 → Manage Faculty → add Ms. Salem and Mr. Khaled | Both are default teachers. |
| 4 | Assign Section A | Tab 3 → Assign Course → Physics 102 → Section A → 2026-T1 → Assign + Cascade | Batch "Grade 7 — Physics 102 — A" is created with 24 students and 2 teachers. |
| 5 | Assign Section B | Tab 3 → Assign Course → Physics 102 → Section B → 2026-T1 → Assign + Cascade | Batch "Grade 7 — Physics 102 — B" is created with 24 students and 2 teachers. |
| 6 | Split the roster | Tab 4 → on Section A's batch click Manage → tab Students → uncheck students 13–24 → Save | Section A has 12 students. |
| 7 | Split the roster | Tab 4 → on Section B's batch click Manage → tab Students → uncheck students 1–12 → Save | Section B has 12 students. |
| 8 | Pin teacher per section | Tab 4 → on Section A's batch click Manage → tab Teachers → keep only Ms. Salem → Save | Section A taught by Ms. Salem. |
| 9 | Pin teacher per section | Tab 4 → on Section B's batch click Manage → tab Teachers → keep only Mr. Khaled → Save | Section B taught by Mr. Khaled. |
Total: 9 clicks beyond the picker dialogs. Steps 6–9 only happen when you need per-section overrides; if every section has the whole homeroom and the same teachers, you stop at step 5.
4. Where assignments come from (the cascade chain)
classroom.student_ids ──┐
├─→ batch.student_ids (on assign-course)
section binding ─┘ (auto-cascade)
↓
per-batch override batch.student_ids
via Tab 4 → Manage (final state)
classroom.teacher_ids ──┐
├─→ batch.teacher_ids (on assign-course)
─┘ (auto-cascade)
↓
per-batch override batch.teacher_ids
via Tab 4 → Manage (final state)
- The cascade only runs on
assign-course. After that, the batch and the classroom roster diverge independently. Adding a new student to the classroom after a batch exists does not enroll them automatically — re-run the assignment or use Tab 4 → Manage. mode: "add"on roster/faculty mutations unions;mode: "set"replaces. The UI usesseteverywhere, so what you see is what you get.
5. Entity isolation guarantees
Every endpoint above is wrapped in the same multi-tenant guard:
- The user's
entity_idsdefines their scope. A user with one or more entities is always entity-scoped, even if they hold Odoo system groups. - Foreign-key IDs sent by the client (course, section, classroom, student, teacher) are validated to belong to the target entity. Cross-entity IDs return HTTP 403.
- Model-level
@api.constrainsrejects direct ORM writes that mix entities — so even shell scripts can't poison the data.
This is verified by the smoke_entity_isolation.py script, which asserts every cross-entity write attempt returns 403 and every cross-entity read leaks zero records.
6. Reference: endpoints used
| Method | Path | Purpose |
|---|---|---|
| POST | /api/classrooms/{id}/students |
Set/add classroom roster |
| DELETE | /api/classrooms/{id}/students |
Remove from roster |
| POST | /api/classrooms/{id}/teachers |
Set/add homeroom teachers |
| GET | /api/courses/{id}/sections |
List sections for a course |
| POST | /api/courses/{id}/sections/generate-defaults |
Generate A/B/C sections |
| POST | /api/classrooms/{id}/assign-course |
Bind course → create batch + cascade |
| DELETE | /api/classrooms/{id}/courses/{course_id} |
Detach course (keeps existing batch) |
| GET | /api/batches/{id}/students |
List batch enrollments |
| POST | /api/batches/{id}/students |
Add students to a batch |
| POST | /api/batches/{id}/students/remove |
Remove students from a batch |
| GET | /api/batches/{id}/teachers |
List batch teachers |
| POST | /api/batches/{id}/teachers |
Set/add batch teachers |
| POST | /api/batches/{id}/teachers/remove |
Remove batch teachers |
All endpoints require a Bearer token from POST /api/login.
7. Smoke tests shipped with the platform
| Script | Verifies |
|---|---|
smoke_assignment_workflow.py |
The full 4-step flow against a live backend: classroom → roster → teachers → assign-course → per-batch override. Asserts the cascade and the override are persisted server-side. |
smoke_entity_isolation.py |
An entity-A admin cannot read or modify entity-B records via any of the endpoints above. |
seed_dynamic_sections_demo_via_api.py |
Idempotently seeds a demo classroom + course-with-sections + batches using only the public API. |
Run from the repo root with .conda-envs/odoo19/bin/python <script>.py.
Last updated: April 2026. Backend module: encoach_lms_api. Frontend page: frontend/src/pages/ClassroomsPage.tsx.