feat: replace per-author cap with interleaving to avoid dropping posts in feed and home
The old `diversify_by_author` capped each author to 2 posts per page, silently dropping excess rows. The new `interleave_by_author` reorders the full result set so no two consecutive posts share an author, preserving every post while breaking up same-author runs. `paginate_diverse` loses its `max_per_author` and `pool` parameters; the home route now fetches exactly 6 rows and interleaves them instead of over-fetching 50 and capping. Tests are rewritten to verify interleaving behavior and per-author chronological order preservation.
This commit is contained in:
+37
-30
@@ -2222,18 +2222,36 @@ def paginate(
|
||||
return rows, next_cursor
|
||||
|
||||
|
||||
def diversify_by_author(rows, max_per_author=2, limit=None, uid_key="user_uid"):
|
||||
counts: dict = {}
|
||||
kept = []
|
||||
for row in rows:
|
||||
def interleave_by_author(rows, uid_key="user_uid"):
|
||||
queues: dict = {}
|
||||
appearance: list = []
|
||||
for position, row in enumerate(rows):
|
||||
owner = row.get(uid_key)
|
||||
if counts.get(owner, 0) >= max_per_author:
|
||||
continue
|
||||
counts[owner] = counts.get(owner, 0) + 1
|
||||
kept.append(row)
|
||||
if limit is not None and len(kept) >= limit:
|
||||
break
|
||||
return kept
|
||||
queue = queues.get(owner)
|
||||
if queue is None:
|
||||
queue = queues[owner] = []
|
||||
appearance.append(owner)
|
||||
queue.append((position, row))
|
||||
spread = []
|
||||
last_owner = object()
|
||||
while len(spread) < len(rows):
|
||||
chosen = None
|
||||
for owner in appearance:
|
||||
queue = queues[owner]
|
||||
if not queue or owner == last_owner:
|
||||
continue
|
||||
count = len(queue)
|
||||
head_position = queue[0][0]
|
||||
if (
|
||||
chosen is None
|
||||
or count > chosen[0]
|
||||
or (count == chosen[0] and head_position < chosen[1])
|
||||
):
|
||||
chosen = (count, head_position, owner)
|
||||
owner = last_owner if chosen is None else chosen[2]
|
||||
spread.append(queues[owner].pop(0)[1])
|
||||
last_owner = owner
|
||||
return spread
|
||||
|
||||
|
||||
def paginate_diverse(
|
||||
@@ -2242,29 +2260,18 @@ def paginate_diverse(
|
||||
before=None,
|
||||
order=None,
|
||||
cursor_field="created_at",
|
||||
max_per_author=2,
|
||||
uid_key="user_uid",
|
||||
pool=200,
|
||||
**filters,
|
||||
):
|
||||
order = order or ["-" + cursor_field]
|
||||
clauses = list(clauses)
|
||||
if table.has_column("deleted_at") and "deleted_at" not in filters:
|
||||
clauses.append(table.table.columns.deleted_at.is_(None))
|
||||
if before:
|
||||
clauses.append(table.table.columns[cursor_field] < before)
|
||||
rows = list(table.find(*clauses, **filters, order_by=order, _limit=pool + 1))
|
||||
exhausted = len(rows) <= pool
|
||||
rows = rows[:pool]
|
||||
kept = diversify_by_author(
|
||||
rows, max_per_author=max_per_author, limit=PAGE_SIZE, uid_key=uid_key
|
||||
rows, next_cursor = paginate(
|
||||
table,
|
||||
*clauses,
|
||||
before=before,
|
||||
order=order,
|
||||
cursor_field=cursor_field,
|
||||
**filters,
|
||||
)
|
||||
if not kept:
|
||||
return [], None
|
||||
last_cursor = kept[-1][cursor_field]
|
||||
older_exists = (not exhausted) or any(r[cursor_field] < last_cursor for r in rows)
|
||||
next_cursor = last_cursor if older_exists else None
|
||||
return kept, next_cursor
|
||||
return interleave_by_author(rows, uid_key=uid_key), next_cursor
|
||||
|
||||
|
||||
def get_user_post_count(user_uid: str) -> int:
|
||||
|
||||
@@ -695,7 +695,7 @@ four ways to sign requests.
|
||||
summary=(
|
||||
"The home page. Guests see the marketing splash; authenticated users see a "
|
||||
"personalized home (welcome, feed shortcut, latest posts, news). It no longer "
|
||||
"redirects to /feed. The latest-posts section shows at most two posts per author."
|
||||
"redirects to /feed. The latest-posts section interleaves authors so no two consecutive posts share an author."
|
||||
),
|
||||
auth="public",
|
||||
interactive=True,
|
||||
@@ -705,7 +705,7 @@ four ways to sign requests.
|
||||
method="GET",
|
||||
path="/feed",
|
||||
title="Browse the feed",
|
||||
summary="The main post feed. Returns an HTML page. Each page shows at most two posts per author.",
|
||||
summary="The main post feed. Returns an HTML page. Each page interleaves authors so no two consecutive posts share an author.",
|
||||
auth="public",
|
||||
interactive=True,
|
||||
params=[
|
||||
|
||||
+3
-3
@@ -31,7 +31,7 @@ from devplacepy.database import (
|
||||
get_news_images_by_uids,
|
||||
get_setting,
|
||||
get_int_setting,
|
||||
diversify_by_author,
|
||||
interleave_by_author,
|
||||
get_user_post_count,
|
||||
)
|
||||
from devplacepy.templating import templates
|
||||
@@ -506,9 +506,9 @@ async def landing(request: Request):
|
||||
if "posts" in db.tables:
|
||||
posts_table = get_table("posts")
|
||||
raw_posts = list(
|
||||
posts_table.find(deleted_at=None, order_by=["-created_at"], _limit=50)
|
||||
posts_table.find(deleted_at=None, order_by=["-created_at"], _limit=6)
|
||||
)
|
||||
raw_posts = diversify_by_author(raw_posts, max_per_author=2, limit=6)
|
||||
raw_posts = interleave_by_author(raw_posts)
|
||||
if raw_posts:
|
||||
post_uids = [p["uid"] for p in raw_posts]
|
||||
author_uids = [p["user_uid"] for p in raw_posts]
|
||||
|
||||
@@ -51,6 +51,12 @@ DOCS_PAGES = [
|
||||
"kind": "prose",
|
||||
"section": SECTION_GENERAL,
|
||||
},
|
||||
{
|
||||
"slug": "feed",
|
||||
"title": "The feed",
|
||||
"kind": "prose",
|
||||
"section": SECTION_GENERAL,
|
||||
},
|
||||
{
|
||||
"slug": "devii",
|
||||
"title": "Devii Assistant",
|
||||
|
||||
@@ -52,7 +52,6 @@ def get_feed_posts(
|
||||
*search_clauses,
|
||||
before=before,
|
||||
order=order,
|
||||
max_per_author=2,
|
||||
)
|
||||
else:
|
||||
filters = {"topic": topic} if topic else {}
|
||||
@@ -61,7 +60,6 @@ def get_feed_posts(
|
||||
*search_clauses,
|
||||
before=before,
|
||||
order=order,
|
||||
max_per_author=2,
|
||||
**filters,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user