Files
full_encoach_platform/docs/ASSIGNMENT_WORKFLOW.md
Yamen Ahmad 3b62075d7e 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
2026-04-28 00:17:51 +04:00

191 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.*