#!/usr/bin/env python3 """ seed_dynamic_sections_demo_via_api.py Idempotent demo data for the dynamic Course → Sections → Classroom → Batch structure, driven entirely through the LMS REST API the frontend uses. Reproduces the user's diagrams: 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 Run while the backend is up on http://127.0.0.1:8069 : .conda-envs/odoo19/bin/python seed_dynamic_sections_demo_via_api.py """ import json import sys from datetime import date import urllib.request import urllib.error API = "http://127.0.0.1:8069" ADMIN_LOGIN = "admin@encoach.test" ADMIN_PASSWORD = "admin123" TERM_NAME = "Semester 1 2026" TERM_KEY = "2026-T1" START = "2026-04-27" END = "2026-12-31" # ── Tiny HTTP helper ────────────────────────────────────────────────────── def _req(method, path, *, token=None, body=None, query=None): url = API + path if query: from urllib.parse import urlencode url += "?" + urlencode(query) headers = {"Content-Type": "application/json"} if token: headers["Authorization"] = f"Bearer {token}" data = json.dumps(body).encode("utf-8") if body is not None else None r = urllib.request.Request(url, data=data, method=method, headers=headers) try: with urllib.request.urlopen(r, timeout=30) as resp: payload = resp.read().decode("utf-8") return resp.status, (json.loads(payload) if payload else {}) except urllib.error.HTTPError as e: body_text = e.read().decode("utf-8", "replace") try: return e.code, json.loads(body_text) except Exception: return e.code, {"raw": body_text} def must(status, payload, ctx): if status >= 400: print(f" ✗ {ctx} -> HTTP {status}: {json.dumps(payload)[:280]}") sys.exit(1) return payload # ── 1. Login as admin and capture the JWT ───────────────────────────────── print("\n" + "=" * 72) print(" EnCoach — dynamic course-section demo (via API)") print("=" * 72) status, payload = _req( "POST", "/api/login", body={"login": ADMIN_LOGIN, "password": ADMIN_PASSWORD}, ) if status != 200: print(f"✗ login failed ({status}): {payload}") sys.exit(1) token = payload.get("access_token") or payload.get("token") me = payload.get("user") or {} print(f"✓ logged in as {me.get('name') or ADMIN_LOGIN} (uid={me.get('id')})") # ── 2. Resolve / create Math 101 + Physics 102 ──────────────────────────── def _find_course_by_code(code): s, p = _req("GET", "/api/courses", token=token, query={"limit": 200}) must(s, p, "list courses") for c in p.get("items", []) or p.get("data", []) or []: if (c.get("code") or "").upper() == code.upper(): return c return None def _ensure_course(name, code, description): found = _find_course_by_code(code) if found: print(f" · reused course {name} ({code}) id={found['id']}") return found s, p = _req("POST", "/api/courses", token=token, body={ "name": name, "code": code, "description": description, "max_capacity": 40, "evaluation_type": "normal", }) must(s, p, f"create course {code}") c = p.get("data") or p print(f" + created course {name} ({code}) id={c['id']}") return c print("\n--- Courses ---") math = _ensure_course("Math 101", "MATH101", "Foundations of mathematics.") physics = _ensure_course("Physics 102", "PHYS102", "Introductory physics.") # ── 3. Generate A/B/C sections per course (idempotent endpoint) ─────────── def _ensure_default_sections(course): s, p = _req("POST", f"/api/courses/{course['id']}/sections/generate-defaults", token=token, body={}) must(s, p, f"generate-defaults {course['code']}") sections = p.get("items") or p.get("data") or [] by_code = {sec["code"]: sec for sec in sections} print(f" · {course['code']} sections: {sorted(by_code.keys())} " f"({sum(1 for sec in sections if sec.get('created'))} created, " f"{sum(1 for sec in sections if not sec.get('created'))} reused)") return by_code print("\n--- Sections ---") math_secs = _ensure_default_sections(math) phys_secs = _ensure_default_sections(physics) # ── 4. Resolve students + faculty (homeroom teachers) ───────────────────── def _list_students(limit=200): s, p = _req("GET", "/api/students", token=token, query={"limit": limit}) must(s, p, "list students") return p.get("items", []) or p.get("data", []) or [] def _list_teachers(limit=200): for path in ("/api/teachers", "/api/faculty"): s, p = _req("GET", path, token=token, query={"limit": limit}) if s == 200: rows = p.get("items", []) or p.get("data", []) or [] if rows: return rows return [] def _match(rows, *needles): """Return the first row whose name/email contains any of the needles.""" needles = [n.lower() for n in needles if n] for r in rows: haystack = " ".join(str(r.get(k) or "") for k in ("name", "email", "first_name", "last_name", "login")).lower() if any(n in haystack for n in needles): return r return None students = _list_students() sarah = _match(students, "sarah ahmed", "sarah@encoach") omar = _match(students, "omar khan", "omar@encoach") layla = _match(students, "layla nasser", "layla@encoach") extra = next( (s for s in students if s and s.get("id") not in {x.get("id") for x in (sarah, omar, layla) if x}), None, ) print("\n--- Demo people ---") print(f" · /api/students returned {len(students)} student(s)") for label, st in [("sarah", sarah), ("omar", omar), ("layla", layla), ("extra", extra)]: if st: print(f" · {label:<6} id={st.get('id'):<5} name={st.get('name')} email={st.get('email')}") else: print(f" · {label:<6} (not found)") teachers = _list_teachers() khalid_rec = _match(teachers, "khalid", "khalid@encoach") fatima_rec = _match(teachers, "fatima", "fatima@encoach") khalid = (khalid_rec or {}).get("id") if khalid_rec else None fatima = (fatima_rec or {}).get("id") if fatima_rec else None print(f" · /api/teachers returned {len(teachers)} faculty record(s)") print(f" · khalid faculty id = {khalid}") print(f" · fatima faculty id = {fatima}") # ── 5. Resolve / create classrooms ──────────────────────────────────────── def _find_classroom_by_code(code): s, p = _req("GET", "/api/classrooms", token=token, query={"limit": 200}) must(s, p, "list classrooms") for c in p.get("items", []) or p.get("data", []) or []: if (c.get("code") or "").upper() == code.upper(): return c return None def _ensure_classroom(name, code): found = _find_classroom_by_code(code) if found: print(f" · reused classroom {name} ({code}) id={found['id']}") return found s, p = _req("POST", "/api/classrooms", token=token, body={ "name": name, "code": code, "capacity": 35, }) must(s, p, f"create classroom {code}") c = p.get("data") or p print(f" + created classroom {name} ({code}) id={c['id']}") return c print("\n--- Classrooms ---") a1010 = _ensure_classroom("Class A1010", "A1010") a1030 = _ensure_classroom("Class A1030", "A1030") # ── 6. Roster + homeroom teachers (idempotent: backend de-dupes) ────────── def _add_students(classroom, *students_): ids = [s["id"] for s in students_ if s] if not ids: return s, p = _req("POST", f"/api/classrooms/{classroom['id']}/students", token=token, body={"student_ids": ids}) if s >= 400: print(f" ✗ add students {ids} to {classroom['code']}: {p}") else: print(f" · {classroom['code']}: roster now has " f"{(p.get('data') or {}).get('students_count', '?')} student(s)") def _add_teachers(classroom, *teacher_ids): ids = [t for t in teacher_ids if t] if not ids: return s, p = _req("POST", f"/api/classrooms/{classroom['id']}/teachers", token=token, body={"teacher_ids": ids}) if s >= 400: print(f" ⚠ add teachers {ids} to {classroom['code']}: {p}") else: print(f" · {classroom['code']}: homeroom teachers attached") print("\n--- Rosters & homeroom teachers ---") _add_students(a1010, sarah, omar) _add_students(a1030, layla, extra) _add_teachers(a1010, khalid, fatima) _add_teachers(a1030, khalid) # ── 7. Bind sections to classrooms (creates batches + auto-enrolls) ─────── def _assign_section(classroom, course, section, teacher_ids=None): body = { "course_id": course["id"], "section_id": section["id"], "term_name": TERM_NAME, "term_key": TERM_KEY, "start_date": START, "end_date": END, } if teacher_ids: body["teacher_ids"] = [t for t in teacher_ids if t] s, p = _req("POST", f"/api/classrooms/{classroom['id']}/assign-section", token=token, body=body) must(s, p, f"assign {classroom['code']} ← {course['code']}/{section['code']}") batch = p.get("batch") or {} print(f" ✓ {classroom['code']:<6} ← {course['code']:<8} / Section {section['code']} " f"batch '{batch.get('name')}' (code {batch.get('code')}, " f"{batch.get('students_count', '?')} students)") print("\n--- Section bindings ---") _assign_section(a1010, math, math_secs["A"], teacher_ids=[khalid]) _assign_section(a1010, physics, phys_secs["B"], teacher_ids=[fatima]) _assign_section(a1030, math, math_secs["B"], teacher_ids=[khalid]) _assign_section(a1030, physics, phys_secs["C"], teacher_ids=[fatima]) # ── 8. Final summary (re-fetch full classroom payload) ──────────────────── print("\n" + "=" * 72) print(" Final state (matches the diagrams)") print("=" * 72) for cls in (a1010, a1030): s, p = _req("GET", f"/api/classrooms/{cls['id']}", token=token) if s != 200: print(f" ✗ refetch {cls['code']}: {p}") continue detail = p.get("data") or p print(f"\n== {detail.get('name')} (code {detail.get('code')}) ==") print(f" roster: {detail.get('students_count', '?')} student(s) -> " f"{[s.get('name') for s in detail.get('students', [])][:6]}") print(f" teachers: {[t.get('name') for t in detail.get('teachers', [])]}") s2, p2 = _req("GET", f"/api/classrooms/{cls['id']}/batches", token=token) batches = (p2.get("items") or p2.get("data") or []) if s2 == 200 else [] print(f" courses: {sorted({b.get('course_name') for b in batches})}") for b in sorted(batches, key=lambda x: (x.get("course_name") or "", x.get("section_code") or "")): print(f" batch: {(b.get('course_name') or ''):<12} / Section " f"{(b.get('section_code') or '-'):<3} -> '{b.get('name')}' " f"({b.get('students_count', '?')} students, term={b.get('term_key')})") print("\n✓ Demo seeded. 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 + 2 batches each") print(" /admin/batches -> 4 section-aware batches (Section column populated)")