api/run/initialization/seed_planned_lessons.py
Kevin Carter ead4452277 feat: add P0 seed scripts for timetable, planned lessons, file cabinets, and curriculum
- seed_kevlarai_timetable.py: Mirror Greenfield timetable structure for KevlarAI
  (8 classes, 2 teachers, 2 students, full slot/materialize/sync pipeline)
- seed_planned_lessons.py: 2-3 planned lessons per teacher across both schools
  (6 plans total, idempotent via title+subject check)
- seed_file_cabinets.py: One file cabinet per class with sample documents
  (14 cabinets, ~28 files, document_artefacts, cabinet_memberships)
- seed_curriculum.py: Exam board specifications and exams (AQA, Edexcel, OCR)
  (6 specs, 12 exam papers, Neo4j curriculum topics per school)
2026-05-29 21:15:05 +01:00

386 lines
15 KiB
Python

"""
seed_planned_lessons.py — Create 2-3 planned lessons per teacher across both schools.
Uses the /lessons/plans API endpoint (POST) to create lesson plans.
Each plan is linked to a class, subject, and year group where possible.
Plans are idempotent: checks for existing plans by title+subject before creating.
Tables: planned_lessons, lesson_collaborators, lesson_deliveries
Run inside ccapi container:
python3 -c "from run.initialization.seed_planned_lessons import seed; seed()"
"""
import os
import time
import requests
from typing import Dict, Any, List, Optional
SUPA_URL = os.environ["SUPABASE_URL"]
SERVICE_KEY = os.environ["SERVICE_ROLE_KEY"]
API_BASE = os.environ.get("API_BASE_URL", "http://localhost:8000")
# ─── Passwords (standardized from T4) ────────────────────────────────────────
PWD_ADMIN = "Admin@Cc2025!"
PWD_TEACHER = "Teacher@Cc2025!"
# ─── Planned lesson templates per school ──────────────────────────────────────
# Each entry: (teacher_email, title, subject, year_group, class_code, objectives, activities)
KEVLARAI_PLANS = [
{
"teacher": "physics@kevlarai.test",
"title": "Introduction to Forces and Motion",
"subject": "Physics",
"year_group": "10",
"class_code": "10K/Ph1",
"objectives": [
{"text": "Define force, mass, and acceleration", "bloom": "remember"},
{"text": "Apply F=ma to solve simple problems", "bloom": "apply"},
],
"activities": [
{"type": "demo", "description": "Demonstrate forces with spring scales"},
{"type": "worksheet", "description": "F=ma calculation practice (10 problems)"},
],
"duration_minutes": 60,
},
{
"teacher": "physics@kevlarai.test",
"title": "Electric Circuits Basics",
"subject": "Physics",
"year_group": "11",
"class_code": "11K/Ph1",
"objectives": [
{"text": "Identify series and parallel circuit components", "bloom": "understand"},
{"text": "Calculate total resistance in series circuits", "bloom": "apply"},
],
"activities": [
{"type": "lab", "description": "Build series circuit with resistors"},
{"type": "quiz", "description": "Resistance calculation quiz (5 questions)"},
],
"duration_minutes": 60,
},
{
"teacher": "maths@kevlarai.test",
"title": "Quadratic Equations — Factorisation Method",
"subject": "Mathematics",
"year_group": "10",
"class_code": "10K/Ma1",
"objectives": [
{"text": "Factorise quadratic expressions of the form x²+bx+c", "bloom": "apply"},
{"text": "Solve quadratic equations by factorisation", "bloom": "analyse"},
],
"activities": [
{"type": "direct_instruction", "description": "Walk through 3 worked examples"},
{"type": "pair_work", "description": "Factorise 8 quadratics with a partner"},
],
"duration_minutes": 60,
},
{
"teacher": "maths@kevlarai.test",
"title": "Probability — Tree Diagrams",
"subject": "Mathematics",
"year_group": "11",
"class_code": "11K/Ma1",
"objectives": [
{"text": "Construct tree diagrams for two-stage events", "bloom": "apply"},
{"text": "Calculate combined probabilities from tree diagrams", "bloom": "analyse"},
],
"activities": [
{"type": "demo", "description": "Coin toss tree diagram on whiteboard"},
{"type": "worksheet", "description": "5 tree diagram probability problems"},
],
"duration_minutes": 60,
},
]
GREENFIELD_PLANS = [
{
"teacher": "physics@greenfieldacademy.test",
"title": "Waves and Sound",
"subject": "Physics",
"year_group": "9",
"class_code": "9P/Ph1",
"objectives": [
{"text": "Describe properties of transverse and longitudinal waves", "bloom": "remember"},
{"text": "Calculate wave speed using v=fλ", "bloom": "apply"},
],
"activities": [
{"type": "demo", "description": "Slinky wave demonstrations"},
{"type": "worksheet", "description": "Wave speed calculations (8 problems)"},
],
"duration_minutes": 60,
},
{
"teacher": "physics@greenfieldacademy.test",
"title": "Energy Transfers and Conservation",
"subject": "Physics",
"year_group": "10",
"class_code": "10P/Ph2",
"objectives": [
{"text": "Identify energy stores and transfer pathways", "bloom": "understand"},
{"text": "Apply conservation of energy to real-world scenarios", "bloom": "analyse"},
],
"activities": [
{"type": "group_work", "description": "Energy audit of a household"},
{"type": "presentation", "description": "Present findings on energy efficiency"},
],
"duration_minutes": 60,
},
{
"teacher": "maths@greenfieldacademy.test",
"title": "Algebra — Expanding Brackets",
"subject": "Mathematics",
"year_group": "9",
"class_code": "9M/Ma1",
"objectives": [
{"text": "Expand single brackets: a(b+c)", "bloom": "apply"},
{"text": "Expand double brackets: (a+b)(c+d)", "bloom": "analyse"},
],
"activities": [
{"type": "direct_instruction", "description": "Area model for expanding brackets"},
{"type": "worksheet", "description": "15 expansion problems (graded difficulty)"},
],
"duration_minutes": 60,
},
{
"teacher": "maths@greenfieldacademy.test",
"title": "Simultaneous Equations — Elimination Method",
"subject": "Mathematics",
"year_group": "10",
"class_code": "10M/Ma1",
"objectives": [
{"text": "Solve simultaneous equations by elimination", "bloom": "apply"},
{"text": "Choose between substitution and elimination strategically", "bloom": "evaluate"},
],
"activities": [
{"type": "direct_instruction", "description": "Walk through 3 elimination examples"},
{"type": "pair_work", "description": "Solve 6 simultaneous equation pairs"},
],
"duration_minutes": 60,
},
{
"teacher": "teacher1@greenfieldacademy.test",
"title": "Shakespeare — Macbeth Act 1 Analysis",
"subject": "English",
"year_group": "9",
"class_code": "9En/1",
"objectives": [
{"text": "Identify key themes in Act 1", "bloom": "understand"},
{"text": "Analyse Shakespeare's use of imagery and language", "bloom": "analyse"},
],
"activities": [
{"type": "close_reading", "description": "Close read Act 1, Scene 3 (witches' prophecy)"},
{"type": "essay", "description": "Short paragraph: How does Shakespeare create tension?"},
],
"duration_minutes": 60,
},
{
"teacher": "teacher2@greenfieldacademy.test",
"title": "The Tudors — Henry VIII's Reforms",
"subject": "History",
"year_group": "10",
"class_code": "10Hs/1",
"objectives": [
{"text": "Describe Henry VIII's religious reforms", "bloom": "remember"},
{"text": "Evaluate the political motivations behind the reforms", "bloom": "evaluate"},
],
"activities": [
{"type": "source_analysis", "description": "Analyze Act of Supremacy 1534"},
{"type": "debate", "description": "Was Henry's break with Rome politically necessary?"},
],
"duration_minutes": 60,
},
]
# ─── Helpers ───────────────────────────────────────────────────────────────────
def _sign_in(email: str, password: str) -> str:
r = requests.post(
f"{SUPA_URL}/auth/v1/token?grant_type=password",
headers={"apikey": SERVICE_KEY, "Content-Type": "application/json"},
json={"email": email, "password": password},
)
r.raise_for_status()
return r.json()["access_token"]
def _api(token: str, method: str, path: str, body: Optional[Dict] = None) -> Dict:
h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
r = getattr(requests, method)(f"{API_BASE}{path}", headers=h, json=body)
try:
return r.json()
except Exception:
return {"_raw": r.text, "_status": r.status_code}
def _get_profile_id(email: str) -> Optional[str]:
"""Look up a profile's UUID by email via Supabase service role."""
r = requests.get(
f"{SUPA_URL}/rest/v1/profiles",
headers={
"apikey": SERVICE_KEY,
"Authorization": f"Bearer {SERVICE_KEY}",
"Content-Type": "application/json",
},
params={"email": f"eq.{email}", "select": "id", "limit": "1"},
)
data = r.json() if r.ok else []
return data[0]["id"] if data else None
def _get_class_id(class_code: str, admin_token: str) -> Optional[str]:
"""Look up a class UUID by class_code via the API."""
r = requests.get(
f"{API_BASE}/database/timetable/classes",
headers={"Authorization": f"Bearer {admin_token}"},
params={"class_code": class_code},
)
data = r.json() if r.ok else []
if isinstance(data, list) and data:
return data[0].get("id") or data[0]
if isinstance(data, dict):
return data.get("id") or data.get("class", {}).get("id")
return None
def _existing_plans_for_teacher(token: str, teacher_email: str) -> List[str]:
"""Return list of existing plan titles for a teacher (to check idempotency)."""
r = requests.get(
f"{API_BASE}/lessons/plans",
headers={"Authorization": f"Bearer {token}"},
)
if r.ok:
plans = r.json().get("plans", [])
return [p.get("title", "") for p in plans]
return []
# ─── Main seed ─────────────────────────────────────────────────────────────────
def seed() -> Dict[str, Any]:
print("=" * 60)
print("Planned lessons seed — both schools")
print("=" * 60)
results: Dict[str, Any] = {}
errors: List[str] = []
# ── Sign in as both school admins ───────────────────────────────────────
print("\n[1] Signing in as school admins...")
admin_tokens = {}
for school, email, pwd in [
("KevlarAI", "admin@kevlarai.test", PWD_ADMIN),
("Greenfield", "admin@greenfieldacademy.test", PWD_ADMIN),
]:
try:
token = _sign_in(email, pwd)
admin_tokens[school] = token
print(f"{school} admin signed in")
except Exception as e:
print(f"{school} admin login failed: {e}")
errors.append(f"{school}_admin_login: {e}")
if not admin_tokens:
return {"success": False, "error": "No admin tokens obtained"}
# ── Resolve class IDs ───────────────────────────────────────────────────
print("\n[2] Resolving class IDs...")
all_class_codes = set()
for plans in [KEVLARAI_PLANS, GREENFIELD_PLANS]:
for p in plans:
if p.get("class_code"):
all_class_codes.add(p["class_code"])
class_code_to_id: Dict[str, str] = {}
for code in all_class_codes:
# Try KevlarAI first, then Greenfield
for school in ["KevlarAI", "Greenfield"]:
cid = _get_class_id(code, admin_tokens[school])
if cid:
class_code_to_id[code] = cid
print(f"{code} -> {cid[:8]}...")
break
else:
print(f" ✗ class not found: {code}")
errors.append(f"class_not_found: {code}")
# ── Seed planned lessons ────────────────────────────────────────────────
print("\n[3] Creating planned lessons...")
created_count = 0
skipped_count = 0
for school, plans in [("KevlarAI", KEVLARAI_PLANS), ("Greenfield", GREENFIELD_PLANS)]:
admin_token = admin_tokens[school]
print(f"\n [{school}]")
for plan_spec in plans:
teacher_email = plan_spec["teacher"]
title = plan_spec["title"]
# Check idempotency
try:
teacher_token = _sign_in(teacher_email, PWD_TEACHER)
except Exception as e:
err = f"login {teacher_email}: {e}"
print(f"{err}")
errors.append(err)
continue
existing = _existing_plans_for_teacher(teacher_token, teacher_email)
if title in existing:
print(f" ~ SKIP (exists): {title}")
skipped_count += 1
continue
# Build class_id
class_id = None
cc = plan_spec.get("class_code")
if cc:
class_id = class_code_to_id.get(cc)
body = {
"title": title,
"subject": plan_spec["subject"],
"year_group": plan_spec["year_group"],
"estimated_duration_minutes": plan_spec.get("duration_minutes", 60),
"objectives": plan_spec["objectives"],
"activities": plan_spec["activities"],
"status": "draft",
"tags": [plan_spec["subject"].lower(), f"yr{plan_spec['year_group']}"],
}
if class_id:
body["class_id"] = class_id
r = _api(teacher_token, "post", "/lessons/plans", body)
plan_id = r.get("id") or (r.get("planned_lesson", {}) or {}).get("id")
if plan_id:
print(f"{title} [{plan_id[:8]}...]")
created_count += 1
else:
err = f"create plan '{title}': {r}"
print(f"{err}")
errors.append(err)
time.sleep(0.2)
# ── Summary ─────────────────────────────────────────────────────────────
print("\n" + "=" * 60)
results["success"] = len(errors) == 0
results["errors"] = errors
results["created"] = created_count
results["skipped"] = skipped_count
if errors:
print(f"COMPLETE with {len(errors)} error(s):")
for e in errors:
print(f"{e}")
else:
print(f"COMPLETE — {created_count} created, {skipped_count} skipped (idempotent)")
print("=" * 60)
return results
if __name__ == "__main__":
import json
print(json.dumps(seed(), indent=2, default=str))