feat(gallery): default-hide presentation chrome (banner/editor screenshot) (#141 step 1)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Failing after 3m41s

The default gallery + facets now implicitly exclude images carrying a
presentation system tag (banner / editor screenshot), reusing the tag-scope
EXISTS machinery. Suppressed when the operator explicitly filters FOR a
presentation tag OR passes include_hidden (the Hidden view — step 2). `wip` is
NOT hidden (real, in-progress art). include_hidden threaded through
scroll/timeline/jump_cursor/facets + the gallery API _parse_filters. Test covers
default-hide, include_hidden, explicit-filter-shows, and wip-stays-visible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
2026-07-06 22:27:00 -04:00
parent 4f4ddecf75
commit e86b91dfe2
3 changed files with 98 additions and 5 deletions
+4
View File
@@ -77,6 +77,9 @@ def _parse_filters():
platform = request.args.get("platform") or None
untagged = request.args.get("untagged") in ("1", "true", "yes")
no_artist = request.args.get("no_artist") in ("1", "true", "yes")
# Show the presentation chrome (banner / editor screenshot) that the default
# gallery hides — the Hidden view sets this (milestone 141).
include_hidden = request.args.get("include_hidden") in ("1", "true", "yes")
date_from = _parse_date(request.args.get("date_from"))
date_to = _parse_date(request.args.get("date_to"))
if date_to is not None:
@@ -88,6 +91,7 @@ def _parse_filters():
"platform": platform,
"untagged": untagged, "no_artist": no_artist,
"date_from": date_from, "date_to": date_to,
"include_hidden": include_hidden,
}
return filters, sort
+52 -5
View File
@@ -187,7 +187,7 @@ def _apply_scope(
stmt, *, tag_ids, post_id, artist_id, media_type,
tag_or_groups=None, tag_exclude=None,
platform=None, untagged=False, no_artist=False,
date_from=None, date_to=None,
date_from=None, date_to=None, hidden_tag_ids=None,
):
"""Apply the composable gallery filters to a statement.
@@ -224,6 +224,12 @@ def _apply_scope(
stmt = stmt.where(image_in_any_tag_scope(group))
if tag_exclude:
stmt = stmt.where(~image_in_any_tag_scope(tag_exclude))
# Presentation chrome (banner / editor screenshot) is hidden from the default
# gallery — an implicit exclude the caller supplies unless the operator asked
# to include hidden or is explicitly filtering for a presentation tag
# (milestone 141). `wip` is NOT hidden. Resolved to ids by _hidden_tag_ids.
if hidden_tag_ids:
stmt = stmt.where(~image_in_any_tag_scope(hidden_tag_ids))
prov = _provenance_clause(post_id, artist_id)
if prov is not None:
stmt = stmt.where(prov)
@@ -410,6 +416,31 @@ class GalleryService:
def __init__(self, session: AsyncSession):
self.session = session
async def _hidden_tag_ids(
self, include_hidden, tag_ids, tag_or_groups,
) -> list[int] | None:
"""Presentation-chrome tag ids to implicitly exclude from a gallery query,
or None. None when the caller asked to include hidden, when the operator
is explicitly filtering FOR a presentation tag (they clearly want to see
it), or when no presentation tags exist. (milestone 141)"""
if include_hidden:
return None
rows = await self.session.execute(
select(Tag.id).where(
Tag.is_system.is_(True),
Tag.name.in_(PRESENTATION_SYSTEM_TAGS),
)
)
pres = [r[0] for r in rows]
if not pres:
return None
explicit = set(tag_ids or [])
for group in tag_or_groups or []:
explicit.update(group)
if explicit & set(pres):
return None
return pres
async def scroll(
self,
cursor: str | None,
@@ -426,12 +457,16 @@ class GalleryService:
no_artist: bool = False,
date_from: datetime | None = None,
date_to: datetime | None = None,
include_hidden: bool = False,
) -> GalleryPage:
if limit < 1 or limit > 200:
raise ValueError("limit must be between 1 and 200")
_require_single_filter(
tag_ids, post_id, artist_id, tag_or_groups, tag_exclude,
)
hidden = await self._hidden_tag_ids(
include_hidden, tag_ids, tag_or_groups,
)
# eff is the ACTIVE sort column (effective_date or earliest_post_date);
# the cursor, ordering and year/month grouping all key off it, so the
@@ -444,7 +479,7 @@ class GalleryService:
artist_id=artist_id, media_type=media_type,
tag_or_groups=tag_or_groups, tag_exclude=tag_exclude,
platform=platform, untagged=untagged, no_artist=no_artist,
date_from=date_from, date_to=date_to,
date_from=date_from, date_to=date_to, hidden_tag_ids=hidden,
)
descending = sort not in _ASCENDING_SORTS
@@ -497,6 +532,7 @@ class GalleryService:
no_artist: bool = False,
date_from: datetime | None = None,
date_to: datetime | None = None,
include_hidden: bool = False,
) -> list[TimelineBucket]:
eff = _effective_date_col()
year_col = func.date_part("year", eff).label("yr")
@@ -508,12 +544,15 @@ class GalleryService:
_require_single_filter(
tag_ids, post_id, artist_id, tag_or_groups, tag_exclude,
)
hidden = await self._hidden_tag_ids(
include_hidden, tag_ids, tag_or_groups,
)
stmt = _apply_scope(
stmt, tag_ids=tag_ids, post_id=post_id,
artist_id=artist_id, media_type=media_type,
tag_or_groups=tag_or_groups, tag_exclude=tag_exclude,
platform=platform, untagged=untagged, no_artist=no_artist,
date_from=date_from, date_to=date_to,
date_from=date_from, date_to=date_to, hidden_tag_ids=hidden,
)
stmt = stmt.group_by(year_col, month_col).order_by(year_col.desc(), month_col.desc())
rows = (await self.session.execute(stmt)).all()
@@ -527,7 +566,7 @@ class GalleryService:
tag_exclude: list[int] | None = None,
platform: str | None = None, untagged: bool = False,
no_artist: bool = False, date_from: datetime | None = None,
date_to: datetime | None = None,
date_to: datetime | None = None, include_hidden: bool = False,
) -> str | None:
"""Returns a cursor that, when passed to scroll() with the same sort,
positions at the first image of the given year-month. None if the
@@ -544,12 +583,15 @@ class GalleryService:
_require_single_filter(
tag_ids, post_id, artist_id, tag_or_groups, tag_exclude,
)
hidden = await self._hidden_tag_ids(
include_hidden, tag_ids, tag_or_groups,
)
stmt = _apply_scope(
stmt, tag_ids=tag_ids, post_id=post_id,
artist_id=artist_id, media_type=media_type,
tag_or_groups=tag_or_groups, tag_exclude=tag_exclude,
platform=platform, untagged=untagged, no_artist=no_artist,
date_from=date_from, date_to=date_to,
date_from=date_from, date_to=date_to, hidden_tag_ids=hidden,
)
descending = sort != "oldest"
if descending:
@@ -574,6 +616,7 @@ class GalleryService:
platform: str | None = None,
untagged: bool = False, no_artist: bool = False,
date_from: datetime | None = None, date_to: datetime | None = None,
include_hidden: bool = False,
) -> GalleryFacets:
"""Live facet counts scoped to the current filter. Each facet GROUP is
computed with all OTHER active filters applied but its OWN selection
@@ -584,10 +627,14 @@ class GalleryService:
_require_single_filter(
tag_ids, post_id, artist_id, tag_or_groups, tag_exclude,
)
hidden = await self._hidden_tag_ids(
include_hidden, tag_ids, tag_or_groups,
)
common = {
"tag_ids": tag_ids, "post_id": post_id,
"artist_id": artist_id, "media_type": media_type,
"tag_or_groups": tag_or_groups, "tag_exclude": tag_exclude,
"hidden_tag_ids": hidden,
}
# total — the full active filter (the headline result count).