feat(post-api): get_post returns uncapped thumbnails — PostModal masonry needs full image list; feed query unchanged (still capped at 6 for previews). _thumbnails_for gains a limit kwarg; get_post passes limit=None.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 20:47:18 -04:00
parent 85b640f32e
commit 42c33e44f9
2 changed files with 69 additions and 12 deletions
+22 -12
View File
@@ -109,7 +109,10 @@ class PostFeedService:
if row is None:
return None
post, artist, source = row
thumbs_map = await self._thumbnails_for([post.id])
# Detail endpoint returns the FULL image list for PostModal's
# masonry grid — feed query still caps at THUMBNAIL_LIMIT via
# the default arg.
thumbs_map = await self._thumbnails_for([post.id], limit=None)
atts_map = await self._attachments_for([post.id])
item = self._to_dict(post, artist, source, thumbs_map, atts_map)
item["description_full"] = html_to_plain(post.description)
@@ -117,15 +120,21 @@ class PostFeedService:
# --- composition helpers ---------------------------------------------
async def _thumbnails_for(self, post_ids: list[int]) -> dict[int, dict]:
"""post_id -> {"thumbs": [...up to 6], "more": int}.
async def _thumbnails_for(
self, post_ids: list[int], *, limit: int | None = THUMBNAIL_LIMIT,
) -> dict[int, dict]:
"""post_id -> {"thumbs": [...up to limit], "more": int}.
Selects THUMBNAIL_LIMIT+1 images per post via window function so we
can detect overflow in a single query.
Selects up to `limit` images per post via window function so we
can detect overflow in a single query. Pass `limit=None` to
return ALL thumbnails per post (used by `get_post` for PostModal's
masonry grid; the feed pass keeps the default cap so payloads
stay small).
"""
if not post_ids:
return {}
# Rank images within each post and fetch only the top THUMBNAIL_LIMIT+1.
# Rank images within each post; cap at `limit` rows per post when
# limit is set, return all when limit is None.
ranked = (
select(
ImageRecord.id,
@@ -143,12 +152,13 @@ class PostFeedService:
.where(ImageRecord.primary_post_id.in_(post_ids))
.subquery()
)
rows = (await self.session.execute(
select(
ranked.c.id, ranked.c.primary_post_id,
ranked.c.sha256, ranked.c.mime, ranked.c.total,
).where(ranked.c.rn <= THUMBNAIL_LIMIT)
)).all()
stmt = select(
ranked.c.id, ranked.c.primary_post_id,
ranked.c.sha256, ranked.c.mime, ranked.c.total,
)
if limit is not None:
stmt = stmt.where(ranked.c.rn <= limit)
rows = (await self.session.execute(stmt)).all()
out: dict[int, dict] = {pid: {"thumbs": [], "more": 0} for pid in post_ids}
for img_id, pid, sha, mime, total in rows:
+47
View File
@@ -117,3 +117,50 @@ async def test_detail_404_for_unknown(client):
assert resp.status_code == 404
body = await resp.get_json()
assert body["error"] == "not_found"
@pytest.mark.asyncio
async def test_detail_returns_uncapped_thumbnails(client, db):
"""Feed query caps thumbnails at 6 for previews; detail endpoint
returns the full list so PostModal can render the masonry grid."""
from backend.app.models import ImageRecord
a = Artist(name="yuki-api", slug="yuki-api")
db.add(a)
await db.flush()
s = Source(
artist_id=a.id, platform="patreon",
url="https://patreon.com/cw/yuki-api", enabled=True,
)
db.add(s)
await db.flush()
p = Post(
source_id=s.id, external_post_id="DETAIL10",
post_title="big post", description="<p>body</p>",
)
db.add(p)
await db.flush()
# Seed 10 ImageRecord rows linked to this post via primary_post_id.
for i in range(10):
sha = f"y{i:x}".ljust(64, "0")[:64]
rec = ImageRecord(
path=f"/images/test-yuki-{i}.jpg",
sha256=sha,
size_bytes=1,
mime="image/jpeg",
width=64,
height=64,
origin="downloaded",
integrity_status="unknown",
primary_post_id=p.id,
artist_id=a.id,
)
db.add(rec)
await db.commit()
resp = await client.get(f"/api/posts/{p.id}")
assert resp.status_code == 200
body = await resp.get_json()
# Detail returns ALL 10 thumbnails (feed would return 6 + thumbnails_more).
assert len(body["thumbnails"]) == 10
assert body["description_full"] == "body"