Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions netbox/dcim/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -1504,8 +1504,12 @@ class CableTypeChoices(ChoiceSet):
(TYPE_AOC, 'Active Optical Cabling (AOC)'),
),
),
(TYPE_USB, _('USB')),
(TYPE_POWER, _('Power')),
(
_('Other'), (
(TYPE_USB, _('USB')),
(TYPE_POWER, _('Power')),
)
)
)


Expand Down
4 changes: 2 additions & 2 deletions netbox/dcim/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from users.models import User
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
NumericArrayFilter, TreeNodeMultipleChoiceFilter, NullableMultipleChoiceFilter,
)
from virtualization.models import Cluster, ClusterGroup
from vpn.models import L2VPN
Expand Down Expand Up @@ -1980,7 +1980,7 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
method='_unterminated',
label=_('Unterminated'),
)
type = django_filters.MultipleChoiceFilter(
type = NullableMultipleChoiceFilter(
choices=CableTypeChoices
)
status = django_filters.MultipleChoiceFilter(
Expand Down
4 changes: 2 additions & 2 deletions netbox/dcim/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from users.models import User
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice, add_empty_filtering_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import NumberWithOptions
Expand Down Expand Up @@ -1052,7 +1052,7 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=add_blank_choice(CableTypeChoices),
choices=add_empty_filtering_choice(add_blank_choice(CableTypeChoices)),
required=False
)
status = forms.MultipleChoiceField(
Expand Down
5 changes: 5 additions & 0 deletions netbox/dcim/tests/test_filtersets.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.test import TestCase
from django.conf import settings

from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.choices import *
Expand Down Expand Up @@ -5247,6 +5248,10 @@ def test_length_unit(self):
def test_type(self):
params = {'type': [CableTypeChoices.TYPE_CAT3, CableTypeChoices.TYPE_CAT5E]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'type': [settings.FILTERS_NULL_CHOICE_VALUE]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
params = {'type': [settings.FILTERS_NULL_CHOICE_VALUE, CableTypeChoices.TYPE_CAT3]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 10)

def test_status(self):
params = {'status': [LinkStatusChoices.STATUS_CONNECTED]}
Expand Down
7 changes: 5 additions & 2 deletions netbox/netbox/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def _get_filter_lookup_dict(existing_filter):
django_filters.ModelChoiceFilter,
django_filters.ModelMultipleChoiceFilter,
TagFilter
)) or existing_filter.extra.get('choices'):
)):
# These filter types support only negation
return FILTER_NEGATION_LOOKUP_MAP

Expand Down Expand Up @@ -172,21 +172,24 @@ def get_additional_lookups(cls, existing_filter_name, existing_filter):
# Create new filters for each lookup expression in the map
for lookup_name, lookup_expr in lookup_map.items():
new_filter_name = f'{existing_filter_name}__{lookup_name}'
existing_filter_extra = deepcopy(existing_filter.extra)

try:
if existing_filter_name in cls.declared_filters:
# The filter field has been explicitly defined on the filterset class so we must manually
# create the new filter with the same type because there is no guarantee the defined type
# is the same as the default type for the field
resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
for field_to_remove in ('choices', 'null_value'):
existing_filter_extra.pop(field_to_remove, None)
filter_cls = django_filters.BooleanFilter if lookup_expr == 'empty' else type(existing_filter)
new_filter = filter_cls(
field_name=field_name,
lookup_expr=lookup_expr,
label=existing_filter.label,
exclude=existing_filter.exclude,
distinct=existing_filter.distinct,
**existing_filter.extra
**existing_filter_extra
)
elif hasattr(existing_filter, 'custom_field'):
# Filter is for a custom field
Expand Down
11 changes: 11 additions & 0 deletions netbox/utilities/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
'MultiValueTimeFilter',
'MultiValueWWNFilter',
'NullableCharFieldFilter',
'NullableMultipleChoiceFilter',
'NumericArrayFilter',
'TreeNodeMultipleChoiceFilter',
)
Expand Down Expand Up @@ -143,6 +144,16 @@ def filter(self, qs, value):
return qs.distinct() if self.distinct else qs


class NullableMultipleChoiceFilter(django_filters.MultipleChoiceFilter):
"""
Similar to NullableCharFieldFilter, but allows multiple values including the special NULL string.
"""
def filter(self, qs, value):
if settings.FILTERS_NULL_CHOICE_VALUE in value:
value.append('')
return super().filter(qs, value)


class NumericArrayFilter(django_filters.NumberFilter):
"""
Filter based on the presence of an integer within an ArrayField.
Expand Down
10 changes: 10 additions & 0 deletions netbox/utilities/forms/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re

from django import forms
from django.conf import settings
from django.forms.models import fields_for_model
from django.utils.translation import gettext as _

Expand All @@ -10,6 +11,7 @@

__all__ = (
'add_blank_choice',
'add_empty_filtering_choice',
'expand_alphanumeric_pattern',
'expand_ipaddress_pattern',
'form_from_model',
Expand Down Expand Up @@ -189,6 +191,14 @@ def add_blank_choice(choices):
return ((None, '---------'),) + tuple(choices)


def add_empty_filtering_choice(choices):
"""
Add an empty (null) choice to the end of a choices list, to be used in filtering classes
such as NullableMultipleChoiceFilter to match on an empty value.
"""
return tuple(choices) + ((settings.FILTERS_NULL_CHOICE_VALUE, '(unset)'),)


def form_from_model(model, fields):
"""
Return a Form class with the specified fields derived from a model. This is useful when we need a form to be used
Expand Down