فهرست منبع

Split Filter and FilterSet classes

jeremystretch 4 سال پیش
والد
کامیت
0de50e0afe

+ 2 - 3
netbox/circuits/filters.py

@@ -5,9 +5,8 @@ from dcim.filters import CableTerminationFilterSet
 from dcim.models import Region, Site, SiteGroup
 from dcim.models import Region, Site, SiteGroup
 from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from tenancy.filters import TenancyFilterSet
 from tenancy.filters import TenancyFilterSet
-from utilities.filters import (
-    BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
-)
+from utilities.filters import TagFilter, TreeNodeMultipleChoiceFilter
+from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
 from .choices import *
 from .choices import *
 from .models import *
 from .models import *
 
 

+ 2 - 2
netbox/dcim/filters.py

@@ -6,9 +6,9 @@ from tenancy.filters import TenancyFilterSet
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.choices import ColorChoices
 from utilities.choices import ColorChoices
 from utilities.filters import (
 from utilities.filters import (
-    BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
-    NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter,
+    MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, TagFilter, TreeNodeMultipleChoiceFilter,
 )
 )
+from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
 from virtualization.models import Cluster
 from virtualization.models import Cluster
 from .choices import *
 from .choices import *
 from .constants import *
 from .constants import *

+ 2 - 1
netbox/extras/filters.py

@@ -6,7 +6,8 @@ from django.forms import DateField, IntegerField, NullBooleanField
 
 
 from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
 from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
-from utilities.filters import BaseFilterSet, ContentTypeFilter
+from utilities.filtersets import BaseFilterSet
+from utilities.filters import ContentTypeFilter
 from virtualization.models import Cluster, ClusterGroup
 from virtualization.models import Cluster, ClusterGroup
 from .choices import *
 from .choices import *
 from .models import *
 from .models import *

+ 3 - 2
netbox/ipam/filters.py

@@ -9,9 +9,10 @@ from dcim.models import Device, Interface, Region, Site, SiteGroup
 from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from tenancy.filters import TenancyFilterSet
 from tenancy.filters import TenancyFilterSet
 from utilities.filters import (
 from utilities.filters import (
-    BaseFilterSet, ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet,
-    NumericArrayFilter, TagFilter, TreeNodeMultipleChoiceFilter,
+    ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TagFilter,
+    TreeNodeMultipleChoiceFilter,
 )
 )
+from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
 from virtualization.models import VirtualMachine, VMInterface
 from virtualization.models import VirtualMachine, VMInterface
 from .choices import *
 from .choices import *
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF

+ 2 - 1
netbox/secrets/filters.py

@@ -3,7 +3,8 @@ from django.db.models import Q
 
 
 from dcim.models import Device
 from dcim.models import Device
 from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
-from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter
+from utilities.filters import TagFilter
+from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
 from .models import Secret, SecretRole
 from .models import Secret, SecretRole
 
 

+ 2 - 1
netbox/tenancy/filters.py

@@ -2,7 +2,8 @@ import django_filters
 from django.db.models import Q
 from django.db.models import Q
 
 
 from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
-from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
+from utilities.filters import TagFilter, TreeNodeMultipleChoiceFilter
+from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
 from .models import Tenant, TenantGroup
 from .models import Tenant, TenantGroup
 
 
 
 

+ 1 - 1
netbox/users/filters.py

@@ -3,7 +3,7 @@ from django.contrib.auth.models import Group, User
 from django.db.models import Q
 from django.db.models import Q
 
 
 from users.models import ObjectPermission
 from users.models import ObjectPermission
-from utilities.filters import BaseFilterSet
+from utilities.filtersets import BaseFilterSet
 
 
 __all__ = (
 __all__ = (
     'GroupFilterSet',
     'GroupFilterSet',

+ 2 - 188
netbox/utilities/filters.py

@@ -1,17 +1,10 @@
 import django_filters
 import django_filters
-from django_filters.constants import EMPTY_VALUES
-from copy import deepcopy
-from dcim.forms import MACAddressField
 from django import forms
 from django import forms
 from django.conf import settings
 from django.conf import settings
-from django.db import models
-from django_filters.utils import get_model_field, resolve_field
+from django_filters.constants import EMPTY_VALUES
 
 
+from dcim.forms import MACAddressField
 from extras.models import Tag
 from extras.models import Tag
-from utilities.constants import (
-    FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
-    FILTER_NUMERIC_BASED_LOOKUP_MAP
-)
 
 
 
 
 def multivalue_field_factory(field_class):
 def multivalue_field_factory(field_class):
@@ -134,182 +127,3 @@ class ContentTypeFilter(django_filters.CharFilter):
                 f'{self.field_name}__model': model
                 f'{self.field_name}__model': model
             }
             }
         )
         )
