Fandom count fix + Explore 3-pane workspace + modal rail layout #131

Merged
bvandeusen merged 3 commits from dev into main 2026-06-26 08:24:27 -04:00
2 changed files with 28 additions and 14 deletions
Showing only changes of commit 1aadf3267b - Show all commits
+12 -13
View File
@@ -55,21 +55,20 @@ class TagDirectoryService:
fandom = aliased(Tag)
member = aliased(Tag)
# image_count aggregates the tag's images INCLUDING, for a fandom, every
# image carrying one of its characters (member.fandom_id == Tag.id) — the
# member subquery is empty for non-fandom tags, so they stay direct-only.
# DISTINCT so an image with both the fandom and ≥1 of its characters
# counts once; a correlated scalar subquery keeps it 1 row per tag with
# no group_by (the fandom self-join below is 1:1).
# image carrying one of its characters. `member` is the tag actually on
# the image; an image counts for the outer Tag if that applied tag IS the
# Tag (direct) OR is a character whose fandom_id is the Tag (the fandom
# leg). Both legs correlate the OUTER Tag.id at a SINGLE level — a nested
# `tag_id IN (SELECT ... WHERE member.fandom_id == Tag.id)` does NOT
# correlate and silently counts every fandom-character globally (~every
# tag collapses to the same inflated number). DISTINCT so an image with
# the fandom AND ≥1 of its characters counts once; a correlated scalar
# subquery keeps it 1 row per tag with no group_by.
count_col = (
select(func.count(image_tag.c.image_record_id.distinct()))
.where(
or_(
image_tag.c.tag_id == Tag.id,
image_tag.c.tag_id.in_(
select(member.id).where(member.fandom_id == Tag.id)
),
)
)
.select_from(image_tag)
.join(member, member.id == image_tag.c.tag_id)
.where(or_(image_tag.c.tag_id == Tag.id, member.fandom_id == Tag.id))
.correlate(Tag)
.scalar_subquery()
.label("image_count")
+16 -1
View File
@@ -47,20 +47,35 @@ async def test_directory_fandom_counts_and_previews_via_characters(db):
char = Tag(name="Ichigo", kind=TagKind.character, fandom_id=fandom.id)
db.add(char)
await db.flush()
# A SECOND, unrelated fandom + character + image. The count for "Bleach"
# must NOT pick this up — guards against the count's fandom leg failing to
# correlate the outer tag and counting EVERY fandom-character globally
# (which inflates every card to the same number).
other_fandom = Tag(name="Naruto", kind=TagKind.fandom)
db.add(other_fandom)
await db.flush()
other_char = Tag(name="Sasuke", kind=TagKind.character, fandom_id=other_fandom.id)
db.add(other_char)
await db.flush()
# img0: character only; img1: fandom direct; img2: BOTH (dedup → counts once)
img0, img1, img2 = await _img(db, 0), await _img(db, 1), await _img(db, 2)
other_img = await _img(db, 3)
await db.execute(image_tag.insert().values([
{"image_record_id": img0.id, "tag_id": char.id, "source": "manual"},
{"image_record_id": img1.id, "tag_id": fandom.id, "source": "manual"},
{"image_record_id": img2.id, "tag_id": char.id, "source": "manual"},
{"image_record_id": img2.id, "tag_id": fandom.id, "source": "manual"},
{"image_record_id": other_img.id, "tag_id": other_char.id, "source": "manual"},
]))
await db.flush()
svc = TagDirectoryService(db)
page = await svc.list_tags(kind="fandom", q=None, cursor=None, limit=10)
card = next(c for c in page.cards if c["name"] == "Bleach")
assert card["image_count"] == 3
assert card["image_count"] == 3 # NOT 4 — Naruto's image must be excluded
assert len(card["preview_thumbnails"]) == 3
# The unrelated fandom counts only its own one image.
other = next(c for c in page.cards if c["name"] == "Naruto")
assert other["image_count"] == 1
@pytest.mark.asyncio