- 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)
386 lines
15 KiB
Python
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))
|