humanize_duration() is a general-purpose helper, newly exposed as a template
filter, so clamping negatives inside it made every present and future caller
suppress the exact symptom of clock skew. It now renders a negative duration
with a leading minus sign, which also fixes the nonsensical output the divmod
decomposition previously produced for one (e.g. "-1d 23h 59m 55s").
The floor moves to Job.elapsed_time, which is the value NetBox displays and
covers the list, the detail panel, the script result view and runscript in one
place. The stored execution_time is untouched, so the API and exports still
surface the anomaly.
Also renames the sub-second branch's variable, which held a value in seconds
rather than milliseconds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The jobs list sorts by the displayed value, so a running job orders by how
long it has been going, while execution_time__gte/__lte match only the
recorded column — a long-running job can therefore top a descending sort yet
be excluded by a filter on the same attribute.
Keeping the filters on the stored column is deliberate: the filterset is
shared with the REST API, where matching against a live, clock-dependent
value would make results non-reproducible. Document the distinction, along
with the export's use of the recorded value, rather than reconciling them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Job.duration has been public since 3.4 and is reachable from user-authored
export templates as well as plugins, so removing it outright was a silent
breaking change. Restore the original implementation verbatim — including the
fallback to `created` when a job never started, and the preformatted string —
so existing templates keep working, and warn on access. Planned for removal
in v5.0, matching the rack legacy fields.
Note that elapsed_time deliberately does not reproduce the `created`
fallback: measuring from creation conflates queue wait time with execution
time, which is what the new field is meant to record.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
JobTable defines both render_execution_time() and value_execution_time(), so
django-tables2 never invoked DurationColumn for that column and the new
timedelta branch was unreachable and untested. Restore the column to its
minutes-only form and use a plain Column, which is what the table was
effectively getting anyway.
The export path also passed through the render path's clamping, so an
anomalous negative execution_time was normalized to zero in the one output
intended for analysis, and a running job's provisional elapsed time was
indistinguishable from a completed job's final value. Export the recorded
value verbatim and leave the still-running distinction to the UI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Renaming the attribute to elapsed_time changed its auto-derived label to
"Elapsed time", disagreeing with the list column, the filter form, the API
field and the model docs. The derived label is also built at runtime before
being passed to gettext, so it would never have been extracted into the
message catalog. An explicit label addresses both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The expression coalesced to Now() - started with no regard for whether the
job had finished, so a row with both started and completed set but a null
execution_time resolved to an ever-growing interval, while the elapsed_time
property returned None for the same row. Sorting the jobs table descending
by execution time therefore ranked those rows above every real value.
Gate the live branch on completed__isnull=True so the expression agrees with
the property.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batching the backfill bounded statement size but not lock duration: sharing
a transaction with the AddField meant the ACCESS EXCLUSIVE lock from ALTER
TABLE was held for the whole run, which is exactly the case the batching was
meant to help. 0025 goes back to adding the column only, and the backfill
moves to 0026 with atomic = False so the lock is released first.
The backfill now also skips rows which already have a value, making it
idempotent and letting an interrupted run simply be resumed. As a separate
migration it additionally reaches installations which had already applied
0025, rather than silently leaving their historical jobs unpopulated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
started__* and completed__* are two halves of the same time range, so
splitting them across the Scheduling and Execution field sets made a run
window awkward to filter. Execution now holds only execution_time, and the
grouping matches JobSchedulingPanel on the detail view.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Normalize RQ timeout values before validating global and per-webhook
timeouts, including duration strings and RQ's default and unlimited values.
Improve timeout logging and visibility in the UI and documentation, raise
the default webhook timeout to 60 seconds, and add coverage for the new
validation and filtering behavior.
* Fixes#22161: Rename filterset test mixin base classes to *TestMixin
Completes the test-class naming standardization begun in #22097, which
renamed concrete test classes to the *TestCase suffix but deliberately
left four filterset test mixin base classes untouched because renaming
them is breaking for plugins that inherit from them.
These four are pure mixins, not concrete test cases, so they follow
NetBox's existing mixin naming convention (RQQueueTestMixin,
ComponentTraceMixin) rather than the *TestCase suffix the issue
originally proposed. The literal *TestCase names also collide with two
existing concrete classes (BaseFilterSetTestCase in
utilities/tests/test_filters.py and ChangeLoggedFilterSetTestCase in
extras/tests/test_filtersets.py).
BaseFilterSetTests -> BaseFilterSetTestMixin
ChangeLoggedFilterSetTests -> ChangeLoggedFilterSetTestMixin
DeviceComponentFilterSetTests -> DeviceComponentFilterSetTestMixin
DeviceComponentTemplateFilterSetTests -> DeviceComponentTemplateFilterSetTestMixin
This is a breaking change for plugins whose test suites import the two
exported mixins from utilities.testing; they must update their imports.
* Fixes#22161: Update add-model skill for renamed test mixin
The add-model skill still referenced ChangeLoggedFilterSetTests in its
example filterset test. Update it to ChangeLoggedFilterSetTestMixin.
* Closes#22770: Allow plugins to register Event Rule action handlers
Introduces an EventRuleAction registration API (netbox.event_rules /
netbox.extras.event_rules) so plugins can add new EventRule action types
the same way they already register search indexes and event types,
replacing the hardcoded webhook/script/notification elif-chain. Core's
own three action types are refactored onto this mechanism.
An EventRule referencing an unregistered action (e.g. its providing
plugin is uninstalled) remains stored, is skipped during processing
without affecting other rules, is visibly marked unavailable in the
UI/API, and triggers a new extras.W001 system check, resuming
automatically once the plugin is reinstalled, with no need to re-save.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Fix CI failure: extras.W001 check must tolerate a not-yet-migrated database
check_event_rule_actions() queried EventRule unconditionally, which broke
`manage.py makemigrations --check` (and a fresh `manage.py migrate`) on a
database with no tables yet, since Django runs system checks before
verifying/applying migrations. Wrap the query and swallow DatabaseError,
matching the existing check_postgresql_version precedent for a database
that may not be ready. Verified against a fresh, unmigrated database that
makemigrations --check, migrate, and manage.py check all behave correctly,
and that the warning still fires once a qualifying EventRule exists.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Fix EventRuleForm action_type widget: HTMXSelect was silently ignored
Meta.widgets only applies to fields the ModelForm auto-generates; action_type
is an explicit class-level field, so its HTMXSelect assignment in Meta.widgets
never took effect, and switching Action type in the browser never refreshed
the action_choice field's label/queryset. Move the widget onto the field
declaration itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Address review feedback from Jeremy Stretch on PR #22793
- Revert action_object_type on_delete to CASCADE (was changed to SET_NULL)
- Make action_type choices dynamic via the model field's own callable
choices=, simplifying EventRule.clean() and making any unavailable
action_type invalid on save, whether new or unchanged
- Rename is_action_available to action_is_available
- Fold new dispatch tests into the existing RQQueueTestMixin test class to
fix a flaky --parallel run (two such classes cross-flush each other's
Redis queues)
- Only catch broad exceptions around plugin-provided actions in
process_event_rules(); let a core action's own bugs propagate
- Add value_action_type() so table exports don't leak the "unavailable"
badge's HTML markup
- Drop the frozen CSVChoiceField on action_type and make action_object
optional at the field level, so bulk import of an object-less action
works
- Map ValidationErrors on unexposed model fields to NON_FIELD_ERRORS in
bulk import instead of letting them surface as a raw ValueError
- Restore EventRuleActionEnum/the enum-based GraphQL filter, built from
the live action registry instead of the static EventRuleActionChoices
- Default EventRuleAction.object_required to False, matching
object_model's default of None; set it explicitly on the three core
actions
- Drop the unused request parameter on get_object_queryset()
- Fix action_object_type's serializer queryset, which incorrectly used
the triggering object_types' feature flag
- Use .format() instead of % in get_action_type_display()
- Remove the extras.W001 system check (a DB query on every management
command) in favor of an action_is_available field on the REST API
- Raise ValidationError instead of a bare Exception on duplicate action
slug registration
- Shorten a couple of overly verbose inline comments
- Split EventRuleAction.validate() into an internal _validate() and a
public no-op validate(), so a subclass's custom validation doesn't
need to remember to call super()
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Trim verbose comments/docstrings added while addressing review feedback
Shortened a number of overly long inline comments and test docstrings
introduced across the previous commit's review-feedback fixes (the
EventRuleActionEnum comment, the _validate()/validate() docstrings, and
several test docstrings that restated context already given elsewhere).
Also drops the auto-generated "Generated by Django" header comment from
migration 0143, matching the rest of this app's hand-touched migrations.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Address findings from automated follow-up review of #22793
- Clear stale action_object_type/action_object_id when an action declares
object_model but is left with no object selected (object_required=False);
previously neither branch of the if/elif fired and the old value from
before the edit silently persisted. Fixed in both EventRuleForm and
EventRuleImportForm (the latter matters for CSV updates of an existing
row). Also resolve the content type from the actual selected object
rather than the action's declared object_model, correctly handling
subclass/proxy instances.
- Validate action slugs at registration time (format, and collision via
enum_key() with an already-registered slug) so a bad third-party slug
is rejected immediately instead of crashing GraphQL schema assembly at
startup.
- Strip the dead-code label/description duplication out of
EventRuleActionChoices.CHOICES -- nothing reads it, and it risked
drifting from WebhookAction/ScriptAction/NotificationAction's own.
- Record whether an action is plugin-provided at registration time
instead of introspecting its module on every dispatch; core's three
actions now register with is_plugin_provided=False explicitly.
- Add an action_is_available filter (API + UI) so event rules with a
now-unavailable action can still be found in bulk, now that the
extras.W001 system check is gone.
- Update the plugin dev docs: fix the OpenTicketAction example (it was
missing object_required=True, the exact gap the action_object fix
above addresses), note that an unavailable rule can't be saved at all
(not just skipped), and move an internal-only note out of the
published class docstring.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Address findings from second automated follow-up review of #22793
- Drop the ChoiceSet base from EventRuleActionChoices. With CHOICES=(),
the previous version made ChoiceField(choices=EventRuleActionChoices)
-- the idiomatic pattern used for every other ChoiceSet in this
codebase, and reachable via `from extras.choices import *` -- silently
reject every value instead of failing at first use.
- Reject slugs containing hyphens or a leading underscore at
registration time: a hyphenated slug (plausible, since plugin
distribution names are conventionally hyphenated) or a slug starting
with an underscore both currently pass validation but produce a
GraphQL-invalid or GraphQL-reserved enum member name once sanitized,
crashing schema assembly at startup. Document the constraint in the
plugin docs and the published slug docstring.
- Raise ImproperlyConfigured instead of ValidationError for all three
registration-failure cases in register_event_rule_action() -- these
are packaging/configuration mistakes surfaced from AppConfig.ready(),
not user input, matching the convention ChoiceSetMeta already uses for
the same class of error.
- Remove is_plugin_provided's class-level default; nothing reads it
before an action is registered in any real code path, and the default
masked a class-vs-instance inconsistency. Move its documentation out
of the published Attributes docstring into a plain comment.
- Simplify EventRuleImportForm.clean()'s action_object_type/id
assignment to match EventRuleForm.clean()'s approach (set both fields
once, unconditionally, from the resolved object) rather than assigning
via the GFK setter and then conditionally overwriting the content
type.
- Split a dense doc sentence in eventrule.md onto its own line.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Address findings from third automated follow-up review of #22793
- Restore is_plugin_provided's class-level default of True. Its only
read is inside process_event_rules()'s exception handler; without a
default, an action reaching dispatch without going through
register_event_rule_action() (e.g. inserted into the registry dict
directly) raised AttributeError while already handling the real
exception, masking it entirely instead of degrading gracefully.
- Move the slug/label presence checks out of __init_subclass__ (which
fired at class-definition time, raising TypeError) and into
register_event_rule_action() as ImproperlyConfigured, unifying them
with the other three registration-time checks. This also resolves a
still-open item from the very first automated review: an intermediate
base class shared by several concrete plugin actions couldn't
previously be defined without a placeholder slug/label of its own.
- Restore the GFK assignment (self.instance.action_object = obj) in
EventRuleImportForm.clean() alongside the explicit content-type
assignment, so EventRule.clean()'s later access to action_object hits
the descriptor cache instead of an extra SELECT per imported row.
- Clarify the slug docstring/docs wording (leading underscore
specifically, not underscores in general; tell authors to use an
underscore instead of a hyphen) and document that intermediate base
classes are now supported. Add a test for an uppercase slug.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Document the frozen-at-import tradeoff on EventRuleSerializer.action_type
The EventRuleActionAPITestCase docstring in test_api.py pointed here for
an explanation of why the choices are materialized once at module-import
time rather than dynamically, but the field itself had no such comment.
* Address findings from fourth automated follow-up review of #22793
- Restore the "must start with a letter" slug constraint dropped from
the docs page and class docstring by the previous round's rewording;
reword to "must begin with a lowercase letter", which covers the
leading-digit case SLUG_RE actually rejects and matches the
ImproperlyConfigured message an author will hit.
- Scope the GFK-cache-priming comment in EventRuleImportForm.clean() to
the non-proxy case it actually holds for, rather than claiming it
unconditionally.
- Trim comments and docstrings that had regrown into reviewer-facing
rationale (why a prior finding was reverted, why a check isn't in
__init_subclass__ anymore rather than API documentation, in
event_rules.py and test_event_rules.py.
EOF
)
* Misc cleanup
* Misc cleanup
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Introduces production PyPI publishing triggered by v* tag pushes, while
Test PyPI now requires manual dispatch. Both indexes never receive the
same run, ensuring proper separation between rehearsal and production.
Fixes#22786
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.
Include comments field with weight 5000 in search indexes for
DeviceRole, L2VPN, MACAddress, and RouteTarget models to enable
full-text search on comment content.
Fixes#22767