feat: add leaderboard router, gamification backfill, and shared content creation helpers

Introduce a new `/leaderboard` endpoint ranking top 50 members by total stars, wire it into the app router and documentation. Implement `_backfill_gamification()` in `database.py` to compute XP/levels for existing accounts from prior posts, comments, votes, and follows. Extract `is_owner()`, `create_content_item()`, and `detail_context()` into `content.py` to centralize content creation, reward awarding, mention notifications, and attachment linking. Update comment creation/deletion in `comments.py` to use `award_rewards()` with `XP_COMMENT` and the new `is_owner()` helper. Add shared template partials documentation for vote bars, star buttons, post headers, and topic selectors to `AGENTS.md`.
This commit is contained in:
2026-05-30 18:16:39 +00:00
parent ab4861562b
commit a136287d36
57 changed files with 1310 additions and 674 deletions
+13 -41
View File
@@ -1,14 +1,13 @@
import logging
from typing import Annotated
from datetime import datetime, timezone
from fastapi import APIRouter, Request, HTTPException, Form
from fastapi import APIRouter, Request, Form
from devplacepy.models import GistForm, GistEditForm
from fastapi.responses import HTMLResponse, RedirectResponse
from devplacepy.database import get_table, get_users_by_uids, get_gist_languages
from devplacepy.content import load_detail, edit_content_item, delete_content_item, enrich_items
from devplacepy.content import load_detail, edit_content_item, delete_content_item, enrich_items, create_content_item, detail_context
from devplacepy.templating import templates
from devplacepy.utils import generate_uid, get_current_user, require_user, make_combined_slug, create_mention_notifications
from devplacepy.seo import base_seo_context, site_url, website_schema, software_source_code_schema
from devplacepy.utils import get_current_user, require_user, not_found, XP_GIST
from devplacepy.seo import base_seo_context, site_url, website_schema, software_source_code_schema, list_page_seo
logger = logging.getLogger(__name__)
router = APIRouter()
@@ -24,7 +23,7 @@ LANGUAGES = [
]
def get_gists_list(user_uid=None, language=None):
def get_gists_list(user_uid=None, language=None, viewer=None):
gists_table = get_table("gists")
filters = {}
if user_uid:
@@ -38,16 +37,15 @@ def get_gists_list(user_uid=None, language=None):
return []
users_map = get_users_by_uids([g["user_uid"] for g in all_gists])
return enrich_items(all_gists, "gist", users_map)
return enrich_items(all_gists, "gist", users_map, user=viewer)
@router.get("", response_class=HTMLResponse)
async def gists_page(request: Request, language: str = None, user_uid: str = None):
user = get_current_user(request)
gists_data = get_gists_list(user_uid, language)
gists_data = get_gists_list(user_uid, language, viewer=user)
total_count = len(gists_data)
base = site_url(request)
seo_ctx = base_seo_context(
seo_ctx = list_page_seo(
request,
title="Gists",
description=f"Browse {total_count} code snippets on DevPlace.",
@@ -55,7 +53,6 @@ async def gists_page(request: Request, language: str = None, user_uid: str = Non
{"name": "Home", "url": "/feed"},
{"name": "Gists", "url": "/gists"},
],
schemas=[website_schema(base)],
)
return templates.TemplateResponse(request, "gists.html", {
**seo_ctx,
@@ -74,7 +71,7 @@ async def gist_detail(request: Request, gist_slug: str):
user = get_current_user(request)
detail = load_detail("gists", "gist", gist_slug, user)
if not detail:
raise HTTPException(status_code=404, detail="Gist not found")
raise not_found("Gist not found")
gist = detail["item"]
base = site_url(request)
@@ -90,19 +87,9 @@ async def gist_detail(request: Request, gist_slug: str):
],
schemas=[website_schema(base), software_source_code_schema(gist, base)],
)
return templates.TemplateResponse(request, "gist_detail.html", {
**seo_ctx,
"request": request,
"user": user,
"gist": gist,
"author": detail["author"],
"is_owner": detail["is_owner"],
"star_count": detail["star_count"],
"time_ago": detail["time_ago"],
"comments": detail["comments"],
return templates.TemplateResponse(request, "gist_detail.html", detail_context(request, user, detail, "gist", seo_ctx, {
"languages": LANGUAGES,
"attachments": detail["attachments"],
})
}))
@router.post("/create")
@@ -117,27 +104,12 @@ async def create_gist(request: Request, data: Annotated[GistForm, Form()]):
if language not in valid_languages:
language = "plaintext"
gists = get_table("gists")
uid = generate_uid()
gist_slug = make_combined_slug(title, uid)
gists.insert({
"uid": uid,
"user_uid": user["uid"],
uid, gist_slug = create_content_item("gists", "gist", user, {
"title": title,
"slug": gist_slug,
"description": description or None,
"source_code": source_code,
"language": language,
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
})
from devplacepy.attachments import link_attachments
link_attachments(data.attachment_uids, "gist", uid)
create_mention_notifications(description or "", user["uid"], f"/gists/{gist_slug}")
logger.info(f"Gist {uid} created by {user['username']}")
}, title, XP_GIST, "First Gist", description or "", data.attachment_uids)
return RedirectResponse(url=f"/gists/{gist_slug}", status_code=302)