Add admin issue deletion workflow
This commit is contained in:
@@ -3519,6 +3519,23 @@ def update_portal_item(
|
||||
return updated
|
||||
|
||||
|
||||
def delete_portal_item(item_id: int) -> bool:
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"UPDATE portal_items SET related_item_id = NULL WHERE related_item_id = ?",
|
||||
(item_id,),
|
||||
)
|
||||
conn.execute("DELETE FROM portal_comments WHERE item_id = ?", (item_id,))
|
||||
conn.execute("DELETE FROM portal_item_activity WHERE item_id = ?", (item_id,))
|
||||
deleted = conn.execute(
|
||||
"DELETE FROM portal_items WHERE id = ?",
|
||||
(item_id,),
|
||||
).rowcount
|
||||
if deleted:
|
||||
logger.info("portal item deleted id=%s", item_id)
|
||||
return bool(deleted)
|
||||
|
||||
|
||||
def add_portal_comment(
|
||||
item_id: int,
|
||||
*,
|
||||
|
||||
@@ -15,6 +15,7 @@ from ..db import (
|
||||
add_portal_comment,
|
||||
count_portal_items,
|
||||
create_portal_item,
|
||||
delete_portal_item,
|
||||
get_portal_item,
|
||||
get_portal_overview,
|
||||
list_portal_comments,
|
||||
@@ -487,6 +488,7 @@ def _serialize_item(item: Dict[str, Any], user: Dict[str, Any]) -> Dict[str, Any
|
||||
"can_edit": is_admin or is_owner,
|
||||
"can_comment": True,
|
||||
"can_moderate": is_admin,
|
||||
"can_delete": is_admin and str(item.get("kind") or "").lower() == "issue",
|
||||
"can_raise_issue": str(item.get("kind") or "") == "request",
|
||||
"can_confirm_resolution": (
|
||||
str(item.get("kind") or "").lower() == "issue"
|
||||
@@ -1176,6 +1178,29 @@ async def portal_get_item(
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/items/{item_id}")
|
||||
async def portal_delete_item(
|
||||
item_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
if not _is_admin(current_user):
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
item = get_portal_item(item_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Portal item not found")
|
||||
if str(item.get("kind") or "").lower() != "issue":
|
||||
raise HTTPException(status_code=400, detail="Only issues can be deleted here")
|
||||
if not delete_portal_item(item_id):
|
||||
raise HTTPException(status_code=404, detail="Issue not found")
|
||||
logger.info(
|
||||
"portal issue deleted id=%s title=%s actor=%s",
|
||||
item_id,
|
||||
item.get("title"),
|
||||
current_user.get("username"),
|
||||
)
|
||||
return {"status": "deleted", "item_id": item_id}
|
||||
|
||||
|
||||
@router.patch("/items/{item_id}")
|
||||
async def portal_update_item(
|
||||
item_id: int,
|
||||
|
||||
@@ -1682,6 +1682,76 @@ class PortalWorkflowTests(TempDatabaseMixin, unittest.TestCase):
|
||||
self.assertEqual(pending_count, 1)
|
||||
|
||||
|
||||
class PortalIssueDeletionTests(TempDatabaseMixin, unittest.IsolatedAsyncioTestCase):
|
||||
def _create_issue(self) -> dict:
|
||||
issue = db.create_portal_item(
|
||||
kind="issue",
|
||||
title="Delete this issue",
|
||||
description="No longer required",
|
||||
created_by_username="reporter",
|
||||
created_by_id=None,
|
||||
issue_type="playback",
|
||||
)
|
||||
db.add_portal_comment(
|
||||
int(issue["id"]),
|
||||
author_username="reporter",
|
||||
author_role="user",
|
||||
message="Issue detail",
|
||||
)
|
||||
db.add_portal_item_activity(
|
||||
int(issue["id"]),
|
||||
event_type="item_created",
|
||||
actor_username="reporter",
|
||||
actor_role="user",
|
||||
message="Issue created",
|
||||
)
|
||||
return issue
|
||||
|
||||
async def test_only_admin_can_delete_an_issue(self) -> None:
|
||||
issue = self._create_issue()
|
||||
|
||||
with self.assertRaises(HTTPException) as context:
|
||||
await portal_router.portal_delete_item(
|
||||
int(issue["id"]),
|
||||
current_user={"username": "reporter", "role": "user"},
|
||||
)
|
||||
|
||||
self.assertEqual(context.exception.status_code, 403)
|
||||
self.assertIsNotNone(db.get_portal_item(int(issue["id"])))
|
||||
|
||||
async def test_delete_issue_removes_its_comments_and_activity(self) -> None:
|
||||
issue = self._create_issue()
|
||||
issue_id = int(issue["id"])
|
||||
|
||||
result = await portal_router.portal_delete_item(
|
||||
issue_id,
|
||||
current_user={"username": "admin", "role": "admin"},
|
||||
)
|
||||
|
||||
self.assertEqual(result, {"status": "deleted", "item_id": issue_id})
|
||||
self.assertIsNone(db.get_portal_item(issue_id))
|
||||
self.assertEqual(db.list_portal_comments(issue_id), [])
|
||||
self.assertEqual(db.list_portal_item_activity(issue_id), [])
|
||||
|
||||
async def test_delete_endpoint_will_not_delete_a_request(self) -> None:
|
||||
request_item = db.create_portal_item(
|
||||
kind="request",
|
||||
title="Keep this request",
|
||||
description="The media workflow must remain intact",
|
||||
created_by_username="reporter",
|
||||
created_by_id=None,
|
||||
)
|
||||
|
||||
with self.assertRaises(HTTPException) as context:
|
||||
await portal_router.portal_delete_item(
|
||||
int(request_item["id"]),
|
||||
current_user={"username": "admin", "role": "admin"},
|
||||
)
|
||||
|
||||
self.assertEqual(context.exception.status_code, 400)
|
||||
self.assertIsNotNone(db.get_portal_item(int(request_item["id"])))
|
||||
|
||||
|
||||
class IssueResolutionWorkflowTests(TempDatabaseMixin, unittest.IsolatedAsyncioTestCase):
|
||||
def _create_issue(self, *, status: str = "in_progress") -> dict:
|
||||
return db.create_portal_item(
|
||||
|
||||
Reference in New Issue
Block a user