Files
Project-SDE-WP-Suite/server/seed_demo.py
n.siegfried 2bdb65e580 Add seed_demo.py loadable demo project + document both test scripts
server/seed_demo.py seeds a realistic DEMO project (complete SOP + a spread of
Work Packages: issued, gated, multi-discipline master with split instances,
overdue, over-threshold draft) via the API. --clean removes it. DEPLOYMENT.md
documents both smoketest.py and seed_demo.py, including the localStorage caveat
(seeded project shows in the UI picker; seeded SOP/WPs are SQL-only until Phase 2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:28:11 -07:00

176 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Seed a realistic DEMO project into the Work Package Suite database via the API.
Creates one project, a complete SOP, and a spread of Work Packages that exercise
the features and dashboard: an issued package, a gated (open-constraint) package,
a multi-discipline master with its split instances (A/B/C), an overdue package,
and an over-threshold draft. Use it to prove the SQL + Python layer end-to-end
and to have data to inspect.
USAGE
python3 server/seed_demo.py https://wp-suite.company.local --insecure
docker compose exec api python /app/server/seed_demo.py http://localhost:8000
python3 server/seed_demo.py https://wp-suite.company.local --clean # remove DEMO-* projects
IMPORTANT — what shows where:
* The DEMO **project** is API/SQL-backed, so it appears in the home-page
project picker immediately (proves the projects → SQL path in the UI).
* The DEMO **SOP and Work Packages** are written to SQL too, but the current
front end still reads SOPs/WPs from the browser (localStorage), so they will
NOT render in the WP Creator / Dashboard yet — that's the pending Phase 2
wiring. Verify them at the SQL/API layer instead:
python3 server/smoketest.py <url> # automated end-to-end check
docker compose exec db psql -U wpsuite -d wpsuite \
-c "select number,subject,status from work_packages order by number;"
"""
import argparse
import json
import ssl
import sys
import urllib.error
import urllib.request
BASE = ""
CTX = None
DEMO_NUMBER = "DEMO-001" # project number prefix used to find/clean demo data
def call(method, path, body=None):
url = BASE + path
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, method=method,
headers={"Content-Type": "application/json", "Accept": "application/json"})
try:
with urllib.request.urlopen(req, context=CTX, timeout=20) as r:
raw = r.read().decode(); status = r.status
except urllib.error.HTTPError as e:
raw = e.read().decode(); status = e.code
try:
parsed = json.loads(raw) if raw else None
except ValueError:
parsed = raw
return status, parsed
def constraints(open_names=()):
base = ["Safety & Permitting", "Quality Control / Inspection", "IFC Drawings & Specs",
"Schedule", "Materials (on site, bagged & tagged)", "Work Access & Laydown"]
return [{"name": n, "status": ("open" if n in open_names else "cleared"),
"comment": ("awaiting delivery" if n in open_names else "")} for n in base]
def main():
global BASE, CTX
ap = argparse.ArgumentParser(description="Seed a demo project into the Work Package Suite")
ap.add_argument("base_url", nargs="?", default="http://localhost:8000",
help="Site root, no /api (default: http://localhost:8000)")
ap.add_argument("--insecure", action="store_true", help="skip TLS verification")
ap.add_argument("--clean", action="store_true", help="delete existing DEMO-* projects and exit")
args = ap.parse_args()
BASE = args.base_url.rstrip("/")
if args.insecure:
CTX = ssl.create_default_context(); CTX.check_hostname = False; CTX.verify_mode = ssl.CERT_NONE
# health gate
try:
st, _ = call("GET", "/api/health")
except urllib.error.URLError as e:
print(f"ABORT: cannot reach {BASE}/api/health — {e}"); return 1
if st != 200:
print(f"ABORT: /api/health returned {st}"); return 1
# --clean: remove any prior demo projects (cascade removes their SOP + WPs)
st, projects = call("GET", "/api/projects")
demos = [p for p in (projects or []) if str(p.get("number", "")).startswith("DEMO-")]
if args.clean:
for p in demos:
call("DELETE", f"/api/projects/{p['id']}")
print(f"Removed {len(demos)} DEMO project(s).")
return 0
if demos:
print(f"Note: {len(demos)} DEMO project(s) already exist. Run with --clean first to avoid duplicates.\n")
# 1) Project
st, proj = call("POST", "/api/projects", {
"name": "DEMO — Micron INC (test data)", "number": DEMO_NUMBER,
"client": "Micron Technology, Inc.", "division": "Semiconductor",
"site": "Boise, ID — Fab", "created_by": "seed_demo"})
pid = proj["id"]
print(f"Project: {proj['name']} ({pid})")
# 2) SOP (complete)
st, sop = call("POST", "/api/sops", {
"project_id": pid, "name": "DEMO SOP", "number": DEMO_NUMBER, "complete": True,
"created_by": "seed_demo",
"data": {"governance": {"woFormat": "WP##-[Sector]-[TYPE]",
"disciplines": ["Mechanical", "Electrical", "Tech"],
"discMode": "choice", "instanceSuffix": "letter",
"woSize": "Standard — 35 days (≈4080 hrs)", "sizeHoursMax": "80"}}})
sid = sop["id"]
print(f"SOP: complete ({sid})")
# 3) Work packages
def wp(number, subject, typ, status, data, parent_id=None):
body = {"project_id": pid, "sop_id": sid, "number": number, "subject": subject,
"type": typ, "status": status, "created_by": "seed_demo", "data": data}
if parent_id:
body["parent_id"] = parent_id
st, w = call("POST", "/api/wps", body)
print(f" WP {number:<16} {status:<12} {subject}")
return w
# a) issued, all clear
wp("WP01-1P-CONDUIT", "1P horn/strobe conduit", "Conduit Install", "Issued",
{"disciplines": ["Electrical"], "hours": "40", "actualHrs": "",
"constraints": constraints(), "due": "2026-06-30"})
# b) gated — one open constraint, still Scheduled
wp("WP02-1P-WIRE", "1P wire pull", "Wire Pull", "Scheduled",
{"disciplines": ["Electrical"], "hours": "60", "actualHrs": "",
"constraints": constraints(open_names=["Materials (on site, bagged & tagged)"]), "due": "2026-07-04"})
# c) multi-discipline master + split instances (master excluded from metrics)
master_id = "wp_demo_master_chiller"
instances = [("WP03-CHILLER_Mech", "Mechanical", "A", "Mechanical Install", "In Progress"),
("WP03-CHILLER_Elec", "Electrical", "B", "Wire Pull", "Scheduled"),
("WP03-CHILLER_Tech", "Tech", "C", "Terminations", "Draft")]
child_ids = []
for num, disc, label, typ, status in instances:
cid = f"wp_demo_{label.lower()}"
child_ids.append(cid)
body = {"project_id": pid, "sop_id": sid, "parent_id": master_id, "id": cid,
"number": num, "subject": "Chiller skid — " + disc, "type": typ, "status": status,
"created_by": "seed_demo",
"data": {"disciplines": [disc], "instanceOf": master_id, "instanceLabel": label,
"parentNumber": "WP03-CHILLER", "hours": "50", "actualHrs": "",
"constraints": constraints(), "due": "2026-07-10"}}
call("POST", "/api/wps", body)
print(f" WP {num:<16} {status:<12} (instance {label})")
wp("WP03-CHILLER", "Chiller skid (multi-discipline master)", "Mechanical Install", "Scheduled",
{"disciplines": ["Mechanical", "Electrical", "Tech"], "split": True, "children": child_ids,
"hours": "150", "constraints": constraints(), "due": "2026-07-10"})
call("POST", "/api/wps", {"project_id": pid, "sop_id": sid, "id": master_id,
"number": "WP03-CHILLER", "subject": "Chiller skid (multi-discipline master)",
"type": "Mechanical Install", "status": "Scheduled", "created_by": "seed_demo",
"data": {"disciplines": ["Mechanical", "Electrical", "Tech"], "split": True,
"children": child_ids, "hours": "150", "constraints": constraints(),
"due": "2026-07-10"}})
# d) overdue, in progress
wp("WP04-2P-TERM", "2P terminations", "Terminations", "In Progress",
{"disciplines": ["Tech"], "hours": "30", "actualHrs": "20",
"constraints": constraints(), "due": "2026-06-10"}) # past today (2026-06-16) → overdue
# e) over-threshold draft (hours > 80)
wp("WP05-3P-PANEL", "3P panel install", "Panel Install", "Draft",
{"disciplines": ["Electrical"], "hours": "120", "actualHrs": "",
"constraints": constraints(open_names=["Schedule"]), "due": "2026-07-20"})
# metrics readback
st, m = call("GET", f"/api/wps/metrics?project_id={pid}")
print(f"\nMetrics (masters excluded): {m}")
print(f"\nDone. The DEMO project '{proj['name']}' now appears in the home-page picker.")
print("SOP/WPs are in SQL (see header note) — verify with smoketest.py or psql.")
print("Remove later with: python3 server/seed_demo.py <url> --clean")
return 0
if __name__ == "__main__":
sys.exit(main())