Extend dismissed-exclusion to mood feeds for consistency
Mood feeds now honor the same dismissed list as the brief: /api/feed accepts an exclude param (over-fetching to stay full), and the client passes the persisted dismissed set. Swapping a story away now keeps it gone everywhere — brief and browse — not just on the home view. Also simplified the feed filter path to the shared _prefs_sql_kw helper. Tests: feed exclude (91 total). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+17
-26
@@ -261,6 +261,7 @@ def create_app() -> FastAPI:
|
||||
limit: int = Query(30, ge=1, le=100),
|
||||
offset: int = Query(0, ge=0),
|
||||
prefs: str | None = Query(None),
|
||||
exclude: str = Query("", description="comma-separated article ids the reader has dismissed"),
|
||||
) -> FeedResponse:
|
||||
if topic and topic.lower() not in TOPICS:
|
||||
raise HTTPException(400, f"unknown topic: {topic}")
|
||||
@@ -268,36 +269,26 @@ def create_app() -> FastAPI:
|
||||
raise HTTPException(400, f"unknown flavor: {flavor}")
|
||||
fp = prefs_from_json(prefs)
|
||||
now = datetime.now(timezone.utc)
|
||||
excl = {int(x) for x in exclude.split(",") if x.strip().lstrip("-").isdigit()}
|
||||
# Categorical filters (include/mute topics+flavors incl. active pauses,
|
||||
# cortisol ceiling) go to SQL so nothing is truncated by ranking. Only
|
||||
# word-boundary avoid-terms and dismissals need a Python pass.
|
||||
kw = _prefs_sql_kw(fp, now)
|
||||
with get_conn() as conn:
|
||||
if fp.is_empty():
|
||||
rows = queries.feed(
|
||||
conn, topic=topic, flavor=flavor, accepted_only=accepted_only, limit=limit, offset=offset
|
||||
if fp.avoid_terms or excl:
|
||||
# Over-fetch enough to cover what the Python pass might remove.
|
||||
fetch_n = min(2000, (offset + limit) * 4 + 50 + len(excl))
|
||||
raw = queries.feed(
|
||||
conn, topic=topic, flavor=flavor, accepted_only=accepted_only,
|
||||
limit=fetch_n, offset=0, **kw,
|
||||
)
|
||||
kept = [a for a in filter_articles(raw, fp, now) if a["id"] not in excl]
|
||||
rows = kept[offset : offset + limit]
|
||||
else:
|
||||
# Categorical filters (include/mute topics+flavors incl. active
|
||||
# pauses, cortisol ceiling) go to SQL so nothing is truncated by
|
||||
# ranking. Only word-boundary avoid-terms need a Python pass, so
|
||||
# over-fetch just enough to cover what they might remove.
|
||||
kw = dict(
|
||||
include_topics=fp.include_topics or None,
|
||||
include_flavors=fp.include_flavors or None,
|
||||
mute_topics=list(fp.muted_topics(now)) or None,
|
||||
mute_flavors=list(fp.muted_flavors(now)) or None,
|
||||
max_cortisol=fp.max_cortisol,
|
||||
max_ragebait=fp.max_ragebait,
|
||||
rows = queries.feed(
|
||||
conn, topic=topic, flavor=flavor, accepted_only=accepted_only,
|
||||
limit=limit, offset=offset, **kw,
|
||||
)
|
||||
if fp.avoid_terms:
|
||||
raw = queries.feed(
|
||||
conn, topic=topic, flavor=flavor, accepted_only=accepted_only,
|
||||
limit=min(2000, (offset + limit) * 4 + 50), offset=0, **kw,
|
||||
)
|
||||
kept = filter_articles(raw, fp, now) # drops avoid-term matches
|
||||
rows = kept[offset : offset + limit]
|
||||
else:
|
||||
rows = queries.feed(
|
||||
conn, topic=topic, flavor=flavor, accepted_only=accepted_only,
|
||||
limit=limit, offset=offset, **kw,
|
||||
)
|
||||
# Keep the top of a browse view readable: stable-sort paywalled items
|
||||
# below readable ones (composite order preserved within each group).
|
||||
rows = sorted(rows, key=lambda r: is_paywalled(r["canonical_url"]))
|
||||
|
||||
Reference in New Issue
Block a user