fix(ui): admin pending-review banner on /corporate-memory (#176)
The /corporate-memory page filters status IN ('approved','mandatory')
and showed no hint that pending items exist. With approval_mode set to
'review_queue' (the default in instance.yaml.example), every collection
run would silently funnel new items into the pending bucket where no
operator ever saw them.
For admins (is_km_admin), the page now renders a banner above the
stats bar:
N pending items awaiting review — review them at /corporate-memory/admin
Non-admins see no change (the route zeroes the count server-side
before passing to the template, so the hint is never leaked).
Tests: tests/test_corporate_memory_page.py.
This commit is contained in:
parent
c3df03beb3
commit
c53c1e1572
3 changed files with 97 additions and 1 deletions
|
|
@ -602,6 +602,11 @@ async def corporate_memory(
|
|||
categories = sorted(set(i.get("category", "") for i in all_items if i.get("category")))
|
||||
domains = sorted(set(i.get("domain", "") for i in all_items if i.get("domain")))
|
||||
|
||||
# #176: surface the pending review queue to admins. Without this the
|
||||
# main page silently filtered status='pending' items and operators had
|
||||
# no breadcrumb to /corporate-memory/admin.
|
||||
pending_count = sum(1 for i in all_items if i.get("status") == "pending")
|
||||
|
||||
# "My contributions" — items the caller authored. Personal items are
|
||||
# always visible to their author regardless of audience filtering;
|
||||
# this is the surface the user uses to mark/unmark `is_personal`.
|
||||
|
|
@ -612,6 +617,7 @@ async def corporate_memory(
|
|||
item["upvotes"] = votes["upvotes"]
|
||||
item["downvotes"] = votes["downvotes"]
|
||||
|
||||
is_admin_view = is_user_admin(user["id"], conn)
|
||||
ctx = _build_context(
|
||||
request, user=user,
|
||||
knowledge_items=items,
|
||||
|
|
@ -621,7 +627,7 @@ async def corporate_memory(
|
|||
domains=domains,
|
||||
stats={"total": len(all_items), "approved": len([i for i in all_items if i.get("status") == "approved"])},
|
||||
user_votes={},
|
||||
is_km_admin=is_user_admin(user["id"], conn),
|
||||
is_km_admin=is_admin_view,
|
||||
user_contributions=user_contributions,
|
||||
user_stats={"authored": len(user_contributions), "votes_given": 0},
|
||||
# Template expects knowledge as object with .items and .total_pages
|
||||
|
|
@ -630,6 +636,8 @@ async def corporate_memory(
|
|||
current_page=1,
|
||||
page=1,
|
||||
per_page=100,
|
||||
# #176: pending banner is admin-only.
|
||||
pending_review_count=pending_count if is_admin_view else 0,
|
||||
)
|
||||
return templates.TemplateResponse(request, "corporate_memory.html", ctx)
|
||||
|
||||
|
|
|
|||
|
|
@ -566,6 +566,23 @@
|
|||
{% include '_app_header.html' %}
|
||||
|
||||
<div class="container-memory">
|
||||
{% if pending_review_count and pending_review_count > 0 %}
|
||||
<!-- Admin-only review-queue banner (#176). Hidden when 0; non-admins
|
||||
never see this because the route zeroes pending_review_count for
|
||||
them server-side. -->
|
||||
<div class="pending-review-banner" style="display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4); background: #fef9c3; border: 1px solid #facc15; border-radius: var(--radius-md); color: #854d0e; font-size: var(--text-sm);">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="12"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>
|
||||
<span>
|
||||
<strong>{{ pending_review_count }} pending item{{ 's' if pending_review_count != 1 else '' }}</strong>
|
||||
awaiting review —
|
||||
<a href="/corporate-memory/admin" style="color: #854d0e; text-decoration: underline; font-weight: var(--font-medium);">review them at /corporate-memory/admin</a>
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Stats Bar -->
|
||||
<div class="stats-bar">
|
||||
<div class="stat-item">
|
||||
|
|
|
|||
71
tests/test_corporate_memory_page.py
Normal file
71
tests/test_corporate_memory_page.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
"""GET /corporate-memory page rendering — pending banner contract.
|
||||
|
||||
The page used to filter `status IN ('approved','mandatory')` with no hint
|
||||
that a `pending` review queue exists. Operators who configured
|
||||
`approval_mode='review_queue'` saw an empty page after every collection
|
||||
run and had no breadcrumb to /corporate-memory/admin. Closes one of
|
||||
five defects in #176.
|
||||
|
||||
Contract:
|
||||
- Admins see a banner when count(*) WHERE status='pending' > 0,
|
||||
with a link to /corporate-memory/admin.
|
||||
- Non-admins see no change to the page.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def _auth(token: str) -> dict:
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def _seed_pending_item(item_id: str = "pending_item_1"):
|
||||
from src.db import get_system_db
|
||||
from src.repositories.knowledge import KnowledgeRepository
|
||||
|
||||
conn = get_system_db()
|
||||
repo = KnowledgeRepository(conn)
|
||||
repo.create(
|
||||
id=item_id,
|
||||
title=f"Pending review item {item_id}",
|
||||
content="awaiting admin triage",
|
||||
category="workflow",
|
||||
status="pending",
|
||||
)
|
||||
conn.close()
|
||||
|
||||
|
||||
class TestPendingBannerForAdmins:
|
||||
def test_admin_sees_pending_banner_when_pending_items_exist(self, seeded_app):
|
||||
_seed_pending_item("p_admin_1")
|
||||
c = seeded_app["client"]
|
||||
token = seeded_app["admin_token"]
|
||||
resp = c.get("/corporate-memory", headers=_auth(token))
|
||||
assert resp.status_code == 200
|
||||
body = resp.text
|
||||
# Banner must mention the pending count and link to the admin queue.
|
||||
assert "pending" in body.lower()
|
||||
assert "/corporate-memory/admin" in body
|
||||
|
||||
def test_admin_no_banner_when_no_pending(self, seeded_app):
|
||||
# Default seed has zero pending items.
|
||||
c = seeded_app["client"]
|
||||
token = seeded_app["admin_token"]
|
||||
resp = c.get("/corporate-memory", headers=_auth(token))
|
||||
assert resp.status_code == 200
|
||||
body = resp.text
|
||||
# The literal banner copy mentions "awaiting review"; absent when no
|
||||
# pending items.
|
||||
assert "awaiting review" not in body.lower()
|
||||
|
||||
|
||||
class TestNonAdminNeverSeesPendingBanner:
|
||||
def test_analyst_does_not_see_banner_even_with_pending_items(self, seeded_app):
|
||||
_seed_pending_item("p_no_admin_1")
|
||||
c = seeded_app["client"]
|
||||
token = seeded_app["analyst_token"]
|
||||
resp = c.get("/corporate-memory", headers=_auth(token))
|
||||
assert resp.status_code == 200
|
||||
body = resp.text
|
||||
# Non-admin must not see the admin-only banner copy.
|
||||
assert "awaiting review" not in body.lower()
|
||||
Loading…
Reference in a new issue