Files
full_encoach_platform/seed_dynamic_sections_demo.py
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

255 lines
9.9 KiB
Python
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.
#!/usr/bin/env python3
"""
seed_dynamic_sections_demo.py — Idempotent demo data for the dynamic
Course → Sections → Classroom → Batch structure.
Reproduces the user diagrams literally:
Courses & Sections Classrooms
────────────────── ───────────
Math 101 ─┬─ Section A Class A1010 hosts:
├─ Section B • Math 101 — Section A
└─ Section C • Physics 102 — Section B
Physics 102 ─┬─ Section A
├─ Section B Class A1030 hosts:
└─ Section C • Math 101 — Section B
• Physics 102 — Section C
Plus the entity-aware homeroom layer:
Entity (DEMO_ACADEMY)
└─ Classroom (A1010 / A1030)
├─ Students (homeroom roster) ← Sarah / Omar / Layla / TestUser
├─ Teachers (homeroom M2M) ← Dr. Khalid + Ms. Fatima
├─ Courses (M2M) ← Math 101 + Physics 102
└─ Batches (auto, one per ← cascade from assign_course()
classroom × course × section × term)
Run inside Odoo shell:
cd /Users/yamenahmad/projects2026/odoo/odoo19
.conda-envs/odoo19/bin/python odoo/odoo-bin shell -c odoo.conf -d encoach_v2 \
< seed_dynamic_sections_demo.py
It expects `seed_demo.py` (and ideally `seed_full_demo.py`) to have run first
so the demo entity, students, and teachers exist. It is safe to re-run any
number of times — every step looks up before creating.
"""
from datetime import date
print("\n" + "=" * 72)
print(" EnCoach — Dynamic course-section demo seed (idempotent)")
print("=" * 72)
Users = env['res.users']
Entity = env['encoach.entity']
OpCourse = env['op.course']
OpStudent = env['op.student']
OpFaculty = env['op.faculty']
Classroom = env['op.classroom']
Section = env['encoach.course.section']
Batch = env['op.batch']
# ── 1. Resolve the demo entity ────────────────────────────────────────────
entity = Entity.search([('code', '=', 'DEMO_ACADEMY')], limit=1) \
or Entity.search([], limit=1)
if not entity:
raise SystemExit("✗ No encoach.entity found. Run seed_demo.py first.")
print(f"✓ Using entity: {entity.name} (id={entity.id})")
# ── 2. Resolve demo students + homeroom teachers by login ─────────────────
def _student_for(login):
user = Users.search([('login', '=', login)], limit=1)
if not user:
return None
if user.op_student_id:
return user.op_student_id
return OpStudent.search([('user_id', '=', user.id)], limit=1) or None
def _faculty_for(login):
user = Users.search([('login', '=', login)], limit=1)
if not user:
return None
if user.op_faculty_id:
return user.op_faculty_id
return OpFaculty.search([('user_id', '=', user.id)], limit=1) or None
sarah = _student_for('sarah@encoach.test')
omar = _student_for('omar@encoach.test')
layla = _student_for('layla@encoach.test')
khalid = _faculty_for('khalid@encoach.test')
fatima = _faculty_for('fatima@encoach.test')
# Pick one extra student for A1030 — first non-(sarah/omar/layla) student available.
extra_student = None
already = {s.id for s in (sarah, omar, layla) if s}
for cand in OpStudent.search([], limit=20):
if cand.id not in already:
extra_student = cand
break
print("✓ Resolved demo people:")
print(f" students: sarah={sarah and sarah.id} omar={omar and omar.id} "
f"layla={layla and layla.id} extra={extra_student and extra_student.id}")
print(f" homeroom: khalid={khalid and khalid.id} fatima={fatima and fatima.id}")
if not all([sarah, omar, layla, khalid, fatima]):
print("⚠ Missing demo users — please run seed_demo.py first; continuing with what we have.")
# ── 3. Create / reuse Math 101 & Physics 102 (entity-scoped) ──────────────
def _ensure_course(name, code, description, capacity=40):
course = OpCourse.search([('code', '=', code)], limit=1)
if not course:
vals = {
'name': name,
'code': code,
'description': description,
'max_capacity': capacity,
'evaluation_type': 'normal',
}
if 'entity_id' in OpCourse._fields:
vals['entity_id'] = entity.id
course = OpCourse.create(vals)
print(f" + created course: {name} ({code}) id={course.id}")
else:
if 'entity_id' in OpCourse._fields and not course.entity_id:
course.write({'entity_id': entity.id})
print(f" · reused course: {name} ({code}) id={course.id}")
return course
print("\n--- Courses ---")
math = _ensure_course("Math 101", "MATH101", "Foundations of mathematics.", 40)
physics = _ensure_course("Physics 102", "PHYS102", "Introductory physics.", 40)
env.cr.commit()
# ── 4. Generate A/B/C sections per course ─────────────────────────────────
def _ensure_sections(course, codes=("A", "B", "C")):
out = {}
for idx, c in enumerate(codes, start=1):
s = Section.search([('course_id', '=', course.id), ('code', '=', c)], limit=1)
if not s:
s = Section.create({
'name': f"Section {c}",
'code': c,
'course_id': course.id,
'sequence': 10 * idx,
'active': True,
})
print(f" + section {course.code}/{c} id={s.id}")
else:
print(f" · section {course.code}/{c} id={s.id} (existing)")
out[c] = s
return out
print("\n--- Sections ---")
math_secs = _ensure_sections(math)
phys_secs = _ensure_sections(physics)
env.cr.commit()
# ── 5. Create / reuse classrooms (A1010, A1030) ───────────────────────────
def _ensure_classroom(name, code, capacity=35):
rec = Classroom.search([('code', '=', code)], limit=1)
if not rec:
vals = {
'name': name,
'code': code,
'capacity': capacity,
}
if 'entity_id' in Classroom._fields:
vals['entity_id'] = entity.id
rec = Classroom.create(vals)
print(f" + created classroom: {name} ({code}) id={rec.id}")
else:
if 'entity_id' in Classroom._fields and not rec.entity_id:
rec.write({'entity_id': entity.id})
print(f" · reused classroom: {name} ({code}) id={rec.id}")
return rec
print("\n--- Classrooms ---")
a1010 = _ensure_classroom("Class A1010", "A1010", 35)
a1030 = _ensure_classroom("Class A1030", "A1030", 35)
env.cr.commit()
# ── 6. Roster + homeroom teachers (idempotent M2M add) ────────────────────
def _add_roster(classroom, students, teachers):
s_cmds = [(4, s.id) for s in students if s]
t_cmds = [(4, t.id) for t in teachers if t]
vals = {}
if s_cmds:
vals['student_ids'] = s_cmds
if t_cmds:
vals['teacher_ids'] = t_cmds
if vals:
classroom.write(vals)
print(f" · {classroom.name}: {len(classroom.student_ids)} student(s), "
f"{len(classroom.teacher_ids)} homeroom teacher(s)")
print("\n--- Rosters & homeroom teachers ---")
_add_roster(a1010, [sarah, omar], [khalid, fatima]) # 2 students, 2 teachers
_add_roster(a1030, [layla, extra_student], [khalid]) # 1-2 students, 1 teacher
env.cr.commit()
# ── 7. Bind sections to classrooms (creates batches + enrolls roster) ─────
DIAGRAM = [
# (classroom, course, section_letter)
(a1010, math, "A"),
(a1010, physics, "B"),
(a1030, math, "B"),
(a1030, physics, "C"),
]
TERM_NAME = "Semester 1 2026"
TERM_KEY = "2026-T1"
START = date(2026, 4, 27)
END = date(2026, 12, 31)
print("\n--- Section bindings (cascade → batches + auto-enroll) ---")
for classroom, course, section_letter in DIAGRAM:
section = (math_secs if course == math else phys_secs)[section_letter]
teacher_ids = [t.id for t in (khalid, fatima) if t]
batch = classroom.assign_course(
course=course,
term_name=TERM_NAME,
start_date=START,
end_date=END,
teacher_ids=teacher_ids,
section_id=section.id,
term_key=TERM_KEY,
)
print(f"{classroom.name:<14}{course.name:<12} / Section {section_letter} "
f"batch '{batch.name}' (code {batch.code}, {len(batch.student_ids)} students)")
env.cr.commit()
# ── 8. Final summary ──────────────────────────────────────────────────────
print("\n" + "=" * 72)
print(" Final state (matches the diagrams)")
print("=" * 72)
for classroom in (a1010, a1030):
print(f"\n== {classroom.name} (code {classroom.code}, entity={classroom.entity_id.name}) ==")
print(f" roster: {len(classroom.student_ids)} student(s) -> "
f"{[s.name for s in classroom.student_ids]}")
print(f" teachers: {[t.name for t in classroom.teacher_ids]} (homeroom)")
print(f" courses: {[c.name for c in classroom.course_ids]}")
for b in classroom.batch_ids.sorted('id'):
sec_code = b.course_section_id.code if b.course_section_id else '-'
print(f" batch: {b.course_id.name:<12} / Section {sec_code} -> "
f"'{b.name}' ({len(b.student_ids)} students, term={b.term_key})")
print("\n✓ seed_dynamic_sections_demo.py — done.")
print(" Try the UI:")
print(" /admin/courses -> Math 101 / Physics 102 each show 3 sections (A/B/C)")
print(" /admin/classrooms -> Class A1010 + A1030 with rosters and 2 batches each")
print(" /admin/batches -> 4 section-aware batches (Section column populated)")