feat(series): operator-set sparse page numbers + gap blocks (#789 tweak)
Replaces the auto-renumbered 1..N position key with operator-OWNED page numbers: sparse, gaps allowed, editable, never auto-renumbered. Order follows the numbers; unnumbered pages sort to the tail. This is the fix for the model that clobbered hand-set numbers on the flatten — numbers are now data, not a derived sequence. - series_service: drop the renumber-on-reorder/remove; order by page_number NULLS LAST; new set_page_number(image_id, n|None); list_pages returns `gaps` (one entry per missing-number run) + each pending group's parsed `start_page`; set_cover renumbers below the current min; place_pending(image_ids, start_page) numbers placed pages sequentially from the start (drop junk first → numbers line up); add_post stamps the parsed start on staged pages. - api/tags: POST /series/<id>/pages/number (set one page's number); /pending/ place takes start_page; removed /reorder. - frontend: per-card editable number input; one gap block per gap with drop-on-edge to assign the adjacent number (middle → type); append drop zone; pending tray gets a "from page N" field + "Place from page N". - tests reworked: sparse numbers + gaps, place-from-start, set-page-number route. No migration; nothing destructive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,10 +44,10 @@ async def test_add_over_cap_400(client):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reorder_mismatch_400(client):
|
||||
sid = await _series(client, "Re")
|
||||
async def test_set_page_number_requires_image_id(client):
|
||||
sid = await _series(client, "Num")
|
||||
resp = await client.post(
|
||||
f"/api/series/{sid}/reorder", json={"image_ids": [1, 2, 3]}
|
||||
f"/api/series/{sid}/pages/number", json={"page_number": 5}
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@@ -76,25 +76,29 @@ async def test_place_pending_appends_and_flips_status(db):
|
||||
imgs = await _post_images(db, post, artist, 3)
|
||||
await svc.add_post(sid, post.id)
|
||||
|
||||
placed = await svc.place_pending(sid, imgs)
|
||||
placed = await svc.place_pending(sid, imgs, start_page=3)
|
||||
assert placed == 3
|
||||
assert await _placed_order(db, sid) == seed + imgs
|
||||
# nothing left pending; page numbers compact 1..5
|
||||
# nothing left pending; the placed pages are numbered 1,2 (seed) + 3,4,5
|
||||
data = await svc.list_pages(sid)
|
||||
assert data["pending"] == []
|
||||
assert [p["page_number"] for p in data["pages"]] == [1, 2, 3, 4, 5]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_place_pending_before_a_placed_page(db):
|
||||
svc, artist, sid, seed = await _setup(db, "PB")
|
||||
a, b = seed
|
||||
async def test_place_pending_numbers_from_start(db):
|
||||
svc, artist, sid, seed = await _setup(db, "PB") # seed numbered 1, 2
|
||||
post = await _post(db, artist, "PB pages", "pb-p")
|
||||
x, y = await _post_images(db, post, artist, 2)
|
||||
await svc.add_post(sid, post.id)
|
||||
|
||||
await svc.place_pending(sid, [x, y], before_image_id=b)
|
||||
assert await _placed_order(db, sid) == [a, x, y, b]
|
||||
await svc.place_pending(sid, [x, y], start_page=10)
|
||||
data = await svc.list_pages(sid)
|
||||
nums = {p["image_id"]: p["page_number"] for p in data["pages"]}
|
||||
assert nums[x] == 10
|
||||
assert nums[y] == 11
|
||||
# order = seed (1, 2) then x (10), y (11)
|
||||
assert [p["image_id"] for p in data["pages"]] == seed + [x, y]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -84,17 +84,24 @@ async def test_remove(db):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reorder_rewrites_and_rejects_mismatch(db):
|
||||
async def test_set_page_number_sparse_with_gaps(db):
|
||||
s = await TagService(db).find_or_create("S4", TagKind.series)
|
||||
i1, i2, i3 = await _img(db), await _img(db), await _img(db)
|
||||
svc = SeriesService(db)
|
||||
await svc.add_images(s.id, [i1, i2, i3])
|
||||
await svc.reorder(s.id, [i3, i1, i2])
|
||||
assert await _order(db, s.id) == [i3, i1, i2]
|
||||
await svc.add_images(s.id, [i1, i2, i3]) # numbered 1, 2, 3
|
||||
# set a sparse number, leaving a gap
|
||||
await svc.set_page_number(s.id, i3, 5)
|
||||
out = await svc.list_pages(s.id)
|
||||
assert [p["page_number"] for p in out["pages"]] == [1, 2, 5]
|
||||
assert len(out["gaps"]) == 1
|
||||
assert out["gaps"][0]["from"] == 3
|
||||
assert out["gaps"][0]["to"] == 4
|
||||
# unnumber a page → it sorts to the tail
|
||||
await svc.set_page_number(s.id, i1, None)
|
||||
out = await svc.list_pages(s.id)
|
||||
assert [p["page_number"] for p in out["pages"]] == [2, 5, None]
|
||||
with pytest.raises(SeriesError):
|
||||
await svc.reorder(s.id, [i1, i2])
|
||||
with pytest.raises(SeriesError):
|
||||
await svc.reorder(s.id, [i1, i2, i3, 99999])
|
||||
await svc.set_page_number(s.id, 99999, 1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user