feat(gallery): sort by earliest post date across all posts (new default)
The gallery's newest/oldest sort keys off image_record.effective_date = COALESCE(primary post's post_date, created_at). The primary post is often the repost/download the file came from, so the grid led with download dates rather than when content was first posted (operator-flagged). Add a second materialized sort key, earliest_post_date = MIN(post_date) across ALL of an image's provenance posts (every post it appears in), else created_at — the original publish date. Mirrors the effective_date pattern so the sort stays a forward index scan. - alembic 0071: add earliest_post_date + index (DESC, id DESC); backfill created_at baseline then MIN over image_provenance ⋈ post. - importer: recompute earliest_post_date whenever a dated post is linked (MIN over the image's provenance, which now includes the just-added row). - gallery_service: new sorts posted_new / posted_old key off earliest_post_date; cursor + year/month grouping follow the active column transparently. - api: accept posted_new|posted_old; DEFAULT is now posted_new so the grid leads with original publish date. newest/oldest (effective_date) still available. - frontend: sort dropdown gains "Newest/Oldest post date" (default Newest post date); existing effective-date sorts relabelled "Newest/Oldest added". - tests: service test asserts posted_new/posted_old key off earliest_post_date; frontend default-sort omission test updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
@@ -55,6 +55,25 @@ def _effective_date_col():
|
||||
return ImageRecord.effective_date
|
||||
|
||||
|
||||
# Sort key -> the materialized column the gallery orders + cursors on. Both are
|
||||
# indexed (DESC, id DESC), so every sort is a forward index range scan.
|
||||
# newest/oldest → effective_date (primary post's date, else download)
|
||||
# posted_new/_old → earliest_post_date (earliest publish across ALL posts)
|
||||
_SORT_COLUMNS = {
|
||||
"newest": ImageRecord.effective_date,
|
||||
"oldest": ImageRecord.effective_date,
|
||||
"posted_new": ImageRecord.earliest_post_date,
|
||||
"posted_old": ImageRecord.earliest_post_date,
|
||||
}
|
||||
_ASCENDING_SORTS = {"oldest", "posted_old"}
|
||||
|
||||
|
||||
def _sort_column(sort: str):
|
||||
"""The materialized date column a gallery sort orders/cursors on (falls back
|
||||
to effective_date for any unknown sort)."""
|
||||
return _SORT_COLUMNS.get(sort, ImageRecord.effective_date)
|
||||
|
||||
|
||||
def _outer_join_primary_post(stmt: Select) -> Select:
|
||||
"""LEFT JOIN Post on ImageRecord.primary_post_id so the COALESCE
|
||||
above sees Post.post_date when available. Images without a post
|
||||
@@ -406,7 +425,10 @@ class GalleryService:
|
||||
tag_ids, post_id, artist_id, tag_or_groups, tag_exclude,
|
||||
)
|
||||
|
||||
eff = _effective_date_col()
|
||||
# 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
|
||||
# 'post date' sort paginates + buckets by original publish transparently.
|
||||
eff = _sort_column(sort)
|
||||
stmt = select(ImageRecord, Post.post_date, eff.label("eff"))
|
||||
stmt = _outer_join_primary_post(stmt)
|
||||
stmt = _apply_scope(
|
||||
@@ -417,7 +439,7 @@ class GalleryService:
|
||||
date_from=date_from, date_to=date_to,
|
||||
)
|
||||
|
||||
descending = sort != "oldest"
|
||||
descending = sort not in _ASCENDING_SORTS
|
||||
if cursor:
|
||||
cur_ts, cur_id = decode_cursor(cursor)
|
||||
# The cursor is just (last eff, last id); the request's sort
|
||||
|
||||
@@ -17,7 +17,7 @@ from enum import StrEnum
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -1411,6 +1411,24 @@ class Importer:
|
||||
# default (matches the old COALESCE(post_date, created_at) fallback).
|
||||
if record.primary_post_id == post.id and post.post_date is not None:
|
||||
record.effective_date = post.post_date
|
||||
# earliest_post_date (alembic 0071) = MIN(post_date) across ALL of this
|
||||
# image's provenance posts, not just the primary — so the gallery can
|
||||
# sort by original publish rather than the download/repost the primary
|
||||
# points at. Recompute from provenance whenever a dated post is linked;
|
||||
# the provenance row for THIS post was committed above, so the MIN
|
||||
# includes it. Leaves the created_at default when no linked post is dated.
|
||||
if post.post_date is not None:
|
||||
earliest = self.session.execute(
|
||||
select(func.min(Post.post_date))
|
||||
.select_from(ImageProvenance)
|
||||
.join(Post, Post.id == ImageProvenance.post_id)
|
||||
.where(
|
||||
ImageProvenance.image_record_id == record.id,
|
||||
Post.post_date.is_not(None),
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if earliest is not None:
|
||||
record.earliest_post_date = earliest
|
||||
self.session.flush()
|
||||
|
||||
def _copy_to_library(
|
||||
|
||||
Reference in New Issue
Block a user