""" 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))