feat(tags): show a character's fandom on its chip (truncated)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 43s
CI / integration (push) Successful in 3m27s

A character chip with a fandom only rendered a bare arrow. Surface the fandom
NAME inline, truncated to 15 chars (full name in the tooltip). Resolve the name
via a Tag self-join in both tag paths the modal uses — list_for_image
(/api/images/<id>/tags) and gallery get_image_with_tags
(/api/gallery/image/<id>) — so chips show the fandom on first open and after any
reload. Falls back to the bare arrow when only fandom_id is known. Operator-asked
2026-06-09.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 19:35:59 -04:00
parent e4cebf70d1
commit 978f49adcc
4 changed files with 53 additions and 10 deletions
+1
View File
@@ -172,6 +172,7 @@ async def list_tags_for_image(image_id: int):
"name": t.name,
"kind": t.kind.value,
"fandom_id": t.fandom_id,
"fandom_name": t.fandom_name,
}
for t in tags
]
+17 -3
View File
@@ -558,13 +558,26 @@ class GalleryService:
record = await self.session.get(ImageRecord, image_id)
if record is None:
return None
# Self-join Tag to resolve a character's fandom NAME (not just id) so the
# modal chip can label it without an N+1 — mirrors list_for_image.
fandom_alias = Tag.__table__.alias("fandom_lookup")
tag_stmt = (
select(Tag)
.join(image_tag, image_tag.c.tag_id == Tag.id)
select(
Tag.id,
Tag.name,
Tag.kind,
Tag.fandom_id,
fandom_alias.c.name.label("fandom_name"),
)
.select_from(
Tag.__table__
.join(image_tag, image_tag.c.tag_id == Tag.id)
.outerjoin(fandom_alias, Tag.fandom_id == fandom_alias.c.id)
)
.where(image_tag.c.image_record_id == image_id)
.order_by(Tag.kind.asc(), Tag.name.asc())
)
tags = (await self.session.execute(tag_stmt)).scalars().all()
tags = (await self.session.execute(tag_stmt)).all()
# Fetch the canonical post.post_date for this image (if any) so
# the modal can show "Posted on <date>" alongside import date.
posted_at = None
@@ -608,6 +621,7 @@ class GalleryService:
"name": t.name,
"kind": t.kind.value if hasattr(t.kind, "value") else t.kind,
"fandom_id": t.fandom_id,
"fandom_name": t.fandom_name,
}
for t in tags
],
+19 -4
View File
@@ -225,14 +225,29 @@ class TagService:
)
)
async def list_for_image(self, image_id: int) -> Sequence[Tag]:
async def list_for_image(self, image_id: int) -> Sequence:
"""Tags on an image, ordered (kind, name). Each row carries the fandom's
NAME (not just fandom_id) via a self-join on Tag, so the UI can label a
character chip with its fandom without an N+1 (mirrors the
autocomplete/directory resolution)."""
fandom_alias = Tag.__table__.alias("fandom_lookup")
stmt = (
select(Tag)
.join(image_tag, image_tag.c.tag_id == Tag.id)
select(
Tag.id,
Tag.name,
Tag.kind,
Tag.fandom_id,
fandom_alias.c.name.label("fandom_name"),
)
.select_from(
Tag.__table__
.join(image_tag, image_tag.c.tag_id == Tag.id)
.outerjoin(fandom_alias, Tag.fandom_id == fandom_alias.c.id)
)
.where(image_tag.c.image_record_id == image_id)
.order_by(Tag.kind.asc(), Tag.name.asc())
)
return (await self.session.execute(stmt)).scalars().all()
return (await self.session.execute(stmt)).all()
async def _keep_as_alias(self, tag_id: int) -> bool:
"""A merged-away tag's old name must survive as an alias iff the ML