Update
All checks were successful
DevPlace CI / test (push) Successful in 5m33s

This commit is contained in:
retoor 2026-05-23 10:54:45 +02:00
parent fe0ed5b7e6
commit 347e5f0f31
4 changed files with 60 additions and 4 deletions

View File

@ -338,6 +338,46 @@ def get_gist_languages() -> set[str]:
return codes 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): def resolve_by_slug(table, slug):
entry = table.find_one(slug=slug) entry = table.find_one(slug=slug)
if not entry: if not entry:

View File

@ -1,7 +1,7 @@
import logging import logging
from fastapi import APIRouter, Request from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse 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.attachments import get_attachments_batch
from devplacepy.content import enrich_items from devplacepy.content import enrich_items
from devplacepy.templating import templates 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): async def feed_page(request: Request, tab: str = "all", topic: str = None, before: str = None):
user = get_current_user(request) user = get_current_user(request)
posts, next_cursor = get_feed_posts(user, tab, topic, before) posts, next_cursor = get_feed_posts(user, tab, topic, before)
users_table = get_table("users")
stats = get_site_stats() 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() daily_topic = get_daily_topic()
post_uids_list = [item["post"]["uid"] for item in posts] post_uids_list = [item["post"]["uid"] for item in posts]

View File

@ -3,7 +3,7 @@ from typing import Annotated
from fastapi import APIRouter, Request, Form from fastapi import APIRouter, Request, Form
from devplacepy.models import ProfileForm from devplacepy.models import ProfileForm
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse 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.templating import templates
from devplacepy.utils import get_current_user, require_user, require_user_api, time_ago, clear_user_cache 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 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) profile_user = users.find_one(username=username)
if not profile_user: if not profile_user:
return RedirectResponse(url="/feed", status_code=302) return RedirectResponse(url="/feed", status_code=302)
profile_user["stars"] = get_user_stars(profile_user["uid"])
posts = [] posts = []
if tab == "posts": if tab == "posts":

View File

@ -49,6 +49,22 @@ def test_post_vote_increment(alice):
assert count == "1", f"expected vote count 1, got {count!r}" 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): def test_post_comments_section(alice):
page, _ = alice page, _ = alice
create_post(page, "rant", "Comment section test") create_post(page, "rant", "Comment section test")