feat(portal): implement dynamic drawer/page navigation management

Replace hardcoded sidebar drawer configuration with admin-manageable
dynamic system. Extend page_status.json with drawer definitions and
page assignments, add drawer CRUD API endpoints, render portal sidebar
via Jinja2 loops, and extend /admin/pages UI with drawer management.
Fix multi-worker cache invalidation via mtime-based staleness detection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
egg
2026-02-09 11:34:04 +08:00
parent 706c8ba52c
commit 9b1d2edc52
20 changed files with 2269 additions and 735 deletions

View File

@@ -280,8 +280,8 @@ class TestPermissionMiddleware:
assert response.status_code == 200
class TestAdminAPI:
"""Tests for admin API endpoints."""
class TestAdminAPI:
"""Tests for admin API endpoints."""
def test_get_pages_without_login(self, client):
"""Test get pages API requires login."""
@@ -289,22 +289,64 @@ class TestAdminAPI:
# Should redirect
assert response.status_code == 302
def test_get_pages_with_login(self, client):
"""Test get pages API with login."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
def test_get_pages_with_login(self, client):
"""Test get pages API with login."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
response = client.get("/admin/api/pages")
assert response.status_code == 200
data = json.loads(response.data)
assert data["success"] is True
assert "pages" in data
def test_update_page_status(self, client, temp_page_status):
"""Test updating page status via API."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
data = json.loads(response.data)
assert data["success"] is True
assert "pages" in data
def test_get_drawers_without_login(self, client):
"""Test drawer API requires login."""
response = client.get("/admin/api/drawers", follow_redirects=False)
assert response.status_code == 302
def test_mutate_drawers_without_login(self, client):
"""Test drawer mutations require login."""
response = client.post(
"/admin/api/drawers",
data=json.dumps({"name": "Unauthorized Drawer"}),
content_type="application/json",
follow_redirects=False,
)
assert response.status_code in (302, 401)
response = client.delete("/admin/api/drawers/reports", follow_redirects=False)
assert response.status_code == 302
def test_get_drawers_with_login(self, client):
"""Test list drawers API with login."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
response = client.get("/admin/api/drawers")
assert response.status_code == 200
data = json.loads(response.data)
assert data["success"] is True
assert "drawers" in data
assert any(drawer["id"] == "reports" for drawer in data["drawers"])
def test_create_drawer_duplicate_name_conflict(self, client):
"""Test creating duplicate drawer name returns 409."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
response = client.post(
"/admin/api/drawers",
data=json.dumps({"name": "報表類", "order": 99}),
content_type="application/json",
)
assert response.status_code == 409
def test_update_page_status(self, client, temp_page_status):
"""Test updating page status via API."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
response = client.put(
"/admin/api/pages/wip-overview",
@@ -329,10 +371,48 @@ class TestAdminAPI:
"/admin/api/pages/wip-overview",
data=json.dumps({"status": "invalid"}),
content_type="application/json"
)
assert response.status_code == 400
)
assert response.status_code == 400
def test_update_page_drawer_assignment(self, client):
"""Test assigning page drawer via page update API."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
response = client.put(
"/admin/api/pages/wip-overview",
data=json.dumps({"drawer_id": "queries", "order": 3}),
content_type="application/json",
)
assert response.status_code == 200
page_registry._cache = None
pages = page_registry.get_all_pages()
page = next(item for item in pages if item["route"] == "/wip-overview")
assert page["drawer_id"] == "queries"
assert page["order"] == 3
def test_update_page_invalid_drawer_assignment(self, client):
"""Test assigning a non-existent drawer returns bad request."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
response = client.put(
"/admin/api/pages/wip-overview",
data=json.dumps({"drawer_id": "missing-drawer"}),
content_type="application/json",
)
assert response.status_code == 400
def test_delete_drawer_with_assigned_pages_conflict(self, client):
"""Test deleting a non-empty drawer returns conflict."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
response = client.delete("/admin/api/drawers/reports")
assert response.status_code == 409
class TestContextProcessor:
"""Tests for template context processor."""