fix(tags): fandom views aggregate images via their characters
A fandom owns characters via Tag.fandom_id, but every image<->tag query went purely through direct image_tag rows, so a fandom only surfaced images literally tagged with it — images carrying one of its characters were invisible to its browse count, previews, and gallery filter. Derive membership at query time instead of materializing fandom rows (which would drift on every reassign/merge/remove). Add one shared predicate in tag_query.py — image_in_tag_scope / image_in_any_tag_scope: an image belongs to a tag if tagged with it directly OR (when the tag is a fandom) carrying a character whose fandom_id is that tag. The character leg is empty for non-fandom tags, so it applies uniformly with no kind branching. Route all read sites through it: - gallery _apply_scope: include, OR-groups, and symmetric exclude - directory image_count: correlated COUNT(DISTINCT) scalar subquery - directory previews: UNION direct + via-character, then ROW_NUMBER<=3 - cleanup count_tag_associations: Tier-B delete prompt now reports a fandom's true blast radius (was 0 for fandoms with no direct rows) find_unused_tags already protected fandoms via used_via_fandom; left as is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,13 @@ from sqlalchemy.orm import aliased
|
||||
from ..models import Artist, ImageProvenance, ImageRecord, Post, Source, Tag
|
||||
from ..models.tag import image_tag
|
||||
from .pagination import decode_cursor, encode_cursor
|
||||
from .tag_query import fandom_join_alias, serialize_tag, tag_columns
|
||||
from .tag_query import (
|
||||
fandom_join_alias,
|
||||
image_in_any_tag_scope,
|
||||
image_in_tag_scope,
|
||||
serialize_tag,
|
||||
tag_columns,
|
||||
)
|
||||
|
||||
# Reserved `platform` filter value selecting images with NO platformed
|
||||
# provenance (filesystem imports). Returned by facets() as a null-valued
|
||||
@@ -180,29 +186,17 @@ def _apply_scope(
|
||||
- no_artist: ImageRecord.artist_id IS NULL.
|
||||
- date_from / date_to: half-open [from, to) bounds on effective_date.
|
||||
"""
|
||||
# Every tag clause goes through image_in_tag_scope/_any: a fandom tag also
|
||||
# matches images carrying any of its characters (Tag.fandom_id). Include,
|
||||
# OR-group, and exclude are all symmetric on that membership.
|
||||
for tid in tag_ids or []:
|
||||
stmt = stmt.where(
|
||||
exists().where(
|
||||
image_tag.c.image_record_id == ImageRecord.id,
|
||||
image_tag.c.tag_id == tid,
|
||||
)
|
||||
)
|
||||
stmt = stmt.where(image_in_tag_scope(tid))
|
||||
for group in tag_or_groups or []:
|
||||
if not group:
|
||||
continue # an empty OR-group would match nothing; treat as absent
|
||||
stmt = stmt.where(
|
||||
exists().where(
|
||||
image_tag.c.image_record_id == ImageRecord.id,
|
||||
image_tag.c.tag_id.in_(group),
|
||||
)
|
||||
)
|
||||
stmt = stmt.where(image_in_any_tag_scope(group))
|
||||
if tag_exclude:
|
||||
stmt = stmt.where(
|
||||
~exists().where(
|
||||
image_tag.c.image_record_id == ImageRecord.id,
|
||||
image_tag.c.tag_id.in_(tag_exclude),
|
||||
)
|
||||
)
|
||||
stmt = stmt.where(~image_in_any_tag_scope(tag_exclude))
|
||||
prov = _provenance_clause(post_id, artist_id)
|
||||
if prov is not None:
|
||||
stmt = stmt.where(prov)
|
||||
|
||||
Reference in New Issue
Block a user