feat: add reactions, bookmarks, polls modules with session remember config

Add three new feature modules (reactions, bookmarks, polls) with corresponding database tables, indexes, and router registrations. Introduce `SESSION_MAX_AGE_REMEMBER` config for extended session duration, add `get_unread_messages` template global, and include operational defaults for rate limiting and session settings in `site_settings`.
This commit is contained in:
2026-06-06 14:31:42 +00:00
parent b4e09f4b37
commit 921e382cbc
79 changed files with 2431 additions and 185 deletions
+87
View File
@@ -0,0 +1,87 @@
# retoor <retoor@molodetz.nl>
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
from devplacepy.templating import templates
from devplacepy.utils import generate_uid, require_user, time_ago
from devplacepy.seo import base_seo_context
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)
for obj in table.find(table.table.columns.uid.in_(uids)):
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 templates.TemplateResponse(request, "saved.html", {
**seo_ctx,
"request": request,
"user": user,
"items": items,
"next_cursor": next_cursor,
})
@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:
bookmarks.delete(id=existing["id"])
saved = False
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(),
})
saved = True
if request.headers.get("x-requested-with") == "fetch":
return JSONResponse({"saved": saved})
return RedirectResponse(url=request.headers.get("Referer", "/feed"), status_code=302)