# retoor import logging from datetime import datetime, timezone from fastapi import APIRouter, Request from fastapi.responses import RedirectResponse, JSONResponse, HTMLResponse from devplacepy.database import get_table, db, paginate, resolve_object_url, _now_iso from devplacepy.utils import generate_uid, require_user, time_ago, redirect_back from devplacepy.seo import base_seo_context from devplacepy.responses import respond from devplacepy.schemas import SavedOut from devplacepy.services.audit import record as audit logger = logging.getLogger(__name__) router = APIRouter() BOOKMARKABLE: set[str] = {"post", "gist", "project", "news"} TABLE_BY_TYPE: dict[str, str] = { "post": "posts", "gist": "gists", "project": "projects", "news": "news", } LABEL_BY_TYPE: dict[str, str] = { "post": "Post", "gist": "Gist", "project": "Project", "news": "Article", } @router.get("/saved", response_class=HTMLResponse) async def saved_page(request: Request, before: str = None): user = require_user(request) bookmarks = get_table("bookmarks") rows, next_cursor = paginate(bookmarks, before=before, user_uid=user["uid"]) uids_by_type: dict[str, list] = {} for row in rows: uids_by_type.setdefault(row["target_type"], []).append(row["target_uid"]) resolved: dict[tuple, dict] = {} for target_type, uids in uids_by_type.items(): table_name = TABLE_BY_TYPE.get(target_type) if not table_name or table_name not in db.tables: continue table = get_table(table_name) clauses = [table.table.columns.uid.in_(uids)] if table.has_column("deleted_at"): clauses.append(table.table.columns.deleted_at.is_(None)) for obj in table.find(*clauses): resolved[(target_type, obj["uid"])] = obj items = [] for row in rows: obj = resolved.get((row["target_type"], row["target_uid"])) if not obj: continue title = obj.get("title") or (obj.get("content", "") or "")[:80] or "Untitled" items.append( { "target_type": row["target_type"], "type_label": LABEL_BY_TYPE.get( row["target_type"], row["target_type"].title() ), "title": title, "url": resolve_object_url(row["target_type"], row["target_uid"]), "time_ago": time_ago(row["created_at"]), } ) seo_ctx = base_seo_context( request, title="Saved", description="Your saved content on DevPlace.", robots="noindex,nofollow", ) return respond( request, "saved.html", { **seo_ctx, "request": request, "user": user, "items": items, "next_cursor": next_cursor, }, model=SavedOut, ) @router.post("/{target_type}/{target_uid}") async def toggle_bookmark(request: Request, target_type: str, target_uid: str): user = require_user(request) if target_type not in BOOKMARKABLE: return JSONResponse({"error": "Invalid target"}, status_code=400) bookmarks = get_table("bookmarks") existing = bookmarks.find_one( user_uid=user["uid"], target_uid=target_uid, target_type=target_type ) if existing and not existing.get("deleted_at"): bookmarks.update( {"id": existing["id"], "deleted_at": _now_iso(), "deleted_by": user["uid"]}, ["id"], ) saved = False elif existing: bookmarks.update( {"id": existing["id"], "deleted_at": None, "deleted_by": None}, ["id"] ) saved = True else: bookmarks.insert( { "uid": generate_uid(), "user_uid": user["uid"], "target_uid": target_uid, "target_type": target_type, "created_at": datetime.now(timezone.utc).isoformat(), "deleted_at": None, "deleted_by": None, } ) saved = True audit.record( request, "bookmark.add" if saved else "bookmark.remove", user=user, target_type=target_type, target_uid=target_uid, summary=f"{user['username']} {'bookmarked' if saved else 'removed bookmark from'} {target_type} {target_uid}", links=[audit.target(target_type, target_uid)], ) if request.headers.get("x-requested-with") == "fetch": return JSONResponse({"saved": saved}) return RedirectResponse(url=redirect_back(request), status_code=302)