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
This commit is contained in:
Jason Novinger
2025-10-26 22:49:59 -05:00
parent afba5b2791
commit e19591484c
15 changed files with 893 additions and 18 deletions
+12 -2
View File
@@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.constants import *
from dcim.filtersets import DeviceFilterSet, RackFilterSet, PowerOutletFilterSet
from dcim.models import *
from extras.forms import LocalConfigContextFilterForm
from extras.models import ConfigTemplate
@@ -16,6 +17,8 @@ from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from users.models import Owner, User
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.filterset_mappings import FILTERSET_MAPPINGS
from utilities.forms.mixins import FilterModifierMixin
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import NumberWithOptions
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
@@ -330,7 +333,7 @@ class RackTypeFilterForm(RackBaseFilterForm):
tag = TagFilterField(model)
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm):
class RackFilterForm(FilterModifierMixin, TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm):
model = Rack
fieldsets = (
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
@@ -778,6 +781,7 @@ class PlatformFilterForm(NestedGroupModelFilterSetForm):
class DeviceFilterForm(
FilterModifierMixin,
LocalConfigContextFilterForm,
TenancyFilterForm,
ContactModelFilterForm,
@@ -1420,7 +1424,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
tag = TagFilterField(model)
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class PowerOutletFilterForm(FilterModifierMixin, PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerOutlet
fieldsets = (
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
@@ -1830,3 +1834,9 @@ class InterfaceConnectionFilterForm(FilterForm):
},
label=_('Device')
)
# Register FilterSet mappings for FilterModifierMixin lookup verification
FILTERSET_MAPPINGS[DeviceFilterForm] = DeviceFilterSet
FILTERSET_MAPPINGS[RackFilterForm] = RackFilterSet
FILTERSET_MAPPINGS[PowerOutletFilterForm] = PowerOutletFilterSet