99 lines
4.6 KiB
Python
99 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Create the Site map collections (site_models, site_progress_events, site_survey_imports).
|
|
|
|
Idempotent: existing collections are verified, not replaced. All API rules stay null — the
|
|
dashboard reaches them only through pbAdmin() on the server.
|
|
|
|
python3 scripts/migrations/2026-10-14-site-map.py
|
|
"""
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
from pb_env import http, superuser_token # noqa: E402
|
|
|
|
PRIVATE = {"listRule": None, "viewRule": None, "createRule": None, "updateRule": None, "deleteRule": None}
|
|
CREATED = {"name": "created", "type": "autodate", "onCreate": True, "onUpdate": False}
|
|
|
|
|
|
def definitions(sites_id, models_id):
|
|
site = {"name": "site", "type": "relation", "required": True, "collectionId": sites_id, "maxSelect": 1, "cascadeDelete": False}
|
|
defs = [{
|
|
"name": "site_models", "type": "base", **PRIVATE,
|
|
"fields": [
|
|
site,
|
|
{"name": "label", "type": "text", "required": True, "max": 200},
|
|
{"name": "status", "type": "select", "required": True, "maxSelect": 1, "values": ["active", "archived"]},
|
|
{"name": "files", "type": "file", "required": True, "maxSelect": 10, "maxSize": 20971520,
|
|
"mimeTypes": ["application/json"], "protected": True},
|
|
{"name": "sources", "type": "json", "required": False, "maxSize": 200000},
|
|
{"name": "stats", "type": "json", "required": False, "maxSize": 200000},
|
|
{"name": "built_at", "type": "date", "required": False},
|
|
CREATED,
|
|
],
|
|
"indexes": ["CREATE INDEX idx_site_models_site_status ON site_models (site, status)"],
|
|
}]
|
|
if models_id is None:
|
|
return defs
|
|
model = {"name": "model", "type": "relation", "required": True, "collectionId": models_id, "maxSelect": 1, "cascadeDelete": False}
|
|
defs += [{
|
|
"name": "site_progress_events", "type": "base", **PRIVATE,
|
|
"fields": [
|
|
site, model,
|
|
{"name": "key", "type": "text", "required": True, "max": 200},
|
|
{"name": "value", "type": "number", "required": False},
|
|
{"name": "user", "type": "text", "required": False, "max": 50},
|
|
{"name": "user_name", "type": "text", "required": False, "max": 200},
|
|
{"name": "source", "type": "select", "required": True, "maxSelect": 1, "values": ["manual", "survey_import", "admin_clear"]},
|
|
CREATED,
|
|
],
|
|
"indexes": ["CREATE INDEX idx_site_progress_events_key ON site_progress_events (site, key, created)"],
|
|
}, {
|
|
"name": "site_survey_imports", "type": "base", **PRIVATE,
|
|
"fields": [
|
|
site, model,
|
|
{"name": "file", "type": "file", "required": True, "maxSelect": 1, "maxSize": 1048576,
|
|
"mimeTypes": ["text/csv", "text/plain", "text/tab-separated-values", "application/vnd.ms-excel"], "protected": True},
|
|
{"name": "matched", "type": "number", "required": False},
|
|
{"name": "unmatched", "type": "json", "required": False, "maxSize": 200000},
|
|
{"name": "user", "type": "text", "required": False, "max": 50},
|
|
{"name": "user_name", "type": "text", "required": False, "max": 200},
|
|
CREATED,
|
|
],
|
|
"indexes": [],
|
|
}]
|
|
return defs
|
|
|
|
|
|
def ensure(token, definition):
|
|
code, actual = http("GET", f"/api/collections/{definition['name']}", token=token)
|
|
if code == 404:
|
|
code, actual = http("POST", "/api/collections", definition, token)
|
|
if code != 200:
|
|
raise RuntimeError(f"Creating {definition['name']} failed: HTTP {code} {actual}")
|
|
print(f"Created {definition['name']}")
|
|
elif code != 200:
|
|
raise RuntimeError(f"Reading {definition['name']} failed: HTTP {code}")
|
|
else:
|
|
names = {f["name"] for f in actual["fields"]}
|
|
missing = [f["name"] for f in definition["fields"] if f["name"] not in names]
|
|
if missing:
|
|
raise RuntimeError(f"{definition['name']} exists but lacks {missing}; fix by hand, not by replacing")
|
|
print(f"Verified {definition['name']}")
|
|
for rule in PRIVATE:
|
|
assert actual.get(rule) is None, f"{definition['name']} must stay private"
|
|
return actual["id"]
|
|
|
|
|
|
def main():
|
|
token = superuser_token()
|
|
code, sites = http("GET", "/api/collections/sites", token=token)
|
|
if code != 200:
|
|
raise RuntimeError("Could not read the sites collection")
|
|
models_id = ensure(token, definitions(sites["id"], None)[0])
|
|
for d in definitions(sites["id"], models_id)[1:]:
|
|
ensure(token, d)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|