# retoor 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}")