feat(gallery): tag→gallery nav from modal chips (#5) + OR/exclude tag scope (#6a)
Cluster A, milestone #97. #5: clicking an image-modal tag chip's body now closes the modal and opens the gallery filtered for that one tag (fresh filter); ✕/kebab stay as the explicit remove/rename controls. #6a (backend of OR/exclude filtering): gallery_service._apply_scope gains a structured tag model — tag_or_groups (AND-of-OR: one EXISTS(tag_id IN group) per group) + tag_exclude (NOT EXISTS(tag_id IN exclude)) — layered additively on the existing tag_ids AND path so cursors/facets/deep-links are untouched. Threaded through scroll/timeline/jump_cursor/facets/similar + facets common dict; _require_single_filter rejects post_id combined with OR/exclude. API parses tag_or (repeatable → one OR-group each) + tag_not (csv exclude). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCUHUGQLrBrkgyk1t49kpX
This commit is contained in:
@@ -37,16 +37,30 @@ def _parse_filters():
|
||||
"""Parse the composable gallery filters from query args, returning
|
||||
``(filters_dict, sort)``. Raises ValueError (→ 400) on malformed ids/dates.
|
||||
|
||||
`tag_id` accepts a single id or a comma-separated list (AND); `media` is
|
||||
image|video; `sort` is newest|oldest; `platform` selects one platform
|
||||
(or the UNSOURCED_PLATFORM sentinel); `untagged`/`no_artist` are boolean
|
||||
flags; `date_from`/`date_to` are inclusive calendar-day bounds (date_to is
|
||||
widened by a day so the whole day is covered by the service's half-open
|
||||
`< date_to`)."""
|
||||
The structured tag filter (#6) is AND-of-OR plus exclusions:
|
||||
- `tag_id` accepts a single id or a comma-separated list — all ANDed
|
||||
(the include common case; back-compat).
|
||||
- `tag_or` is REPEATABLE; each instance is a comma-separated OR-group, and
|
||||
the image must match at least one tag from EACH group (groups ANDed).
|
||||
- `tag_not` is a comma-separated exclude list (image must carry none).
|
||||
|
||||
`media` is image|video; `sort` is newest|oldest; `platform` selects one
|
||||
platform (or the UNSOURCED_PLATFORM sentinel); `untagged`/`no_artist` are
|
||||
boolean flags; `date_from`/`date_to` are inclusive calendar-day bounds
|
||||
(date_to is widened by a day so the whole day is covered by the service's
|
||||
half-open `< date_to`)."""
|
||||
tag_raw = request.args.get("tag_id")
|
||||
tag_ids = (
|
||||
[int(x) for x in tag_raw.split(",") if x.strip()] if tag_raw else None
|
||||
) or None
|
||||
tag_or_groups = [
|
||||
grp for raw in request.args.getlist("tag_or")
|
||||
if (grp := [int(x) for x in raw.split(",") if x.strip()])
|
||||
] or None
|
||||
not_raw = request.args.get("tag_not")
|
||||
tag_exclude = (
|
||||
[int(x) for x in not_raw.split(",") if x.strip()] if not_raw else None
|
||||
) or None
|
||||
post_id_raw = request.args.get("post_id")
|
||||
post_id = int(post_id_raw) if post_id_raw else None
|
||||
artist_id_raw = request.args.get("artist_id")
|
||||
@@ -64,7 +78,9 @@ def _parse_filters():
|
||||
date_to += timedelta(days=1) # inclusive of the date_to calendar day
|
||||
filters = {
|
||||
"tag_ids": tag_ids, "post_id": post_id, "artist_id": artist_id,
|
||||
"media_type": media_type, "platform": platform,
|
||||
"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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user