feat(review): mark-reviewed action + §7 catalog unreviewed filter (§22.4c)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+44
-3
@@ -611,13 +611,16 @@ def make_router(
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
@router.get("/api/rfcs")
|
||||
async def list_rfcs(request: Request) -> dict[str, Any]:
|
||||
async def list_rfcs(request: Request, unreviewed: str | None = None) -> dict[str, Any]:
|
||||
"""§7's left pane data.
|
||||
|
||||
The chip-filter / sort / search combinatorics live on the
|
||||
client — the server returns the full set and lets the chips
|
||||
narrow it. The set is small (hundreds, not thousands) for the
|
||||
foreseeable future, so paginating here would buy nothing.
|
||||
|
||||
§22.4c: pass `?unreviewed=true` to narrow to active entries
|
||||
still awaiting owner review.
|
||||
"""
|
||||
viewer = auth.current_user(request)
|
||||
viewer_id = viewer.user_id if viewer else None
|
||||
@@ -627,6 +630,10 @@ def make_router(
|
||||
if not visible:
|
||||
return {"items": []}
|
||||
placeholders = ",".join("?" for _ in visible)
|
||||
params = list(visible)
|
||||
unreviewed_clause = ""
|
||||
if unreviewed is not None and unreviewed.lower() in ("1", "true", "yes"):
|
||||
unreviewed_clause = " AND unreviewed = 1 AND state = 'active'"
|
||||
rows = db.conn().execute(
|
||||
f"""
|
||||
SELECT slug, title, state, rfc_id, repo,
|
||||
@@ -634,10 +641,10 @@ def make_router(
|
||||
last_main_commit_at, last_entry_commit_at, updated_at
|
||||
FROM cached_rfcs
|
||||
WHERE state IN ('super-draft', 'active')
|
||||
AND project_id IN ({placeholders})
|
||||
AND project_id IN ({placeholders}){unreviewed_clause}
|
||||
ORDER BY COALESCE(last_main_commit_at, last_entry_commit_at) DESC
|
||||
""",
|
||||
visible,
|
||||
params,
|
||||
).fetchall()
|
||||
|
||||
starred = set()
|
||||
@@ -700,6 +707,40 @@ def make_router(
|
||||
payload["proposed_use_case"] = uc["use_case"] if uc else None
|
||||
return payload
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# §22.4c: mark-reviewed — clear an active entry's `unreviewed` flag
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
@router.post("/api/projects/{project_id}/rfcs/{slug}/mark-reviewed")
|
||||
async def mark_reviewed(project_id: str, slug: str, request: Request) -> dict[str, Any]:
|
||||
"""§22.4c — clear an active entry's `unreviewed` flag. Authority is the
|
||||
§22.7 project superuser (project_admin or deployment owner/admin)."""
|
||||
viewer = auth.require_user(request)
|
||||
auth.require_project_readable(viewer, project_id)
|
||||
if not auth.is_project_superuser(viewer, project_id):
|
||||
raise HTTPException(403, "Only a project owner/admin can mark an entry reviewed")
|
||||
row = db.conn().execute(
|
||||
"SELECT state, unreviewed FROM cached_rfcs WHERE slug = ? AND project_id = ?",
|
||||
(slug, project_id),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(404, "Not found")
|
||||
if row["state"] != "active" or not row["unreviewed"]:
|
||||
raise HTTPException(409, "Entry is not an unreviewed active entry")
|
||||
try:
|
||||
await bot.mark_entry_reviewed(
|
||||
viewer.as_actor(),
|
||||
org=config.gitea_org,
|
||||
meta_repo=projects_mod.default_content_repo(config),
|
||||
slug=slug,
|
||||
reviewed_by=viewer.gitea_login,
|
||||
reviewed_at=entry_mod.today(),
|
||||
)
|
||||
except GiteaError as e:
|
||||
raise HTTPException(502, f"Gitea: {e.detail}")
|
||||
await cache.refresh_meta_repo(config, gitea)
|
||||
return {"ok": True}
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# §7.3 / §9.3: pending ideas
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
@@ -1069,6 +1069,56 @@ class Bot:
|
||||
)
|
||||
return pr
|
||||
|
||||
# ----- §22.4c: mark-reviewed (direct main write) -----
|
||||
|
||||
async def mark_entry_reviewed(
|
||||
self,
|
||||
actor: Actor,
|
||||
*,
|
||||
org: str,
|
||||
meta_repo: str,
|
||||
slug: str,
|
||||
reviewed_by: str,
|
||||
reviewed_at: str,
|
||||
) -> None:
|
||||
"""Clear §22.4c unreviewed on an active entry by rewriting its
|
||||
frontmatter on main. Stamps the commit with the §6.5 On-behalf-of
|
||||
trailer and writes an actions-log row, mirroring the graduation
|
||||
stamp's bot-write shape."""
|
||||
from . import entry as entry_mod
|
||||
|
||||
path = f"rfcs/{slug}.md"
|
||||
result = await self._gitea.read_file(org, meta_repo, path, ref="main")
|
||||
if result is None:
|
||||
raise GiteaError(404, f"{path} not found")
|
||||
text, sha = result
|
||||
e = entry_mod.parse(text)
|
||||
e.unreviewed = False
|
||||
e.reviewed_at = reviewed_at
|
||||
e.reviewed_by = reviewed_by
|
||||
commit_message = _stamp_single(f"Mark {slug} reviewed", actor)
|
||||
result2 = await self._gitea.update_file(
|
||||
org, meta_repo, path,
|
||||
content=entry_mod.serialize(e),
|
||||
sha=sha,
|
||||
message=commit_message,
|
||||
branch="main",
|
||||
author_name=actor.display_name,
|
||||
author_email=actor.email or f"{actor.gitea_login}@users.noreply",
|
||||
)
|
||||
commit_sha = (
|
||||
result2.get("commit", {}).get("sha")
|
||||
or result2.get("content", {}).get("sha")
|
||||
or ""
|
||||
)
|
||||
_log(
|
||||
actor,
|
||||
"mark_reviewed",
|
||||
rfc_slug=slug,
|
||||
bot_commit_sha=commit_sha,
|
||||
details={"reviewed_by": reviewed_by, "reviewed_at": reviewed_at},
|
||||
)
|
||||
|
||||
# ----- Per-RFC repo: seeding (test/dev fixtures, future graduation) -----
|
||||
|
||||
async def ensure_rfc_repo_seed(
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""§22.4c — owner/admin mark-reviewed clears the flag; catalog unreviewed filter."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from test_propose_vertical import ( # noqa: F401
|
||||
app_with_fake_gitea, tmp_env, provision_user_row, sign_in_as,
|
||||
)
|
||||
|
||||
|
||||
def _seed_unreviewed_active(fake, slug="feat"):
|
||||
"""Put an active+unreviewed entry on the meta repo main + cache."""
|
||||
from app import cache, entry as entry_mod
|
||||
body = entry_mod.serialize(entry_mod.Entry(
|
||||
slug=slug, title="Feat", state="active", unreviewed=True,
|
||||
owners=["ben"], proposed_by="ben",
|
||||
))
|
||||
fake.files[("wiggleverse", "meta", "main", f"rfcs/{slug}.md")] = {"content": body, "sha": "s1"}
|
||||
cache._upsert_cached_rfc(entry_mod.parse(body), body_sha="s1")
|
||||
|
||||
|
||||
def test_catalog_unreviewed_filter(app_with_fake_gitea):
|
||||
app, fake = app_with_fake_gitea
|
||||
with TestClient(app) as client:
|
||||
_seed_unreviewed_active(fake, "feat")
|
||||
from app import cache, entry as entry_mod
|
||||
ok = entry_mod.serialize(entry_mod.Entry(slug="ok", title="OK", state="active", owners=["ben"]))
|
||||
fake.files[("wiggleverse", "meta", "main", "rfcs/ok.md")] = {"content": ok, "sha": "s2"}
|
||||
cache._upsert_cached_rfc(entry_mod.parse(ok), body_sha="s2")
|
||||
r = client.get("/api/rfcs", params={"unreviewed": "true"})
|
||||
slugs = {i["slug"] for i in r.json()["items"]}
|
||||
assert slugs == {"feat"}
|
||||
|
||||
|
||||
def test_mark_reviewed_clears_flag(app_with_fake_gitea):
|
||||
from app import db
|
||||
app, fake = app_with_fake_gitea
|
||||
with TestClient(app) as client:
|
||||
_seed_unreviewed_active(fake, "feat")
|
||||
provision_user_row(user_id=1, login="ben", role="owner")
|
||||
sign_in_as(client, user_id=1, gitea_login="ben", display_name="Ben", role="owner")
|
||||
r = client.post("/api/projects/default/rfcs/feat/mark-reviewed")
|
||||
assert r.status_code == 200
|
||||
row = db.conn().execute(
|
||||
"SELECT unreviewed, reviewed_by FROM cached_rfcs WHERE slug='feat'"
|
||||
).fetchone()
|
||||
assert row["unreviewed"] == 0
|
||||
assert row["reviewed_by"] == "ben"
|
||||
|
||||
|
||||
def test_mark_reviewed_forbidden_for_non_superuser(app_with_fake_gitea):
|
||||
app, fake = app_with_fake_gitea
|
||||
with TestClient(app) as client:
|
||||
_seed_unreviewed_active(fake, "feat")
|
||||
provision_user_row(user_id=2, login="carol", role="contributor")
|
||||
sign_in_as(client, user_id=2, gitea_login="carol", display_name="Carol", role="contributor")
|
||||
r = client.post("/api/projects/default/rfcs/feat/mark-reviewed")
|
||||
assert r.status_code == 403
|
||||
Reference in New Issue
Block a user