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)
This commit is contained in:
Jason Novinger 2025-12-04 14:34:14 -06:00
parent d9e4c78dcc
commit 44362dc191
2 changed files with 53 additions and 32 deletions

View File

@ -196,11 +196,11 @@ class FilterModifierMixin:
if filterset: if filterset:
lookups = self._verify_lookups_with_filterset(field_name, lookups, filterset) lookups = self._verify_lookups_with_filterset(field_name, lookups, filterset)
if len(lookups) > 1: if len(lookups) > 1:
field.widget = FilterModifierWidget( field.widget = FilterModifierWidget(
widget=field.widget, widget=field.widget,
lookups=lookups lookups=lookups
) )
def _get_lookup_choices(self, field): def _get_lookup_choices(self, field):
"""Determine the available lookup choices for a given field. """Determine the available lookup choices for a given field.

View File

@ -1,10 +1,14 @@
from django import forms from django import forms
from django.db import models
from django.http import QueryDict from django.http import QueryDict
from django.template import Context from django.template import Context
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
import dcim.filtersets # noqa: F401 - Import to register Device filterset
from dcim.forms.filtersets import DeviceFilterForm from dcim.forms.filtersets import DeviceFilterForm
from dcim.models import Device from dcim.models import Device
from netbox.filtersets import BaseFilterSet
from netbox.plugins.registration import register_filterset
from users.models import User from users.models import User
from utilities.forms.fields import TagFilterField from utilities.forms.fields import TagFilterField
from utilities.forms.mixins import FilterModifierMixin from utilities.forms.mixins import FilterModifierMixin
@ -12,6 +16,28 @@ from utilities.forms.widgets import FilterModifierWidget
from utilities.templatetags.helpers import applied_filters from utilities.templatetags.helpers import applied_filters
# Test model for FilterModifierMixin tests
class TestModel(models.Model):
"""Dummy model for testing filter modifiers."""
char_field = models.CharField(max_length=100, blank=True)
integer_field = models.IntegerField(null=True, blank=True)
decimal_field = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
date_field = models.DateField(null=True, blank=True)
boolean_field = models.BooleanField(default=False)
class Meta:
app_label = 'utilities'
managed = False # Don't create actual database table
# Test filterset using BaseFilterSet to automatically generate lookups
@register_filterset
class TestFilterSet(BaseFilterSet):
class Meta:
model = TestModel
fields = ['char_field', 'integer_field', 'decimal_field', 'date_field', 'boolean_field']
class FilterModifierWidgetTest(TestCase): class FilterModifierWidgetTest(TestCase):
"""Tests for FilterModifierWidget value extraction and rendering.""" """Tests for FilterModifierWidget value extraction and rendering."""
@ -100,81 +126,76 @@ class FilterModifierMixinTest(TestCase):
def test_mixin_enhances_char_field_with_modifiers(self): def test_mixin_enhances_char_field_with_modifiers(self):
"""CharField should be enhanced with contains/starts/ends modifiers.""" """CharField should be enhanced with contains/starts/ends modifiers."""
class TestForm(FilterModifierMixin, forms.Form): class TestForm(FilterModifierMixin, forms.Form):
name = forms.CharField(required=False) char_field = forms.CharField(required=False)
model = TestModel
form = TestForm() form = TestForm()
self.assertIsInstance(form.fields['name'].widget, FilterModifierWidget) self.assertIsInstance(form.fields['char_field'].widget, FilterModifierWidget)
lookup_codes = [lookup[0] for lookup in form.fields['name'].widget.lookups] lookup_codes = [lookup[0] for lookup in form.fields['char_field'].widget.lookups]
expected_lookups = ['exact', 'n', 'ic', 'isw', 'iew', 'ie', 'regex', 'iregex', 'empty_true', 'empty_false'] expected_lookups = ['exact', 'n', 'ic', 'isw', 'iew', 'ie', 'regex', 'iregex', 'empty_true', 'empty_false']
self.assertEqual(lookup_codes, expected_lookups) self.assertEqual(lookup_codes, expected_lookups)
def test_mixin_skips_boolean_fields(self): def test_mixin_skips_boolean_fields(self):
"""Boolean fields should not be enhanced.""" """Boolean fields should not be enhanced."""
class TestForm(FilterModifierMixin, forms.Form): class TestForm(FilterModifierMixin, forms.Form):
active = forms.BooleanField(required=False) boolean_field = forms.BooleanField(required=False)
model = TestModel
form = TestForm() form = TestForm()
self.assertNotIsInstance(form.fields['active'].widget, FilterModifierWidget) self.assertNotIsInstance(form.fields['boolean_field'].widget, FilterModifierWidget)
def test_mixin_enhances_tag_filter_field(self): def test_mixin_enhances_tag_filter_field(self):
"""TagFilterField should be enhanced even though it's a MultipleChoiceField.""" """TagFilterField should be enhanced even though it's a MultipleChoiceField."""
class TestForm(FilterModifierMixin, forms.Form): class TestForm(FilterModifierMixin, forms.Form):
tag = TagFilterField(Device) tag = TagFilterField(Device)
model = Device
form = TestForm() form = TestForm()
self.assertIsInstance(form.fields['tag'].widget, FilterModifierWidget) self.assertIsInstance(form.fields['tag'].widget, FilterModifierWidget)
tag_lookups = [lookup[0] for lookup in form.fields['tag'].widget.lookups] tag_lookups = [lookup[0] for lookup in form.fields['tag'].widget.lookups]
expected_lookups = ['exact', 'n', 'empty_true', 'empty_false'] # Device filterset has tag and tag__n but not tag__empty
expected_lookups = ['exact', 'n']
self.assertEqual(tag_lookups, expected_lookups) self.assertEqual(tag_lookups, expected_lookups)
def test_mixin_enhances_multi_choice_field(self):
"""Plain MultipleChoiceField should be enhanced with choice-appropriate lookups."""
class TestForm(FilterModifierMixin, forms.Form):
status = forms.MultipleChoiceField(choices=[('a', 'A'), ('b', 'B')], required=False)
form = TestForm()
self.assertIsInstance(form.fields['status'].widget, FilterModifierWidget)
status_lookups = [lookup[0] for lookup in form.fields['status'].widget.lookups]
expected_lookups = ['exact', 'n', 'empty_true', 'empty_false']
self.assertEqual(status_lookups, expected_lookups)
def test_mixin_enhances_integer_field(self): def test_mixin_enhances_integer_field(self):
"""IntegerField should be enhanced with comparison modifiers.""" """IntegerField should be enhanced with comparison modifiers."""
class TestForm(FilterModifierMixin, forms.Form): class TestForm(FilterModifierMixin, forms.Form):
count = forms.IntegerField(required=False) integer_field = forms.IntegerField(required=False)
model = TestModel
form = TestForm() form = TestForm()
self.assertIsInstance(form.fields['count'].widget, FilterModifierWidget) self.assertIsInstance(form.fields['integer_field'].widget, FilterModifierWidget)
lookup_codes = [lookup[0] for lookup in form.fields['count'].widget.lookups] lookup_codes = [lookup[0] for lookup in form.fields['integer_field'].widget.lookups]
expected_lookups = ['exact', 'n', 'gt', 'gte', 'lt', 'lte', 'empty_true', 'empty_false'] expected_lookups = ['exact', 'n', 'gt', 'gte', 'lt', 'lte', 'empty_true', 'empty_false']
self.assertEqual(lookup_codes, expected_lookups) self.assertEqual(lookup_codes, expected_lookups)
def test_mixin_enhances_decimal_field(self): def test_mixin_enhances_decimal_field(self):
"""DecimalField should be enhanced with comparison modifiers.""" """DecimalField should be enhanced with comparison modifiers."""
class TestForm(FilterModifierMixin, forms.Form): class TestForm(FilterModifierMixin, forms.Form):
weight = forms.DecimalField(required=False) decimal_field = forms.DecimalField(required=False)
model = TestModel
form = TestForm() form = TestForm()
self.assertIsInstance(form.fields['weight'].widget, FilterModifierWidget) self.assertIsInstance(form.fields['decimal_field'].widget, FilterModifierWidget)
lookup_codes = [lookup[0] for lookup in form.fields['weight'].widget.lookups] lookup_codes = [lookup[0] for lookup in form.fields['decimal_field'].widget.lookups]
expected_lookups = ['exact', 'n', 'gt', 'gte', 'lt', 'lte', 'empty_true', 'empty_false'] expected_lookups = ['exact', 'n', 'gt', 'gte', 'lt', 'lte', 'empty_true', 'empty_false']
self.assertEqual(lookup_codes, expected_lookups) self.assertEqual(lookup_codes, expected_lookups)
def test_mixin_enhances_date_field(self): def test_mixin_enhances_date_field(self):
"""DateField should be enhanced with date-appropriate modifiers.""" """DateField should be enhanced with date-appropriate modifiers."""
class TestForm(FilterModifierMixin, forms.Form): class TestForm(FilterModifierMixin, forms.Form):
created = forms.DateField(required=False) date_field = forms.DateField(required=False)
model = TestModel
form = TestForm() form = TestForm()
self.assertIsInstance(form.fields['created'].widget, FilterModifierWidget) self.assertIsInstance(form.fields['date_field'].widget, FilterModifierWidget)
lookup_codes = [lookup[0] for lookup in form.fields['created'].widget.lookups] lookup_codes = [lookup[0] for lookup in form.fields['date_field'].widget.lookups]
expected_lookups = ['exact', 'n', 'gt', 'gte', 'lt', 'lte', 'empty_true', 'empty_false'] expected_lookups = ['exact', 'n', 'gt', 'gte', 'lt', 'lte', 'empty_true', 'empty_false']
self.assertEqual(lookup_codes, expected_lookups) self.assertEqual(lookup_codes, expected_lookups)