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 %}
+
+{% 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 %}
-
- {% endif %}
+ {% include "_load_more.html" %}