2026-05-10 09:08:12 +02:00
|
|
|
import logging
|
|
|
|
|
from fastapi import APIRouter, Request
|
2026-05-12 13:08:38 +02:00
|
|
|
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
|
|
|
|
from devplacepy.database import get_table, db
|
2026-05-10 09:08:12 +02:00
|
|
|
from devplacepy.templating import templates
|
|
|
|
|
from devplacepy.utils import get_current_user, require_user, time_ago
|
2026-05-11 05:30:51 +02:00
|
|
|
from devplacepy.seo import base_seo_context, site_url, website_schema, breadcrumb_schema, profile_page_schema, combine
|
2026-05-10 09:08:12 +02:00
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{username}", response_class=HTMLResponse)
|
|
|
|
|
async def profile_page(request: Request, username: str, tab: str = "posts"):
|
|
|
|
|
current_user = get_current_user(request)
|
|
|
|
|
users = get_table("users")
|
|
|
|
|
profile_user = users.find_one(username=username)
|
|
|
|
|
if not profile_user:
|
|
|
|
|
return RedirectResponse(url="/feed", status_code=302)
|
|
|
|
|
|
|
|
|
|
posts = []
|
|
|
|
|
if tab == "posts":
|
|
|
|
|
posts_table = get_table("posts")
|
2026-05-11 03:14:43 +02:00
|
|
|
raw_posts = list(posts_table.find(user_uid=profile_user["uid"], order_by=["-created_at"]))
|
|
|
|
|
if raw_posts:
|
|
|
|
|
from devplacepy.database import get_comment_counts_by_post_uids
|
|
|
|
|
counts = get_comment_counts_by_post_uids([p["uid"] for p in raw_posts])
|
|
|
|
|
else:
|
|
|
|
|
counts = {}
|
2026-05-10 09:08:12 +02:00
|
|
|
for p in raw_posts:
|
|
|
|
|
posts.append({
|
|
|
|
|
"post": p,
|
|
|
|
|
"time_ago": time_ago(p["created_at"]),
|
2026-05-11 03:14:43 +02:00
|
|
|
"comment_count": counts.get(p["uid"], 0),
|
2026-05-10 09:08:12 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
badges = list(get_table("badges").find(user_uid=profile_user["uid"]))
|
|
|
|
|
projects = list(get_table("projects").find(user_uid=profile_user["uid"]))
|
2026-05-13 21:17:57 +02:00
|
|
|
gists_raw = list(get_table("gists").find(user_uid=profile_user["uid"]))
|
|
|
|
|
gists = []
|
|
|
|
|
for g in gists_raw:
|
|
|
|
|
gists.append({"gist": g, "time_ago": time_ago(g["created_at"])})
|
2026-05-11 03:14:43 +02:00
|
|
|
posts_count = len(posts) or len(list(get_table("posts").find(user_uid=profile_user["uid"])))
|
2026-05-11 00:41:41 +02:00
|
|
|
|
|
|
|
|
activities = []
|
|
|
|
|
if tab == "activity":
|
|
|
|
|
posts_table = get_table("posts")
|
|
|
|
|
for p in posts_table.find(user_uid=profile_user["uid"], order_by=["-created_at"], _limit=10):
|
2026-05-11 03:14:43 +02:00
|
|
|
activities.append({"type": "post", "content": p.get("title") or p["content"][:80], "time_ago": time_ago(p["created_at"]), "created_at": p["created_at"], "uid": p["uid"]})
|
2026-05-11 00:41:41 +02:00
|
|
|
comments_table = get_table("comments")
|
|
|
|
|
for c in comments_table.find(user_uid=profile_user["uid"], order_by=["-created_at"], _limit=10):
|
2026-05-13 21:17:57 +02:00
|
|
|
target_uid = c.get("target_uid", c.get("post_uid", ""))
|
|
|
|
|
target_type = c.get("target_type", "post")
|
|
|
|
|
activities.append({"type": "comment", "content": c["content"][:80], "time_ago": time_ago(c["created_at"]), "created_at": c["created_at"], "uid": target_uid, "target_type": target_type})
|
2026-05-11 03:14:43 +02:00
|
|
|
activities.sort(key=lambda a: a["created_at"], reverse=True)
|
2026-05-11 00:41:41 +02:00
|
|
|
|
|
|
|
|
is_following = False
|
|
|
|
|
if current_user:
|
|
|
|
|
follows = get_table("follows")
|
|
|
|
|
is_following = bool(follows.find_one(follower_uid=current_user["uid"], following_uid=profile_user["uid"]))
|
2026-05-10 09:08:12 +02:00
|
|
|
|
2026-05-11 05:30:51 +02:00
|
|
|
base = site_url(request)
|
|
|
|
|
robots = "noindex,follow" if posts_count < 2 else "index,follow"
|
|
|
|
|
bio = profile_user.get("bio", "")
|
|
|
|
|
desc = bio or f"View {profile_user['username']}'s profile on DevPlace. {posts_count} posts."
|
|
|
|
|
seo_ctx = base_seo_context(
|
|
|
|
|
request,
|
|
|
|
|
title=f"{profile_user['username']} (@{profile_user['username']})",
|
|
|
|
|
description=desc,
|
|
|
|
|
robots=robots,
|
|
|
|
|
breadcrumbs=[
|
|
|
|
|
{"name": "Home", "url": "/feed"},
|
|
|
|
|
{"name": profile_user['username'], "url": f"/profile/{profile_user['username']}"},
|
|
|
|
|
],
|
|
|
|
|
schemas=[
|
|
|
|
|
website_schema(base),
|
|
|
|
|
profile_page_schema(profile_user, posts_count, base),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
|
feat: pass request as first positional arg to all TemplateResponse calls across routers
The diff shows a systematic refactor across 14 router files and main.py where every invocation of `templates.TemplateResponse` now receives the `request` object as its first positional argument, shifting the template name from first to second position. This change touches 30+ call sites in auth, feed, gists, messages, news, notifications, posts, profile, projects, services, bugs, admin, and the main error handlers, ensuring consistent Jinja2 template rendering with explicit request context injection for all HTTP responses.
2026-05-13 23:03:06 +02:00
|
|
|
return templates.TemplateResponse(request, "profile.html", {
|
2026-05-11 05:30:51 +02:00
|
|
|
**seo_ctx,
|
2026-05-10 09:08:12 +02:00
|
|
|
"request": request,
|
|
|
|
|
"user": current_user,
|
|
|
|
|
"profile_user": profile_user,
|
|
|
|
|
"posts": posts,
|
|
|
|
|
"badges": badges,
|
|
|
|
|
"projects": projects,
|
2026-05-12 15:07:34 +02:00
|
|
|
"gists": gists,
|
2026-05-10 09:08:12 +02:00
|
|
|
"current_tab": tab,
|
2026-05-11 00:41:41 +02:00
|
|
|
"posts_count": posts_count,
|
|
|
|
|
"is_following": is_following,
|
|
|
|
|
"activities": activities,
|
2026-05-10 09:08:12 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
2026-05-12 13:08:38 +02:00
|
|
|
@router.get("/search")
|
|
|
|
|
async def search_users(request: Request, q: str = ""):
|
|
|
|
|
require_user(request)
|
|
|
|
|
if not q or len(q) < 1:
|
|
|
|
|
return JSONResponse({"results": []})
|
|
|
|
|
if "users" in db.tables:
|
|
|
|
|
rows = db.query(
|
|
|
|
|
"SELECT uid, username FROM users WHERE username LIKE :q LIMIT 10",
|
|
|
|
|
q=f"%{q}%",
|
|
|
|
|
)
|
|
|
|
|
results = [{"uid": r["uid"], "username": r["username"]} for r in rows]
|
|
|
|
|
else:
|
|
|
|
|
results = []
|
|
|
|
|
return JSONResponse({"results": results})
|
|
|
|
|
|
|
|
|
|
|
2026-05-10 09:08:12 +02:00
|
|
|
@router.post("/update")
|
|
|
|
|
async def update_profile(request: Request):
|
|
|
|
|
user = require_user(request)
|
|
|
|
|
form = await request.form()
|
|
|
|
|
bio = form.get("bio", "").strip()
|
|
|
|
|
location = form.get("location", "").strip()
|
|
|
|
|
git_link = form.get("git_link", "").strip()
|
|
|
|
|
website = form.get("website", "").strip()
|
|
|
|
|
users = get_table("users")
|
2026-05-11 03:14:43 +02:00
|
|
|
users.update({
|
2026-05-10 09:08:12 +02:00
|
|
|
"uid": user["uid"],
|
|
|
|
|
"bio": bio,
|
|
|
|
|
"location": location,
|
|
|
|
|
"git_link": git_link,
|
|
|
|
|
"website": website,
|
2026-05-11 03:14:43 +02:00
|
|
|
}, ["uid"])
|
2026-05-10 09:08:12 +02:00
|
|
|
|
|
|
|
|
logger.info(f"Profile updated for {user['username']}")
|
|
|
|
|
return RedirectResponse(url=f"/profile/{user['username']}", status_code=302)
|