Is your feature request related to a problem? Please describe.
Sub-issues 01–06 build the entire persistence machinery, but none of it is visible in the admin yet. An operator opening the mass-upgrade confirmation page sees no is_persistent checkbox, the operation list has no way to find pending ops, the detail page doesn't show retry_count or next_retry_at, the batch list doesn't show whether a batch is persistent, and the WebSocket-driven progress display renders pending ops with the wrong indicator (it'd default to the in-progress spinner).
This sub-issue is the admin-side polish: form, list view, detail view, list filters, and the static CSS/JS that makes the pending status visually distinct.
Describe the solution I would implement
I would like to extend the admin form, list, detail, and frontend assets so every persistence affordance the rest of the work introduces is actually reachable.
-
Add an is_persistent BooleanField to BatchUpgradeConfirmationForm:
initial=True so the checkbox is pre-checked when an operator opens the page.
required=False so unchecking it submits False (Django populates cleaned_data["is_persistent"] either way).
help_text explaining the retry-until-online behaviour.
- Add
"is_persistent" to the form's Meta.fields.
-
Update BuildAdmin.upgrade_selected (admin.py:185–301):
- The existing pattern reads each form input from
request.POST, rebuilds BatchUpgradeConfirmationForm with that data, calls form.full_clean(), then reads form.cleaned_data to invoke build.batch_upgrade(...). I'd extend it: read is_persistent from request.POST, include it in the form data, then pass persistent=form.cleaned_data["is_persistent"] to batch_upgrade().
- Update
Build.batch_upgrade() (base/models.py:164–191) from (self, firmwareless, upgrade_options=None, group=None, location=None) to add is_is_persistent=True, and set the value on the BatchUpgradeOperation instance before batch.full_clean().
-
Update UpgradeOperationAdmin:
list_display: add is_persistent and retry_count.
list_filter: add is_persistent. The existing status filter automatically picks up "pending" once sub-issue 01 extends STATUS_CHOICES.
readonly_fields and fields: add is_persistent, retry_count, next_retry_at so they render as read-only on the detail form.
-
Update BatchUpgradeOperationAdmin:
- Add
is_persistent to list_display so admins can tell persistent batches apart at a glance.
- Add
is_persistent to readonly_fields for the detail view — UI-level reinforcement of sub-issue 01's model-level immutability guard.
-
Add frontend handling for the new pending status under openwisp_firmware_upgrader/static/firmware-upgrader/:
- CSS: a
.status-pending class (e.g., orange/amber) visually distinct from the in-progress spinner.
- JS: map
pending in the WebSocket message handler to a "waiting for device" indicator, and consume the new pending field that sub-issue 03 adds to the batch-status WebSocket payload. The per-op UpgradeProgressPublisher already forwards instance.status verbatim, but the consumer-side snapshot methods and publish_batch_status payload shape are getting changes in sub-issue 03 — not "no Python change", just changes localised there.
-
Update the batch detail page progress display to surface the pending count alongside completed (e.g., "5 complete, 2 pending out of 7"), reading from sub-issue 03's new pending_count property on AbstractBatchUpgradeOperation.
-
The admin's cancel UI is a template button (templates/admin/upgrade_selected_confirmation.html:97, JS handler in admin/js/cancel.js) that calls the REST API endpoint UpgradeOperationCancelView at api/views.py:381–451 — there's no Django admin action for cancellation. Once sub-issue 03 extends _CANCELLABLE_STATUS, the shared instance.cancel() model method covers pending ops, so the button works without any admin-side change. Templates use get_status_display() rather than hardcoded status strings, so no template edits are required either.
-
Admin tests (Django test client, not Selenium — that lives in sub-issue 09) covering: form renders the checkbox pre-checked; submitting with is_is_persistent=False/True produces a correctly-flagged batch and propagates to children; ?status=pending filter returns only pending ops; detail page exposes the new read-only fields; batch list shows the is_persistent column; cancel button transitions a pending op to cancelled.
Is your feature request related to a problem? Please describe.
Sub-issues 01–06 build the entire persistence machinery, but none of it is visible in the admin yet. An operator opening the mass-upgrade confirmation page sees no
is_persistentcheckbox, the operation list has no way to find pending ops, the detail page doesn't showretry_countornext_retry_at, the batch list doesn't show whether a batch is persistent, and the WebSocket-driven progress display renders pending ops with the wrong indicator (it'd default to the in-progress spinner).This sub-issue is the admin-side polish: form, list view, detail view, list filters, and the static CSS/JS that makes the pending status visually distinct.
Describe the solution I would implement
I would like to extend the admin form, list, detail, and frontend assets so every persistence affordance the rest of the work introduces is actually reachable.
Add an
is_persistentBooleanFieldtoBatchUpgradeConfirmationForm:initial=Trueso the checkbox is pre-checked when an operator opens the page.required=Falseso unchecking it submitsFalse(Django populatescleaned_data["is_persistent"]either way).help_textexplaining the retry-until-online behaviour."is_persistent"to the form'sMeta.fields.Update
BuildAdmin.upgrade_selected(admin.py:185–301):request.POST, rebuildsBatchUpgradeConfirmationFormwith that data, callsform.full_clean(), then readsform.cleaned_datato invokebuild.batch_upgrade(...). I'd extend it: readis_persistentfromrequest.POST, include it in the form data, then passpersistent=form.cleaned_data["is_persistent"]tobatch_upgrade().Build.batch_upgrade()(base/models.py:164–191) from(self, firmwareless, upgrade_options=None, group=None, location=None)to addis_is_persistent=True, and set the value on theBatchUpgradeOperationinstance beforebatch.full_clean().Update
UpgradeOperationAdmin:list_display: addis_persistentandretry_count.list_filter: addis_persistent. The existingstatusfilter automatically picks up "pending" once sub-issue 01 extendsSTATUS_CHOICES.readonly_fieldsandfields: addis_persistent,retry_count,next_retry_atso they render as read-only on the detail form.Update
BatchUpgradeOperationAdmin:is_persistenttolist_displayso admins can tell persistent batches apart at a glance.is_persistenttoreadonly_fieldsfor the detail view — UI-level reinforcement of sub-issue 01's model-level immutability guard.Add frontend handling for the new
pendingstatus underopenwisp_firmware_upgrader/static/firmware-upgrader/:.status-pendingclass (e.g., orange/amber) visually distinct from the in-progress spinner.pendingin the WebSocket message handler to a "waiting for device" indicator, and consume the newpendingfield that sub-issue 03 adds to the batch-status WebSocket payload. The per-opUpgradeProgressPublisheralready forwardsinstance.statusverbatim, but the consumer-side snapshot methods andpublish_batch_statuspayload shape are getting changes in sub-issue 03 — not "no Python change", just changes localised there.Update the batch detail page progress display to surface the pending count alongside completed (e.g., "5 complete, 2 pending out of 7"), reading from sub-issue 03's new
pending_countproperty onAbstractBatchUpgradeOperation.The admin's cancel UI is a template button (
templates/admin/upgrade_selected_confirmation.html:97, JS handler inadmin/js/cancel.js) that calls the REST API endpointUpgradeOperationCancelViewatapi/views.py:381–451— there's no Django admin action for cancellation. Once sub-issue 03 extends_CANCELLABLE_STATUS, the sharedinstance.cancel()model method covers pending ops, so the button works without any admin-side change. Templates useget_status_display()rather than hardcoded status strings, so no template edits are required either.Admin tests (Django test client, not Selenium — that lives in sub-issue 09) covering: form renders the checkbox pre-checked; submitting with
is_is_persistent=False/Trueproduces a correctly-flagged batch and propagates to children;?status=pendingfilter returns only pending ops; detail page exposes the new read-only fields; batch list shows theis_persistentcolumn; cancel button transitions a pending op tocancelled.