From 347e5f0f31a30b3f99d68a65f16452c80a41dde4 Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 23 May 2026 10:54:45 +0200 Subject: [PATCH] Update --- devplacepy/database.py | 40 +++++++++++++++++++++++++++++++++++ devplacepy/routers/feed.py | 5 ++--- devplacepy/routers/profile.py | 3 ++- tests/test_post.py | 16 ++++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/devplacepy/database.py b/devplacepy/database.py index 038db7d..3434e7b 100644 --- a/devplacepy/database.py +++ b/devplacepy/database.py @@ -338,6 +338,46 @@ def get_gist_languages() -> set[str]: return codes +_STARRED_CONTENT_TABLES = ("posts", "projects", "gists") + +_top_authors_cache = TTLCache(ttl=60) + + +def get_top_authors(limit: int = 5) -> list: + cached = _top_authors_cache.get("top") + if cached is not None: + return cached + sources = [table for table in _STARRED_CONTENT_TABLES if table in db.tables] + if not sources: + _top_authors_cache.set("top", []) + return [] + union = " UNION ALL ".join(f"SELECT user_uid, stars FROM {table}" for table in sources) + rows = db.query( + f"SELECT user_uid, SUM(stars) AS total FROM ({union}) " + f"GROUP BY user_uid HAVING total > 0 ORDER BY total DESC LIMIT {int(limit)}" + ) + ranked = [(row["user_uid"], row["total"]) for row in rows] + users_map = get_users_by_uids([uid for uid, _ in ranked]) + authors = [] + for uid, total in ranked: + user = users_map.get(uid) + if user: + author = dict(user) + author["stars"] = total + authors.append(author) + _top_authors_cache.set("top", authors) + return authors + + +def get_user_stars(user_uid: str) -> int: + total = 0 + for table in _STARRED_CONTENT_TABLES: + if table in db.tables: + for row in db.query(f"SELECT COALESCE(SUM(stars), 0) AS s FROM {table} WHERE user_uid = :u", u=user_uid): + total += row["s"] or 0 + return total + + def resolve_by_slug(table, slug): entry = table.find_one(slug=slug) if not entry: diff --git a/devplacepy/routers/feed.py b/devplacepy/routers/feed.py index 8abe3ed..997503e 100644 --- a/devplacepy/routers/feed.py +++ b/devplacepy/routers/feed.py @@ -1,7 +1,7 @@ import logging from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse -from devplacepy.database import get_table, get_daily_topic, get_users_by_uids, get_comment_counts_by_post_uids, get_site_stats +from devplacepy.database import get_table, get_daily_topic, get_users_by_uids, get_comment_counts_by_post_uids, get_site_stats, get_top_authors from devplacepy.attachments import get_attachments_batch from devplacepy.content import enrich_items from devplacepy.templating import templates @@ -60,9 +60,8 @@ def get_feed_posts(user, tab: str = "all", topic: str = None, before: str = None async def feed_page(request: Request, tab: str = "all", topic: str = None, before: str = None): user = get_current_user(request) posts, next_cursor = get_feed_posts(user, tab, topic, before) - users_table = get_table("users") stats = get_site_stats() - top_authors = list(users_table.find(stars={">": 0}, order_by=["-stars"], _limit=5)) + top_authors = get_top_authors(5) daily_topic = get_daily_topic() post_uids_list = [item["post"]["uid"] for item in posts] diff --git a/devplacepy/routers/profile.py b/devplacepy/routers/profile.py index 3cb1150..6cc5e0b 100644 --- a/devplacepy/routers/profile.py +++ b/devplacepy/routers/profile.py @@ -3,7 +3,7 @@ from typing import Annotated from fastapi import APIRouter, Request, Form from devplacepy.models import ProfileForm from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse -from devplacepy.database import get_table, db +from devplacepy.database import get_table, db, get_user_stars from devplacepy.templating import templates from devplacepy.utils import get_current_user, require_user, require_user_api, time_ago, clear_user_cache from devplacepy.seo import base_seo_context, site_url, website_schema, profile_page_schema @@ -35,6 +35,7 @@ async def profile_page(request: Request, username: str, tab: str = "posts"): profile_user = users.find_one(username=username) if not profile_user: return RedirectResponse(url="/feed", status_code=302) + profile_user["stars"] = get_user_stars(profile_user["uid"]) posts = [] if tab == "posts": diff --git a/tests/test_post.py b/tests/test_post.py index cd254ca..3378c07 100644 --- a/tests/test_post.py +++ b/tests/test_post.py @@ -49,6 +49,22 @@ def test_post_vote_increment(alice): assert count == "1", f"expected vote count 1, got {count!r}" +def _profile_stars(page, username): + page.goto(f"{BASE_URL}/profile/{username}", wait_until="domcontentloaded") + value = page.locator(".profile-stat:has(.profile-stat-label:has-text('Stars')) .profile-stat-value").first + return int(value.text_content().strip()) + + +def test_profile_stars_reflect_content_votes(alice): + page, user = alice + before = _profile_stars(page, user["username"]) + create_post(page, "devlog", "Reputation contribution post") + page.locator(".post-action-btn.vote-up").first.click() + page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded") + after = _profile_stars(page, user["username"]) + assert after == before + 1, f"expected stars {before + 1}, got {after}" + + def test_post_comments_section(alice): page, _ = alice create_post(page, "rant", "Comment section test")