feat(platform): course-plan student visibility, multi-voice TTS, OCR sources, branches

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
This commit is contained in:
Yamen Ahmad
2026-04-28 00:17:51 +04:00
parent a6f2140a9d
commit 3b62075d7e
55 changed files with 11150 additions and 878 deletions

190
docs/ASSIGNMENT_WORKFLOW.md Normal file
View File

@@ -0,0 +1,190 @@
# 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:
1. Pick a course from the dropdown.
2. (Optional) Pick a section. Sections are loaded dynamically from `/api/courses/{course_id}/sections`. Leave on *Auto* to use the whole course.
3. (Optional) Enter a term key (e.g. `2026-T1`).
4. 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 students
- `POST /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}/teachers`
- `POST /api/batches/{id}/teachers` with `{ 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 1324 → Save | Section A has 12 students. |
| 7 | Split the roster | Tab 4 → on Section B's batch click *Manage* → tab Students → uncheck students 112 → 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 69 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 uses `set` everywhere, so what you see is what you get.
---
## 5. Entity isolation guarantees
Every endpoint above is wrapped in the same multi-tenant guard:
1. The user's `entity_ids` defines their scope. A user with one or more entities is **always** entity-scoped, even if they hold Odoo system groups.
2. 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**.
3. Model-level `@api.constrains` rejects 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`.*