MEGAINFRA-WEBAPP/scripts/migrations/2026-10-14-site-map.py
DEls f06d545e02 fix(site-map): allow tab-separated survey files
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:52:19 +02:00

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()