§22 S1: thread collection_id through backend + update tests (N=1 unchanged, 454 green)
- collections.py resolution helpers (default_collection_id, type, initial_state) - registry mirror writes project grouping fields + default-collection corpus fields - auth.project_of_rfc joins collections; project_member_role reads memberships - cache/api_*/funder writers+readers re-keyed to collection_id (cached_prs + denormalised project_id tags unchanged); api_deployment reads type/initial_state from the default collection - projects.py restamp detects bootstrap via collections; initial_state via collection - tests updated to the three-tier schema; test_migration_028 retired (superseded by 029) - add @S1 acceptance test (collection grain + N=1 serving) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,7 @@ from typing import Any
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from . import auth, db, projects as projects_mod
|
||||
from . import auth, collections as collections_mod, db, projects as projects_mod
|
||||
from .config import Config
|
||||
|
||||
|
||||
@@ -33,12 +33,21 @@ def make_router(config: Config) -> APIRouter:
|
||||
).fetchone()
|
||||
# §22.5: enumerate only public + (member-)gated; unlisted is never listed.
|
||||
visible = set(auth.visible_project_ids(viewer))
|
||||
# §22 three-tier: `type` is a per-corpus field on the (default) collection
|
||||
# now; surface the default collection's type for each project.
|
||||
rows = db.conn().execute(
|
||||
"SELECT id, name, type, visibility FROM projects "
|
||||
"SELECT id, name, visibility FROM projects "
|
||||
"WHERE visibility != 'unlisted' ORDER BY name"
|
||||
).fetchall()
|
||||
projects = [
|
||||
{"id": r["id"], "name": r["name"], "type": r["type"], "visibility": r["visibility"]}
|
||||
{
|
||||
"id": r["id"],
|
||||
"name": r["name"],
|
||||
"type": collections_mod.collection_type(
|
||||
collections_mod.default_collection_id(r["id"])
|
||||
),
|
||||
"visibility": r["visibility"],
|
||||
}
|
||||
for r in rows
|
||||
if r["id"] in visible
|
||||
]
|
||||
@@ -60,8 +69,7 @@ def make_router(config: Config) -> APIRouter:
|
||||
# an unknown id). unlisted is readable by direct id.
|
||||
auth.require_project_readable(viewer, project_id)
|
||||
row = db.conn().execute(
|
||||
"SELECT id, name, type, visibility, initial_state, config_json "
|
||||
"FROM projects WHERE id = ?",
|
||||
"SELECT id, name, visibility, config_json FROM projects WHERE id = ?",
|
||||
(project_id,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
@@ -71,13 +79,16 @@ def make_router(config: Config) -> APIRouter:
|
||||
except (ValueError, TypeError):
|
||||
cfg = {}
|
||||
dep = db.conn().execute("SELECT tagline FROM deployment WHERE id = 1").fetchone()
|
||||
# §22 three-tier: type + initial_state moved down to the (default)
|
||||
# collection in migration 029.
|
||||
cid = collections_mod.default_collection_id(row["id"])
|
||||
return {
|
||||
"id": row["id"],
|
||||
"name": row["name"],
|
||||
"tagline": (dep["tagline"] if dep else "") or "",
|
||||
"type": row["type"],
|
||||
"type": collections_mod.collection_type(cid),
|
||||
"visibility": row["visibility"],
|
||||
"initial_state": row["initial_state"],
|
||||
"initial_state": collections_mod.collection_initial_state(cid),
|
||||
"theme": cfg.get("theme") or {},
|
||||
}
|
||||
|
||||
@@ -86,21 +97,27 @@ def make_router(config: Config) -> APIRouter:
|
||||
# is permanent, so external "RFC-0001" links and bookmarks land correctly.
|
||||
# nginx routes /rfc/ and /proposals/ to the backend so these are reached
|
||||
# before the SPA's index.html fallback.
|
||||
# §22 three-tier (S1): the canonical entry route now carries the collection
|
||||
# segment /p/<project>/c/<collection>/…. The legacy roots redirect through
|
||||
# the default project's default collection.
|
||||
@router.get("/rfc/{slug}")
|
||||
async def redirect_old_rfc(slug: str) -> RedirectResponse:
|
||||
default_id = projects_mod.resolved_default_id(config)
|
||||
return RedirectResponse(url=f"/p/{default_id}/e/{slug}", status_code=308)
|
||||
cid = collections_mod.default_collection_id(default_id)
|
||||
return RedirectResponse(url=f"/p/{default_id}/c/{cid}/e/{slug}", status_code=308)
|
||||
|
||||
@router.get("/rfc/{slug}/pr/{pr_number}")
|
||||
async def redirect_old_rfc_pr(slug: str, pr_number: int) -> RedirectResponse:
|
||||
default_id = projects_mod.resolved_default_id(config)
|
||||
cid = collections_mod.default_collection_id(default_id)
|
||||
return RedirectResponse(
|
||||
url=f"/p/{default_id}/e/{slug}/pr/{pr_number}", status_code=308
|
||||
url=f"/p/{default_id}/c/{cid}/e/{slug}/pr/{pr_number}", status_code=308
|
||||
)
|
||||
|
||||
@router.get("/proposals/{pr_number}")
|
||||
async def redirect_old_proposal(pr_number: int) -> RedirectResponse:
|
||||
default_id = projects_mod.resolved_default_id(config)
|
||||
return RedirectResponse(url=f"/p/{default_id}/proposals/{pr_number}", status_code=308)
|
||||
cid = collections_mod.default_collection_id(default_id)
|
||||
return RedirectResponse(url=f"/p/{default_id}/c/{cid}/proposals/{pr_number}", status_code=308)
|
||||
|
||||
return router
|
||||
|
||||
Reference in New Issue
Block a user