-
-
-#
-# FilterSets
-#
-
-class BaseFilterSet(django_filters.FilterSet):
-    """
-    A base filterset which provides common functionaly to all NetBox filtersets
-    """
-    FILTER_DEFAULTS = deepcopy(django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS)
-    FILTER_DEFAULTS.update({
-        models.AutoField: {
-            'filter_class': MultiValueNumberFilter
-        },
-        models.CharField: {
-            'filter_class': MultiValueCharFilter
-        },
-        models.DateField: {
-            'filter_class': MultiValueDateFilter
-        },
-        models.DateTimeField: {
-            'filter_class': MultiValueDateTimeFilter
-        },
-        models.DecimalField: {
-            'filter_class': MultiValueNumberFilter
-        },
-        models.EmailField: {
-            'filter_class': MultiValueCharFilter
-        },
-        models.FloatField: {
-            'filter_class': MultiValueNumberFilter
-        },
-        models.IntegerField: {
-            'filter_class': MultiValueNumberFilter
-        },
-        models.PositiveIntegerField: {
-            'filter_class': MultiValueNumberFilter
-        },
-        models.PositiveSmallIntegerField: {
-            'filter_class': MultiValueNumberFilter
-        },
-        models.SlugField: {
-            'filter_class': MultiValueCharFilter
-        },
-        models.SmallIntegerField: {
-            'filter_class': MultiValueNumberFilter
-        },
-        models.TimeField: {
-            'filter_class': MultiValueTimeFilter
-        },
-        models.URLField: {
-            'filter_class': MultiValueCharFilter
-        },
-        MACAddressField: {
-            'filter_class': MultiValueMACAddressFilter
-        },
-    })
-
-    @staticmethod
-    def _get_filter_lookup_dict(existing_filter):
-        # Choose the lookup expression map based on the filter type
-        if isinstance(existing_filter, (
-            MultiValueDateFilter,
-            MultiValueDateTimeFilter,
-            MultiValueNumberFilter,
-            MultiValueTimeFilter
-        )):
-            lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP
-
-        elif isinstance(existing_filter, (
-            TreeNodeMultipleChoiceFilter,
-        )):
-            # TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression
-            lookup_map = FILTER_TREENODE_NEGATION_LOOKUP_MAP
-
-        elif isinstance(existing_filter, (
-            django_filters.ModelChoiceFilter,
-            django_filters.ModelMultipleChoiceFilter,
-            TagFilter
-        )) or existing_filter.extra.get('choices'):
-            # These filter types support only negation
-            lookup_map = FILTER_NEGATION_LOOKUP_MAP
-
-        elif isinstance(existing_filter, (
-            django_filters.filters.CharFilter,
-            django_filters.MultipleChoiceFilter,
-            MultiValueCharFilter,
-            MultiValueMACAddressFilter
-        )):
-            lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP
-
-        else:
-            lookup_map = None
-
-        return lookup_map
-
-    @classmethod
-    def get_filters(cls):
-        """
-        Override filter generation to support dynamic lookup expressions for certain filter types.
-
-        For specific filter types, new filters are created based on defined lookup expressions in
-        the form `<field_name>__<lookup_expr>`
-        """
-        filters = super().get_filters()
-
-        new_filters = {}
-        for existing_filter_name, existing_filter in filters.items():
-            # Loop over existing filters to extract metadata by which to create new filters
-
-            # If the filter makes use of a custom filter method or lookup expression skip it
-            # as we cannot sanely handle these cases in a generic mannor
-            if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']:
-                continue
-
-            # Choose the lookup expression map based on the filter type
-            lookup_map = cls._get_filter_lookup_dict(existing_filter)
-            if lookup_map is None:
-                # Do not augment this filter type with more lookup expressions
-                continue
-
-            # Get properties of the existing filter for later use
-            field_name = existing_filter.field_name
-            field = get_model_field(cls._meta.model, field_name)
-
-            # Create new filters for each lookup expression in the map
-            for lookup_name, lookup_expr in lookup_map.items():
-                new_filter_name = '{}__{}'.format(existing_filter_name, lookup_name)
-
-                try:
-                    if existing_filter_name in cls.declared_filters:
-                        # The filter field has been explicity 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
-                        new_filter = type(existing_filter)(
-                            field_name=field_name,
-                            lookup_expr=lookup_expr,
-                            label=existing_filter.label,
-                            exclude=existing_filter.exclude,
-                            distinct=existing_filter.distinct,
-                            **existing_filter.extra
-                        )
-                    else:
-                        # The filter field is listed in Meta.fields so we can safely rely on default behaviour
-                        # Will raise FieldLookupError if the lookup is invalid
-                        new_filter = cls.filter_for_field(field, field_name, lookup_expr)
-                except django_filters.exceptions.FieldLookupError:
-                    # The filter could not be created because the lookup expression is not supported on the field
-                    continue
-
-                if lookup_name.startswith('n'):
-                    # This is a negation filter which requires a queryset.exclude() clause
-                    # Of course setting the negation of the existing filter's exclude attribute handles both cases
-                    new_filter.exclude = not existing_filter.exclude
-
-                new_filters[new_filter_name] = new_filter
-
-        filters.update(new_filters)
-        return filters
-
-
-class NameSlugSearchFilterSet(django_filters.FilterSet):
-    """
-    A base class for adding the search method to models which only expose the `name` and `slug` fields
-    """
-    q = django_filters.CharFilter(
-        method='search',
-        label='Search',
-    )
-
-    def search(self, queryset, name, value):
-        if not value.strip():
-            return queryset
-        return queryset.filter(
-            models.Q(name__icontains=value) |
-            models.Q(slug__icontains=value)
-        )

