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:
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user