# retoor <retoor@molodetz.nl>
import logging
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from devplacepy.database import (
get_table,
list_deleted,
count_deleted,
restore_event,
purge_event,
get_users_by_uids,
resolve_object_url,
)
from devplacepy.attachments import _unlink_attachment_files
from devplacepy.project_files import _unlink_blob
from devplacepy.utils import require_admin, not_found
from devplacepy.seo import base_seo_context, site_url, website_schema
from devplacepy.responses import respond, action_result
from devplacepy.schemas import AdminTrashOut
from devplacepy.services.audit import record as audit
logger = logging.getLogger(__name__)
router = APIRouter()
TRASH_TABLES = [
{"key": "posts", "label": "Posts", "icon": "\U0001f4dd", "type": "post"},
{"key": "comments", "label": "Comments", "icon": "\U0001f4ac", "type": "comment"},
{"key": "gists", "label": "Gists", "icon": "\U0001f4cb", "type": "gist"},
{"key": "projects", "label": "Projects", "icon": "\U0001f680", "type": "project"},
{"key": "news", "label": "News", "icon": "\U0001f4f0", "type": "news"},
{"key": "awards", "label": "Awards", "icon": "\U0001f3c6", "type": "award"},
{"key": "project_files", "label": "Project files", "icon": "\U0001f4c1", "type": None},
{"key": "attachments", "label": "Attachments", "icon": "\U0001f4ce", "type": None},
]
_TRASH_KEYS = {entry["key"] for entry in TRASH_TABLES}
_TRASH_TYPE = {entry["key"]: entry["type"] for entry in TRASH_TABLES}
def _trash_label(table: str, row: dict) -> str:
for field in ("title", "name", "path", "original_filename", "question", "content"):
value = row.get(field)
if value:
return str(value)[:80]
return row.get("slug") or row.get("uid", "")
def _trash_target_url(table: str, row: dict) -> str | None:
target_type = _TRASH_TYPE.get(table)
if target_type:
return resolve_object_url(target_type, row["uid"])
return None
@router.get("/trash", response_class=HTMLResponse)
async def admin_trash(request: Request, table: str = "posts", page: int = 1):
admin = require_admin(request)
if table not in _TRASH_KEYS:
table = "posts"
rows, pagination = list_deleted(table, page)
actor_ids = [r.get("deleted_by") for r in rows if r.get("deleted_by")]
actors = get_users_by_uids([a for a in actor_ids if a]) if actor_ids else {}
items = []
for row in rows:
actor = actors.get(row.get("deleted_by"))
items.append(
{
"uid": row["uid"],
"table": table,
"label": _trash_label(table, row),
"deleted_at": row.get("deleted_at"),
"deleted_by": actor["username"] if actor else row.get("deleted_by"),
"target_url": _trash_target_url(table, row),
}
)
tabs = [
{**entry, "count": count_deleted(entry["key"]), "active": entry["key"] == table}
for entry in TRASH_TABLES
]
base = site_url(request)
seo_ctx = base_seo_context(
request,
title="Trash - Admin",
description="Restore or permanently remove soft-deleted content.",
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Admin", "url": "/admin"},
{"name": "Trash", "url": "/admin/trash"},
],
schemas=[website_schema(base)],
)
return respond(
request,
"admin_trash.html",
{
**seo_ctx,
"request": request,
"user": admin,
"items": items,
"table": table,
"tables": tabs,
"pagination": pagination,
"admin_section": "trash",
},
model=AdminTrashOut,
)
@router.post("/trash/{table}/{uid}/restore")
async def admin_trash_restore(request: Request, table: str, uid: str):
admin = require_admin(request)
if table not in _TRASH_KEYS:
raise not_found("Unknown trash table")
row = get_table(table).find_one(uid=uid)
if row and row.get("deleted_at"):
restored = restore_event(row["deleted_at"])
if table == "awards":
from devplacepy.database.awards import recompute_user_award_stats
recompute_user_award_stats(row.get("receiver_uid", ""))
logger.info(
f"Admin {admin['username']} restored {table} {uid} ({restored} rows)"
)
audit.record(
request,
"admin.trash.restore",
user=admin,
target_type=table,
target_uid=uid,
target_label=_trash_label(table, row),
metadata={"table": table, "rows": restored},
summary=f"{admin['username']} restored {table} {uid}",
)
return action_result(request, f"/admin/trash?table={table}")
@router.post("/trash/{table}/{uid}/purge")
async def admin_trash_purge(request: Request, table: str, uid: str):
admin = require_admin(request)
if table not in _TRASH_KEYS:
raise not_found("Unknown trash table")
row = get_table(table).find_one(uid=uid)
if row and row.get("deleted_at"):
purged = purge_event(row["deleted_at"])
for purged_table, purged_rows in purged:
if purged_table == "attachments":
for attachment in purged_rows:
_unlink_attachment_files(attachment)
elif purged_table == "project_files":
for node in purged_rows:
if node.get("is_binary"):
_unlink_blob(node)
logger.info(f"Admin {admin['username']} purged {table} {uid}")
audit.record(
request,
"admin.trash.purge",
user=admin,
target_type=table,
target_uid=uid,
target_label=_trash_label(table, row),
metadata={"table": table, "tables": [t for t, _ in purged]},
summary=f"{admin['username']} permanently purged {table} {uid}",
)
return action_result(request, f"/admin/trash?table={table}")