- batch_delete_jobs now writes through the same DB alias it reads from. In JobsMixin.delete
the read queryset is bound to the instance's DB while Job.objects would use the router
default; if those diverged on a multi-DB setup the deleted rows never left the read side
and the batch loop never terminated.
- JobsMixin.delete and ScriptModule.delete honor a caller-supplied `using`, matching
DeleteMixin.delete, instead of always recomputing it.
- Raise JOB_DELETE_BATCH_SIZE from 100 to 1000 and correct its rationale. With only('pk')
the per-batch payload is gone, so the size now bounds per-cycle work rather than memory;
1000 matches EXPORT_CHUNK_SIZE and was the fastest of 100/1000/5000 when benchmarked
against a 200k-job deletion.
- Document that JobsMixin must precede DeleteMixin in the MRO or the batching is bypassed,
and scope the ScriptModule.delete comment so it doesn't imply the on-disk file removal is
transactional.
- Add a module-path rollback test alongside the existing script-path one.
Fixes form check input border contrast in dark mode by using solid grey
instead of translucent border. Updates checked checkbox glyph color to
rich black for better visibility against teal primary background.
Fixes#22879
Prevent out-of-order dynamic select responses from restoring options for an
earlier dependency state.
Track the latest load request, preserve valid selections across superseding
loads, and properly finalize stale requests and their loading state. Clear any
pending restored value when a request cannot be made or fails.
The batched job delete can't fast-delete (a global pre_delete receiver forces
per-instance signals), so each batch still instantiates its Job rows. Load only the PK
via only('pk') so those instances don't pull the large data/log_entries payloads,
cutting the resident set per batch. Also drop a dead `no-toggle` CSS class from the
delete-confirmation template (it is defined nowhere and, under Tabler, has no effect)
and use JobStatusChoices.STATUS_COMPLETED in the tests instead of a string literal.
Django's Collector calls into the jobs GenericRelation branch unconditionally, so
ConfirmCollector recorded a zero count for objects with no jobs. _get_dependent_objects
then added a CountOnly(0), and the delete-confirmation page rendered "The following
objects will be deleted as a result of this action." plus a "0 jobs" row for every
jobless JobsMixin object. Only record a count when there are actually jobs.
Deleting a Script via the UI is only possible by deleting its parent ScriptModule
(no Script delete view exists). That cascades to the child Script rows, and the
collector materialized every one of those Scripts' jobs — the memory blowup, which
scales with jobs-per-script. JobsMixin.delete() only batched the deleted object's own
jobs, and a ScriptModule has none; the jobs live on its child Scripts.
Extract the chunked job-deletion loop from JobsMixin.delete() into a shared
batch_delete_jobs() helper, and add a ScriptModule.delete() override that batch-deletes
its child Scripts' jobs (in a single queryset keyed on the script PKs, no per-script
loop) before delegating to the cascade. This bounds peak memory to one batch regardless
of how many jobs the module's scripts hold.
Scripts triggered by Event Rules now respect notifications_default and
job_timeout from script Meta class. Updates documentation to clarify
this behavior and adds regression test coverage.
Fixes#22852
Deleting a Script (or any JobsMixin object) with thousands of associated jobs could
consume several GB of memory and exhaust the host, because Django's deletion collector
loads every related Job into memory. Jobs can never be fast-deleted (a global pre_delete
receiver forces per-instance signal dispatch), and each Job carries potentially large
data and log_entries payloads.
Two paths loaded the full job set independently, so both are addressed:
- The delete cascade: JobsMixin.delete() now deletes the object's jobs in batches before
delegating to super().delete(), wrapped in a transaction so a failure in the parent
delete rolls the job deletions back. After the loop the cascade collector finds no jobs
to materialize.
- The delete-confirmation page: _get_dependent_objects() uses a ConfirmCollector that
counts the jobs relation rather than descending into it, so the page never instantiates
the jobs. Counted relations render as a non-expandable row (via a CountOnly stand-in)
alongside the itemized dependents.
CablePath.save() and delete() wrote the _path back-reference onto the path's
origin object, and PathTraceView built the trace SVG URL from the origin's REST
API action. Both assume the origin is a PathEndpoint, but a CircuitTermination
is a valid cable-path origin (per from_origin) without the _path field or a
-trace API action, so those paths raised FieldDoesNotExist and NoReverseMatch
respectively. Guard the _path writes and the SVG URL on PathEndpoint membership,
and skip the SVG block in the template when no URL is available.
Adds `member_type_id` filter field to support filtering by
ContentType ID alongside existing `member_type` filter. Includes
test coverage verifying correct filtering when member IDs overlap across
different Content Types.
Fixes#22800
Render select and multiselect custom field values as colored badges in
table views when their associated choices define colors.
For multiselect fields, render all selected values as badges when any
selected choice has a color, using the secondary badge color for
uncolored choices. Preserve comma-separated text when none of the
selected choices has a color.
Add test coverage for colored, uncolored, empty, mixed, and
HTML-sensitive choice values.
Co-authored-by: Martin Hauser <mhauser@netboxlabs.com>
Clarifies that backslashes in constraint values must be escaped in JSON.
Includes example showing regex pattern escaping and adds table entry
demonstrating regex constraint usage.
Fixes#22498
BaseFilterSet resolved referenced SavedFilters without a visibility check, so
a private (shared=False) filter owned by one user could have its parameters
applied to another user's request. Restrict resolution to shared or owned
filters via restrict_to_shared(), matching the visibility enforced on the UI,
REST, and GraphQL SavedFilter surfaces.