Răsfoiți Sursa

Consolidate FilterSet classes

jeremystretch 4 ani în urmă
părinte
comite
c4e88fd11a

+ 6 - 7
netbox/circuits/filters.py

@@ -3,10 +3,9 @@ from django.db.models import Q
 
 from dcim.filters import CableTerminationFilterSet
 from dcim.models import Region, Site, SiteGroup
-from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from tenancy.filters import TenancyFilterSet
 from utilities.filters import TagFilter, TreeNodeMultipleChoiceFilter
-from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
+from utilities.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
 from .choices import *
 from .models import *
 
@@ -19,7 +18,7 @@ __all__ = (
 )
 
 
-class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class ProviderFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -79,7 +78,7 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdated
         )
 
 
-class ProviderNetworkFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class ProviderNetworkFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -109,14 +108,14 @@ class ProviderNetworkFilterSet(BaseFilterSet, CustomFieldModelFilterSet, Created
         ).distinct()
 
 
-class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class CircuitTypeFilterSet(OrganizationalModelFilterSet):
 
     class Meta:
         model = CircuitType
         fields = ['id', 'name', 'slug']
 
 
-class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
+class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -206,7 +205,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
         ).distinct()
 
 
-class CircuitTerminationFilterSet(BaseFilterSet, CreatedUpdatedFilterSet, CableTerminationFilterSet):
+class CircuitTerminationFilterSet(ChangeLoggedModelFilterSet, CableTerminationFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',

+ 43 - 58
netbox/dcim/filters.py

@@ -1,14 +1,16 @@
 import django_filters
 from django.contrib.auth.models import User
 
-from extras.filters import CustomFieldModelFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
+from extras.filtersets import LocalConfigContextFilterSet
 from tenancy.filters import TenancyFilterSet
 from tenancy.models import Tenant
 from utilities.choices import ColorChoices
 from utilities.filters import (
     MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, TagFilter, TreeNodeMultipleChoiceFilter,
 )
-from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
+from utilities.filtersets import (
+    BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
+)
 from virtualization.models import Cluster
 from .choices import *
 from .constants import *
@@ -57,7 +59,7 @@ __all__ = (
 )
 
 
-class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class RegionFilterSet(OrganizationalModelFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Region.objects.all(),
         label='Parent region (ID)',
@@ -74,7 +76,7 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilt
         fields = ['id', 'name', 'slug', 'description']
 
 
-class SiteGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class SiteGroupFilterSet(OrganizationalModelFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         label='Parent site group (ID)',
@@ -91,7 +93,7 @@ class SiteGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedF
         fields = ['id', 'name', 'slug', 'description']
 
 
-class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -154,7 +156,7 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
         return queryset.filter(qs_filter)
 
 
-class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class LocationFilterSet(OrganizationalModelFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         field_name='site__region',
@@ -218,14 +220,14 @@ class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFi
         )
 
 
-class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class RackRoleFilterSet(OrganizationalModelFilterSet):
 
     class Meta:
         model = RackRole
         fields = ['id', 'name', 'slug', 'color']
 
 
-class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -323,7 +325,7 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
         )
 
 
-class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -383,14 +385,14 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModel
         )
 
 
-class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class ManufacturerFilterSet(OrganizationalModelFilterSet):
 
     class Meta:
         model = Manufacturer
         fields = ['id', 'name', 'slug', 'description']
 
 
-class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class DeviceTypeFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -476,7 +478,7 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdat
         return queryset.exclude(devicebaytemplates__isnull=value)
 
 
-class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class DeviceTypeComponentFilterSet(django_filters.FilterSet):
     devicetype_id = django_filters.ModelMultipleChoiceFilter(
         queryset=DeviceType.objects.all(),
         field_name='device_type_id',
@@ -484,28 +486,28 @@ class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet, CreatedUpdatedFilter
     )
 
 
-class ConsolePortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
 
     class Meta:
         model = ConsolePortTemplate
         fields = ['id', 'name', 'type']
 
 
-class ConsoleServerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
 
     class Meta:
         model = ConsoleServerPortTemplate
         fields = ['id', 'name', 'type']
 
 
-class PowerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
 
     class Meta:
         model = PowerPortTemplate
         fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
 
 
-class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
     feed_leg = django_filters.MultipleChoiceFilter(
         choices=PowerOutletFeedLegChoices,
         null_value=None
@@ -516,7 +518,7 @@ class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
         fields = ['id', 'name', 'type', 'feed_leg']
 
 
-class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
         choices=InterfaceTypeChoices,
         null_value=None
@@ -527,7 +529,7 @@ class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
         fields = ['id', 'name', 'type', 'mgmt_only']
 
 
-class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
         choices=PortTypeChoices,
         null_value=None
@@ -538,7 +540,7 @@ class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
         fields = ['id', 'name', 'type']
 
 
-class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
         choices=PortTypeChoices,
         null_value=None
@@ -549,21 +551,21 @@ class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
         fields = ['id', 'name', 'type', 'positions']
 
 
-class DeviceBayTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
 
     class Meta:
         model = DeviceBayTemplate
         fields = ['id', 'name']
 
 
-class DeviceRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class DeviceRoleFilterSet(OrganizationalModelFilterSet):
 
     class Meta:
         model = DeviceRole
         fields = ['id', 'name', 'slug', 'color', 'vm_role']
 
 
-class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class PlatformFilterSet(OrganizationalModelFilterSet):
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
         field_name='manufacturer',
         queryset=Manufacturer.objects.all(),
@@ -581,13 +583,7 @@ class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFi
         fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
 
 
-class DeviceFilterSet(
-    BaseFilterSet,
-    TenancyFilterSet,
-    LocalConfigContextFilterSet,
-    CustomFieldModelFilterSet,
-    CreatedUpdatedFilterSet
-):
+class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -792,7 +788,7 @@ class DeviceFilterSet(
         return queryset.exclude(devicebays__isnull=value)
 
 
-class DeviceComponentFilterSet(CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class DeviceComponentFilterSet(django_filters.FilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -876,7 +872,7 @@ class PathEndpointFilterSet(django_filters.FilterSet):
             return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
 
 
-class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
+class ConsolePortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
     type = django_filters.MultipleChoiceFilter(
         choices=ConsolePortTypeChoices,
         null_value=None
@@ -887,12 +883,7 @@ class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina
         fields = ['id', 'name', 'label', 'description']
 
 
-class ConsoleServerPortFilterSet(
-    BaseFilterSet,
-    DeviceComponentFilterSet,
-    CableTerminationFilterSet,
-    PathEndpointFilterSet
-):
+class ConsoleServerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
     type = django_filters.MultipleChoiceFilter(
         choices=ConsolePortTypeChoices,
         null_value=None
@@ -903,7 +894,7 @@ class ConsoleServerPortFilterSet(
         fields = ['id', 'name', 'label', 'description']
 
 
-class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
+class PowerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
     type = django_filters.MultipleChoiceFilter(
         choices=PowerPortTypeChoices,
         null_value=None
@@ -914,7 +905,7 @@ class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
         fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description']
 
 
-class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
+class PowerOutletFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
     type = django_filters.MultipleChoiceFilter(
         choices=PowerOutletTypeChoices,
         null_value=None
@@ -929,7 +920,7 @@ class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina
         fields = ['id', 'name', 'label', 'feed_leg', 'description']
 
 
-class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
+class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -1027,7 +1018,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
         }.get(value, queryset.none())
 
 
-class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
+class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
     type = django_filters.MultipleChoiceFilter(
         choices=PortTypeChoices,
         null_value=None
@@ -1038,7 +1029,7 @@ class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
         fields = ['id', 'name', 'label', 'type', 'description']
 
 
-class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
+class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
     type = django_filters.MultipleChoiceFilter(
         choices=PortTypeChoices,
         null_value=None
@@ -1049,14 +1040,14 @@ class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminatio
         fields = ['id', 'name', 'label', 'type', 'positions', 'description']
 
 
-class DeviceBayFilterSet(BaseFilterSet, DeviceComponentFilterSet):
+class DeviceBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
 
     class Meta:
         model = DeviceBay
         fields = ['id', 'name', 'label', 'description']
 
 
-class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
+class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -1129,7 +1120,7 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
         return queryset.filter(qs_filter)
 
 
-class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class VirtualChassisFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -1209,7 +1200,7 @@ class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedU
         return queryset.filter(qs_filter).distinct()
 
 
-class CableFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class CableFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -1273,7 +1264,7 @@ class CableFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFil
         return queryset
 
 
-class ConnectionFilterSet:
+class ConnectionFilterSet(BaseFilterSet):
 
     def filter_site(self, queryset, name, value):
         if not value.strip():
@@ -1286,7 +1277,7 @@ class ConnectionFilterSet:
         return queryset.filter(**{f'{name}__in': value})
 
 
-class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
+class ConsoleConnectionFilterSet(ConnectionFilterSet):
     site = django_filters.CharFilter(
         method='filter_site',
         label='Site (slug)',
@@ -1304,7 +1295,7 @@ class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
         fields = ['name']
 
 
-class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
+class PowerConnectionFilterSet(ConnectionFilterSet):
     site = django_filters.CharFilter(
         method='filter_site',
         label='Site (slug)',
@@ -1322,7 +1313,7 @@ class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
         fields = ['name']
 
 
-class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
+class InterfaceConnectionFilterSet(ConnectionFilterSet):
     site = django_filters.CharFilter(
         method='filter_site',
         label='Site (slug)',
@@ -1340,7 +1331,7 @@ class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
         fields = []
 
 
-class PowerPanelFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class PowerPanelFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -1402,13 +1393,7 @@ class PowerPanelFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdat
         return queryset.filter(qs_filter)
 
 
-class PowerFeedFilterSet(
-    BaseFilterSet,
-    CableTerminationFilterSet,
-    PathEndpointFilterSet,
-    CustomFieldModelFilterSet,
-    CreatedUpdatedFilterSet
-):
+class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',

+ 12 - 12
netbox/extras/api/views.py

@@ -9,7 +9,7 @@ from rest_framework.routers import APIRootView
 from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
 from rq import Worker
 
-from extras import filters
+from extras import filtersets
 from extras.choices import JobResultStatusChoices
 from extras.models import *
 from extras.models import CustomField
@@ -61,7 +61,7 @@ class WebhookViewSet(ModelViewSet):
     metadata_class = ContentTypeMetadata
     queryset = Webhook.objects.all()
     serializer_class = serializers.WebhookSerializer
-    filterset_class = filters.WebhookFilterSet
+    filterset_class = filtersets.WebhookFilterSet
 
 
 #
@@ -72,7 +72,7 @@ class CustomFieldViewSet(ModelViewSet):
     metadata_class = ContentTypeMetadata
     queryset = CustomField.objects.all()
     serializer_class = serializers.CustomFieldSerializer
-    filterset_class = filters.CustomFieldFilterSet
+    filterset_class = filtersets.CustomFieldFilterSet
 
 
 class CustomFieldModelViewSet(ModelViewSet):
@@ -101,7 +101,7 @@ class CustomLinkViewSet(ModelViewSet):
     metadata_class = ContentTypeMetadata
     queryset = CustomLink.objects.all()
     serializer_class = serializers.CustomLinkSerializer
-    filterset_class = filters.CustomLinkFilterSet
+    filterset_class = filtersets.CustomLinkFilterSet
 
 
 #
@@ -112,7 +112,7 @@ class ExportTemplateViewSet(ModelViewSet):
     metadata_class = ContentTypeMetadata
     queryset = ExportTemplate.objects.all()
     serializer_class = serializers.ExportTemplateSerializer
-    filterset_class = filters.ExportTemplateFilterSet
+    filterset_class = filtersets.ExportTemplateFilterSet
 
 
 #
@@ -124,7 +124,7 @@ class TagViewSet(ModelViewSet):
         tagged_items=count_related(TaggedItem, 'tag')
     )
     serializer_class = serializers.TagSerializer
-    filterset_class = filters.TagFilterSet
+    filterset_class = filtersets.TagFilterSet
 
 
 #
@@ -135,7 +135,7 @@ class ImageAttachmentViewSet(ModelViewSet):
     metadata_class = ContentTypeMetadata
     queryset = ImageAttachment.objects.all()
     serializer_class = serializers.ImageAttachmentSerializer
-    filterset_class = filters.ImageAttachmentFilterSet
+    filterset_class = filtersets.ImageAttachmentFilterSet
 
 
 #
@@ -146,7 +146,7 @@ class JournalEntryViewSet(ModelViewSet):
     metadata_class = ContentTypeMetadata
     queryset = JournalEntry.objects.all()
     serializer_class = serializers.JournalEntrySerializer
-    filterset_class = filters.JournalEntryFilterSet
+    filterset_class = filtersets.JournalEntryFilterSet
 
 
 #
@@ -158,7 +158,7 @@ class ConfigContextViewSet(ModelViewSet):
         'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
     )
     serializer_class = serializers.ConfigContextSerializer
-    filterset_class = filters.ConfigContextFilterSet
+    filterset_class = filtersets.ConfigContextFilterSet
 
 
 #
@@ -358,7 +358,7 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
     metadata_class = ContentTypeMetadata
     queryset = ObjectChange.objects.prefetch_related('user')
     serializer_class = serializers.ObjectChangeSerializer
-    filterset_class = filters.ObjectChangeFilterSet
+    filterset_class = filtersets.ObjectChangeFilterSet
 
 
 #
@@ -371,7 +371,7 @@ class JobResultViewSet(ReadOnlyModelViewSet):
     """
     queryset = JobResult.objects.prefetch_related('user')
     serializer_class = serializers.JobResultSerializer
-    filterset_class = filters.JobResultFilterSet
+    filterset_class = filtersets.JobResultFilterSet
 
 
 #
@@ -384,4 +384,4 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
     """
     queryset = ContentType.objects.order_by('app_label', 'model')
     serializer_class = serializers.ContentTypeSerializer
-    filterset_class = filters.ContentTypeFilterSet
+    filterset_class = filtersets.ContentTypeFilterSet

+ 0 - 366
netbox/extras/filters.py

@@ -1,32 +1,10 @@
 import django_filters
-from django.contrib.auth.models import User
-from django.contrib.contenttypes.models import ContentType
-from django.db.models import Q
 from django.forms import DateField, IntegerField, NullBooleanField
 
-from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
-from tenancy.models import Tenant, TenantGroup
-from utilities.filtersets import BaseFilterSet
-from utilities.filters import ContentTypeFilter
-from virtualization.models import Cluster, ClusterGroup
 from .choices import *
-from .models import *
-
 
 __all__ = (
-    'ConfigContextFilterSet',
-    'ContentTypeFilterSet',
-    'CreatedUpdatedFilterSet',
     'CustomFieldFilter',
-    'CustomLinkFilterSet',
-    'CustomFieldModelFilterSet',
-    'ExportTemplateFilterSet',
-    'ImageAttachmentFilterSet',
-    'JournalEntryFilterSet',
-    'LocalConfigContextFilterSet',
-    'ObjectChangeFilterSet',
-    'TagFilterSet',
-    'WebhookFilterSet',
 )
 
 EXACT_FILTER_TYPES = (
@@ -37,41 +15,6 @@ EXACT_FILTER_TYPES = (
 )
 
 
-class CreatedUpdatedFilterSet(django_filters.FilterSet):
-    created = django_filters.DateFilter()
-    created__gte = django_filters.DateFilter(
-        field_name='created',
-        lookup_expr='gte'
-    )
-    created__lte = django_filters.DateFilter(
-        field_name='created',
-        lookup_expr='lte'
-    )
-    last_updated = django_filters.DateTimeFilter()
-    last_updated__gte = django_filters.DateTimeFilter(
-        field_name='last_updated',
-        lookup_expr='gte'
-    )
-    last_updated__lte = django_filters.DateTimeFilter(
-        field_name='last_updated',
-        lookup_expr='lte'
-    )
-
-
-class WebhookFilterSet(BaseFilterSet):
-    content_types = ContentTypeFilter()
-    http_method = django_filters.MultipleChoiceFilter(
-        choices=WebhookHttpMethodChoices
-    )
-
-    class Meta:
-        model = Webhook
-        fields = [
-            'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled',
-            'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
-        ]
-
-
 class CustomFieldFilter(django_filters.Filter):
     """
     Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.
@@ -93,312 +36,3 @@ class CustomFieldFilter(django_filters.Filter):
         if custom_field.type not in EXACT_FILTER_TYPES:
             if custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_LOOSE:
                 self.lookup_expr = 'icontains'
-
-
-class CustomFieldModelFilterSet(django_filters.FilterSet):
-    """
-    Dynamically add a Filter for each CustomField applicable to the parent model.
-    """
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        custom_fields = CustomField.objects.filter(
-            content_types=ContentType.objects.get_for_model(self._meta.model)
-        ).exclude(
-            filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
-        )
-        for cf in custom_fields:
-            self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
-
-
-class CustomFieldFilterSet(django_filters.FilterSet):
-    content_types = ContentTypeFilter()
-
-    class Meta:
-        model = CustomField
-        fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight']
-
-
-class CustomLinkFilterSet(BaseFilterSet):
-
-    class Meta:
-        model = CustomLink
-        fields = ['id', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', 'new_window']
-
-
-class ExportTemplateFilterSet(BaseFilterSet):
-
-    class Meta:
-        model = ExportTemplate
-        fields = ['id', 'content_type', 'name']
-
-
-class ImageAttachmentFilterSet(BaseFilterSet):
-    content_type = ContentTypeFilter()
-
-    class Meta:
-        model = ImageAttachment
-        fields = ['id', 'content_type_id', 'object_id', 'name']
-
-
-class JournalEntryFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
-    q = django_filters.CharFilter(
-        method='search',
-        label='Search',
-    )
-    created = django_filters.DateTimeFromToRangeFilter()
-    assigned_object_type = ContentTypeFilter()
-    created_by_id = django_filters.ModelMultipleChoiceFilter(
-        queryset=User.objects.all(),
-        label='User (ID)',
-    )
-    created_by = django_filters.ModelMultipleChoiceFilter(
-        field_name='created_by__username',
-        queryset=User.objects.all(),
-        to_field_name='username',
-        label='User (name)',
-    )
-    kind = django_filters.MultipleChoiceFilter(
-        choices=JournalEntryKindChoices
-    )
-
-    class Meta:
-        model = JournalEntry
-        fields = ['id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind']
-
-    def search(self, queryset, name, value):
-        if not value.strip():
-            return queryset
-        return queryset.filter(comments__icontains=value)
-
-
-class TagFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
-    q = django_filters.CharFilter(
-        method='search',
-        label='Search',
-    )
-
-    class Meta:
-        model = Tag
-        fields = ['id', 'name', 'slug', 'color']
-
-    def search(self, queryset, name, value):
-        if not value.strip():
-            return queryset
-        return queryset.filter(
-            Q(name__icontains=value) |
-            Q(slug__icontains=value)
-        )
-
-
-class ConfigContextFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
-    q = django_filters.CharFilter(
-        method='search',
-        label='Search',
-    )
-    region_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='regions',
-        queryset=Region.objects.all(),
-        label='Region',
-    )
-    region = django_filters.ModelMultipleChoiceFilter(
-        field_name='regions__slug',
-        queryset=Region.objects.all(),
-        to_field_name='slug',
-        label='Region (slug)',
-    )
-    site_group = django_filters.ModelMultipleChoiceFilter(
-        field_name='site_groups__slug',
-        queryset=SiteGroup.objects.all(),
-        to_field_name='slug',
-        label='Site group (slug)',
-    )
-    site_group_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='site_groups',
-        queryset=SiteGroup.objects.all(),
-        label='Site group',
-    )
-    site_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='sites',
-        queryset=Site.objects.all(),
-        label='Site',
-    )
-    site = django_filters.ModelMultipleChoiceFilter(
-        field_name='sites__slug',
-        queryset=Site.objects.all(),
-        to_field_name='slug',
-        label='Site (slug)',
-    )
-    device_type_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='device_types',
-        queryset=DeviceType.objects.all(),
-        label='Device type',
-    )
-    role_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='roles',
-        queryset=DeviceRole.objects.all(),
-        label='Role',
-    )
-    role = django_filters.ModelMultipleChoiceFilter(
-        field_name='roles__slug',
-        queryset=DeviceRole.objects.all(),
-        to_field_name='slug',
-        label='Role (slug)',
-    )
-    platform_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='platforms',
-        queryset=Platform.objects.all(),
-        label='Platform',
-    )
-    platform = django_filters.ModelMultipleChoiceFilter(
-        field_name='platforms__slug',
-        queryset=Platform.objects.all(),
-        to_field_name='slug',
-        label='Platform (slug)',
-    )
-    cluster_group_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='cluster_groups',
-        queryset=ClusterGroup.objects.all(),
-        label='Cluster group',
-    )
-    cluster_group = django_filters.ModelMultipleChoiceFilter(
-        field_name='cluster_groups__slug',
-        queryset=ClusterGroup.objects.all(),
-        to_field_name='slug',
-        label='Cluster group (slug)',
-    )
-    cluster_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='clusters',
-        queryset=Cluster.objects.all(),
-        label='Cluster',
-    )
-    tenant_group_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='tenant_groups',
-        queryset=TenantGroup.objects.all(),
-        label='Tenant group',
-    )
-    tenant_group = django_filters.ModelMultipleChoiceFilter(
-        field_name='tenant_groups__slug',
-        queryset=TenantGroup.objects.all(),
-        to_field_name='slug',
-        label='Tenant group (slug)',
-    )
-    tenant_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='tenants',
-        queryset=Tenant.objects.all(),
-        label='Tenant',
-    )
-    tenant = django_filters.ModelMultipleChoiceFilter(
-        field_name='tenants__slug',
-        queryset=Tenant.objects.all(),
-        to_field_name='slug',
-        label='Tenant (slug)',
-    )
-    tag = django_filters.ModelMultipleChoiceFilter(
-        field_name='tags__slug',
-        queryset=Tag.objects.all(),
-        to_field_name='slug',
-        label='Tag (slug)',
-    )
-
-    class Meta:
-        model = ConfigContext
-        fields = ['id', 'name', 'is_active']
-
-    def search(self, queryset, name, value):
-        if not value.strip():
-            return queryset
-        return queryset.filter(
-            Q(name__icontains=value) |
-            Q(description__icontains=value) |
-            Q(data__icontains=value)
-        )
-
-
-#
-# Filter for Local Config Context Data
-#
-
-class LocalConfigContextFilterSet(django_filters.FilterSet):
-    local_context_data = django_filters.BooleanFilter(
-        method='_local_context_data',
-        label='Has local config context data',
-    )
-
-    def _local_context_data(self, queryset, name, value):
-        return queryset.exclude(local_context_data__isnull=value)
-
-
-class ObjectChangeFilterSet(BaseFilterSet):
-    q = django_filters.CharFilter(
-        method='search',
-        label='Search',
-    )
-    time = django_filters.DateTimeFromToRangeFilter()
-    changed_object_type = ContentTypeFilter()
-    user_id = django_filters.ModelMultipleChoiceFilter(
-        queryset=User.objects.all(),
-        label='User (ID)',
-    )
-    user = django_filters.ModelMultipleChoiceFilter(
-        field_name='user__username',
-        queryset=User.objects.all(),
-        to_field_name='username',
-        label='User name',
-    )
-
-    class Meta:
-        model = ObjectChange
-        fields = [
-            'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id',
-            'object_repr',
-        ]
-
-    def search(self, queryset, name, value):
-        if not value.strip():
-            return queryset
-        return queryset.filter(
-            Q(user_name__icontains=value) |
-            Q(object_repr__icontains=value)
-        )
-
-
-#
-# Job Results
-#
-
-class JobResultFilterSet(BaseFilterSet):
-    q = django_filters.CharFilter(
-        method='search',
-        label='Search',
-    )
-    created = django_filters.DateTimeFilter()
-    completed = django_filters.DateTimeFilter()
-    status = django_filters.MultipleChoiceFilter(
-        choices=JobResultStatusChoices,
-        null_value=None
-    )
-
-    class Meta:
-        model = JobResult
-        fields = [
-            'id', 'created', 'completed', 'status', 'user', 'obj_type', 'name'
-        ]
-
-    def search(self, queryset, name, value):
-        if not value.strip():
-            return queryset
-        return queryset.filter(
-            Q(user__username__icontains=value)
-        )
-
-
-#
-# ContentTypes
-#
-
-class ContentTypeFilterSet(django_filters.FilterSet):
-
-    class Meta:
-        model = ContentType
-        fields = ['id', 'app_label', 'model']

+ 340 - 0
netbox/extras/filtersets.py

@@ -0,0 +1,340 @@
+import django_filters
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.db.models import Q
+
+from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
+from tenancy.models import Tenant, TenantGroup
+from utilities.filters import ContentTypeFilter
+from utilities.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet
+from virtualization.models import Cluster, ClusterGroup
+from .choices import *
+from .models import *
+
+
+__all__ = (
+    'ConfigContextFilterSet',
+    'ContentTypeFilterSet',
+    'CustomLinkFilterSet',
+    'ExportTemplateFilterSet',
+    'ImageAttachmentFilterSet',
+    'JournalEntryFilterSet',
+    'LocalConfigContextFilterSet',
+    'ObjectChangeFilterSet',
+    'TagFilterSet',
+    'WebhookFilterSet',
+)
+
+EXACT_FILTER_TYPES = (
+    CustomFieldTypeChoices.TYPE_BOOLEAN,
+    CustomFieldTypeChoices.TYPE_DATE,
+    CustomFieldTypeChoices.TYPE_INTEGER,
+    CustomFieldTypeChoices.TYPE_SELECT,
+)
+
+
+class WebhookFilterSet(BaseFilterSet):
+    content_types = ContentTypeFilter()
+    http_method = django_filters.MultipleChoiceFilter(
+        choices=WebhookHttpMethodChoices
+    )
+
+    class Meta:
+        model = Webhook
+        fields = [
+            'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled',
+            'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
+        ]
+
+
+class CustomFieldFilterSet(django_filters.FilterSet):
+    content_types = ContentTypeFilter()
+
+    class Meta:
+        model = CustomField
+        fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight']
+
+
+class CustomLinkFilterSet(BaseFilterSet):
+
+    class Meta:
+        model = CustomLink
+        fields = ['id', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', 'new_window']
+
+
+class ExportTemplateFilterSet(BaseFilterSet):
+
+    class Meta:
+        model = ExportTemplate
+        fields = ['id', 'content_type', 'name']
+
+
+class ImageAttachmentFilterSet(BaseFilterSet):
+    content_type = ContentTypeFilter()
+
+    class Meta:
+        model = ImageAttachment
+        fields = ['id', 'content_type_id', 'object_id', 'name']
+
+
+class JournalEntryFilterSet(ChangeLoggedModelFilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
+    created = django_filters.DateTimeFromToRangeFilter()
+    assigned_object_type = ContentTypeFilter()
+    created_by_id = django_filters.ModelMultipleChoiceFilter(
+        queryset=User.objects.all(),
+        label='User (ID)',
+    )
+    created_by = django_filters.ModelMultipleChoiceFilter(
+        field_name='created_by__username',
+        queryset=User.objects.all(),
+        to_field_name='username',
+        label='User (name)',
+    )
+    kind = django_filters.MultipleChoiceFilter(
+        choices=JournalEntryKindChoices
+    )
+
+    class Meta:
+        model = JournalEntry
+        fields = ['id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind']
+
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        return queryset.filter(comments__icontains=value)
+
+
+class TagFilterSet(ChangeLoggedModelFilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
+
+    class Meta:
+        model = Tag
+        fields = ['id', 'name', 'slug', 'color']
+
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        return queryset.filter(
+            Q(name__icontains=value) |
+            Q(slug__icontains=value)
+        )
+
+
+class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
+    region_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='regions',
+        queryset=Region.objects.all(),
+        label='Region',
+    )
+    region = django_filters.ModelMultipleChoiceFilter(
+        field_name='regions__slug',
+        queryset=Region.objects.all(),
+        to_field_name='slug',
+        label='Region (slug)',
+    )
+    site_group = django_filters.ModelMultipleChoiceFilter(
+        field_name='site_groups__slug',
+        queryset=SiteGroup.objects.all(),
+        to_field_name='slug',
+        label='Site group (slug)',
+    )
+    site_group_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='site_groups',
+        queryset=SiteGroup.objects.all(),
+        label='Site group',
+    )
+    site_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='sites',
+        queryset=Site.objects.all(),
+        label='Site',
+    )
+    site = django_filters.ModelMultipleChoiceFilter(
+        field_name='sites__slug',
+        queryset=Site.objects.all(),
+        to_field_name='slug',
+        label='Site (slug)',
+    )
+    device_type_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='device_types',
+        queryset=DeviceType.objects.all(),
+        label='Device type',
+    )
+    role_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='roles',
+        queryset=DeviceRole.objects.all(),
+        label='Role',
+    )
+    role = django_filters.ModelMultipleChoiceFilter(
+        field_name='roles__slug',
+        queryset=DeviceRole.objects.all(),
+        to_field_name='slug',
+        label='Role (slug)',
+    )
+    platform_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='platforms',
+        queryset=Platform.objects.all(),
+        label='Platform',
+    )
+    platform = django_filters.ModelMultipleChoiceFilter(
+        field_name='platforms__slug',
+        queryset=Platform.objects.all(),
+        to_field_name='slug',
+        label='Platform (slug)',
+    )
+    cluster_group_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='cluster_groups',
+        queryset=ClusterGroup.objects.all(),
+        label='Cluster group',
+    )
+    cluster_group = django_filters.ModelMultipleChoiceFilter(
+        field_name='cluster_groups__slug',
+        queryset=ClusterGroup.objects.all(),
+        to_field_name='slug',
+        label='Cluster group (slug)',
+    )
+    cluster_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='clusters',
+        queryset=Cluster.objects.all(),
+        label='Cluster',
+    )
+    tenant_group_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='tenant_groups',
+        queryset=TenantGroup.objects.all(),
+        label='Tenant group',
+    )
+    tenant_group = django_filters.ModelMultipleChoiceFilter(
+        field_name='tenant_groups__slug',
+        queryset=TenantGroup.objects.all(),
+        to_field_name='slug',
+        label='Tenant group (slug)',
+    )
+    tenant_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='tenants',
+        queryset=Tenant.objects.all(),
+        label='Tenant',
+    )
+    tenant = django_filters.ModelMultipleChoiceFilter(
+        field_name='tenants__slug',
+        queryset=Tenant.objects.all(),
+        to_field_name='slug',
+        label='Tenant (slug)',
+    )
+    tag = django_filters.ModelMultipleChoiceFilter(
+        field_name='tags__slug',
+        queryset=Tag.objects.all(),
+        to_field_name='slug',
+        label='Tag (slug)',
+    )
+
+    class Meta:
+        model = ConfigContext
+        fields = ['id', 'name', 'is_active']
+
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        return queryset.filter(
+            Q(name__icontains=value) |
+            Q(description__icontains=value) |
+            Q(data__icontains=value)
+        )
+
+
+#
+# Filter for Local Config Context Data
+#
+
+class LocalConfigContextFilterSet(django_filters.FilterSet):
+    local_context_data = django_filters.BooleanFilter(
+        method='_local_context_data',
+        label='Has local config context data',
+    )
+
+    def _local_context_data(self, queryset, name, value):
+        return queryset.exclude(local_context_data__isnull=value)
+
+
+class ObjectChangeFilterSet(BaseFilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
+    time = django_filters.DateTimeFromToRangeFilter()
+    changed_object_type = ContentTypeFilter()
+    user_id = django_filters.ModelMultipleChoiceFilter(
+        queryset=User.objects.all(),
+        label='User (ID)',
+    )
+    user = django_filters.ModelMultipleChoiceFilter(
+        field_name='user__username',
+        queryset=User.objects.all(),
+        to_field_name='username',
+        label='User name',
+    )
+
+    class Meta:
+        model = ObjectChange
+        fields = [
+            'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id',
+            'object_repr',
+        ]
+
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        return queryset.filter(
+            Q(user_name__icontains=value) |
+            Q(object_repr__icontains=value)
+        )
+
+
+#
+# Job Results
+#
+
+class JobResultFilterSet(BaseFilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
+    created = django_filters.DateTimeFilter()
+    completed = django_filters.DateTimeFilter()
+    status = django_filters.MultipleChoiceFilter(
+        choices=JobResultStatusChoices,
+        null_value=None
+    )
+
+    class Meta:
+        model = JobResult
+        fields = [
+            'id', 'created', 'completed', 'status', 'user', 'obj_type', 'name'
+        ]
+
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        return queryset.filter(
+            Q(user__username__icontains=value)
+        )
+
+
+#
+# ContentTypes
+#
+
+class ContentTypeFilterSet(django_filters.FilterSet):
+
+    class Meta:
+        model = ContentType
+        fields = ['id', 'app_label', 'model']

+ 1 - 1
netbox/extras/tests/test_filters.py

@@ -6,7 +6,7 @@ from django.test import TestCase
 
 from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
 from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
-from extras.filters import *
+from extras.filtersets import *
 from extras.models import *
 from ipam.models import IPAddress
 from tenancy.models import Tenant, TenantGroup

+ 8 - 8
netbox/extras/views.py

@@ -13,7 +13,7 @@ from utilities.forms import ConfirmationForm
 from utilities.tables import paginate_table
 from utilities.utils import copy_safe_request, count_related, shallow_compare_dict
 from utilities.views import ContentTypePermissionRequiredMixin
-from . import filters, forms, tables
+from . import filtersets, forms, tables
 from .choices import JobResultStatusChoices
 from .models import ConfigContext, ImageAttachment, JournalEntry, ObjectChange, JobResult, Tag, TaggedItem
 from .reports import get_report, get_reports, run_report
@@ -28,7 +28,7 @@ class TagListView(generic.ObjectListView):
     queryset = Tag.objects.annotate(
         items=count_related(TaggedItem, 'tag')
     )
-    filterset = filters.TagFilterSet
+    filterset = filtersets.TagFilterSet
     filterset_form = forms.TagFilterForm
     table = tables.TagTable
 
@@ -94,7 +94,7 @@ class TagBulkDeleteView(generic.BulkDeleteView):
 
 class ConfigContextListView(generic.ObjectListView):
     queryset = ConfigContext.objects.all()
-    filterset = filters.ConfigContextFilterSet
+    filterset = filtersets.ConfigContextFilterSet
     filterset_form = forms.ConfigContextFilterForm
     table = tables.ConfigContextTable
     action_buttons = ('add',)
@@ -127,7 +127,7 @@ class ConfigContextEditView(generic.ObjectEditView):
 
 class ConfigContextBulkEditView(generic.BulkEditView):
     queryset = ConfigContext.objects.all()
-    filterset = filters.ConfigContextFilterSet
+    filterset = filtersets.ConfigContextFilterSet
     table = tables.ConfigContextTable
     form = forms.ConfigContextBulkEditForm
 
@@ -173,7 +173,7 @@ class ObjectConfigContextView(generic.ObjectView):
 
 class ObjectChangeListView(generic.ObjectListView):
     queryset = ObjectChange.objects.all()
-    filterset = filters.ObjectChangeFilterSet
+    filterset = filtersets.ObjectChangeFilterSet
     filterset_form = forms.ObjectChangeFilterForm
     table = tables.ObjectChangeTable
     template_name = 'extras/objectchange_list.html'
@@ -300,7 +300,7 @@ class ImageAttachmentDeleteView(generic.ObjectDeleteView):
 
 class JournalEntryListView(generic.ObjectListView):
     queryset = JournalEntry.objects.all()
-    filterset = filters.JournalEntryFilterSet
+    filterset = filtersets.JournalEntryFilterSet
     filterset_form = forms.JournalEntryFilterForm
     table = tables.JournalEntryTable
     action_buttons = ('export',)
@@ -338,14 +338,14 @@ class JournalEntryDeleteView(generic.ObjectDeleteView):
 
 class JournalEntryBulkEditView(generic.BulkEditView):
     queryset = JournalEntry.objects.prefetch_related('created_by')
-    filterset = filters.JournalEntryFilterSet
+    filterset = filtersets.JournalEntryFilterSet
     table = tables.JournalEntryTable
     form = forms.JournalEntryBulkEditForm
 
 
 class JournalEntryBulkDeleteView(generic.BulkDeleteView):
     queryset = JournalEntry.objects.prefetch_related('created_by')
-    filterset = filters.JournalEntryFilterSet
+    filterset = filtersets.JournalEntryFilterSet
     table = tables.JournalEntryTable
 
 

+ 11 - 12
netbox/ipam/filters.py

@@ -6,13 +6,12 @@ from django.db.models import Q
 from netaddr.core import AddrFormatError
 
 from dcim.models import Device, Interface, Region, Site, SiteGroup
-from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from tenancy.filters import TenancyFilterSet
 from utilities.filters import (
     ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TagFilter,
     TreeNodeMultipleChoiceFilter,
 )
-from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
+from utilities.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
 from virtualization.models import VirtualMachine, VMInterface
 from .choices import *
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
@@ -32,7 +31,7 @@ __all__ = (
 )
 
 
-class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -75,7 +74,7 @@ class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, C
         fields = ['id', 'name', 'rd', 'enforce_unique']
 
 
-class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -117,14 +116,14 @@ class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilt
         fields = ['id', 'name']
 
 
-class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class RIRFilterSet(OrganizationalModelFilterSet):
 
     class Meta:
         model = RIR
         fields = ['id', 'name', 'slug', 'is_private', 'description']
 
 
-class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -174,7 +173,7 @@ class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
             return queryset.none()
 
 
-class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class RoleFilterSet(OrganizationalModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -185,7 +184,7 @@ class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilter
         fields = ['id', 'name', 'slug']
 
 
-class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -370,7 +369,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
         )
 
 
-class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -536,7 +535,7 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
         return queryset.exclude(assigned_object_id__isnull=value)
 
 
-class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class VLANGroupFilterSet(OrganizationalModelFilterSet):
     scope_type = ContentTypeFilter()
     region = django_filters.NumberFilter(
         method='filter_scope'
@@ -571,7 +570,7 @@ class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedF
         )
 
 
-class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -667,7 +666,7 @@ class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
         return queryset.get_for_virtualmachine(value)
 
 
-class ServiceFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
+class ServiceFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',

+ 3 - 4
netbox/secrets/filters.py

@@ -2,9 +2,8 @@ import django_filters
 from django.db.models import Q
 
 from dcim.models import Device
-from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from utilities.filters import TagFilter
-from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
+from utilities.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
 from virtualization.models import VirtualMachine
 from .models import Secret, SecretRole
 
@@ -15,14 +14,14 @@ __all__ = (
 )
 
 
-class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class SecretRoleFilterSet(OrganizationalModelFilterSet):
 
     class Meta:
         model = SecretRole
         fields = ['id', 'name', 'slug']
 
 
-class SecretFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class SecretFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',

+ 3 - 4
netbox/tenancy/filters.py

@@ -1,9 +1,8 @@
 import django_filters
 from django.db.models import Q
 
-from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from utilities.filters import TagFilter, TreeNodeMultipleChoiceFilter
-from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
+from utilities.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
 from .models import Tenant, TenantGroup
 
 
@@ -14,7 +13,7 @@ __all__ = (
 )
 
 
-class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class TenantGroupFilterSet(OrganizationalModelFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=TenantGroup.objects.all(),
         label='Tenant group (ID)',
@@ -31,7 +30,7 @@ class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdate
         fields = ['id', 'name', 'slug', 'description']
 
 
-class TenantFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class TenantFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',

+ 50 - 2
netbox/utilities/filtersets.py

@@ -1,9 +1,13 @@
 import django_filters
 from copy import deepcopy
-from dcim.forms import MACAddressField
+from django.contrib.contenttypes.models import ContentType
 from django.db import models
 from django_filters.utils import get_model_field, resolve_field
 
+from dcim.forms import MACAddressField
+from extras.choices import CustomFieldFilterLogicChoices
+from extras.filters import CustomFieldFilter
+from extras.models import CustomField
 from utilities.constants import (
     FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
     FILTER_NUMERIC_BASED_LOOKUP_MAP
@@ -11,6 +15,14 @@ from utilities.constants import (
 from utilities import filters
 
 
+__all__ = (
+    'BaseFilterSet',
+    'ChangeLoggedModelFilterSet',
+    'OrganizationalModelFilterSet',
+    'PrimaryModelFilterSet',
+)
+
+
 #
 # FilterSets
 #
@@ -172,7 +184,43 @@ class BaseFilterSet(django_filters.FilterSet):
         return filters
 
 
-class NameSlugSearchFilterSet(django_filters.FilterSet):
+class ChangeLoggedModelFilterSet(BaseFilterSet):
+    created = django_filters.DateFilter()
+    created__gte = django_filters.DateFilter(
+        field_name='created',
+        lookup_expr='gte'
+    )
+    created__lte = django_filters.DateFilter(
+        field_name='created',
+        lookup_expr='lte'
+    )
+    last_updated = django_filters.DateTimeFilter()
+    last_updated__gte = django_filters.DateTimeFilter(
+        field_name='last_updated',
+        lookup_expr='gte'
+    )
+    last_updated__lte = django_filters.DateTimeFilter(
+        field_name='last_updated',
+        lookup_expr='lte'
+    )
+
+
+class PrimaryModelFilterSet(ChangeLoggedModelFilterSet):
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Dynamically add a Filter for each CustomField applicable to the parent model
+        custom_fields = CustomField.objects.filter(
+            content_types=ContentType.objects.get_for_model(self._meta.model)
+        ).exclude(
+            filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
+        )
+        for cf in custom_fields:
+            self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
+
+
+class OrganizationalModelFilterSet(PrimaryModelFilterSet):
     """
     A base class for adding the search method to models which only expose the `name` and `slug` fields
     """

+ 7 - 13
netbox/virtualization/filters.py

@@ -2,10 +2,10 @@ import django_filters
 from django.db.models import Q
 
 from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
-from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet, LocalConfigContextFilterSet
+from extras.filtersets import LocalConfigContextFilterSet
 from tenancy.filters import TenancyFilterSet
 from utilities.filters import MultiValueMACAddressFilter, TagFilter, TreeNodeMultipleChoiceFilter
-from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
+from utilities.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
 from .choices import *
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 
@@ -18,21 +18,21 @@ __all__ = (
 )
 
 
-class ClusterTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class ClusterTypeFilterSet(OrganizationalModelFilterSet):
 
     class Meta:
         model = ClusterType
         fields = ['id', 'name', 'slug', 'description']
 
 
-class ClusterGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class ClusterGroupFilterSet(OrganizationalModelFilterSet):
 
     class Meta:
         model = ClusterGroup
         fields = ['id', 'name', 'slug', 'description']
 
 
-class ClusterFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -108,13 +108,7 @@ class ClusterFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSe
         )
 
 
-class VirtualMachineFilterSet(
-    BaseFilterSet,
-    LocalConfigContextFilterSet,
-    TenancyFilterSet,
-    CustomFieldModelFilterSet,
-    CreatedUpdatedFilterSet
-):
+class VirtualMachineFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',
@@ -235,7 +229,7 @@ class VirtualMachineFilterSet(
         return queryset.exclude(params)
 
 
-class VMInterfaceFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class VMInterfaceFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
         label='Search',