+ 190 - 0
netbox/utilities/filtersets.py

@@ -0,0 +1,190 @@
+import django_filters
+from copy import deepcopy
+from dcim.forms import MACAddressField
+from django.db import models
+from django_filters.utils import get_model_field, resolve_field
+
+from utilities.constants import (
+    FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
+    FILTER_NUMERIC_BASED_LOOKUP_MAP
+)
+from utilities import filters
+
+
+#
+# FilterSets
+#
+
+class BaseFilterSet(django_filters.FilterSet):
+    """
+    A base filterset which provides common functionaly to all NetBox filtersets
+    """
+    FILTER_DEFAULTS = deepcopy(django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS)
+    FILTER_DEFAULTS.update({
+        models.AutoField: {
+            'filter_class': filters.MultiValueNumberFilter
+        },
+        models.CharField: {
+            'filter_class': filters.MultiValueCharFilter
+        },
+        models.DateField: {
+            'filter_class': filters.MultiValueDateFilter
+        },
+        models.DateTimeField: {
+            'filter_class': filters.MultiValueDateTimeFilter
+        },
+        models.DecimalField: {
+            'filter_class': filters.MultiValueNumberFilter
+        },
+        models.EmailField: {
+            'filter_class': filters.MultiValueCharFilter
+        },
+        models.FloatField: {
+            'filter_class': filters.MultiValueNumberFilter
+        },
+        models.IntegerField: {
+            'filter_class': filters.MultiValueNumberFilter
+        },
+        models.PositiveIntegerField: {
+            'filter_class': filters.MultiValueNumberFilter
+        },
+        models.PositiveSmallIntegerField: {
+            'filter_class': filters.MultiValueNumberFilter
+        },
+        models.SlugField: {
+            'filter_class': filters.MultiValueCharFilter
+        },
+        models.SmallIntegerField: {
+            'filter_class': filters.MultiValueNumberFilter
+        },
+        models.TimeField: {
+            'filter_class': filters.MultiValueTimeFilter
+        },
+        models.URLField: {
+            'filter_class': filters.MultiValueCharFilter
+        },
+        MACAddressField: {
+            'filter_class': filters.MultiValueMACAddressFilter
+        },
+    })
+
+    @staticmethod
+    def _get_filter_lookup_dict(existing_filter):
+        # Choose the lookup expression map based on the filter type
+        if isinstance(existing_filter, (
+            filters.MultiValueDateFilter,
+            filters.MultiValueDateTimeFilter,
+            filters.MultiValueNumberFilter,
+            filters.MultiValueTimeFilter
+        )):
+            lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP
+
+        elif isinstance(existing_filter, (
+            filters.TreeNodeMultipleChoiceFilter,
+        )):
+            # TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression
+            lookup_map = FILTER_TREENODE_NEGATION_LOOKUP_MAP
+
+        elif isinstance(existing_filter, (
+            django_filters.ModelChoiceFilter,
+            django_filters.ModelMultipleChoiceFilter,
+            filters.TagFilter
+        )) or existing_filter.extra.get('choices'):
+            # These filter types support only negation
+            lookup_map = FILTER_NEGATION_LOOKUP_MAP
+
+        elif isinstance(existing_filter, (
+            django_filters.filters.CharFilter,
+            django_filters.MultipleChoiceFilter,
+            filters.MultiValueCharFilter,
+            filters.MultiValueMACAddressFilter
+        )):
+            lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP
+
+        else:
+            lookup_map = None
+
+        return lookup_map
+
+    @classmethod
+    def get_filters(cls):
+        """
+        Override filter generation to support dynamic lookup expressions for certain filter types.
+
+        For specific filter types, new filters are created based on defined lookup expressions in
+        the form `<field_name>__<lookup_expr>`
+        """
+        filters = super().get_filters()
+
+        new_filters = {}
+        for existing_filter_name, existing_filter in filters.items():
+            # Loop over existing filters to extract metadata by which to create new filters
+
+            # If the filter makes use of a custom filter method or lookup expression skip it
+            # as we cannot sanely handle these cases in a generic mannor
+            if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']:
+                continue
+
+            # Choose the lookup expression map based on the filter type
+            lookup_map = cls._get_filter_lookup_dict(existing_filter)
+            if lookup_map is None:
+                # Do not augment this filter type with more lookup expressions
+                continue
+
+            # Get properties of the existing filter for later use
+            field_name = existing_filter.field_name
+            field = get_model_field(cls._meta.model, field_name)
+
+            # Create new filters for each lookup expression in the map
+            for lookup_name, lookup_expr in lookup_map.items():
+                new_filter_name = '{}__{}'.format(existing_filter_name, lookup_name)
+
+                try:
+                    if existing_filter_name in cls.declared_filters:
+                        # The filter field has been explicity 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
+                        new_filter = type(existing_filter)(
+                            field_name=field_name,
+                            lookup_expr=lookup_expr,
+                            label=existing_filter.label,
+                            exclude=existing_filter.exclude,
+                            distinct=existing_filter.distinct,
+                            **existing_filter.extra
+                        )
+                    else:
+                        # The filter field is listed in Meta.fields so we can safely rely on default behaviour
+                        # Will raise FieldLookupError if the lookup is invalid
+                        new_filter = cls.filter_for_field(field, field_name, lookup_expr)
+                except django_filters.exceptions.FieldLookupError:
+                    # The filter could not be created because the lookup expression is not supported on the field
+                    continue
+
+                if lookup_name.startswith('n'):
+                    # This is a negation filter which requires a queryset.exclude() clause
+                    # Of course setting the negation of the existing filter's exclude attribute handles both cases
+                    new_filter.exclude = not existing_filter.exclude
+
+                new_filters[new_filter_name] = new_filter
+
+        filters.update(new_filters)
+        return filters
+
+
+class NameSlugSearchFilterSet(django_filters.FilterSet):
+    """
+    A base class for adding the search method to models which only expose the `name` and `slug` fields
+    """
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
+
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        return queryset.filter(
+            models.Q(name__icontains=value) |
+            models.Q(slug__icontains=value)
+        )

