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)
This commit is contained in:
parent
e66c8ec291
commit
ead4452277
366
run/initialization/seed_curriculum.py
Normal file
366
run/initialization/seed_curriculum.py
Normal file
@ -0,0 +1,366 @@
|
||||
"""
|
||||
seed_curriculum.py — Create curriculum data: exam board specifications and exams.
|
||||
|
||||
Seeds eb_specifications and eb_exams tables with realistic UK exam board data
|
||||
(AQA, Edexcel, OCR) for Physics, Maths, and Computer Science across both schools.
|
||||
|
||||
Also seeds curriculum_topics in Neo4j for the school databases.
|
||||
|
||||
Tables: eb_specifications, eb_exams
|
||||
Neo4j: curriculum topic nodes in school databases
|
||||
|
||||
Run inside ccapi container:
|
||||
python3 -c "from run.initialization.seed_curriculum import seed; seed()"
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
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")
|
||||
|
||||
# ─── School constants ────────────────────────────────────────────────────────
|
||||
|
||||
KEVLARAI_ID = "6585bf91-6ae8-4d72-ab54-cddf3ba4e648"
|
||||
GREENFIELD_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
|
||||
# ─── Exam board specifications ───────────────────────────────────────────────
|
||||
# Realistic UK exam board data for the subjects we teach.
|
||||
|
||||
SPECIFICATIONS = [
|
||||
# AQA Physics
|
||||
{
|
||||
"spec_code": "AQA-PHYS-8201",
|
||||
"exam_board_code": "AQA",
|
||||
"award_code": "8201",
|
||||
"subject_code": "PHYSICS",
|
||||
"first_teach": "2016",
|
||||
"spec_ver": "1.3",
|
||||
"storage_loc": "cc.public.snapshots/curriculum/aqa/physics/8201_spec.pdf",
|
||||
"doc_type": "pdf",
|
||||
},
|
||||
{
|
||||
"spec_code": "AQA-PHYS-8203",
|
||||
"exam_board_code": "AQA",
|
||||
"award_code": "8203",
|
||||
"subject_code": "PHYSICS",
|
||||
"first_teach": "2016",
|
||||
"spec_ver": "1.3",
|
||||
"storage_loc": "cc.public.snapshots/curriculum/aqa/physics/8203_spec.pdf",
|
||||
"doc_type": "pdf",
|
||||
},
|
||||
# Edexcel Maths
|
||||
{
|
||||
"spec_code": "EDX-MATH-1MA1",
|
||||
"exam_board_code": "EDexcel",
|
||||
"award_code": "1MA1",
|
||||
"subject_code": "MATHEMATICS",
|
||||
"first_teach": "2015",
|
||||
"spec_ver": "2.0",
|
||||
"storage_loc": "cc.public.snapshots/curriculum/edexcel/maths/1MA1_spec.pdf",
|
||||
"doc_type": "pdf",
|
||||
},
|
||||
# OCR Maths
|
||||
{
|
||||
"spec_code": "OCR-MATH-FMH1",
|
||||
"exam_board_code": "OCR",
|
||||
"award_code": "FMH1",
|
||||
"subject_code": "MATHEMATICS",
|
||||
"first_teach": "2017",
|
||||
"spec_ver": "1.1",
|
||||
"storage_loc": "cc.public.snapshots/curriculum/ocr/maths/FMH1_spec.pdf",
|
||||
"doc_type": "pdf",
|
||||
},
|
||||
# AQA Computer Science
|
||||
{
|
||||
"spec_code": "AQA-COMP-7516",
|
||||
"exam_board_code": "AQA",
|
||||
"award_code": "7516",
|
||||
"subject_code": "COMPUTER SCIENCE",
|
||||
"first_teach": "2016",
|
||||
"spec_ver": "1.2",
|
||||
"storage_loc": "cc.public.snapshots/curriculum/aqa/cs/7516_spec.pdf",
|
||||
"doc_type": "pdf",
|
||||
},
|
||||
# Edexcel Computer Science
|
||||
{
|
||||
"spec_code": "EDX-COMP-X042",
|
||||
"exam_board_code": "Edexcel",
|
||||
"award_code": "X042",
|
||||
"subject_code": "COMPUTER SCIENCE",
|
||||
"first_teach": "2016",
|
||||
"spec_ver": "1.0",
|
||||
"storage_loc": "cc.public.snapshots/curriculum/edexcel/cs/X042_spec.pdf",
|
||||
"doc_type": "pdf",
|
||||
},
|
||||
]
|
||||
|
||||
# ─── Exam papers ─────────────────────────────────────────────────────────────
|
||||
# Realistic exam paper references linked to specifications.
|
||||
|
||||
EXAMS = [
|
||||
# AQA Physics 8201/1 (Foundation)
|
||||
{"exam_code": "AQA-PHYS-8201-1-23-JUN", "spec_code": "AQA-PHYS-8201", "paper_code": "8201/1",
|
||||
"tier": "foundation", "session": "June", "type_code": "QP"},
|
||||
{"exam_code": "AQA-PHYS-8201-MS-23-JUN", "spec_code": "AQA-PHYS-8201", "paper_code": "8201/1",
|
||||
"tier": "foundation", "session": "June", "type_code": "MS"},
|
||||
{"exam_code": "AQA-PHYS-8201-ER-23-JUN", "spec_code": "AQA-PHYS-8201", "paper_code": "8201/1",
|
||||
"tier": "foundation", "session": "June", "type_code": "ER"},
|
||||
|
||||
# AQA Physics 8201/2 (Higher)
|
||||
{"exam_code": "AQA-PHYS-8201-2-23-JUN", "spec_code": "AQA-PHYS-8201", "paper_code": "8201/2",
|
||||
"tier": "higher", "session": "June", "type_code": "QP"},
|
||||
{"exam_code": "AQA-PHYS-8201-MS-23-JUN-H", "spec_code": "AQA-PHYS-8201", "paper_code": "8201/2",
|
||||
"tier": "higher", "session": "June", "type_code": "MS"},
|
||||
|
||||
# Edexcel Maths 1MA1/1 (Foundation)
|
||||
{"exam_code": "EDX-MATH-1MA1-1-24-JUN", "spec_code": "EDX-MATH-1MA1", "paper_code": "1MA1/1F",
|
||||
"tier": "foundation", "session": "June", "type_code": "QP"},
|
||||
{"exam_code": "EDX-MATH-1MA1-MS-24-JUN", "spec_code": "EDX-MATH-1MA1", "paper_code": "1MA1/1F",
|
||||
"tier": "foundation", "session": "June", "type_code": "MS"},
|
||||
|
||||
# Edexcel Maths 1MA1/2 (Higher)
|
||||
{"exam_code": "EDX-MATH-1MA1-2-24-JUN", "spec_code": "EDX-MATH-1MA1", "paper_code": "1MA1/2H",
|
||||
"tier": "higher", "session": "June", "type_code": "QP"},
|
||||
{"exam_code": "EDX-MATH-1MA1-MS-24-JUN-H", "spec_code": "EDX-MATH-1MA1", "paper_code": "1MA1/2H",
|
||||
"tier": "higher", "session": "June", "type_code": "MS"},
|
||||
|
||||
# OCR Maths FMH1/1
|
||||
{"exam_code": "OCR-MATH-FMH1-1-24-JUN", "spec_code": "OCR-MATH-FMH1", "paper_code": "FMH1/1",
|
||||
"tier": "higher", "session": "June", "type_code": "QP"},
|
||||
{"exam_code": "OCR-MATH-FMH1-MS-24-JUN", "spec_code": "OCR-MATH-FMH1", "paper_code": "FMH1/1",
|
||||
"tier": "higher", "session": "June", "type_code": "MS"},
|
||||
|
||||
# AQA CS 7516/1
|
||||
{"exam_code": "AQA-COMP-7516-1-23-JUN", "spec_code": "AQA-COMP-7516", "paper_code": "7516/1",
|
||||
"tier": None, "session": "June", "type_code": "QP"},
|
||||
{"exam_code": "AQA-COMP-7516-MS-23-JUN", "spec_code": "AQA-COMP-7516", "paper_code": "7516/1",
|
||||
"tier": None, "session": "June", "type_code": "MS"},
|
||||
|
||||
# AQA CS 7516/2
|
||||
{"exam_code": "AQA-COMP-7516-2-23-JUN", "spec_code": "AQA-COMP-7516", "paper_code": "7516/2",
|
||||
"tier": None, "session": "June", "type_code": "QP"},
|
||||
{"exam_code": "AQA-COMP-7516-ER-23-JUN", "spec_code": "AQA-COMP-7516", "paper_code": "7516/2",
|
||||
"tier": None, "session": "June", "type_code": "ER"},
|
||||
]
|
||||
|
||||
|
||||
# ─── Neo4j curriculum topics ─────────────────────────────────────────────────
|
||||
# Curriculum topics stored in Neo4j school databases (not Supabase).
|
||||
|
||||
CURRICULUM_TOPICS = {
|
||||
"Physics": [
|
||||
{"topic_code": "PHYS-KS3-01", "title": "Forces", "year_group": "9", "key_stage": "3",
|
||||
"description": "Contact and non-contact forces, resultant forces, moments"},
|
||||
{"topic_code": "PHYS-KS3-02", "title": "Energy", "year_group": "9", "key_stage": "3",
|
||||
"description": "Energy stores, transfers, conservation, dissipation"},
|
||||
{"topic_code": "PHYS-KS3-03", "title": "Waves", "year_group": "9", "key_stage": "3",
|
||||
"description": "Transverse and longitudinal waves, reflection, refraction, diffraction"},
|
||||
{"topic_code": "PHYS-KS4-01", "title": "Electricity", "year_group": "10", "key_stage": "4",
|
||||
"description": "Circuits, current, potential difference, resistance, power"},
|
||||
{"topic_code": "PHYS-KS4-02", "title": "Magnetism and Electromagnetism", "year_group": "10", "key_stage": "4",
|
||||
"description": "Magnetic fields, electromagnets, motors, generators"},
|
||||
{"topic_code": "PHYS-KS4-03", "title": "Atomic Structure", "year_group": "10", "key_stage": "4",
|
||||
"description": "Atoms, isotopes, radioactivity, half-life"},
|
||||
{"topic_code": "PHYS-KS4-04", "title": "Particle Physics", "year_group": "11", "key_stage": "4",
|
||||
"description": "Standard model, quarks, leptons, bosons"},
|
||||
{"topic_code": "PHYS-KS4-05", "title": "Cosmology", "year_group": "11", "key_stage": "4",
|
||||
"description": "Big Bang, stellar evolution, redshift"},
|
||||
],
|
||||
"Mathematics": [
|
||||
{"topic_code": "MATH-KS3-01", "title": "Number", "year_group": "9", "key_stage": "3",
|
||||
"description": "Integers, fractions, decimals, percentages, ratio, proportion"},
|
||||
{"topic_code": "MATH-KS3-02", "title": "Algebra", "year_group": "9", "key_stage": "3",
|
||||
"description": "Expressions, equations, inequalities, sequences"},
|
||||
{"topic_code": "MATH-KS3-03", "title": "Geometry", "year_group": "9", "key_stage": "3",
|
||||
"description": "Angles, polygons, circles, transformations, constructions"},
|
||||
{"topic_code": "MATH-KS4-01", "title": "Number and Algebra", "year_group": "10", "key_stage": "4",
|
||||
"description": "Surds, indices, standard form, expanding brackets, factorising"},
|
||||
{"topic_code": "MATH-KS4-02", "title": "Graphs and Functions", "year_group": "10", "key_stage": "4",
|
||||
"description": "Linear, quadratic, cubic graphs, gradients, intercepts"},
|
||||
{"topic_code": "MATH-KS4-03", "title": "Statistics and Probability", "year_group": "10", "key_stage": "4",
|
||||
"description": "Data types, charts, expected frequency, tree diagrams, two-way tables"},
|
||||
{"topic_code": "MATH-KS4-04", "title": "Geometry and Measures", "year_group": "10", "key_stage": "4",
|
||||
"description": "Area, volume, surface area, Pythagoras, trigonometry, bearings"},
|
||||
{"topic_code": "MATH-KS4-05", "title": "Simultaneous Equations and Quadratics", "year_group": "11", "key_stage": "4",
|
||||
"description": "Solving simultaneous equations, completing the square, quadratic formula"},
|
||||
],
|
||||
"Computer Science": [
|
||||
{"topic_code": "CS-KS4-01", "title": "Data Representation", "year_group": "10", "key_stage": "4",
|
||||
"description": "Binary, hexadecimal, bit operations, compression, encryption"},
|
||||
{"topic_code": "CS-KS4-02", "title": "Computer Systems", "year_group": "10", "key_stage": "4",
|
||||
"description": "CPU architecture, memory, storage, networks, topologies"},
|
||||
{"topic_code": "CS-KS4-03", "title": "Algorithms and Programming", "year_group": "10", "key_stage": "4",
|
||||
"description": "Algorithms, flowcharts, pseudocode, debugging, testing"},
|
||||
{"topic_code": "CS-KS4-04", "title": "Data Types and Structures", "year_group": "11", "key_stage": "4",
|
||||
"description": "Strings, arrays, lists, records, 2D arrays"},
|
||||
{"topic_code": "CS-KS4-05", "title": "Boolean Logic and Search", "year_group": "11", "key_stage": "4",
|
||||
"description": "Boolean operators, linear search, binary search, sorting"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ─── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _sb_headers() -> Dict:
|
||||
return {
|
||||
"apikey": SERVICE_KEY,
|
||||
"Authorization": f"Bearer {SERVICE_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
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"]
|
||||
|
||||
|
||||
# ─── Main seed ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def seed() -> Dict[str, Any]:
|
||||
print("=" * 60)
|
||||
print("Curriculum seed — exam board specs and exams")
|
||||
print("=" * 60)
|
||||
results: Dict[str, Any] = {}
|
||||
errors: List[str] = []
|
||||
|
||||
# ── [1] Seed eb_specifications ──────────────────────────────────────────
|
||||
print("\n[1] Seeding exam board specifications...")
|
||||
specs_created = 0
|
||||
specs_skipped = 0
|
||||
|
||||
for spec in SPECIFICATIONS:
|
||||
r = requests.post(
|
||||
f"{SUPA_URL}/rest/v1/eb_specifications",
|
||||
headers={**_sb_headers(), "Prefer": "return=representation"},
|
||||
json={
|
||||
**spec,
|
||||
"id": str(uuid.uuid4()),
|
||||
"doc_details": {},
|
||||
"docling_docs": {},
|
||||
},
|
||||
params={"on_conflict": "spec_code"},
|
||||
)
|
||||
if r.status_code in (200, 201):
|
||||
specs_created += 1
|
||||
print(f" ✓ {spec['spec_code']} ({spec['exam_board_code']}/{spec['subject_code']})")
|
||||
elif r.status_code == 409:
|
||||
specs_skipped += 1
|
||||
print(f" ~ SKIP (exists): {spec['spec_code']}")
|
||||
else:
|
||||
err = f"spec {spec['spec_code']}: {r.status_code} {r.text[:100]}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
|
||||
results["specifications"] = {"created": specs_created, "skipped": specs_skipped}
|
||||
|
||||
# ── [2] Seed eb_exams ───────────────────────────────────────────────────
|
||||
print("\n[2] Seeding exam papers...")
|
||||
exams_created = 0
|
||||
exams_skipped = 0
|
||||
|
||||
for exam in EXAMS:
|
||||
r = requests.post(
|
||||
f"{SUPA_URL}/rest/v1/eb_exams",
|
||||
headers={**_sb_headers(), "Prefer": "return=representation"},
|
||||
json={
|
||||
**exam,
|
||||
"id": str(uuid.uuid4()),
|
||||
"doc_details": {},
|
||||
"docling_docs": {},
|
||||
},
|
||||
params={"on_conflict": "exam_code"},
|
||||
)
|
||||
if r.status_code in (200, 201):
|
||||
exams_created += 1
|
||||
print(f" ✓ {exam['exam_code']} ({exam['type_code']})")
|
||||
elif r.status_code == 409:
|
||||
exams_skipped += 1
|
||||
print(f" ~ SKIP (exists): {exam['exam_code']}")
|
||||
else:
|
||||
err = f"exam {exam['exam_code']}: {r.status_code} {r.text[:100]}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
|
||||
results["exams"] = {"created": exams_created, "skipped": exams_skipped}
|
||||
|
||||
# ── [3] Seed Neo4j curriculum topics ────────────────────────────────────
|
||||
print("\n[3] Seeding Neo4j curriculum topics...")
|
||||
try:
|
||||
from neo4j import GraphDatabase
|
||||
driver = GraphDatabase.driver("bolt://192.168.0.209:7687", auth=("neo4j", "&%N304j&%"))
|
||||
|
||||
topics_created = 0
|
||||
topics_skipped = 0
|
||||
|
||||
for school_id, school_name in [(KEVLARAI_ID, "KevlarAI"), (GREENFIELD_ID, "Greenfield Academy")]:
|
||||
db_name = f"cc.institutes.{school_id.replace('-', '')}"
|
||||
print(f"\n [{school_name}] -> {db_name}")
|
||||
|
||||
with driver.session(database=db_name) as s:
|
||||
for subject, topics in CURRICULUM_TOPICS.items():
|
||||
# Create subject node
|
||||
s.run(
|
||||
"MERGE (s:Subject {code: $subject}) "
|
||||
"SET s.title = $title, s.school_id = $school_id",
|
||||
subject=subject, title=subject, school_id=school_id,
|
||||
)
|
||||
|
||||
for topic in topics:
|
||||
result = s.run(
|
||||
"MERGE (t:CurriculumTopic {code: $code}) "
|
||||
"SET t.title = $title, "
|
||||
" t.year_group = $year_group, "
|
||||
" t.key_stage = $key_stage, "
|
||||
" t.description = $description, "
|
||||
" t.subject_code = $subject, "
|
||||
" t.school_id = $school_id "
|
||||
"MERGE (s:Subject {code: $subject}) "
|
||||
"MERGE (s)-[:CONTAINS_TOPIC]->(t)",
|
||||
code=topic["topic_code"],
|
||||
title=topic["title"],
|
||||
year_group=topic["year_group"],
|
||||
key_stage=topic["key_stage"],
|
||||
description=topic["description"],
|
||||
subject=subject,
|
||||
school_id=school_id,
|
||||
)
|
||||
# Check if it was created or matched
|
||||
topics_created += 1
|
||||
|
||||
print(f" ✓ {school_name}: {len(CURRICULUM_TOPICS) * len(list(CURRICULUM_TOPICS.values())[0])} topic nodes")
|
||||
|
||||
driver.close()
|
||||
results["neo4j_topics"] = {"created": topics_created}
|
||||
|
||||
except Exception as e:
|
||||
err = f"neo4j_topics: {e}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
results["neo4j_topics"] = {"error": str(e)}
|
||||
|
||||
# ── Summary ─────────────────────────────────────────────────────────────
|
||||
print("\n" + "=" * 60)
|
||||
results["success"] = len(errors) == 0
|
||||
results["errors"] = errors
|
||||
print(f"COMPLETE — {specs_created} specs, {exams_created} exams, "
|
||||
f"{results.get('neo4j_topics', {}).get('created', '?')} topics")
|
||||
if errors:
|
||||
print(f"Errors ({len(errors)}):")
|
||||
for e in errors:
|
||||
print(f" ✗ {e}")
|
||||
print("=" * 60)
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
print(json.dumps(seed(), indent=2, default=str))
|
||||
423
run/initialization/seed_file_cabinets.py
Normal file
423
run/initialization/seed_file_cabinets.py
Normal file
@ -0,0 +1,423 @@
|
||||
"""
|
||||
seed_file_cabinets.py — Create one file cabinet per class with sample document references.
|
||||
|
||||
Creates file_cabinets, files, and cabinet_memberships rows via Supabase REST API
|
||||
using the service role key. Also creates document_artefacts entries for sample files.
|
||||
|
||||
Each cabinet is owned by the class's primary teacher and shared with students
|
||||
in that class via cabinet_memberships.
|
||||
|
||||
Tables: file_cabinets, files, cabinet_memberships, document_artefacts
|
||||
|
||||
Run inside ccapi container:
|
||||
python3 -c "from run.initialization.seed_file_cabinets import seed; seed()"
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
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!"
|
||||
PWD_STUDENT = "Student@Cc2025!"
|
||||
|
||||
|
||||
# ─── Sample file data ────────────────────────────────────────────────────────
|
||||
# Each cabinet gets 2-3 sample files with realistic paths.
|
||||
|
||||
SAMPLE_FILES = {
|
||||
# Physics cabinets
|
||||
"lesson_plans": [
|
||||
{"name": "forces_motion_plan.pdf", "path": "cc.public.snapshots/lesson_plans/forces_motion.pdf", "mime_type": "application/pdf", "size": "245KB"},
|
||||
{"name": "electric_circuits_plan.pdf", "path": "cc.public.snapshots/lesson_plans/electric_circuits.pdf", "mime_type": "application/pdf", "size": "312KB"},
|
||||
],
|
||||
"worksheets": [
|
||||
{"name": "worksheet_fma.pdf", "path": "cc.public.snapshots/worksheets/fma_practice.pdf", "mime_type": "application/pdf", "size": "128KB"},
|
||||
{"name": "worksheet_resistance.pdf", "path": "cc.public.snapshots/worksheets/resistance_calc.pdf", "mime_type": "application/pdf", "size": "95KB"},
|
||||
],
|
||||
"presentations": [
|
||||
{"name": "intro_forces.pptx", "path": "cc.public.snapshots/presentations/forces_intro.pptx", "mime_type": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "size": "2.1MB"},
|
||||
],
|
||||
# Maths cabinets
|
||||
"lesson_plans": [
|
||||
{"name": "quadratic_factorisation_plan.pdf", "path": "cc.public.snapshots/lesson_plans/quadratics.pdf", "mime_type": "application/pdf", "size": "278KB"},
|
||||
],
|
||||
"worksheets": [
|
||||
{"name": "worksheet_quadratics.pdf", "path": "cc.public.snapshots/worksheets/quadratic_practice.pdf", "mime_type": "application/pdf", "size": "156KB"},
|
||||
{"name": "worksheet_tree_diagrams.pdf", "path": "cc.public.snapshots/worksheets/tree_diagrams.pdf", "mime_type": "application/pdf", "size": "134KB"},
|
||||
],
|
||||
# CS cabinets
|
||||
"lesson_plans": [
|
||||
{"name": "intro_python_plan.pdf", "path": "cc.public.snapshots/lesson_plans/intro_python.pdf", "mime_type": "application/pdf", "size": "198KB"},
|
||||
],
|
||||
"code_samples": [
|
||||
{"name": "hello_world.py", "path": "cc.public.snapshots/code_samples/hello_world.py", "mime_type": "text/x-python", "size": "0.5KB"},
|
||||
{"name": "variables.py", "path": "cc.public.snapshots/code_samples/variables.py", "mime_type": "text/x-python", "size": "1.2KB"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ─── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _sb_headers() -> Dict:
|
||||
return {
|
||||
"apikey": SERVICE_KEY,
|
||||
"Authorization": f"Bearer {SERVICE_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
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 _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=_sb_headers(),
|
||||
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_info(admin_token: str, class_code: str) -> Optional[Dict]:
|
||||
"""Get class info including teacher and students."""
|
||||
r = requests.get(
|
||||
f"{API_BASE}/database/timetable/classes",
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
params={"class_code": class_code},
|
||||
)
|
||||
if not r.ok:
|
||||
return None
|
||||
data = r.json()
|
||||
if isinstance(data, list) and data:
|
||||
return data[0]
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
return None
|
||||
|
||||
|
||||
def _get_class_students(admin_token: str, class_id: str) -> List[str]:
|
||||
"""Get student profile IDs enrolled in a class."""
|
||||
r = requests.get(
|
||||
f"{API_BASE}/database/timetable/classes/{class_id}/students",
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
)
|
||||
if r.ok:
|
||||
data = r.json()
|
||||
if isinstance(data, list):
|
||||
return [s.get("student_id") or s.get("id") for s in data if s.get("student_id") or s.get("id")]
|
||||
return []
|
||||
|
||||
|
||||
# ─── Main seed ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def seed() -> Dict[str, Any]:
|
||||
print("=" * 60)
|
||||
print("File cabinets 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 codes per school ──────────────────────────────────────
|
||||
print("\n[2] Resolving classes per school...")
|
||||
|
||||
# KevlarAI classes
|
||||
kevlarai_classes = [
|
||||
("10K/Ph1", "physics@kevlarai.test"),
|
||||
("11K/Ph1", "physics@kevlarai.test"),
|
||||
("10K/Ma1", "maths@kevlarai.test"),
|
||||
("11K/Ma1", "maths@kevlarai.test"),
|
||||
("10K/CS1", "physics@kevlarai.test"),
|
||||
("11K/CS1", "maths@kevlarai.test"),
|
||||
("9K/Ph1", "physics@kevlarai.test"),
|
||||
("9K/Ma1", "maths@kevlarai.test"),
|
||||
]
|
||||
|
||||
# Greenfield classes (subset — just a few for cabinet seeding)
|
||||
greenfield_classes = [
|
||||
("9P/Ph1", "physics@greenfieldacademy.test"),
|
||||
("10P/Ph2", "physics@greenfieldacademy.test"),
|
||||
("9M/Ma1", "maths@greenfieldacademy.test"),
|
||||
("10M/Ma1", "maths@greenfieldacademy.test"),
|
||||
("9En/1", "teacher1@greenfieldacademy.test"),
|
||||
("10Hs/1", "teacher2@greenfieldacademy.test"),
|
||||
]
|
||||
|
||||
# ── Seed KevlarAI cabinets ──────────────────────────────────────────────
|
||||
print("\n[3] Seeding KevlarAI file cabinets...")
|
||||
results["kevlarai"] = {"cabinets": 0, "files": 0, "memberships": 0}
|
||||
|
||||
for class_code, teacher_email in kevlarai_classes:
|
||||
try:
|
||||
# Get class info
|
||||
class_info = _get_class_info(admin_tokens["KevlarAI"], class_code)
|
||||
if not class_info:
|
||||
print(f" ✗ class not found: {class_code}")
|
||||
errors.append(f"class_not_found: {class_code}")
|
||||
continue
|
||||
|
||||
class_id = class_info.get("id") or class_info
|
||||
teacher_pid = _get_profile_id(teacher_email)
|
||||
if not teacher_pid:
|
||||
print(f" ✗ teacher profile not found: {teacher_email}")
|
||||
errors.append(f"teacher_profile_not_found: {teacher_email}")
|
||||
continue
|
||||
|
||||
# Get students in this class
|
||||
student_ids = _get_class_students(admin_tokens["KevlarAI"], str(class_id))
|
||||
|
||||
# Determine file category based on subject
|
||||
subject = (class_info.get("subject") or "").lower()
|
||||
if "physics" in subject:
|
||||
file_category = "lesson_plans"
|
||||
elif "math" in subject:
|
||||
file_category = "worksheets"
|
||||
elif "cs" in subject or "computer" in subject:
|
||||
file_category = "code_samples"
|
||||
else:
|
||||
file_category = "lesson_plans"
|
||||
|
||||
files_list = SAMPLE_FILES.get(file_category, SAMPLE_FILES["lesson_plans"])
|
||||
|
||||
# Create cabinet
|
||||
cabinet_id = str(uuid.uuid4())
|
||||
cabinet_name = f"{class_code} — {class_info.get('name', class_code)}"
|
||||
|
||||
r = requests.post(
|
||||
f"{SUPA_URL}/rest/v1/file_cabinets",
|
||||
headers={**_sb_headers(), "Prefer": "return=representation"},
|
||||
json={"id": cabinet_id, "user_id": teacher_pid, "name": cabinet_name},
|
||||
params={"on_conflict": "id"},
|
||||
)
|
||||
if r.status_code in (200, 201):
|
||||
print(f" ✓ Cabinet: {cabinet_name}")
|
||||
results["kevlarai"]["cabinets"] += 1
|
||||
else:
|
||||
print(f" ✗ Cabinet create failed ({class_code}): {r.text[:100]}")
|
||||
errors.append(f"cabinet_create: {class_code}")
|
||||
continue
|
||||
|
||||
# Create files in cabinet
|
||||
for fi in files_list:
|
||||
file_id = str(uuid.uuid4())
|
||||
r = requests.post(
|
||||
f"{SUPA_URL}/rest/v1/files",
|
||||
headers={**_sb_headers(), "Prefer": "return=representation"},
|
||||
json={
|
||||
"id": file_id,
|
||||
"cabinet_id": cabinet_id,
|
||||
"name": fi["name"],
|
||||
"path": fi["path"],
|
||||
"bucket": "file-cabinets",
|
||||
"mime_type": fi.get("mime_type"),
|
||||
"size": fi.get("size"),
|
||||
"metadata": {},
|
||||
},
|
||||
params={"on_conflict": "id"},
|
||||
)
|
||||
if r.status_code in (200, 201):
|
||||
results["kevlarai"]["files"] += 1
|
||||
|
||||
# Create document_artefact for this file
|
||||
artefact_id = str(uuid.uuid4())
|
||||
requests.post(
|
||||
f"{SUPA_URL}/rest/v1/document_artefacts",
|
||||
headers={**_sb_headers(), "Prefer": "return=representation"},
|
||||
json={
|
||||
"id": artefact_id,
|
||||
"file_id": file_id,
|
||||
"type": fi.get("mime_type", "application/octet-stream").split("/")[-1],
|
||||
"rel_path": fi["path"],
|
||||
"status": "processed",
|
||||
"extra": {"seeded": True, "source": "seed_file_cabinets"},
|
||||
},
|
||||
params={"on_conflict": "id"},
|
||||
)
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
# Create cabinet memberships for students
|
||||
for sid in student_ids:
|
||||
r = requests.post(
|
||||
f"{SUPA_URL}/rest/v1/cabinet_memberships",
|
||||
headers={**_sb_headers(), "Prefer": "return=minimal"},
|
||||
json={
|
||||
"cabinet_id": cabinet_id,
|
||||
"profile_id": sid,
|
||||
"role": "viewer",
|
||||
},
|
||||
params={"on_conflict": "cabinet_id,profile_id"},
|
||||
)
|
||||
if r.status_code in (200, 201, 409):
|
||||
results["kevlarai"]["memberships"] += 1
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
err = f"cabinet seed {class_code}: {e}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
|
||||
# ── Seed Greenfield cabinets ────────────────────────────────────────────
|
||||
print("\n[4] Seeding Greenfield file cabinets...")
|
||||
results["greenfield"] = {"cabinets": 0, "files": 0, "memberships": 0}
|
||||
|
||||
for class_code, teacher_email in greenfield_classes:
|
||||
try:
|
||||
class_info = _get_class_info(admin_tokens["Greenfield"], class_code)
|
||||
if not class_info:
|
||||
print(f" ✗ class not found: {class_code}")
|
||||
errors.append(f"class_not_found: {class_code}")
|
||||
continue
|
||||
|
||||
class_id = class_info.get("id") or class_info
|
||||
teacher_pid = _get_profile_id(teacher_email)
|
||||
if not teacher_pid:
|
||||
print(f" ✗ teacher profile not found: {teacher_email}")
|
||||
errors.append(f"teacher_profile_not_found: {teacher_email}")
|
||||
continue
|
||||
|
||||
student_ids = _get_class_students(admin_tokens["Greenfield"], str(class_id))
|
||||
|
||||
subject = (class_info.get("subject") or "").lower()
|
||||
if "physics" in subject:
|
||||
file_category = "lesson_plans"
|
||||
elif "math" in subject:
|
||||
file_category = "worksheets"
|
||||
elif "english" in subject:
|
||||
file_category = "presentations"
|
||||
elif "history" in subject:
|
||||
file_category = "lesson_plans"
|
||||
else:
|
||||
file_category = "lesson_plans"
|
||||
|
||||
files_list = SAMPLE_FILES.get(file_category, SAMPLE_FILES["lesson_plans"])
|
||||
|
||||
cabinet_id = str(uuid.uuid4())
|
||||
cabinet_name = f"{class_code} — {class_info.get('name', class_code)}"
|
||||
|
||||
r = requests.post(
|
||||
f"{SUPA_URL}/rest/v1/file_cabinets",
|
||||
headers={**_sb_headers(), "Prefer": "return=representation"},
|
||||
json={"id": cabinet_id, "user_id": teacher_pid, "name": cabinet_name},
|
||||
params={"on_conflict": "id"},
|
||||
)
|
||||
if r.status_code in (200, 201):
|
||||
print(f" ✓ Cabinet: {cabinet_name}")
|
||||
results["greenfield"]["cabinets"] += 1
|
||||
else:
|
||||
print(f" ✗ Cabinet create failed ({class_code}): {r.text[:100]}")
|
||||
errors.append(f"cabinet_create: {class_code}")
|
||||
continue
|
||||
|
||||
for fi in files_list:
|
||||
file_id = str(uuid.uuid4())
|
||||
r = requests.post(
|
||||
f"{SUPA_URL}/rest/v1/files",
|
||||
headers={**_sb_headers(), "Prefer": "return=representation"},
|
||||
json={
|
||||
"id": file_id,
|
||||
"cabinet_id": cabinet_id,
|
||||
"name": fi["name"],
|
||||
"path": fi["path"],
|
||||
"bucket": "file-cabinets",
|
||||
"mime_type": fi.get("mime_type"),
|
||||
"size": fi.get("size"),
|
||||
"metadata": {},
|
||||
},
|
||||
params={"on_conflict": "id"},
|
||||
)
|
||||
if r.status_code in (200, 201):
|
||||
results["greenfield"]["files"] += 1
|
||||
|
||||
artefact_id = str(uuid.uuid4())
|
||||
requests.post(
|
||||
f"{SUPA_URL}/rest/v1/document_artefacts",
|
||||
headers={**_sb_headers(), "Prefer": "return=representation"},
|
||||
json={
|
||||
"id": artefact_id,
|
||||
"file_id": file_id,
|
||||
"type": fi.get("mime_type", "application/octet-stream").split("/")[-1],
|
||||
"rel_path": fi["path"],
|
||||
"status": "processed",
|
||||
"extra": {"seeded": True, "source": "seed_file_cabinets"},
|
||||
},
|
||||
params={"on_conflict": "id"},
|
||||
)
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
for sid in student_ids:
|
||||
r = requests.post(
|
||||
f"{SUPA_URL}/rest/v1/cabinet_memberships",
|
||||
headers={**_sb_headers(), "Prefer": "return=minimal"},
|
||||
json={
|
||||
"cabinet_id": cabinet_id,
|
||||
"profile_id": sid,
|
||||
"role": "viewer",
|
||||
},
|
||||
params={"on_conflict": "cabinet_id,profile_id"},
|
||||
)
|
||||
if r.status_code in (200, 201, 409):
|
||||
results["greenfield"]["memberships"] += 1
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
err = f"cabinet seed {class_code}: {e}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
|
||||
# ── Summary ─────────────────────────────────────────────────────────────
|
||||
print("\n" + "=" * 60)
|
||||
results["success"] = len(errors) == 0
|
||||
results["errors"] = errors
|
||||
total_cabinets = results["kevlarai"]["cabinets"] + results["greenfield"]["cabinets"]
|
||||
total_files = results["kevlarai"]["files"] + results["greenfield"]["files"]
|
||||
total_memberships = results["kevlarai"]["memberships"] + results["greenfield"]["memberships"]
|
||||
print(f"COMPLETE — {total_cabinets} cabinets, {total_files} files, {total_memberships} memberships")
|
||||
if errors:
|
||||
print(f"Errors ({len(errors)}):")
|
||||
for e in errors:
|
||||
print(f" ✗ {e}")
|
||||
print("=" * 60)
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
print(json.dumps(seed(), indent=2, default=str))
|
||||
456
run/initialization/seed_kevlarai_timetable.py
Normal file
456
run/initialization/seed_kevlarai_timetable.py
Normal file
@ -0,0 +1,456 @@
|
||||
"""
|
||||
seed_kevlarai_timetable.py — Full timetable + class + student seed for KevlarAI school.
|
||||
|
||||
Mirrors Greenfield's structure so both schools are testable.
|
||||
KevlarAI gets 8 classes across 3 subjects (Physics, Maths, Computer Science),
|
||||
2 teachers, and 2 students.
|
||||
|
||||
Flow:
|
||||
1. POST /timetable/setup — academic year, 3 terms, periods → Supabase
|
||||
2. POST /timetable/materialize-periods — academic_periods rows (days x template)
|
||||
3. Create classes — 8 classes with correct metadata
|
||||
4. Add teachers to classes — primary teacher per class
|
||||
5. POST /timetable/init + slots — TeacherTimetable + slot assignments
|
||||
6. Patch slot class_ids — write class_id FK onto teacher_timetable_slots
|
||||
7. Enroll students in classes — student1→Yr10, student2→Yr11
|
||||
8. POST /timetable/materialize — taught_lessons with class_id populated
|
||||
9. POST /timetable/sync-lessons — Neo4j TaughtLesson nodes (B.10)
|
||||
|
||||
Run inside ccapi container:
|
||||
python3 -c "from run.initialization.seed_kevlarai_timetable import seed; seed()"
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import requests
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
SUPA_URL = os.environ["SUPABASE_URL"]
|
||||
SERVICE_KEY = os.environ["SERVICE_ROLE_KEY"]
|
||||
API_BASE = os.environ.get("API_BASE_URL", "http://localhost:8000")
|
||||
|
||||
KEVLARAI_ADMIN_EMAIL = "admin@kevlarai.test"
|
||||
KEVLARAI_ADMIN_PWD = "Admin@Cc2025!"
|
||||
PWD_TEACHER = "Teacher@Cc2025!"
|
||||
PWD_STUDENT = "Student@Cc2025!"
|
||||
|
||||
# ─── Period templates (same as Greenfield) ─────────────────────────────────────
|
||||
|
||||
PERIODS = [
|
||||
{"code": "REG", "name": "Registration", "start_time": "08:45", "end_time": "09:00", "period_type": "registration"},
|
||||
{"code": "P1", "name": "Period 1", "start_time": "09:00", "end_time": "10:00", "period_type": "lesson"},
|
||||
{"code": "P2", "name": "Period 2", "start_time": "10:00", "end_time": "11:00", "period_type": "lesson"},
|
||||
{"code": "BRK", "name": "Break", "start_time": "11:00", "end_time": "11:20", "period_type": "break"},
|
||||
{"code": "P3", "name": "Period 3", "start_time": "11:20", "end_time": "12:20", "period_type": "lesson"},
|
||||
{"code": "P4", "name": "Period 4", "start_time": "12:20", "end_time": "13:20", "period_type": "lesson"},
|
||||
{"code": "LUN", "name": "Lunch", "start_time": "13:20", "end_time": "14:00", "period_type": "break"},
|
||||
{"code": "P5", "name": "Period 5", "start_time": "14:00", "end_time": "15:00", "period_type": "lesson"},
|
||||
]
|
||||
|
||||
PERIOD_TIMES = {p["code"]: (p["start_time"], p["end_time"]) for p in PERIODS}
|
||||
|
||||
# ─── Academic year ─────────────────────────────────────────────────────────────
|
||||
|
||||
TERMS = [
|
||||
{"name": "Autumn Term", "term_number": 1, "start_date": "2025-09-03", "end_date": "2025-12-19"},
|
||||
{"name": "Spring Term", "term_number": 2, "start_date": "2026-01-05", "end_date": "2026-04-01"},
|
||||
{"name": "Summer Term", "term_number": 3, "start_date": "2026-04-20", "end_date": "2026-07-17"},
|
||||
]
|
||||
|
||||
# ─── Class definitions ─────────────────────────────────────────────────────────
|
||||
# KevlarAI: 8 classes across Physics, Maths, Computer Science
|
||||
|
||||
CLASSES = [
|
||||
# Physics
|
||||
{"class_code": "10K/Ph1", "name": "Year 10 Physics Group 1", "subject": "Physics", "year_group": "10", "key_stage": "4", "teacher": "physics@kevlarai.test"},
|
||||
{"class_code": "11K/Ph1", "name": "Year 11 Physics Group 1", "subject": "Physics", "year_group": "11", "key_stage": "4", "teacher": "physics@kevlarai.test"},
|
||||
# Maths
|
||||
{"class_code": "10K/Ma1", "name": "Year 10 Maths Group 1", "subject": "Mathematics", "year_group": "10", "key_stage": "4", "teacher": "maths@kevlarai.test"},
|
||||
{"class_code": "11K/Ma1", "name": "Year 11 Maths Group 1", "subject": "Mathematics", "year_group": "11", "key_stage": "4", "teacher": "maths@kevlarai.test"},
|
||||
# Computer Science
|
||||
{"class_code": "10K/CS1", "name": "Year 10 CS Group 1", "subject": "Computer Science", "year_group": "10", "key_stage": "4", "teacher": "physics@kevlarai.test"},
|
||||
{"class_code": "11K/CS1", "name": "Year 11 CS Group 1", "subject": "Computer Science", "year_group": "11", "key_stage": "4", "teacher": "maths@kevlarai.test"},
|
||||
# Additional KS3 classes for breadth
|
||||
{"class_code": "9K/Ph1", "name": "Year 9 Physics Group 1", "subject": "Physics", "year_group": "9", "key_stage": "3", "teacher": "physics@kevlarai.test"},
|
||||
{"class_code": "9K/Ma1", "name": "Year 9 Maths Group 1", "subject": "Mathematics", "year_group": "9", "key_stage": "3", "teacher": "maths@kevlarai.test"},
|
||||
]
|
||||
|
||||
# ─── Teacher slot assignments ──────────────────────────────────────────────────
|
||||
|
||||
TEACHER_SLOTS = {
|
||||
"physics@kevlarai.test": [
|
||||
("Monday", "P1", "10K/Ph1"),
|
||||
("Monday", "P3", "11K/Ph1"),
|
||||
("Tuesday", "P2", "10K/CS1"),
|
||||
("Tuesday", "P4", "9K/Ph1"),
|
||||
("Wednesday", "P1", "11K/Ph1"),
|
||||
("Wednesday", "P5", "10K/Ph1"),
|
||||
("Thursday", "P3", "10K/CS1"),
|
||||
("Thursday", "P5", "9K/Ph1"),
|
||||
],
|
||||
"maths@kevlarai.test": [
|
||||
("Monday", "P2", "10K/Ma1"),
|
||||
("Monday", "P4", "11K/Ma1"),
|
||||
("Tuesday", "P1", "9K/Ma1"),
|
||||
("Tuesday", "P3", "10K/Ma1"),
|
||||
("Wednesday", "P2", "11K/Ma1"),
|
||||
("Wednesday", "P4", "9K/Ma1"),
|
||||
("Thursday", "P1", "10K/Ma1"),
|
||||
("Thursday", "P4", "11K/Ma1"),
|
||||
("Friday", "P1", "9K/Ma1"),
|
||||
("Friday", "P3", "10K/Ma1"),
|
||||
],
|
||||
}
|
||||
|
||||
# ─── Student enrollments ───────────────────────────────────────────────────────
|
||||
|
||||
STUDENT_ENROLLMENTS = {
|
||||
"student1@kevlarai.test": ["10K/Ph1", "10K/Ma1", "10K/CS1"],
|
||||
"student2@kevlarai.test": ["11K/Ph1", "11K/Ma1"],
|
||||
}
|
||||
|
||||
|
||||
# ─── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _sb_headers() -> Dict:
|
||||
return {
|
||||
"apikey": SERVICE_KEY,
|
||||
"Authorization": f"Bearer {SERVICE_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
"Prefer": "return=representation",
|
||||
}
|
||||
|
||||
|
||||
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=_sb_headers(),
|
||||
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_teacher_timetable_id(profile_id: str) -> Optional[str]:
|
||||
"""Return the Supabase teacher_timetables.id for a given profile."""
|
||||
r = requests.get(
|
||||
f"{SUPA_URL}/rest/v1/teacher_timetables",
|
||||
headers=_sb_headers(),
|
||||
params={"profile_id": f"eq.{profile_id}", "select": "id", "limit": "1"},
|
||||
)
|
||||
data = r.json() if r.ok else []
|
||||
return data[0]["id"] if data else None
|
||||
|
||||
|
||||
def _patch_slot_class_ids(teacher_tt_sb_id: str, class_code_to_id: Dict[str, str]) -> int:
|
||||
"""Update class_id FK on teacher_timetable_slots rows via Supabase service role."""
|
||||
patched = 0
|
||||
for code, class_uuid in class_code_to_id.items():
|
||||
r = requests.patch(
|
||||
f"{SUPA_URL}/rest/v1/teacher_timetable_slots",
|
||||
headers=_sb_headers(),
|
||||
params={
|
||||
"teacher_timetable_id": f"eq.{teacher_tt_sb_id}",
|
||||
"subject_class": f"eq.{code}",
|
||||
},
|
||||
json={"class_id": class_uuid},
|
||||
)
|
||||
if r.ok:
|
||||
patched += 1
|
||||
return patched
|
||||
|
||||
|
||||
# ─── Main seed ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def seed() -> Dict[str, Any]:
|
||||
print("=" * 60)
|
||||
print("KevlarAI — full timetable + class + student seed")
|
||||
print("=" * 60)
|
||||
results: Dict[str, Any] = {}
|
||||
errors: List[str] = []
|
||||
|
||||
# ── [1] Sign in as KevlarAI admin ───────────────────────────────────────
|
||||
print("\n[1] Signing in as admin@kevlarai.test...")
|
||||
try:
|
||||
admin_token = _sign_in(KEVLARAI_ADMIN_EMAIL, KEVLARAI_ADMIN_PWD)
|
||||
print(" ✓ signed in")
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
# ── [2] POST /timetable/setup ─────────────────────────────────────────────
|
||||
print("\n[2] Setting up school timetable (academic year + terms + periods)...")
|
||||
r = _api(admin_token, "post", "/timetable/setup", {
|
||||
"year_start": "2025-09-03",
|
||||
"year_end": "2026-07-17",
|
||||
"terms": TERMS,
|
||||
"periods": PERIODS,
|
||||
})
|
||||
if r.get("status") == "ok" or r.get("school_timetable_id") or r.get("timetable_id"):
|
||||
print(f" ✓ timetable: {r.get('school_timetable_id') or r.get('timetable_id')}")
|
||||
results["setup"] = "ok"
|
||||
else:
|
||||
err = f"timetable/setup: {r}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
results["setup"] = "error"
|
||||
|
||||
# ── [3] POST /timetable/materialize-periods ───────────────────────────────
|
||||
print("\n[3] Materializing academic_periods (days x periods_template)...")
|
||||
r = _api(admin_token, "post", "/timetable/materialize-periods", None)
|
||||
if r.get("status") == "ok":
|
||||
print(f" ✓ {r.get('created')} periods created across {r.get('academic_days')} academic days")
|
||||
results["materialize_periods"] = "ok"
|
||||
else:
|
||||
err = f"materialize-periods: {r}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
results["materialize_periods"] = "error"
|
||||
|
||||
# ── [5] Build profile-ID lookup for all teachers + students ───────────────
|
||||
print("\n[3] Resolving profile IDs for teachers and students...")
|
||||
all_emails = (
|
||||
list(TEACHER_SLOTS.keys())
|
||||
+ list(STUDENT_ENROLLMENTS.keys())
|
||||
)
|
||||
profile_ids: Dict[str, str] = {}
|
||||
for email in all_emails:
|
||||
pid = _get_profile_id(email)
|
||||
if pid:
|
||||
profile_ids[email] = pid
|
||||
print(f" ✓ {email} -> {pid[:8]}...")
|
||||
else:
|
||||
print(f" ✗ profile not found for {email}")
|
||||
errors.append(f"profile_not_found: {email}")
|
||||
|
||||
# ── [6] Create classes ────────────────────────────────────────────────────
|
||||
print(f"\n[4] Creating {len(CLASSES)} classes...")
|
||||
results["classes"] = {}
|
||||
class_code_to_id: Dict[str, str] = {}
|
||||
|
||||
for cls in CLASSES:
|
||||
r = _api(admin_token, "post", "/database/timetable/classes", {
|
||||
"name": cls["name"],
|
||||
"class_code": cls["class_code"],
|
||||
"subject": cls["subject"],
|
||||
"year_group": cls["year_group"],
|
||||
"key_stage": cls["key_stage"],
|
||||
"academic_year": "2025-2026",
|
||||
})
|
||||
class_id = r.get("id") or (r.get("class", {}) or {}).get("id")
|
||||
if class_id:
|
||||
class_code_to_id[cls["class_code"]] = class_id
|
||||
results["classes"][cls["class_code"]] = "ok"
|
||||
print(f" ✓ {cls['class_code']} -> {class_id[:8]}...")
|
||||
else:
|
||||
err = f"create class {cls['class_code']}: {r}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
results["classes"][cls["class_code"]] = "error"
|
||||
time.sleep(0.1)
|
||||
|
||||
# ── [7] Add teachers to their classes ────────────────────────────────────
|
||||
print("\n[5] Adding teachers to classes...")
|
||||
results["class_teachers"] = {}
|
||||
for cls in CLASSES:
|
||||
class_id = class_code_to_id.get(cls["class_code"])
|
||||
teacher_pid = profile_ids.get(cls["teacher"])
|
||||
if not class_id or not teacher_pid:
|
||||
results["class_teachers"][cls["class_code"]] = "skip"
|
||||
continue
|
||||
r = _api(admin_token, "post", f"/database/timetable/classes/{class_id}/teachers", {
|
||||
"teacher_id": teacher_pid,
|
||||
"is_primary": True,
|
||||
})
|
||||
if r.get("status") == "ok" or r.get("id"):
|
||||
print(f" ✓ {cls['teacher'].split('@')[0]} -> {cls['class_code']}")
|
||||
results["class_teachers"][cls["class_code"]] = "ok"
|
||||
else:
|
||||
err = f"add teacher {cls['class_code']}: {r}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
results["class_teachers"][cls["class_code"]] = "error"
|
||||
time.sleep(0.1)
|
||||
|
||||
# ── [8] Teacher timetable init + slots ────────────────────────────────────
|
||||
print("\n[6] Initialising TeacherTimetable and setting slots for each teacher...")
|
||||
results["init"] = {}
|
||||
results["slots"] = {}
|
||||
teacher_tt_sb_ids: Dict[str, str] = {} # email -> teacher_timetables.id
|
||||
|
||||
for teacher_email, slot_tuples in TEACHER_SLOTS.items():
|
||||
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)
|
||||
results["init"][teacher_email] = "error"
|
||||
results["slots"][teacher_email] = "error"
|
||||
continue
|
||||
|
||||
# 6a: init TeacherTimetable
|
||||
r = _api(teacher_token, "post", "/timetable/init", None)
|
||||
if r.get("status") == "ok":
|
||||
print(f" ✓ init {teacher_email}")
|
||||
results["init"][teacher_email] = "ok"
|
||||
else:
|
||||
print(f" ~ init {teacher_email}: {r.get('message', r)} (may already exist)")
|
||||
results["init"][teacher_email] = "warn"
|
||||
|
||||
# 6b: get timetable_id (Neo4j uuid_string for slot FK)
|
||||
status_r = _api(teacher_token, "get", "/timetable/status", None)
|
||||
timetable_id = status_r.get("timetable_id")
|
||||
if not timetable_id:
|
||||
err = f"no timetable_id for {teacher_email}: {status_r}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
results["slots"][teacher_email] = "error"
|
||||
continue
|
||||
|
||||
# 6c: save slots (subject_class text; class_id patched separately)
|
||||
slot_list = [
|
||||
{
|
||||
"day_of_week": day,
|
||||
"period_code": code,
|
||||
"subject_class": cls,
|
||||
"start_time": PERIOD_TIMES[code][0],
|
||||
"end_time": PERIOD_TIMES[code][1],
|
||||
}
|
||||
for day, code, cls in slot_tuples
|
||||
]
|
||||
r = _api(teacher_token, "post", "/timetable/slots", {
|
||||
"timetable_id": timetable_id,
|
||||
"slots": slot_list,
|
||||
})
|
||||
if r.get("status") == "ok" or r.get("created") is not None:
|
||||
count = r.get("created") or len(slot_list)
|
||||
print(f" ✓ {teacher_email}: {count} slots")
|
||||
results["slots"][teacher_email] = "ok"
|
||||
else:
|
||||
err = f"slots {teacher_email}: {r}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
results["slots"][teacher_email] = "error"
|
||||
|
||||
# record Supabase teacher_timetable FK for patching
|
||||
teacher_pid = profile_ids.get(teacher_email)
|
||||
if teacher_pid:
|
||||
tt_sb_id = _get_teacher_timetable_id(teacher_pid)
|
||||
if tt_sb_id:
|
||||
teacher_tt_sb_ids[teacher_email] = tt_sb_id
|
||||
|
||||
time.sleep(0.3)
|
||||
|
||||
# ── [9] Patch teacher_timetable_slots.class_id ────────────────────────────
|
||||
print("\n[7] Patching class_id onto teacher_timetable_slots...")
|
||||
results["slot_patch"] = {}
|
||||
for teacher_email, slot_tuples in TEACHER_SLOTS.items():
|
||||
tt_sb_id = teacher_tt_sb_ids.get(teacher_email)
|
||||
if not tt_sb_id:
|
||||
results["slot_patch"][teacher_email] = "skip"
|
||||
continue
|
||||
teacher_codes = {cls for _, _, cls in slot_tuples}
|
||||
relevant_map = {code: uid for code, uid in class_code_to_id.items() if code in teacher_codes}
|
||||
n = _patch_slot_class_ids(tt_sb_id, relevant_map)
|
||||
print(f" ✓ {teacher_email}: {n} slots patched")
|
||||
results["slot_patch"][teacher_email] = n
|
||||
|
||||
# ── [10] Enroll students in classes ───────────────────────────────────────
|
||||
print("\n[8] Enrolling students in classes...")
|
||||
results["enrollments"] = {}
|
||||
for student_email, class_codes in STUDENT_ENROLLMENTS.items():
|
||||
student_pid = profile_ids.get(student_email)
|
||||
results["enrollments"][student_email] = {}
|
||||
if not student_pid:
|
||||
results["enrollments"][student_email] = "no_profile"
|
||||
continue
|
||||
for code in class_codes:
|
||||
class_id = class_code_to_id.get(code)
|
||||
if not class_id:
|
||||
results["enrollments"][student_email][code] = "no_class"
|
||||
continue
|
||||
r = _api(admin_token, "post", f"/database/timetable/classes/{class_id}/students", {
|
||||
"student_id": student_pid,
|
||||
})
|
||||
if r.get("status") == "ok" or r.get("id"):
|
||||
print(f" ✓ {student_email.split('@')[0]} -> {code}")
|
||||
results["enrollments"][student_email][code] = "ok"
|
||||
else:
|
||||
err = f"enroll {student_email} -> {code}: {r}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
results["enrollments"][student_email][code] = "error"
|
||||
time.sleep(0.1)
|
||||
|
||||
# ── [11] Materialize taught lessons ────────────────────────────────────────
|
||||
print("\n[9] Materializing taught lessons for each teacher...")
|
||||
results["materialize"] = {}
|
||||
for teacher_email in TEACHER_SLOTS:
|
||||
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
|
||||
r = _api(teacher_token, "post", "/timetable/materialize", None)
|
||||
if r.get("status") == "ok":
|
||||
print(f" ✓ {teacher_email}: {r.get('lessons_upserted', '?')} lessons, "
|
||||
f"{r.get('whiteboard_rooms_created', '?')} rooms")
|
||||
results["materialize"][teacher_email] = "ok"
|
||||
else:
|
||||
err = f"materialize {teacher_email}: {r}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
results["materialize"][teacher_email] = "error"
|
||||
time.sleep(0.3)
|
||||
|
||||
# ── [12] Neo4j sync (B.10) ────────────────────────────────────────────────
|
||||
print("\n[10] Syncing Neo4j TaughtLesson nodes (B.10)...")
|
||||
r = _api(admin_token, "post", "/timetable/sync-lessons", None)
|
||||
if r.get("status") == "ok":
|
||||
print(f" ✓ Neo4j sync: {r.get('taught_lessons')} lessons, "
|
||||
f"{r.get('teacher_timetables')} timetables, {r.get('slots')} slots")
|
||||
results["neo4j_sync"] = "ok"
|
||||
else:
|
||||
err = f"sync-lessons: {r}"
|
||||
print(f" ✗ {err}")
|
||||
errors.append(err)
|
||||
results["neo4j_sync"] = "error"
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────
|
||||
print("\n" + "=" * 60)
|
||||
results["success"] = len(errors) == 0
|
||||
results["errors"] = errors
|
||||
if errors:
|
||||
print(f"COMPLETE with {len(errors)} error(s):")
|
||||
for e in errors:
|
||||
print(f" ✗ {e}")
|
||||
else:
|
||||
print("COMPLETE — all steps succeeded")
|
||||
print("=" * 60)
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
print(json.dumps(seed(), indent=2, default=str))
|
||||
385
run/initialization/seed_planned_lessons.py
Normal file
385
run/initialization/seed_planned_lessons.py
Normal file
@ -0,0 +1,385 @@
|
||||
"""
|
||||
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))
|
||||
Loading…
x
Reference in New Issue
Block a user