The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
106 lines
3.8 KiB
Python
106 lines
3.8 KiB
Python
"""Adding a video AT a position: the list page's "Undo" of a removal.
|
|
|
|
The row has to come back where it was in ONE request. The two-call shape (append, then send the
|
|
whole order) leaves it at the end of the list whenever the second call fails, and the reorder would
|
|
also overwrite any change another tab made in between. Positions can have gaps after removals, so
|
|
the position is an index over the ORDERED rows, and the list is renumbered around it.
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from app.models import Channel, Playlist, PlaylistItem, Video
|
|
|
|
|
|
def _seed(db, user, ids, *, positions=None, **kw):
|
|
db.add(Channel(id="UCtest", title="Test"))
|
|
now = datetime.now(timezone.utc)
|
|
for v in [*ids, "vX"]:
|
|
db.add(Video(id=v, channel_id="UCtest", published_at=now))
|
|
pl = Playlist(user_id=user.id, name="L", **kw)
|
|
db.add(pl)
|
|
db.flush()
|
|
for i, vid in enumerate(ids):
|
|
db.add(PlaylistItem(playlist_id=pl.id, video_id=vid, position=(positions or range(99))[i]))
|
|
db.commit()
|
|
return pl
|
|
|
|
|
|
def _ids(db, pl_id):
|
|
db.expire_all()
|
|
return [
|
|
r.video_id
|
|
for r in db.execute(
|
|
PlaylistItem.__table__.select()
|
|
.where(PlaylistItem.playlist_id == pl_id)
|
|
.order_by(PlaylistItem.position)
|
|
).all()
|
|
]
|
|
|
|
|
|
def _add(client, pl_id, body):
|
|
return client.post(f"/api/playlists/{pl_id}/items", json=body)
|
|
|
|
|
|
def test_a_position_puts_the_video_back_in_its_place(db, make_user, as_user):
|
|
user = make_user()
|
|
pl = _seed(db, user, ["a", "b", "c"])
|
|
r = _add(as_user(user), pl.id, {"video_id": "vX", "position": 1})
|
|
assert r.status_code == 200
|
|
assert _ids(db, pl.id) == ["a", "vX", "b", "c"]
|
|
|
|
|
|
def test_the_position_counts_rows_not_stored_numbers(db, make_user, as_user):
|
|
# A removal leaves a gap (0, 5, 9): index 2 is before the THIRD row, not "position 2".
|
|
user = make_user()
|
|
pl = _seed(db, user, ["a", "b", "c"], positions=[0, 5, 9])
|
|
_add(as_user(user), pl.id, {"video_id": "vX", "position": 2})
|
|
assert _ids(db, pl.id) == ["a", "b", "vX", "c"]
|
|
|
|
|
|
def test_a_position_past_the_end_appends(db, make_user, as_user):
|
|
user = make_user()
|
|
pl = _seed(db, user, ["a", "b"])
|
|
_add(as_user(user), pl.id, {"video_id": "vX", "position": 40})
|
|
assert _ids(db, pl.id) == ["a", "b", "vX"]
|
|
|
|
|
|
def test_no_position_still_appends(db, make_user, as_user):
|
|
user = make_user()
|
|
pl = _seed(db, user, ["a", "b"])
|
|
_add(as_user(user), pl.id, {"video_id": "vX"})
|
|
assert _ids(db, pl.id) == ["a", "b", "vX"]
|
|
|
|
|
|
def test_a_video_already_in_the_list_does_not_move(db, make_user, as_user):
|
|
# The add is idempotent; a position must not turn it into a reorder.
|
|
user = make_user()
|
|
pl = _seed(db, user, ["a", "b", "c"])
|
|
_add(as_user(user), pl.id, {"video_id": "c", "position": 0})
|
|
assert _ids(db, pl.id) == ["a", "b", "c"]
|
|
|
|
|
|
def test_a_negative_position_is_refused(db, make_user, as_user):
|
|
user = make_user()
|
|
pl = _seed(db, user, ["a"])
|
|
r = _add(as_user(user), pl.id, {"video_id": "vX", "position": -1})
|
|
assert r.status_code == 422
|
|
assert _ids(db, pl.id) == ["a"]
|
|
|
|
|
|
def test_putting_a_removed_video_back_leaves_a_linked_list_clean(db, make_user, as_user):
|
|
# Remove + undo is no change at all: a YouTube-linked list must not stay "unsynced" after it.
|
|
user = make_user()
|
|
pl = _seed(db, user, ["a", "b", "c"], yt_playlist_id="PLx")
|
|
client = as_user(user)
|
|
from app.sync.playlists import current_fingerprint
|
|
|
|
pl.synced_fingerprint = current_fingerprint(db, pl)
|
|
pl.dirty = False
|
|
db.commit()
|
|
assert client.delete(f"/api/playlists/{pl.id}/items/b").status_code == 200
|
|
db.expire_all()
|
|
assert db.get(Playlist, pl.id).dirty is True
|
|
_add(client, pl.id, {"video_id": "b", "position": 1})
|
|
db.expire_all()
|
|
assert db.get(Playlist, pl.id).dirty is False
|