+ 3 - 2
netbox/utilities/tests/test_filters.py

@@ -13,9 +13,10 @@ from dcim.models import (
 )
 )
 from extras.models import TaggedItem
 from extras.models import TaggedItem
 from utilities.filters import (
 from utilities.filters import (
-    BaseFilterSet, MACAddressFilter, MultiValueCharFilter, MultiValueDateFilter, MultiValueDateTimeFilter,
-    MultiValueNumberFilter, MultiValueTimeFilter, TagFilter, TreeNodeMultipleChoiceFilter,
+    MACAddressFilter, MultiValueCharFilter, MultiValueDateFilter, MultiValueDateTimeFilter, MultiValueNumberFilter,
+    MultiValueTimeFilter, TagFilter, TreeNodeMultipleChoiceFilter,
 )
 )
+from utilities.filtersets import BaseFilterSet
 
 
 
 
 class TreeNodeMultipleChoiceFilterTest(TestCase):
 class TreeNodeMultipleChoiceFilterTest(TestCase):

+ 2 - 4
netbox/virtualization/filters.py

@@ -4,10 +4,8 @@ from django.db.models import Q
 from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
 from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
 from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet, LocalConfigContextFilterSet
 from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet, LocalConfigContextFilterSet
 from tenancy.filters import TenancyFilterSet
 from tenancy.filters import TenancyFilterSet
-from utilities.filters import (
-    BaseFilterSet, MultiValueMACAddressFilter, NameSlugSearchFilterSet, TagFilter,
-    TreeNodeMultipleChoiceFilter,
-)
+from utilities.filters import MultiValueMACAddressFilter, TagFilter, TreeNodeMultipleChoiceFilter
+from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
 from .choices import *
 from .choices import *
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface