api/run/initialization/seed_test_environment.py
kcar 52532ce00f feat(phase-c): lesson plans library backend — CRUD, delivery linking, AI suggest
Adds lesson_plans_router.py with 10 endpoints under /lessons/plans:
GET/POST /plans, GET/PATCH/DELETE /plans/{id}, POST /plans/{id}/deliver,
GET /plans/{id}/deliveries, POST/DELETE /plans/{id}/collaborators,
POST /plans/{id}/suggest (Ollama-backed per-field AI suggestions).

objectives and activities stored as JSONB arrays with Bloom taxonomy support.
Registers router in run/routers.py. Adds seed_test_environment.py for
platform-admin triggered reset + seed of demo users and Neo4j.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 03:59:26 +01:00

409 lines
17 KiB
Python

"""
Seed Test Environment — idempotent full-environment setup for CC development.
Creates:
- kcar@kevlarai.com → platform super-admin (admin_profiles)
- KevlarAI school → already exists; adds 3 student users
- Greenfield Academy → new second school with full staff + students
Run inside ccapi container:
python3 main.py --mode seed-test
Or directly:
cd ~/api && python3 -c "
from run.initialization.seed_test_environment import seed_test_environment
import json; print(json.dumps(seed_test_environment(), indent=2))
"
"""
import os
import time
import requests
import uuid
from typing import Dict, Any, Optional, List
from modules.logger_tool import initialise_logger
logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), "default", True)
# ─── Existing KevlarAI school ────────────────────────────────────────────────
KEVLARAI_INSTITUTE_ID = "6585bf91-6ae8-4d72-ab54-cddf3ba4e648"
KEVLARAI_INSTITUTE_DB = "cc.institutes.6585bf916ae84d72ab54cddf3ba4e648"
# ─── Second test school ──────────────────────────────────────────────────────
GREENFIELD_INSTITUTE_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" # deterministic UUID
GREENFIELD_URN = "TEST-GFA-001"
GREENFIELD_NAME = "Greenfield Academy"
# ─── User definitions ────────────────────────────────────────────────────────
# Format: email, password, username, full_name, display_name, user_type, role, institute_id
TEST_USERS: List[Dict] = [
# ── KevlarAI students ────────────────────────────────────────────────────
{
"email": "student1@kevlarai.com",
"password": "Student1@KevlarAI!",
"username": "student1.kevlarai",
"full_name": "Alice Nguyen",
"display_name": "Alice",
"user_type": "student",
"role": "student",
"institute_id": KEVLARAI_INSTITUTE_ID,
"institute_db": KEVLARAI_INSTITUTE_DB,
"metadata": {"year_group": "Year 10"},
},
{
"email": "student2@kevlarai.com",
"password": "Student2@KevlarAI!",
"username": "student2.kevlarai",
"full_name": "Ben Okafor",
"display_name": "Ben",
"user_type": "student",
"role": "student",
"institute_id": KEVLARAI_INSTITUTE_ID,
"institute_db": KEVLARAI_INSTITUTE_DB,
"metadata": {"year_group": "Year 10"},
},
{
"email": "student3@kevlarai.com",
"password": "Student3@KevlarAI!",
"username": "student3.kevlarai",
"full_name": "Chloe Park",
"display_name": "Chloe",
"user_type": "student",
"role": "student",
"institute_id": KEVLARAI_INSTITUTE_ID,
"institute_db": KEVLARAI_INSTITUTE_DB,
"metadata": {"year_group": "Year 11"},
},
# ── Greenfield Academy admin ─────────────────────────────────────────────
{
"email": "head@greenfieldacademy.test",
"password": "Admin@Greenfield1!",
"username": "head.greenfield",
"full_name": "Dr James Whitmore",
"display_name": "Dr Whitmore",
"user_type": "teacher",
"role": "school_admin",
"institute_id": GREENFIELD_INSTITUTE_ID,
"institute_db": None, # populated after school provisioning
"metadata": {},
},
# ── Greenfield teachers ──────────────────────────────────────────────────
{
"email": "physics@greenfieldacademy.test",
"password": "Teacher1@Greenfield1!",
"username": "physics.greenfield",
"full_name": "Priya Sharma",
"display_name": "Priya",
"user_type": "teacher",
"role": "teacher",
"institute_id": GREENFIELD_INSTITUTE_ID,
"institute_db": None,
"metadata": {"subject": "Physics"},
},
{
"email": "english@greenfieldacademy.test",
"password": "Teacher2@Greenfield1!",
"username": "english.greenfield",
"full_name": "Tom Bradley",
"display_name": "Tom",
"user_type": "teacher",
"role": "teacher",
"institute_id": GREENFIELD_INSTITUTE_ID,
"institute_db": None,
"metadata": {"subject": "English"},
},
# ── Greenfield students ──────────────────────────────────────────────────
{
"email": "alice@greenfieldacademy.test",
"password": "Student1@Greenfield1!",
"username": "alice.greenfield",
"full_name": "Alice Thornton",
"display_name": "Alice T",
"user_type": "student",
"role": "student",
"institute_id": GREENFIELD_INSTITUTE_ID,
"institute_db": None,
"metadata": {"year_group": "Year 9"},
},
{
"email": "bob@greenfieldacademy.test",
"password": "Student2@Greenfield1!",
"username": "bob.greenfield",
"full_name": "Bob Ivanov",
"display_name": "Bob",
"user_type": "student",
"role": "student",
"institute_id": GREENFIELD_INSTITUTE_ID,
"institute_db": None,
"metadata": {"year_group": "Year 9"},
},
{
"email": "carol@greenfieldacademy.test",
"password": "Student3@Greenfield1!",
"username": "carol.greenfield",
"full_name": "Carol Mensah",
"display_name": "Carol",
"user_type": "student",
"role": "student",
"institute_id": GREENFIELD_INSTITUTE_ID,
"institute_db": None,
"metadata": {"year_group": "Year 10"},
},
]
def seed_test_environment() -> Dict[str, Any]:
from modules.database.supabase.utils.client import SupabaseServiceRoleClient
from modules.database.services.provisioning_service import ProvisioningService
sb_client = SupabaseServiceRoleClient()
supabase_url = os.environ["SUPABASE_URL"]
service_key = os.environ["SERVICE_ROLE_KEY"]
headers = {
"apikey": service_key,
"Authorization": f"Bearer {service_key}",
"Content-Type": "application/json",
}
def auth_get(path, params=None):
r = requests.get(f"{supabase_url}/auth/v1/admin{path}", headers=headers, params=params)
r.raise_for_status()
return r.json()
def auth_post(path, data):
r = requests.post(f"{supabase_url}/auth/v1/admin{path}", headers=headers, json=data)
return r
def sb_upsert(table, data, on_conflict):
h = {**headers, "Prefer": "resolution=merge-duplicates,return=representation"}
r = requests.post(
f"{supabase_url}/rest/v1/{table}",
headers=h,
json=data,
params={"on_conflict": on_conflict},
)
return r
def sb_select(table, eq_col, eq_val):
r = requests.get(
f"{supabase_url}/rest/v1/{table}",
headers=headers,
params={"select": "*", eq_col: f"eq.{eq_val}"},
)
r.raise_for_status()
return r.json()
errors: List[str] = []
results: Dict[str, Any] = {"steps": {}}
# ── Step 1: Ensure kcar is a platform super-admin ─────────────────────────
logger.info("Step 1: Platform super-admin setup...")
try:
kcar_id = "d9e1d1a9-04c4-4611-bb05-57babf4a9a28" # known from profiles
r = sb_upsert("admin_profiles", {
"id": kcar_id,
"email": "kcar@kevlarai.com",
"display_name": "Kevin Carroll",
"admin_role": "super_admin",
"is_super_admin": True,
"metadata": {"seeded": True},
}, on_conflict="id")
if r.status_code in (200, 201):
logger.info(" kcar → admin_profiles super_admin ✓")
results["steps"]["super_admin"] = "ok"
else:
raise Exception(f"Upsert failed: {r.text[:200]}")
except Exception as e:
msg = f"super_admin setup: {e}"
logger.error(f" {msg}")
errors.append(msg)
results["steps"]["super_admin"] = "error"
# ── Step 2: Provision Greenfield Academy ──────────────────────────────────
logger.info("Step 2: Greenfield Academy school provisioning...")
greenfield_db = None
try:
# Check if already exists
existing = sb_select("institutes", "id", GREENFIELD_INSTITUTE_ID)
if not existing:
# Determine neo4j_uuid_string (same sanitization as provisioning_service)
neo4j_uuid = GREENFIELD_INSTITUTE_ID.replace("-", "")
r = sb_upsert("institutes", {
"id": GREENFIELD_INSTITUTE_ID,
"name": GREENFIELD_NAME,
"urn": GREENFIELD_URN,
"status": "active",
"address": {"line1": "1 Academy Road", "city": "Testville", "postcode": "TE1 1ST"},
"website": "https://greenfieldacademy.test",
"metadata": {"headteacher": "Dr James Whitmore", "seeded": True},
"neo4j_uuid_string": neo4j_uuid,
}, on_conflict="id")
if r.status_code not in (200, 201):
raise Exception(f"Institute upsert: {r.text[:200]}")
logger.info(f" Greenfield Academy created [{GREENFIELD_INSTITUTE_ID[:8]}]")
# Provision Neo4j DB
provisioner = ProvisioningService()
prov_result = provisioner.ensure_school(GREENFIELD_INSTITUTE_ID)
greenfield_db = prov_result.get("db_name")
logger.info(f" Neo4j DB provisioned: {greenfield_db}")
else:
neo4j_uuid = existing[0].get("neo4j_uuid_string") or GREENFIELD_INSTITUTE_ID.replace("-", "")
greenfield_db = f"cc.institutes.{neo4j_uuid}"
logger.info(f" Greenfield Academy already exists → {greenfield_db}")
results["steps"]["greenfield_school"] = greenfield_db
except Exception as e:
msg = f"greenfield_school: {e}"
logger.error(f" {msg}")
errors.append(msg)
results["steps"]["greenfield_school"] = "error"
greenfield_db = f"cc.institutes.{GREENFIELD_INSTITUTE_ID.replace('-', '')}"
# Update institute_db for Greenfield users
for u in TEST_USERS:
if u["institute_id"] == GREENFIELD_INSTITUTE_ID:
u["institute_db"] = greenfield_db
# ── Step 3: Create / verify all test users ────────────────────────────────
logger.info("Step 3: Creating test users...")
created_users: Dict[str, Dict] = {}
try:
all_users = auth_get("/users", params={"per_page": 200}).get("users", [])
existing_by_email = {u["email"]: u for u in all_users}
except Exception as e:
msg = f"auth/users list: {e}"
logger.error(msg)
errors.append(msg)
existing_by_email = {}
for spec in TEST_USERS:
email = spec["email"]
if email in existing_by_email:
uid = existing_by_email[email]["id"]
logger.info(f" {email}: exists [{uid[:8]}]")
created_users[email] = {"id": uid, **spec}
continue
r = auth_post("/users", {
"email": email,
"password": spec["password"],
"email_confirm": True,
"user_metadata": {
"username": spec["username"],
"full_name": spec["full_name"],
"display_name": spec["display_name"],
"user_type": spec["user_type"],
},
})
if r.status_code in (200, 201):
uid = r.json()["id"]
logger.info(f" {email}: created [{uid[:8]}]")
created_users[email] = {"id": uid, **spec}
else:
msg = f"create {email}: {r.text[:200]}"
logger.error(f" {msg}")
errors.append(msg)
time.sleep(0.25)
results["steps"]["users_created"] = list(created_users.keys())
# ── Step 4: Upsert profiles + memberships ─────────────────────────────────
logger.info("Step 4: Upserting profiles and memberships...")
for spec in TEST_USERS:
u = created_users.get(spec["email"])
if not u:
continue
try:
sb_upsert("profiles", {
"id": u["id"],
"email": spec["email"],
"user_type": spec["user_type"],
"username": spec["username"],
"full_name": spec["full_name"],
"display_name": spec["display_name"],
"school_id": spec["institute_id"],
"neo4j_sync_status": "pending",
}, on_conflict="id")
sb_upsert("institute_memberships", {
"profile_id": u["id"],
"institute_id": spec["institute_id"],
"role": spec["role"],
"metadata": spec.get("metadata", {}),
}, on_conflict="profile_id,institute_id")
except Exception as e:
msg = f"profile/membership {spec['email']}: {e}"
logger.error(f" {msg}")
errors.append(msg)
results["steps"]["profiles_memberships"] = "ok"
# ── Step 5: Neo4j Teacher/Student nodes for all users ────────────────────
logger.info("Step 5: Creating Neo4j worker nodes...")
try:
from neo4j import GraphDatabase
driver = GraphDatabase.driver("bolt://192.168.0.209:7687", auth=("neo4j", "&%N304j&%"))
# Group users by institute DB
by_db: Dict[str, List[Dict]] = {}
for spec in TEST_USERS:
u = created_users.get(spec["email"])
if not u or not spec.get("institute_db"):
continue
by_db.setdefault(spec["institute_db"], []).append({**spec, "uid": u["id"]})
for db, users in by_db.items():
with driver.session(database=db) as s:
for u in users:
label = "Teacher" if u["user_type"] == "teacher" else "Student"
s.run(
f"MERGE (n:{label} {{uuid_string: $uid}}) "
"SET n.worker_email = $email, "
" n.worker_name = $name, "
" n.unique_id = $uid, "
" n.user_type = $user_type, "
" n.worker_type = $user_type",
uid=u["uid"], email=u["email"],
name=u["full_name"], user_type=u["user_type"],
)
logger.info(f" [{db[:30]}] {label}: {u['email']}")
driver.close()
results["steps"]["neo4j_nodes"] = "ok"
except Exception as e:
msg = f"neo4j_nodes: {e}"
logger.error(f" {msg}")
errors.append(msg)
results["steps"]["neo4j_nodes"] = "error"
# ── Summary ───────────────────────────────────────────────────────────────
results["success"] = len(errors) == 0
results["errors"] = errors
results["message"] = (
f"Seed complete — {len(created_users)} users across 2 schools"
if not errors
else f"{len(errors)} error(s): {errors[0]}"
)
# Print credential sheet
logger.info("\n" + "=" * 60)
logger.info("TEST CREDENTIAL SHEET")
logger.info("=" * 60)
logger.info(f"{'ROLE':<20} {'EMAIL':<40} {'PASSWORD'}")
logger.info("-" * 90)
logger.info(f"{'[PLATFORM ADMIN]':<20} {'kcar@kevlarai.com':<40} KevlarAI2025!")
logger.info("-" * 90)
logger.info(f"[KevlarAI School]")
for spec in TEST_USERS:
if spec["institute_id"] == KEVLARAI_INSTITUTE_ID:
logger.info(f" {spec['role']:<18} {spec['email']:<40} {spec['password']}")
logger.info("-" * 90)
logger.info(f"[Greenfield Academy]")
for spec in TEST_USERS:
if spec["institute_id"] == GREENFIELD_INSTITUTE_ID:
logger.info(f" {spec['role']:<18} {spec['email']:<40} {spec['password']}")
logger.info("=" * 60)
return results