Defer CachedValue updates from post_save/post_delete signal handlers to a
SearchCacheJob that runs after the surrounding transaction commits. Coalesce
pending updates per database alias and savepoint scope, preserving rollback
semantics while reducing synchronous write latency.
When no worker is available, or Redis cannot be reached during dispatch, apply
the update inline so existing single-process installs continue to behave
correctly. Replay the originating database alias during deferred reads and
writes so cache updates remain routed to the schema that emitted the signal.
Keep deferral private to CachedValueSearchBackend so custom SEARCH_BACKEND
implementations continue to use the existing synchronous cache/remove contract.
Move the job runner to search/jobs.py and keep the CachedValue-specific update
logic on the backend.
Document the eventual consistency of global search results and add coverage for
coalescing, rollback/savepoint behavior, worker dispatch, inline fallback,
deleted objects, and the custom backend contract.
Adds a nullable DurationField computed as completed - started, set in
Job.terminate(). Exposes it as an orderable table column, detail-panel
attr, REST API field, and UI/API range filters (execution_time__gte /
execution_time__lte).
A plain stored field (not a GeneratedField) keeps the migration
metadata-only, avoiding a full table rewrite on core_job.
* #21025: WIP
* Fixes#22357: Remove unused `local_context_data` field from dcim.Module (#22364)
* Add partial index for checking null CC data
* Ensure the data returned by get_config_context() is safe for mutation
* Implement selective backup queryset annotation to avoid n+1 overhead on cold cache
* Fix migration conflict
* Replace MPTT with Ltree per #21418
Bulk write operations (create/update/delete a JSON list at a model's list
endpoint) can opt into background processing with the ?background=true query
parameter. The request is validated synchronously and, if accepted, an
AsyncAPIJob is enqueued and a 202 Accepted is returned with the job id and
poll URL; the write is performed later by a worker that re-invokes the same
viewset action, so behavior matches the synchronous path (including
all-or-nothing transaction semantics).
- AsyncAPIJob reconstructs the request in the worker, re-applies object
permissions, runs within the request processors (change logging/events),
and captures the action's response into job.data as {status_code, data}.
- Handled rejections are translated to match the synchronous API: APIException
via handle_exception(), and AbortRequest/ProtectedError/RestrictedError via a
new NetBoxModelViewSet.exception_to_response() helper. These terminate the
job as "failed" (reserving "errored" for unexpected crashes).
- Background processing is refused with 503 when no worker is servicing the
queue, and rejected with 400 when combined with an If-Match precondition
(which cannot be honored once execution is deferred).
- Single-object writes, GET requests, and non-list payloads ignore the
parameter and run synchronously.
exception_to_response() intentionally duplicates the translation logic in
dispatch() rather than dispatch() being refactored to call it; consolidating
the two is left as a follow-up to keep this change off the synchronous hot path.
* Address code review feedback (#21992)
- Carry the request's scheme and host into the background worker so absolute
URLs in the captured job result point at the real server instead of a
hardcoded http://localhost/.
- Emit the same protected-delete warning log in exception_to_response() that
dispatch() produces, restoring application-log parity for background failures.
- Drop the inert `_authenticator = None` assignment: setting request.user
already prevents lazy re-authentication via the public API, and nothing on
the worker's action path reads the authenticator.
- Remove the redundant success-path job.save() (JobRunner.handle() ->
terminate() persists job.data) and hoist the AsyncAPIJob import in mixins.py
to module level (no real import cycle through it).
- Add a test asserting result URLs reflect the request host.
* Fix IPv6 host parsing in background API request reconstruction
Parse the carried host with urlsplit (and pass it verbatim as HTTP_HOST)
instead of host.partition(':'), which split bracketed IPv6 hosts like
[::1]:8443 on their inner colons. Extract request construction into
AsyncAPIJob._build_request and add a test asserting the IPv6 host round-trips.
* Address review feedback (#21992)
- Make the bulk mixins safe to use without BackgroundOperationMixin: guard the
_background_requested / _maybe_background_bulk_create calls with a getattr
fallback so BulkUpdateModelMixin/BulkDestroyModelMixin/SequentialBulkCreatesMixin
retain their standalone behavior in custom viewset composition.
- Add a test covering the background ProtectedError/RestrictedError path: a bulk
delete of a protected object records the same 409 the synchronous API returns
(job failed, status_code 409, object preserved), via exception_to_response().
Follow-up to #22363: align the plugin hook names with the already-renamed
JINJA_FILTERS setting (#22288) and with the rest of the codebase's 'Jinja'
spelling convention.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add support for literal `{lat}` and `{lon}` placeholders in `MAPS_URL`
when rendering GPS coordinate links. Existing configurations continue to
work by falling back to appending `lat,lon` when no coordinate placeholders
are present.
Move map URL handling into shared UI helpers so `GPSCoordinatesAttr` and
`AddressAttr` use consistent placeholder detection. When `MAPS_URL` contains
coordinate placeholders, suppress address-based map links to avoid rendering
invalid URLs.
Add tests for placeholder replacement, decimal coordinate values, fallback
behavior, and address link suppression. Also document the address link behavior
in the `MAPS_URL` configuration description.
Adds warning and examples for validating NetBox objects before saving
in Custom Scripts. Direct ORM writes bypass UI/API validation and can
introduce invalid data. Recommends calling `full_clean()` explicitly.
Fixes#22249
Fix miscounting of total_vlan_ids when VLANGroup vid_ranges use
non-canonical bounds (e.g. '[]'). Normalize ranges to '[)' on save and
add migration to recompute existing totals. Prevent division-by-zero in
utilization queries for legacy rows with miscounted totals.
Fixes#22228
Allow IP ranges where start_address equals end_address to model
single-IP pools like DHCP or NAT reservations. Add validation tests,
filterset coverage, and display logic to render both endpoints.
Fixes#21993
Display a warning in the UI whenever a user goes to provision a v1
token (both via the admin token form and the user profile token form).
Update documentation to note that v1 tokens are deprecated and will be
removed in NetBox v5.0.
Removes the deprecated querystring template tag from utilities/templatetags/
helpers.py and updates all 30 call sites across templates to use Django's
built-in querystring tag (available since Django 5.1). The request argument
is dropped since the built-in tag reads from the template context automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PostgreSQL 14 reaches end-of-life in November 2026 and Django 6.1 will
require PostgreSQL 15+. Updates all documentation references from 14 to
15, and removes the "needed on PostgreSQL 15 and later" conditional
comment from the database creation instructions (since 15 is now the
minimum).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drops the deprecated registry['models'] key, the __getitem__ deprecation
warning, and the population code in register_model(). Registered models
should be retrieved via ObjectType.objects.public() instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drops the LEGACY_ACTIONS constant and ActionsMixin._convert_legacy_actions()
method. Action views must now define the actions attribute as a list of
ObjectAction subclasses rather than as a legacy permission dict.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes SENTRY_DSN, SENTRY_SAMPLE_RATE, SENTRY_SEND_DEFAULT_PII, and
SENTRY_TRACES_SAMPLE_RATE. These were superseded by SENTRY_CONFIG in
v4.4.2. Documentation updated to use SENTRY_CONFIG exclusively.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the deprecated _DEFAULT_ACTION_PERMISSIONS dict and its
__getattr__ compatibility shim from netbox/constants.py. Action
permissions should be defined via ObjectAction subclasses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The command was deprecated in v4.6.0; all housekeeping tasks are now
handled automatically by NetBox's built-in job scheduler.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add ModelAction and register_model_actions() API for custom permission actions
* Add ObjectTypeSplitMultiSelectWidget and RegisteredActionsWidget
* Integrate registered actions into ObjectPermissionForm
* Add JavaScript for registered actions show/hide
* Register custom actions for DataSource, Device, and VirtualMachine
* Add tests for ModelAction and register_model_actions
* Refine registered actions widget UI
- Use verbose labels (App | Model) for action group headers
- Simplify template layout with h5 headers instead of cards
- Consolidate Standard/Custom/Additional Actions into single Actions fieldset
* Hide custom actions field when no applicable models selected
The entire field row is now hidden when no selected object types
have registered custom actions, avoiding an empty "Custom actions"
label.
* Add documentation for custom model actions
- Add plugin development guide for registering custom actions
- Update admin permissions docs to mention custom actions UI
- Add docstrings to ModelAction and register_model_actions
* Add RESERVED_ACTIONS constant and fix dedup in registered actions
- Define RESERVED_ACTIONS in users/constants.py for the four built-in
permission actions (view, add, change, delete)
- Replace hardcoded action lists in ObjectPermissionForm with the constant
- Fix duplicate action names in clean() when the same action is registered
across multiple models (e.g. render_config for Device and VirtualMachine)
- Fix template substring matching bug in objectpermission.html detail view
by passing RESERVED_ACTIONS through view context for proper list membership
* Fix shared action pre-selection and additional actions leakage on edit
* Prevent duplicate action registration in register_model_actions()
* Remove stale comment in RegisteredActionsWidget
* Rebuild frontend assets after rebase onto feature
* Refactor SplitMultiSelectWidget to use class attributes for widget classes
* Reject reserved action names in register_model_actions()
* Show all registered actions with enable/disable instead of show/hide
* Validate action name is not empty and clarify RESERVED_ACTIONS origin
* Adapt custom actions panel for declarative layout system
Convert the ObjectPermission detail view to use the new panel-based
layout from #21568. Add ObjectPermissionCustomActionsPanel that
cross-references assigned object types with the model_actions registry
to display which models each custom action applies to.
Also fix dark-mode visibility of disabled action checkboxes in the
permission form by overriding Bootstrap's disabled opacity.
* Flatten registered actions UI and declare via Meta.permissions
Implement two changes requested in review of #21560:
1. Use Meta.permissions for action declaration
- Add Meta.permissions to DataSource, Device, and VirtualMachine
- register_models() auto-registers actions from Meta.permissions
- Remove explicit register_model_actions() calls from apps.py
- Add get_action_model_map() utility to utilities/permissions.py
2. Flatten the ObjectPermission form UI
- Show a single deduplicated list of action checkboxes (one per
unique action name) instead of grouped-by-model checkboxes
- RegisteredActionsWidget uses create_option() to inject model_keys
and help_text; JS enables/disables based on selected object types
- render_field.html bypasses outer wrapper for registeredactionswidget
so widget emits rows with identical DOM structure to CRUD checkboxes
- Unchecking a model now also unchecks unsupported action checkboxes
Fixes#21357
* Address review feedback on registered actions
- Sort model_keys in data-models attribute for deterministic output
- Rename registered_actions field label to 'Registered actions'
- Target object_types selected list via data-object-types-selected
attribute instead of hardcoded DOM ID
- Reduce setTimeout delay to 0ms since moveOption() is synchronous
* Consolidate ObjectPermission detail view actions panel
Merge ObjectPermissionActionsPanel and ObjectPermissionCustomActionsPanel
into a single Actions panel that shows CRUD booleans and all registered
actions in one table, matching the form's consolidated layout.
Also fix data-object-types-selected attribute value (True -> 'true') and
update plugin docs to show Meta.permissions as the primary registration
approach.
* Address additional bot review feedback
- clean() collects all validation errors before raising instead of stopping at the first
- Fix stale admin docs (still referenced "Custom actions" and "grouped by model")
* Update netbox/netbox/registry.py
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
* Fix model_actions registry to use set operations
The registry was changed to defaultdict(set) but the registration
code still used list methods. Update .append() to .add() and fix
tests to use set-compatible access patterns.
* Rename permission migrations for clarity
* Move ModelAction validation into __post_init__
* Drop model name from permission descriptions
* Simplify ObjectPermission form and remove custom widgets
Replace the dynamic UI with standard BooleanField checkboxes for each
registered action. No custom widgets, no JavaScript, no template
changes.
- Remove RegisteredActionsWidget, ObjectTypeSplitMultiSelectWidget,
and registeredActions.ts
- Use dynamic BooleanFields for registered actions (renders identically
to CRUD checkboxes)
- Move action-resolution logic from panel to ObjectPermission model
- Remove object-type cross-validation from form clean()
- Remove unused get_action_model_map utility
* Remove register_model_actions from public API
Meta.permissions is the documented approach for plugins. The
register_model_actions function is now an internal implementation
detail.
* Sort registered actions and improve test coverage
Sort action names alphabetically for stable display order. Add tests
for cloning, empty registry, and models_csv output.
* Add help_text to registered action checkboxes
* Return model_keys as list from get_registered_actions()
Move string joining to the template so callers get native
list data instead of a pre-formatted CSV string.
* Improve detail view: human-friendly descriptions and additional actions
Return dicts from get_registered_actions() with help_text and verbose
model names. Add get_additional_actions() for manually-entered actions
that aren't CRUD or registered. Show both in the Actions panel.
* Renumber permission migrations after feature merge
Resolve migration conflicts with default_ordering_indexes migrations.
Renumber to 0023 (core), 0232 (dcim), 0056 (virtualization) and
update dependencies.
---------
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Update STORAGES configuration examples to include all three storage
backends (default, staticfiles, scripts) with complete option sets.
Add region_name to environment variable example and clarify usage for
S3-compatible services.
Fixes#21864
* Misc cleanup
* Include permissions in TemplatedAttr context
* Introduce CircuitTerminationPanel to replace generic panel
* Replace all instantiations of Panel with TemplatePanel
* Misc cleanup for layouts
* Enable specifying column grid width
* Panel.render() should pass the request to render_to_string()
* CopyContent does not need to override render()
* Avoid setting mutable panel actions
* Catch exceptions raised when rendering embedded plugin content
* Handle panel title when object is not available
* Introduce should_render() method on Panel class
* Misc cleanup
* Pass the value returned by get_context() to should_render()
* Yet more cleanup
* Fix typos
* Clean up object attrs
* Replace candidate template panels with ObjectAttributesPanel subclasses
* Add tests for object attrs
* Remove beta warning
* PluginContentPanel should not call should_render()
* Clean up AddObject
* speed.html should reference value for port_speed
* Address PR feedback
* #21701 allow upload script via API
* #21701 allow upload script via API
* add extra test
* change to use Script api endpoint
* ruff fix
* review feedback:
* review feedback:
* review feedback:
* Fix permission check, perform_create delegation, and test mock setup
- destroy() now checks extras.delete_script (queryset is Script.objects.all())
- create() delegates to self.perform_create() instead of calling serializer.save() directly
- Add comment explaining why update/partial_update intentionally return 405
- Fix test_upload_script_module: set mock_storage.save.return_value so file_path
receives a real string after the _save_upload return-value fix; add DB existence check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Return 400 instead of 500 on duplicate script module upload
Catch IntegrityError from the unique (file_root, file_path) constraint
and re-raise as a ValidationError so the API returns a 400 with a clear
message rather than a 500.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Validate upload_file + data_source conflict for multipart requests
DRF 3.16 Serializer.get_value() uses parse_html_dict() or empty for all
HTML/multipart input. A flat key like data_source=2 produces an empty
dict ({}), which is falsy, so it falls back to empty and the nested
field is silently skipped. data.get('data_source') is therefore always
None in multipart requests, bypassing the conflict check.
Fix: also check self.initial_data for data_source and data_file in all
three guards in validate(), so the raw submitted value is detected even
when DRF's HTML parser drops the deserialized object.
Add test_upload_with_data_source_fails to cover the multipart conflict
path explicitly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Require data_file when data_source is specified
data_source alone is not a valid creation payload — a data_file must
also be provided to identify which file within the source to sync.
Add the corresponding validation error and a test to cover the case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Align ManagedFileForm validation with API serializer rules
Add the missing checks to ManagedFileForm.clean():
- upload_file + data_source is rejected (matches API)
- data_source without data_file is rejected with a specific message
- Update the 'nothing provided' error to mention data source + data file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Revert "Align ManagedFileForm validation with API serializer rules"
This reverts commit f0ac7c3bd2.
* Align API validation messages with UI; restore complete checks
- Match UI error messages for upload+data_file conflict and no-source case
- Keep API-only guards for upload+data_source and data_source-without-data_file
- Restore test_upload_with_data_source_fails
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Run source/file conflict checks before super().validate() / full_clean()
super().validate() calls full_clean() on the model instance, which raises
a unique-constraint error for (file_root, file_path) when file_path is
empty (e.g. data_source-only requests). Move the conflict guards above the
super() call so they produce clear, actionable error messages before
full_clean() has a chance to surface confusing database-level errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* destroy() deletes ScriptModule, not Script
DELETE /api/extras/scripts/<pk>/ now deletes the entire ScriptModule
(matching the UI's delete view), including modules with no Script
children (e.g. sync hasn't run yet). Permission check updated to
delete_scriptmodule. The queryset restriction for destroy is removed
since the module is deleted via script.module, not super().destroy().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* review feedback:
* cleanup
* cleanup
* cleanup
* cleanup
* change to ScriptModule
* change to ScriptModule
* change to ScriptModule
* update docs
* cleanup
* restore file
* cleanup
* cleanup
* cleanup
* cleanup
* cleanup
* keep only upload functionality
* cleanup
* cleanup
* cleanup
* change to scripts/upload api
* cleanup
* cleanup
* cleanup
* cleanup
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Implement {module} position inheritance for nested module bays (#19796)
Enables a single ModuleType to produce correctly named components at any
nesting depth by resolving {module} in module bay position fields during
tree traversal. The user controls the separator through the position
field template itself (e.g. {module}/1 vs {module}-1 vs {module}.1).
Model layer:
- Add _get_inherited_positions() to resolve {module} in positions as
the module tree is walked from root to leaf
- Update _resolve_module_placeholder() with single-token logic: one
{module} resolves to the leaf bay's inherited position; multi-token
continues level-by-level replacement for backwards compatibility
Form layer:
- Update _get_module_bay_tree() to resolve {module} in positions during
traversal, propagating parent positions through the tree
- Extract validation into _validate_module_tokens() private method
Tests:
- Position inheritance at depth 2 and 3
- Custom separator (dot notation)
- Multi-token backwards compatibility
- Documentation for position inheritance
Fixes: #19796
* Consolidate {module} placeholder logic into shared utilities and add API validation
Extract get_module_bay_positions() and resolve_module_placeholder() into
dcim/utils.py as shared routines used by the model, form, and API serializer.
This eliminates duplicated traversal and resolution logic across three layers.
Key changes:
- Add position inheritance: {module} tokens in bay position fields resolve
using the parent bay's position during hierarchy traversal
- Single {module} token now resolves to the leaf bay's inherited position
- Mismatched token count vs tree depth now raises ValueError instead of
silently producing partial strings
- API serializer validation uses shared utilities for parity with the form
- Fix error message wording ("levels deep" instead of "in tree")
Align the plugin search example with the recommended registration
pattern used in the general search documentation and NetBox core.
Replace the legacy `indexes = [...]` example with decorator-based
registration to make the preferred approach clearer for plugin authors.
Introduce `VirtualMachineType` to classify virtual machines and apply
default platform, vCPU, and memory values when creating a VM.
This adds the new model and its relationship to `VirtualMachine`, and
wires it through forms, filtersets, tables, views, the REST API,
GraphQL, navigation, search, documentation, and tests.
Explicit values set on a virtual machine continue to take precedence,
and changes to a type do not retroactively update existing VMs.
Enable VMs to be assigned to a standalone device without requiring a
cluster. Add device-scoped uniqueness constraints, update validation
logic, and enhance placement flexibility. Site is now auto-inherited
from the cluster or device.
* Drop mkdocs from `requirements.txt` and add Zensical
* Replace mkdocs with Zensical in CI and pre-commit tasks
* Remove `.info` from the `docs/` build directory (obsolete)
* Update the legacy ReadTheDocs configuration
* Update upgrade script to use Zensical
* Remove custom docs footer
* Remove obsolete CSS
Adds a `request` key to the webhook data if a request is associated with the origination of the webhook.
Note: We're not attaching a complete representation of the request in the interest of both security and brevity.
Add an `enabled` boolean field to ModuleBay, ModuleBayTemplate,
DeviceBay, and DeviceBayTemplate models. Disabled bays prevent component
installation and display accordingly in the UI. Update serializers,
filters, forms, and tables to support the new field.
Fixes#20152
Clarify webhook context variable names and event types.
Replace `model` with `object_type`, update event values to match actual
output (`created` vs. `create`), and refresh example JSON to reflect the
current API response format, including new fields like `display` and
`display_url`.
Fixes#21489
Introduce a new configuration parameter,
`CHANGELOG_RETAIN_CREATE_LAST_UPDATE`, to retain each object's create
record and most recent update record when pruning expired changelog
entries (per `CHANGELOG_RETENTION`).
Update documentation, templates, and forms to reflect this change.
Fixes#21409
Pin Ruff to v0.15.2 in CI and pre-commit to avoid breakages from
upstream releases. Run Ruff via astral-sh/ruff-action (pinned by SHA)
instead of installing Ruff via pip.
Document where Ruff is pinned and keep the release checklist/style guide
in sync.
Fixes#21472Fixes#21497
* Add searchable deprecation comments on request_id and username fields in EventContext
* Add deprecation note in webhooks documentation
* Expand deprecation note/warning
* Add version number to deprecation warning
* Add deprecation warning to two other places
* Allow REDIS KWARGS to be set in configuration.py
* cleanup
* cleanup
* cleanup
* Update netbox/netbox/settings.py
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
* Update netbox/netbox/settings.py
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
* document in REDIS config section
---------
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
NetBox now accepts case-insensitive model identifiers in configuration, allowing
both lowercase (e.g. "dcim.site") and PascalCase (e.g. "dcim.Site") for
DEFAULT_DASHBOARD, CUSTOM_VALIDATORS, and PROTECTION_RULES.
This makes model name handling consistent with FIELD_CHOICES.
- Add a shared case-insensitive config lookup helper (get_config_value_ci())
- Use the helper in extras/signals.py and core/signals.py
- Update FIELD_CHOICES ChoiceSetMeta to support case-insensitive replace/extend
(only compute extend choices if no replacement is defined)
- Add unit tests for get_config_value_ci()
- Add integration tests for case-insensitive FIELD_CHOICES replacement/extension
- Update documentation examples to use PascalCase consistently
Introduce support for omitting specific serializer fields via an
`omit` parameter, acting as the inverse of `fields`.
Wire it through the API viewset and queryset optimization helpers
so omitted fields don’t trigger unnecessary annotations/prefetches,
and document the new behavior.
Update the installation and administration guides to reference
Ubuntu 24.04 instead of 22.04 where applicable, and refresh examples
to match NetBox v4.5.
This includes updates to Python version requirements, NetBox shell
commands, Redis configuration, and sample outputs to align with current
compatibility and best practices.
Fixes#21297
- Updated menu path from 'Admin > Extras > Configuration Revisions'
to 'Admin > System > Configuration History'
- Reflects actual location in NetBox admin interface
* Fixes#7604: Add filter modifier dropdowns for advanced lookup operators
Implements dynamic filter modifier UI that allows users to select lookup operators
(exact, contains, starts with, regex, negation, empty/not empty) directly in filter
forms without manual URL parameter editing.
Supports filters for all scalar types and strings, as well as some
related object filters. Explicitly does not support filters on fields
that use APIWidget. That has been broken out in to follow up work.
**Backend:**
- FilterModifierWidget: Wraps form widgets with lookup modifier dropdown
- FilterModifierMixin: Auto-enhances filterset fields with appropriate lookups
- Extended lookup support: Adds negation (n), regex, iregex, empty_true/false lookups
- Field-type-aware: CharField gets text lookups, IntegerField gets comparison operators, etc.
**Frontend:**
- TypeScript handler syncs modifier dropdown with URL parameters
- Dynamically updates form field names (serial → serial__ic) on modifier change
- Flexible-width modifier dropdowns with semantic CSS classes
* Remove extraneous TS comments
* Fix import order
* Fix CircuitFilterForm inheritance
* Enable filter form modifiers on DCIM models
* Enable filter form modifiers on Tenancy models
* Enable filter form modifiers on Wireless models
* Enable filter form modifiers on IPAM models
* Enable filter form modifiers on VPN models
* Enable filter form modifiers on Virtualization models
* Enable filter form modifiers on Circuit models
* Enable filter form modifiers on Users models
* Enable filter form modifiers on Core models
* Enable filter form modifiers on Extras models
* Add ChoiceField support to FilterModifierMixin
Enable filter modifiers for single-choice ChoiceFields in addition to the
existing MultipleChoiceField support. ChoiceFields can now display modifier
dropdowns with "Is", "Is Not", "Is Empty", and "Is Not Empty" options when
the corresponding FilterSet defines those lookups.
The mixin correctly verifies lookup availability against the FilterSet, so
modifiers only appear when multiple lookup options are actually supported.
Currently most FilterSets only define 'exact' for single-choice fields, but
this change enables future FilterSet enhancements to expose additional
lookups for ChoiceFields.
* Address PR feedback: Replace global filterset mappings with registry
* Address PR feedback: Move FilterModifierMixin into base filter form classes
Incorporates FilterModifierMixin into NetBoxModelFilterSetForm and FilterForm,
making filter modifiers automatic for all filter forms throughout the application.
* Fix filter modifier form submission bug with 'action' field collision
Forms with a field named "action" (e.g., ObjectChangeFilterForm) were causing
the form.action property to be shadowed by the field element, resulting in
[object HTMLSelectElement] appearing in the URL path.
Use form.getAttribute('action') instead of form.action to reliably retrieve
the form's action URL without collision from form fields.
Fixes form submission on /core/changelog/ and any other forms with an 'action'
field using filter modifiers.
* Address PR feedback: Move FORM_FIELD_LOOKUPS to module-level constant
Extracts the field type to lookup mappings from FilterModifierMixin class
attribute to a module-level constant for better reusability.
* Address PR feedback: Refactor and consolidate field filtering logic
Consolidated field enhancement logic in FilterModifierMixin by:
- Creating QueryField marker type (CharField subclass) for search fields
- Updating FilterForm and NetBoxModelFilterSetForm to use QueryField for 'q'
- Moving all skip logic into _get_lookup_choices() to return empty list for
fields that shouldn't be enhanced
- Removing separate _should_skip_field() method
- Removing unused field_name parameter from _get_lookup_choices()
- Replacing hardcoded field name check ('q') with type-based detection
* Address PR feedback: Refactor applied_filters to use FORM_FIELD_LOOKUPS
* Address PR feedback: Rename FilterModifierWidget parameter to widget
* Fix registry pattern to use model identifiers as keys
Changed filterset registration to use model identifiers ('{app_label}.{model_name}')
as registry keys instead of form classes, matching NetBox's pattern for search indexes.
* Address PR feedback: refactor brittle test for APISelect useage
Now checks if widget is actually APISelect, rather than trying to infer
from the class name.
* Refactor register_filterset to be more generic and simple
* Remove unneeded imports left from earlier registry work
* Update app registry for new `filtersets` store
* Remove unused star import, leftover from earlier work
* Enables filter modifiers on APISelect based fields
* Support filter modifiers for ChoiceField
* Include MODIFIER_EMPTY_FALSE/_TRUE in __all__
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
* Fix filterset registration for doubly-registered models
* Removed explicit checks against QueryField and [Null]BooleanField
I did add them to FORM_FIELD_LOOKUPS, though, to underscore that they
were considered and are intentially empty for future devs.
* Switch to sentence case for filter pill text
* Fix applied_filters template tag to use field-type-specific lookup labelsresolves
E.g. resolves gt="after" for dates vs "greater than" for numbers
* Verifies that filter pills for exact matches (no lookup
Add test for exact lookup filter pill rendering
* Add guard for FilterModifierWidget with no lookups
* Remove comparison symbols from numeric filter labels
* Match complete tags in widget rendering test assertions
* Check all expected lookups in field enhancement tests
* Move register_filterset to netbox.plugins.registration
* Require registered filterset for filter modifier enhancements
Updates FilterModifierMixin to only enhance form fields when the
associated model has a registered filterset. This provides plugin
safety by ensuring unregistered plugin filtersets fall back to
simple filters without lookup modifiers.
Test changes:
- Create TestModel and TestFilterSet using BaseFilterSet for
automatic lookup generation
- Import dcim.filtersets to ensure Device filterset registration
- Adjust tag field expectations to match actual Device filterset
(has exact/n but not empty lookups)
* Attempt to resolve static conflicts
* Move register_filterset() back to utilities.filtersets
* Add register_filterset() to plugins documentation for filtersets
* Reorder import statements
---------
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
The bookmarks link was pointing to ../features/customization.md#bookmarks
but the bookmarks section is actually in ../features/user-preferences.md#bookmarks.
This fixes the broken anchor link.