Commit 3f05ead
feat(server): add bulk asset management (enable/disable/edit/delete) (#3048)
* feat(server): add bulk asset management (enable/disable/edit/delete)
Operators with large libraries previously had to act on assets one
row at a time — a recurring forum request for years (#3046). The home
page now has per-row selection checkboxes, a per-section select-all in
each surface header, and a floating bulk-action bar (Enable, Disable,
Edit, Delete) that appears whenever a selection is active.
Backend: two new server-rendered endpoints reuse the shared
_asset_table_response so the whole batch swaps the table partial and
nudges the viewer once, instead of one round-trip per row.
- assets_bulk_action — enable/disable/delete a selected set; delete
goes through delete_asset_with_file so on-disk cleanup matches the
per-asset path (#2908).
- assets_bulk_update — applies common schedule fields (start/end
dates, duration, play-from/until times, weekdays). Each group is
opt-in via an apply_* flag so an operator only overwrites the
fields they ticked, and everything is parsed before any row is
mutated so a bad date/time toasts without a half-applied batch.
Video duration stays owned by the probe task, mirroring
assets_update.
Frontend: selection lives in the homeApp Alpine state (selectedIds),
so row :checked bindings re-evaluate after the table's 5s HTMX swap and
the selection survives. setVisible() is re-published from the table
partial on every swap to drive the select-all state and prune a
selection of rows that have since disappeared. The bulk-edit modal
reuses the existing flatpickr date/time inputs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(server): address Copilot review on bulk asset management
- asset_ids filter: stop mark_safe'ing the JSON. It lands in a
double-quoted x-init="…" attribute, so its own double quotes were
closing the attribute early and breaking the Alpine expression.
Return a plain str and let Django autoescaping entity-encode the
quotes; the browser decodes them back to valid JSON. Adds a
regression test asserting the rendered attribute is escaped.
- bulk delete: pass delete_asset_with_file(nudge_viewer=False) per row
and fire a single viewer reload after the batch, instead of one
reload per asset. New keyword-only flag defaults True so the API /
single-delete paths are unchanged.
- bulk enable/disable: collapse the per-row save() loop into one
queryset update() (no model signals here); its row count drives the
toast and the empty-selection guard.
- bulk duration: a blank field with apply_duration on no longer
clobbers every asset's duration to 0 — it toasts and changes
nothing (mirrors the per-asset edit form's preserve-on-blank intent).
Negative values are rejected too, and the modal input is now
required as client-side defense-in-depth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(server): collapse bulk_update writes into a single bulk_update()
Second Copilot pass on #3048: assets_bulk_update still did one
asset.save() per selected row, so a large selection (the point of bulk
editing) fired N UPDATEs. Mutate the in-memory objects, track exactly
which columns were touched (honouring the skip-videos-for-duration
rule), and write them all with one Asset.objects.bulk_update(). An
edit that ends up touching nothing (apply_dates ticked with both
fields blank, or a duration-only edit on an all-video selection) now
short-circuits with the "nothing to change" toast, since bulk_update()
rejects an empty field list. Adds a test asserting exactly one UPDATE
statement for a 5-asset batch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(server): keep bulk duration write off video rows; clear modal default
Third Copilot pass on #3048:
- The single bulk_update() folded `duration` into the all-rows field
list, which writes every video row's stale in-memory duration back
and can clobber a concurrent probe_video_duration UPDATE. Write
duration on its own bulk_update() over only the non-video subset, so
video rows are never touched by the duration column at all. Added a
test spying on bulk_update to assert no video object is in a
duration write.
- The bulk-edit duration input was prefilled with 10, so toggling
"Duration" and submitting would silently set every non-video asset
to 10s. Drop the default to an empty placeholder and rely on the
existing `required` to force an intentional value.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(server): prune selection once across both sections; accurate bulk count
Fourth Copilot pass on #3048:
- syncVisibleIds(active, inactive): the table partial now publishes both
sections' ids in one call and selectedIds is pruned once against
their union. The previous two sequential setVisible() calls pruned on
the first call while the other section's list was still stale, so a
row that moved between sections (e.g. an enabled asset just disabled)
lost its selection across the swap.
- assets_bulk_update toast now reports only the rows actually written:
a duration-only edit skips videos, so a mixed selection reports the
non-video count instead of claiming every row was updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(server): bulk forms clear only on real success; O(n) section selection
Fifth Copilot pass on #3048:
- Bulk endpoints return 200 even when they refuse the input (bad date,
partial time window, blank duration, "nothing to change") and just
ride an error/info toast on HX-Trigger. The forms cleared the
selection / closed the modal on any 2xx, wiping the operator's work
when nothing was applied. New isSuccessResponse(event) helper gates
the clear/close on a 2xx whose toast is absent or kind 'success';
wired into all four bulk forms (enable/disable/delete/edit).
- sectionAllSelected/sectionSomeSelected did linear includes() against
selectedIds, i.e. O(visible × selected) on every reactive
re-evaluation. Build a Set once per call so they stay O(n) for the
large selections bulk editing targets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(server): trim bulk ids; guard enable/disable on matched-rows count
Sixth Copilot pass on #3048:
- _bulk_ids() now strips each CSV segment, so a hand-built "a, b" (with
spaces) still matches its rows instead of silently no-op'ing.
- enable/disable take the count from a separate matched-rows count()
rather than update()'s return value, which reports rows *changed* on
some backends — re-enabling already-enabled assets would otherwise
count 0 and (returning no toast) make the client treat it as success
and clear the selection. A genuinely empty match now returns an info
toast so the client's success gate keeps the selection. The empty
bulk-delete case gets the same info toast.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(server): info toast on bulk_update with no matching ids
Seventh Copilot pass on #3048: assets_bulk_update returned a silent
no-toast 2xx when the posted ids matched no rows (stale selection), so
the client's success gate cleared the selection / closed the modal
even though nothing applied. Return the same info toast the
enable/disable path uses so the selection is kept. Adds a test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(server): bulk_update via uniform queryset update()s; toast on no-op action
Eighth Copilot pass on #3048:
- assets_bulk_action no longer returns a silent no-toast 2xx for an
unknown action or empty id set — an unknown action toasts an error,
an empty selection an info toast, so the client's success gate keeps
the selection instead of treating it as success.
- assets_bulk_update applies the (uniform) new values with plain
queryset update()s instead of loading every selected row and building
a bulk_update() CASE. Shared fields go in one update(**shared);
duration goes through a separate exclude(mimetype='video').update(),
which keeps the never-touch-video-duration guarantee at the SQL level
(no in-memory staleness). Row counts come from matched-rows count()s,
not update()'s changed-rows return. Replaced the bulk_update-spy test
with a SQL-level video-duration-untouched test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(server): bridge bulk hx-on handlers into Alpine via window event
Ninth Copilot pass on #3048 — and a real runtime bug: the bulk forms'
hx-on::after-request handlers called Alpine component methods
(isSuccessResponse / clearSelection / closeBulkEdit / bulkDeleteOpen)
directly, but hx-on runs in GLOBAL scope, so those are undefined there
and the handler threw ReferenceError — the selection never cleared and
the modals never closed on success.
Fixed with the same dispatch-to-window bridge the Add modal already
uses for 'asset-saved': hx-on now calls the global window.bulkSucceeded()
gate and, on success, dispatches a 'bulk-done' window CustomEvent (with
detail flags for which modal to close). The root x-data handles it via
@bulk-done.window="onBulkDone($event)" in Alpine scope. Added a render
test asserting the forms use the bridge and never call Alpine methods
from hx-on.
Also: assets_bulk_update now returns an info toast on an empty id set
(was a silent no-toast 2xx the success gate would mis-read as success).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent 9094a20 commit 3f05ead
13 files changed
Lines changed: 1566 additions & 17 deletions
File tree
- src/anthias_server/app
- static
- sass
- src
- templates
- templatetags
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
116 | 116 | | |
117 | 117 | | |
118 | 118 | | |
119 | | - | |
| 119 | + | |
120 | 120 | | |
121 | 121 | | |
122 | 122 | | |
| |||
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
135 | 139 | | |
136 | 140 | | |
137 | 141 | | |
| |||
147 | 151 | | |
148 | 152 | | |
149 | 153 | | |
150 | | - | |
| 154 | + | |
| 155 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
477 | 477 | | |
478 | 478 | | |
479 | 479 | | |
480 | | - | |
481 | | - | |
482 | | - | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
483 | 485 | | |
484 | 486 | | |
485 | 487 | | |
486 | 488 | | |
487 | | - | |
488 | | - | |
489 | | - | |
490 | | - | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
491 | 502 | | |
492 | 503 | | |
493 | 504 | | |
494 | 505 | | |
495 | | - | |
496 | | - | |
| 506 | + | |
| 507 | + | |
497 | 508 | | |
498 | 509 | | |
499 | 510 | | |
| |||
506 | 517 | | |
507 | 518 | | |
508 | 519 | | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
509 | 553 | | |
510 | 554 | | |
511 | 555 | | |
| |||
1206 | 1250 | | |
1207 | 1251 | | |
1208 | 1252 | | |
| 1253 | + | |
| 1254 | + | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
1209 | 1263 | | |
1210 | 1264 | | |
1211 | 1265 | | |
| |||
2335 | 2389 | | |
2336 | 2390 | | |
2337 | 2391 | | |
| 2392 | + | |
| 2393 | + | |
| 2394 | + | |
| 2395 | + | |
| 2396 | + | |
| 2397 | + | |
| 2398 | + | |
| 2399 | + | |
| 2400 | + | |
| 2401 | + | |
| 2402 | + | |
| 2403 | + | |
| 2404 | + | |
| 2405 | + | |
| 2406 | + | |
| 2407 | + | |
| 2408 | + | |
| 2409 | + | |
| 2410 | + | |
| 2411 | + | |
| 2412 | + | |
| 2413 | + | |
| 2414 | + | |
| 2415 | + | |
| 2416 | + | |
| 2417 | + | |
| 2418 | + | |
| 2419 | + | |
| 2420 | + | |
| 2421 | + | |
| 2422 | + | |
| 2423 | + | |
| 2424 | + | |
| 2425 | + | |
| 2426 | + | |
| 2427 | + | |
| 2428 | + | |
| 2429 | + | |
| 2430 | + | |
| 2431 | + | |
| 2432 | + | |
| 2433 | + | |
| 2434 | + | |
| 2435 | + | |
| 2436 | + | |
| 2437 | + | |
| 2438 | + | |
| 2439 | + | |
| 2440 | + | |
| 2441 | + | |
| 2442 | + | |
| 2443 | + | |
| 2444 | + | |
| 2445 | + | |
| 2446 | + | |
| 2447 | + | |
| 2448 | + | |
| 2449 | + | |
| 2450 | + | |
| 2451 | + | |
| 2452 | + | |
| 2453 | + | |
| 2454 | + | |
| 2455 | + | |
| 2456 | + | |
| 2457 | + | |
| 2458 | + | |
| 2459 | + | |
| 2460 | + | |
| 2461 | + | |
| 2462 | + | |
| 2463 | + | |
| 2464 | + | |
| 2465 | + | |
| 2466 | + | |
| 2467 | + | |
| 2468 | + | |
| 2469 | + | |
| 2470 | + | |
| 2471 | + | |
| 2472 | + | |
| 2473 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
| |||
37 | 38 | | |
38 | 39 | | |
39 | 40 | | |
| 41 | + | |
| 42 | + | |
40 | 43 | | |
41 | 44 | | |
42 | 45 | | |
| |||
48 | 51 | | |
49 | 52 | | |
50 | 53 | | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
51 | 59 | | |
52 | 60 | | |
53 | 61 | | |
| |||
58 | 66 | | |
59 | 67 | | |
60 | 68 | | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
61 | 80 | | |
62 | 81 | | |
63 | 82 | | |
| |||
107 | 126 | | |
108 | 127 | | |
109 | 128 | | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
110 | 133 | | |
111 | 134 | | |
112 | 135 | | |
| |||
117 | 140 | | |
118 | 141 | | |
119 | 142 | | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
120 | 148 | | |
121 | 149 | | |
122 | 150 | | |
| |||
222 | 250 | | |
223 | 251 | | |
224 | 252 | | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
225 | 330 | | |
226 | 331 | | |
227 | 332 | | |
| |||
585 | 690 | | |
586 | 691 | | |
587 | 692 | | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
588 | 717 | | |
589 | 718 | | |
590 | 719 | | |
| |||
0 commit comments