Skip to content

Sort and read by alternate series (#360) - #830

Merged
ajslater merged 1 commit into
developfrom
claude/codex-issue-360-plan-pjbmz1
Aug 30, 2026
Merged

Sort and read by alternate series (#360)#830
ajslater merged 1 commit into
developfrom
claude/codex-issue-360-plan-pjbmz1

Conversation

@ajslater

Copy link
Copy Markdown
Owner

Closes #360.

ComicInfo AlternateSeries / AlternateNumber already import into the Reprint model, and there's already an "Alternate Series" table column, filter and full-text search field. What was missing is the two things the issue actually asks for: ordering by the alternate numbering, and reading an alternate series through in order. Alternate series tags make durable reading lists — they survive a re-import, rename or re-tag — which is the requester's use case.

Alternate Number sort

Reprint.issue is a string, so #10 sorted before #2. Split it into issue_number / issue_suffix columns mirroring Comic's, derived in presave() and backfilled by migration 0053. Reprint.save() now calls presave() (the importer's bulk paths already did).

The sort scopes to the alternate series picked by the reprints filter through a FilteredRelation, the same way story_arc_number scopes to the browsed arc — there's no alternate-series browse collection, so the filter is the only context. Comics carrying no alternate number fall back to their own issue number (coalesced inside the aggregate, so each comic contributes its own effective value) rather than collapsing to NULL. With no filter at all the key degrades to the plain issue sort.

Alternate Series sort in cover view

Added to the cover-view sort dropdown. Comic rows now sort through a Coalesce onto the real series sort_name, so comics with no alternate series interleave by their actual series instead of clumping under an empty aggregate. The table cell still renders the full JSON label list — the fallback is a sort-only annotation.

Reader reading order

The reader offers an alternate series as a reading order; next/prev follow AlternateNumber (number, then suffix, then date). An alternate series is identified by (series_name, volume_number, language) — the Reprint unique key minus the issue — so a v1 and a v2, or an English and a Spanish edition, stay separate reading orders.

The arc handle is the whole group's Reprint pks, not just the current comic's: each issue of an alternate series is its own Reprint row, so a per-comic handle would report different ids on the next book and silently drop the reading order.

Reprint rows join the TimestampUpdater re-stamp flow and the mtime endpoint accepts reprint arcs, so an open reader still notices a re-import. reprints is a reader-only pseudo-collection, deliberately not a Collection enum member — those maps are exhaustive over the enum and assume a browse route and a cover exist.

No new browse collection and no OPDS navigation changes, per the agreed scope.

Incidental fix

An M2M primary sort outside table view (cover cards, OPDS feeds) emitted an ORDER BY alias that was only annotated in table mode, raising a FieldError. It was unreachable through the UI before, but surfacing the Alternate Series sort in cover view makes it a normal path, so the annotation gate is fixed here.

Testing

  • tests/test_browser_reprints_column.py — numeric ordering (2, 3, 10 where a string sort gives 10, 2, 3), reverse, suffix tiebreak, fallback to issue number, degradation with no filter, collection-row aggregation, cover-view label sort, series-name fallback, and multi-sort-extra alias resolution.
  • tests/test_reader.py — the arc is offered and named, the handle covers the whole group, prev/next follow 2 → 3 → 10, arc selection survives the next book, an absent alternate series falls back rather than erroring, the mtime probe accepts the arc, and TimestampUpdater re-stamps reprints.
  • frontend/tests/unit/order-by-caption.test.js, frontend/tests/unit/reader-arc-select.test.js.
  • Full backend suite: 995 passed. ruff and ty clean. Frontend: 437 passed; stats-tab.test.js fails identically on unmodified develop (pre-existing, unrelated).

Frontend tests need Node 24 — this container's Node 22 lacks Intl.DurationFormat, which src/datetime.js constructs at import; I ran them behind a local-only shim.


Generated by Claude Code

ComicInfo AlternateSeries / AlternateNumber already imported into the
Reprint model, but there was no way to order by the alternate numbering
and no way to read an alternate series through in order. Alternate
series tags make durable reading lists — they survive a re-import,
rename or re-tag — which is what issue #360 asks for.

Add an "Alternate Number" sort. Reprint.issue is a string, so "#10"
sorted before "#2"; split it into issue_number / issue_suffix columns
mirroring Comic's, derived in presave and backfilled by migration. The
sort scopes to the alternate series picked by the reprints filter via a
FilteredRelation, the way story_arc_number scopes to the browsed arc.
Comics carrying no alternate number fall back to their own issue number
so a mixed listing stays readable rather than collapsing to NULL.

Surface the existing "Alternate Series" sort in cover view too, and
sort it through a Coalesce onto the real series sort_name so untagged
comics interleave instead of clumping under an empty aggregate.

Let the reader follow an alternate series as a reading order. An
alternate series is identified by (series_name, volume_number,
language) — the Reprint unique key minus the issue — and the arc handle
is the whole group's pks, since each issue is its own Reprint row and a
per-comic handle would change from book to book. Reprint rows join the
TimestampUpdater re-stamp flow and the mtime probe accepts them, so an
open reader still notices a re-import.

Also fixes a latent error: an M2M primary sort outside table view
(cover cards, OPDS) emitted an ORDER BY alias that was only annotated
in table mode. Unreachable through the UI before, but surfacing the
Alternate Series sort in cover view makes it a normal path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrB4hZ7HtGE9YjV5EkXeQ3
@ajslater
ajslater marked this pull request as ready for review August 30, 2026 06:51
@ajslater
ajslater merged commit 9cdb6fa into develop Aug 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants