From c6c0ec6c39af62a5579495df9452ac75537befdc Mon Sep 17 00:00:00 2001 From: retoor Date: Fri, 5 Jun 2026 05:36:18 +0200 Subject: [PATCH] Update --- devplacepy/database.py | 15 ++++++++ devplacepy/routers/feed.py | 22 +++--------- devplacepy/routers/gists.py | 21 +++++------ devplacepy/routers/news.py | 14 +++++--- devplacepy/routers/projects.py | 25 +++++++------ devplacepy/templates/_load_more.html | 5 +++ devplacepy/templates/feed.html | 6 +--- devplacepy/templates/gists.html | 2 ++ devplacepy/templates/news.html | 2 ++ devplacepy/templates/projects.html | 4 ++- tests/test_feed.py | 45 ++++++++++++++++++++++++ tests/test_gists.py | 51 +++++++++++++++++++++++++++ tests/test_news.py | 41 ++++++++++++++++++++-- tests/test_post.py | 2 +- tests/test_projects.py | 52 ++++++++++++++++++++++++++++ tests/test_xss.py | 4 +-- 16 files changed, 256 insertions(+), 55 deletions(-) create mode 100644 devplacepy/templates/_load_more.html diff --git a/devplacepy/database.py b/devplacepy/database.py index b675a56..e3bd1f6 100644 --- a/devplacepy/database.py +++ b/devplacepy/database.py @@ -528,6 +528,21 @@ def get_target_owner_uid(target_type: str, target_uid: str) -> str | None: return row["user_uid"] if row else None +PAGE_SIZE = 25 + + +def paginate(table, *clauses, before=None, order=None, cursor_field="created_at", **filters): + order = order or ["-" + cursor_field] + clauses = list(clauses) + if before: + clauses.append(table.table.columns[cursor_field] < before) + rows = list(table.find(*clauses, **filters, order_by=order, _limit=PAGE_SIZE + 1)) + has_more = len(rows) > PAGE_SIZE + rows = rows[:PAGE_SIZE] + next_cursor = rows[-1][cursor_field] if has_more and rows else None + return rows, next_cursor + + def build_pagination(page, total, per_page=25): total_pages = max(1, __import__("math").ceil(total / per_page)) page = max(1, min(page, total_pages)) diff --git a/devplacepy/routers/feed.py b/devplacepy/routers/feed.py index bf43f14..29f1f2f 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, get_top_authors +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, paginate from devplacepy.attachments import get_attachments_batch from devplacepy.content import enrich_items from devplacepy.templating import templates @@ -11,8 +11,6 @@ from devplacepy.seo import list_page_seo logger = logging.getLogger(__name__) router = APIRouter() -PAGE_SIZE = 25 - def get_feed_posts(user, tab: str = "all", topic: str = None, before: str = None): posts_table = get_table("posts") @@ -25,22 +23,10 @@ def get_feed_posts(user, tab: str = "all", topic: str = None, before: str = None following = [f["following_uid"] for f in follows.find(follower_uid=user["uid"])] if not following: return [], None - clauses = [posts_table.table.columns.user_uid.in_(following)] - if before: - clauses.append(posts_table.table.columns.created_at < before) - posts = list(posts_table.find(*clauses, order_by=order, _limit=PAGE_SIZE + 1)) + posts, next_cursor = paginate(posts_table, posts_table.table.columns.user_uid.in_(following), before=before, order=order) else: - filters = {} - if topic: - filters["topic"] = topic - if before: - filters["created_at"] = {"<": before} - posts = list(posts_table.find(**filters, order_by=order, _limit=PAGE_SIZE + 1)) - - has_more = len(posts) > PAGE_SIZE - posts = posts[:PAGE_SIZE] - - next_cursor = posts[-1]["created_at"] if has_more and posts else None + filters = {"topic": topic} if topic else {} + posts, next_cursor = paginate(posts_table, before=before, order=order, **filters) if not posts: return [], next_cursor diff --git a/devplacepy/routers/gists.py b/devplacepy/routers/gists.py index 25f3f47..915749b 100644 --- a/devplacepy/routers/gists.py +++ b/devplacepy/routers/gists.py @@ -3,7 +3,7 @@ from typing import Annotated 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.database import get_table, get_users_by_uids, get_gist_languages, paginate 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 get_current_user, require_user, not_found, XP_GIST @@ -23,7 +23,7 @@ LANGUAGES = [ ] -def get_gists_list(user_uid=None, language=None, viewer=None): +def get_gists_list(user_uid=None, language=None, before=None, viewer=None): gists_table = get_table("gists") filters = {} if user_uid: @@ -31,20 +31,20 @@ def get_gists_list(user_uid=None, language=None, viewer=None): if language: filters["language"] = language - all_gists = list(gists_table.find(**filters, order_by=["-created_at"])) + total = gists_table.count(**filters) + gists, next_cursor = paginate(gists_table, before=before, **filters) - if not all_gists: - return [] + if not gists: + return [], next_cursor, total - users_map = get_users_by_uids([g["user_uid"] for g in all_gists]) - return enrich_items(all_gists, "gist", users_map, user=viewer) + users_map = get_users_by_uids([g["user_uid"] for g in gists]) + return enrich_items(gists, "gist", users_map, user=viewer), next_cursor, total @router.get("", response_class=HTMLResponse) -async def gists_page(request: Request, language: str = None, user_uid: str = None): +async def gists_page(request: Request, language: str = None, user_uid: str = None, before: str = None): user = get_current_user(request) - gists_data = get_gists_list(user_uid, language, viewer=user) - total_count = len(gists_data) + gists_data, next_cursor, total_count = get_gists_list(user_uid, language, before, viewer=user) seo_ctx = list_page_seo( request, title="Gists", @@ -60,6 +60,7 @@ async def gists_page(request: Request, language: str = None, user_uid: str = Non "user": user, "gists": gists_data, "total_count": total_count, + "next_cursor": next_cursor, "current_language": language, "languages": LANGUAGES, "gist_language_codes": get_gist_languages(), diff --git a/devplacepy/routers/news.py b/devplacepy/routers/news.py index b6872ea..6ad90e5 100644 --- a/devplacepy/routers/news.py +++ b/devplacepy/routers/news.py @@ -2,7 +2,7 @@ import logging from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse -from devplacepy.database import get_table, db, load_comments, resolve_by_slug, get_news_images_by_uids +from devplacepy.database import get_table, db, load_comments, resolve_by_slug, get_news_images_by_uids, paginate from devplacepy.templating import templates from devplacepy.utils import get_current_user, time_ago, not_found from devplacepy.seo import base_seo_context, website_schema, site_url, news_article_schema, list_page_seo @@ -14,16 +14,19 @@ NEWS_MAX_AGE_DAYS = 4 @router.get("", response_class=HTMLResponse) -async def news_page(request: Request): +async def news_page(request: Request, before: str = None): user = get_current_user(request) cutoff = (datetime.now(timezone.utc) - timedelta(days=NEWS_MAX_AGE_DAYS)).isoformat() news_table = get_table("news") - articles = list(news_table.find( + articles, next_cursor = paginate( + news_table, + before=before, + order=["-grade", "-synced_at"], + cursor_field="synced_at", status="published", synced_at={">=": cutoff}, - order_by=["-grade", "-synced_at"], - )) + ) article_uids = [a["uid"] for a in articles] images_by_news = get_news_images_by_uids(article_uids) @@ -49,6 +52,7 @@ async def news_page(request: Request): "request": request, "user": user, "articles": enriched, + "next_cursor": next_cursor, }) diff --git a/devplacepy/routers/projects.py b/devplacepy/routers/projects.py index 38a10c5..24df8cc 100644 --- a/devplacepy/routers/projects.py +++ b/devplacepy/routers/projects.py @@ -4,7 +4,7 @@ from sqlalchemy import or_ from fastapi import APIRouter, Request, Form from devplacepy.models import ProjectForm from fastapi.responses import HTMLResponse, RedirectResponse -from devplacepy.database import get_table, get_users_by_uids, get_site_stats, get_user_votes +from devplacepy.database import get_table, get_users_by_uids, get_site_stats, get_user_votes, paginate from devplacepy.content import load_detail, delete_content_item, create_content_item, detail_context from devplacepy.templating import templates from devplacepy.utils import get_current_user, require_user, not_found, XP_PROJECT @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) router = APIRouter() -def get_projects_list(tab: str = "recent", search: str = "", user_uid: str = None, project_type: str = None, viewer: dict = None): +def get_projects_list(tab: str = "recent", search: str = "", user_uid: str = None, project_type: str = None, before: str = None, viewer: dict = None): projects = get_table("projects") filters = {} @@ -31,17 +31,18 @@ def get_projects_list(tab: str = "recent", search: str = "", user_uid: str = Non clauses.append(or_(projects.table.columns.title.ilike(like), projects.table.columns.description.ilike(like))) order = ["-stars", "-created_at"] if tab == "popular" else ["-created_at"] - all_projects = list(projects.find(*clauses, **filters, order_by=order)) + total = projects.count(*clauses, **filters) + page, next_cursor = paginate(projects, *clauses, before=before, order=order, **filters) - if all_projects: - users_map = get_users_by_uids([p["user_uid"] for p in all_projects]) - my_votes = get_user_votes(viewer["uid"], [p["uid"] for p in all_projects]) if viewer else {} - for p in all_projects: + if page: + users_map = get_users_by_uids([p["user_uid"] for p in page]) + my_votes = get_user_votes(viewer["uid"], [p["uid"] for p in page]) if viewer else {} + for p in page: author = users_map.get(p["user_uid"]) p["author_name"] = author["username"] if author else "Unknown" p["my_vote"] = my_votes.get(p["uid"], 0) - return all_projects + return page, next_cursor, total @router.get("", response_class=HTMLResponse) @@ -51,15 +52,16 @@ async def projects_page( search: str = "", user_uid: str = None, project_type: str = None, + before: str = None, ): user = get_current_user(request) - projects = get_projects_list(tab, search, user_uid, project_type, viewer=user) + projects, next_cursor, total_count = get_projects_list(tab, search, user_uid, project_type, before, viewer=user) total_members = get_site_stats()["total_members"] seo_ctx = list_page_seo( request, title="Projects", - description=f"Explore {len(projects)} developer projects on DevPlace. Games, software, mobile apps and more.", + description=f"Explore {total_count} developer projects on DevPlace. Games, software, mobile apps and more.", breadcrumbs=[ {"name": "Home", "url": "/feed"}, {"name": "Projects", "url": "/projects"}, @@ -73,7 +75,8 @@ async def projects_page( "current_tab": tab, "search": search, "project_type": project_type, - "total_count": len(projects), + "total_count": total_count, + "next_cursor": next_cursor, "total_members": total_members, }) diff --git a/devplacepy/templates/_load_more.html b/devplacepy/templates/_load_more.html new file mode 100644 index 0000000..727628d --- /dev/null +++ b/devplacepy/templates/_load_more.html @@ -0,0 +1,5 @@ +{% if next_cursor %} +
+ 📄Load More +
+{% endif %} diff --git a/devplacepy/templates/feed.html b/devplacepy/templates/feed.html index 898aa4f..ce42ddd 100644 --- a/devplacepy/templates/feed.html +++ b/devplacepy/templates/feed.html @@ -61,11 +61,7 @@ {% endfor %} - {% if next_cursor %} -
- 📄Load More -
- {% endif %} + {% include "_load_more.html" %}