chore: remove stale agent reports and add PYTHONDONTWRITEBYTECODE export to Makefile
Delete four stale SEO and style agent report JSON/MD files from agents/reports/ that had zero findings or were superseded. Export PYTHONDONTWRITEBYTECODE=1 in the Makefile so all make targets suppress bytecode generation, and add a `tree` target that lists the repository file tree via git ls-tree. Update AGENTS.md and CLAUDE.md documentation to reflect that routers now form a directory tree mirroring URL paths and that bytecode writing is disabled project-wide.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
# 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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
TRASH_TABLES = [
|
||||
{"key": "posts", "label": "Posts", "type": "post"},
|
||||
{"key": "comments", "label": "Comments", "type": "comment"},
|
||||
{"key": "gists", "label": "Gists", "type": "gist"},
|
||||
{"key": "projects", "label": "Projects", "type": "project"},
|
||||
{"key": "news", "label": "News", "type": "news"},
|
||||
{"key": "project_files", "label": "Project files", "type": None},
|
||||
{"key": "attachments", "label": "Attachments", "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"])
|
||||
logger.info(
|
||||
f"Admin {admin['username']} restored {table} {uid} ({restored} rows)"
|
||||
)
|
||||
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}")
|
||||
return action_result(request, f"/admin/trash?table={table}")
|
||||
Reference in New Issue
Block a user