Преглед изворни кода

Consolidate FilterSet classes

jeremystretch пре 4 година
родитељ
комит
c4e88fd11a

+ 6 - 7
netbox/circuits/filters.py

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

+ 43 - 58
netbox/dcim/filters.py

@@ -1,14 +1,16 @@
 import django_filters
 import django_filters
 from django.contrib.auth.models import User
 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.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 (
     MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, TagFilter, TreeNodeMultipleChoiceFilter,
     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 virtualization.models import Cluster
 from .choices import *
 from .choices import *
 from .constants import *
 from .constants import *
@@ -57,7 +59,7 @@ __all__ = (
 )
 )
 
 
 
 
-class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class RegionFilterSet(OrganizationalModelFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         label='Parent region (ID)',
         label='Parent region (ID)',
@@ -74,7 +76,7 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilt
         fields = ['id', 'name', 'slug', 'description']
         fields = ['id', 'name', 'slug', 'description']
 
 
 
 
-class SiteGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class SiteGroupFilterSet(OrganizationalModelFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         queryset=SiteGroup.objects.all(),
         label='Parent site group (ID)',
         label='Parent site group (ID)',
@@ -91,7 +93,7 @@ class SiteGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedF
         fields = ['id', 'name', 'slug', 'description']
         fields = ['id', 'name', 'slug', 'description']
 
 
 
 
-class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -154,7 +156,7 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class LocationFilterSet(OrganizationalModelFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         field_name='site__region',
         field_name='site__region',
@@ -218,14 +220,14 @@ class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFi
         )
         )
 
 
 
 
-class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class RackRoleFilterSet(OrganizationalModelFilterSet):
 
 
     class Meta:
     class Meta:
         model = RackRole
         model = RackRole
         fields = ['id', 'name', 'slug', 'color']
         fields = ['id', 'name', 'slug', 'color']
 
 
 
 
-class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='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(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -383,14 +385,14 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModel
         )
         )
 
 
 
 
-class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class ManufacturerFilterSet(OrganizationalModelFilterSet):
 
 
     class Meta:
     class Meta:
         model = Manufacturer
         model = Manufacturer
         fields = ['id', 'name', 'slug', 'description']
         fields = ['id', 'name', 'slug', 'description']
 
 
 
 
-class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class DeviceTypeFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -476,7 +478,7 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdat
         return queryset.exclude(devicebaytemplates__isnull=value)
         return queryset.exclude(devicebaytemplates__isnull=value)
 
 
 
 
-class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class DeviceTypeComponentFilterSet(django_filters.FilterSet):
     devicetype_id = django_filters.ModelMultipleChoiceFilter(
     devicetype_id = django_filters.ModelMultipleChoiceFilter(
         queryset=DeviceType.objects.all(),
         queryset=DeviceType.objects.all(),
         field_name='device_type_id',
         field_name='device_type_id',
@@ -484,28 +486,28 @@ class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet, CreatedUpdatedFilter
     )
     )
 
 
 
 
-class ConsolePortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = ConsolePortTemplate
         model = ConsolePortTemplate
         fields = ['id', 'name', 'type']
         fields = ['id', 'name', 'type']
 
 
 
 
-class ConsoleServerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = ConsoleServerPortTemplate
         model = ConsoleServerPortTemplate
         fields = ['id', 'name', 'type']
         fields = ['id', 'name', 'type']
 
 
 
 
-class PowerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = PowerPortTemplate
         model = PowerPortTemplate
         fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
         fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
 
 
 
 
-class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
     feed_leg = django_filters.MultipleChoiceFilter(
     feed_leg = django_filters.MultipleChoiceFilter(
         choices=PowerOutletFeedLegChoices,
         choices=PowerOutletFeedLegChoices,
         null_value=None
         null_value=None
@@ -516,7 +518,7 @@ class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
         fields = ['id', 'name', 'type', 'feed_leg']
         fields = ['id', 'name', 'type', 'feed_leg']
 
 
 
 
-class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=InterfaceTypeChoices,
         choices=InterfaceTypeChoices,
         null_value=None
         null_value=None
@@ -527,7 +529,7 @@ class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
         fields = ['id', 'name', 'type', 'mgmt_only']
         fields = ['id', 'name', 'type', 'mgmt_only']
 
 
 
 
-class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=PortTypeChoices,
         choices=PortTypeChoices,
         null_value=None
         null_value=None
@@ -538,7 +540,7 @@ class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
         fields = ['id', 'name', 'type']
         fields = ['id', 'name', 'type']
 
 
 
 
-class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=PortTypeChoices,
         choices=PortTypeChoices,
         null_value=None
         null_value=None
@@ -549,21 +551,21 @@ class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
         fields = ['id', 'name', 'type', 'positions']
         fields = ['id', 'name', 'type', 'positions']
 
 
 
 
-class DeviceBayTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
+class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = DeviceBayTemplate
         model = DeviceBayTemplate
         fields = ['id', 'name']
         fields = ['id', 'name']
 
 
 
 
-class DeviceRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class DeviceRoleFilterSet(OrganizationalModelFilterSet):
 
 
     class Meta:
     class Meta:
         model = DeviceRole
         model = DeviceRole
         fields = ['id', 'name', 'slug', 'color', 'vm_role']
         fields = ['id', 'name', 'slug', 'color', 'vm_role']
 
 
 
 
-class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class PlatformFilterSet(OrganizationalModelFilterSet):
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
         field_name='manufacturer',
         field_name='manufacturer',
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
@@ -581,13 +583,7 @@ class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFi
         fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
         fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
 
 
 
 
-class DeviceFilterSet(
-    BaseFilterSet,
-    TenancyFilterSet,
-    LocalConfigContextFilterSet,
-    CustomFieldModelFilterSet,
-    CreatedUpdatedFilterSet
-):
+class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -792,7 +788,7 @@ class DeviceFilterSet(
         return queryset.exclude(devicebays__isnull=value)
         return queryset.exclude(devicebays__isnull=value)
 
 
 
 
-class DeviceComponentFilterSet(CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class DeviceComponentFilterSet(django_filters.FilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -876,7 +872,7 @@ class PathEndpointFilterSet(django_filters.FilterSet):
             return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
             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(
     type = django_filters.MultipleChoiceFilter(
         choices=ConsolePortTypeChoices,
         choices=ConsolePortTypeChoices,
         null_value=None
         null_value=None
@@ -887,12 +883,7 @@ class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina
         fields = ['id', 'name', 'label', 'description']
         fields = ['id', 'name', 'label', 'description']
 
 
 
 
-class ConsoleServerPortFilterSet(
-    BaseFilterSet,
-    DeviceComponentFilterSet,
-    CableTerminationFilterSet,
-    PathEndpointFilterSet
-):
+class ConsoleServerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=ConsolePortTypeChoices,
         choices=ConsolePortTypeChoices,
         null_value=None
         null_value=None
@@ -903,7 +894,7 @@ class ConsoleServerPortFilterSet(
         fields = ['id', 'name', 'label', 'description']
         fields = ['id', 'name', 'label', 'description']
 
 
 
 
-class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
+class PowerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=PowerPortTypeChoices,
         choices=PowerPortTypeChoices,
         null_value=None
         null_value=None
@@ -914,7 +905,7 @@ class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
         fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description']
         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(
     type = django_filters.MultipleChoiceFilter(
         choices=PowerOutletTypeChoices,
         choices=PowerOutletTypeChoices,
         null_value=None
         null_value=None
@@ -929,7 +920,7 @@ class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina
         fields = ['id', 'name', 'label', 'feed_leg', 'description']
         fields = ['id', 'name', 'label', 'feed_leg', 'description']
 
 
 
 
-class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
+class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -1027,7 +1018,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
         }.get(value, queryset.none())
         }.get(value, queryset.none())
 
 
 
 
-class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
+class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=PortTypeChoices,
         choices=PortTypeChoices,
         null_value=None
         null_value=None
@@ -1038,7 +1029,7 @@ class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
         fields = ['id', 'name', 'label', 'type', 'description']
         fields = ['id', 'name', 'label', 'type', 'description']
 
 
 
 
-class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
+class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=PortTypeChoices,
         choices=PortTypeChoices,
         null_value=None
         null_value=None
@@ -1049,14 +1040,14 @@ class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminatio
         fields = ['id', 'name', 'label', 'type', 'positions', 'description']
         fields = ['id', 'name', 'label', 'type', 'positions', 'description']
 
 
 
 
-class DeviceBayFilterSet(BaseFilterSet, DeviceComponentFilterSet):
+class DeviceBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = DeviceBay
         model = DeviceBay
         fields = ['id', 'name', 'label', 'description']
         fields = ['id', 'name', 'label', 'description']
 
 
 
 
-class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
+class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -1129,7 +1120,7 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class VirtualChassisFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -1209,7 +1200,7 @@ class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedU
         return queryset.filter(qs_filter).distinct()
         return queryset.filter(qs_filter).distinct()
 
 
 
 
-class CableFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class CableFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -1273,7 +1264,7 @@ class CableFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFil
         return queryset
         return queryset
 
 
 
 
-class ConnectionFilterSet:
+class ConnectionFilterSet(BaseFilterSet):
 
 
     def filter_site(self, queryset, name, value):
     def filter_site(self, queryset, name, value):
         if not value.strip():
         if not value.strip():
@@ -1286,7 +1277,7 @@ class ConnectionFilterSet:
         return queryset.filter(**{f'{name}__in': value})
         return queryset.filter(**{f'{name}__in': value})
 
 
 
 
-class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
+class ConsoleConnectionFilterSet(ConnectionFilterSet):
     site = django_filters.CharFilter(
     site = django_filters.CharFilter(
         method='filter_site',
         method='filter_site',
         label='Site (slug)',
         label='Site (slug)',
@@ -1304,7 +1295,7 @@ class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
         fields = ['name']
         fields = ['name']
 
 
 
 
-class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
+class PowerConnectionFilterSet(ConnectionFilterSet):
     site = django_filters.CharFilter(
     site = django_filters.CharFilter(
         method='filter_site',
         method='filter_site',
         label='Site (slug)',
         label='Site (slug)',
@@ -1322,7 +1313,7 @@ class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
         fields = ['name']
         fields = ['name']
 
 
 
 
-class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
+class InterfaceConnectionFilterSet(ConnectionFilterSet):
     site = django_filters.CharFilter(
     site = django_filters.CharFilter(
         method='filter_site',
         method='filter_site',
         label='Site (slug)',
         label='Site (slug)',
@@ -1340,7 +1331,7 @@ class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
         fields = []
         fields = []
 
 
 
 
-class PowerPanelFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class PowerPanelFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -1402,13 +1393,7 @@ class PowerPanelFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdat
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class PowerFeedFilterSet(
-    BaseFilterSet,
-    CableTerminationFilterSet,
-    PathEndpointFilterSet,
-    CustomFieldModelFilterSet,
-    CreatedUpdatedFilterSet
-):
+class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='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 rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
 from rq import Worker
 from rq import Worker
 
 
-from extras import filters
+from extras import filtersets
 from extras.choices import JobResultStatusChoices
 from extras.choices import JobResultStatusChoices
 from extras.models import *
 from extras.models import *
 from extras.models import CustomField
 from extras.models import CustomField
@@ -61,7 +61,7 @@ class WebhookViewSet(ModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = Webhook.objects.all()
     queryset = Webhook.objects.all()
     serializer_class = serializers.WebhookSerializer
     serializer_class = serializers.WebhookSerializer
-    filterset_class = filters.WebhookFilterSet
+    filterset_class = filtersets.WebhookFilterSet
 
 
 
 
 #
 #
@@ -72,7 +72,7 @@ class CustomFieldViewSet(ModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = CustomField.objects.all()
     queryset = CustomField.objects.all()
     serializer_class = serializers.CustomFieldSerializer
     serializer_class = serializers.CustomFieldSerializer
-    filterset_class = filters.CustomFieldFilterSet
+    filterset_class = filtersets.CustomFieldFilterSet
 
 
 
 
 class CustomFieldModelViewSet(ModelViewSet):
 class CustomFieldModelViewSet(ModelViewSet):
@@ -101,7 +101,7 @@ class CustomLinkViewSet(ModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = CustomLink.objects.all()
     queryset = CustomLink.objects.all()
     serializer_class = serializers.CustomLinkSerializer
     serializer_class = serializers.CustomLinkSerializer
-    filterset_class = filters.CustomLinkFilterSet
+    filterset_class = filtersets.CustomLinkFilterSet
 
 
 
 
 #
 #
@@ -112,7 +112,7 @@ class ExportTemplateViewSet(ModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = ExportTemplate.objects.all()
     queryset = ExportTemplate.objects.all()
     serializer_class = serializers.ExportTemplateSerializer
     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')
         tagged_items=count_related(TaggedItem, 'tag')
     )
     )
     serializer_class = serializers.TagSerializer
     serializer_class = serializers.TagSerializer
-    filterset_class = filters.TagFilterSet
+    filterset_class = filtersets.TagFilterSet
 
 
 
 
 #
 #
@@ -135,7 +135,7 @@ class ImageAttachmentViewSet(ModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = ImageAttachment.objects.all()
     queryset = ImageAttachment.objects.all()
     serializer_class = serializers.ImageAttachmentSerializer
     serializer_class = serializers.ImageAttachmentSerializer
-    filterset_class = filters.ImageAttachmentFilterSet
+    filterset_class = filtersets.ImageAttachmentFilterSet
 
 
 
 
 #
 #
@@ -146,7 +146,7 @@ class JournalEntryViewSet(ModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = JournalEntry.objects.all()
     queryset = JournalEntry.objects.all()
     serializer_class = serializers.JournalEntrySerializer
     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',
         'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
     )
     )
     serializer_class = serializers.ConfigContextSerializer
     serializer_class = serializers.ConfigContextSerializer
-    filterset_class = filters.ConfigContextFilterSet
+    filterset_class = filtersets.ConfigContextFilterSet
 
 
 
 
 #
 #
@@ -358,7 +358,7 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = ObjectChange.objects.prefetch_related('user')
     queryset = ObjectChange.objects.prefetch_related('user')
     serializer_class = serializers.ObjectChangeSerializer
     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')
     queryset = JobResult.objects.prefetch_related('user')
     serializer_class = serializers.JobResultSerializer
     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')
     queryset = ContentType.objects.order_by('app_label', 'model')
     serializer_class = serializers.ContentTypeSerializer
     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
 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 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 .choices import *
-from .models import *
-
 
 
 __all__ = (
 __all__ = (
-    'ConfigContextFilterSet',
-    'ContentTypeFilterSet',
-    'CreatedUpdatedFilterSet',
     'CustomFieldFilter',
     'CustomFieldFilter',
-    'CustomLinkFilterSet',
-    'CustomFieldModelFilterSet',
-    'ExportTemplateFilterSet',
-    'ImageAttachmentFilterSet',
-    'JournalEntryFilterSet',
-    'LocalConfigContextFilterSet',
-    'ObjectChangeFilterSet',
-    'TagFilterSet',
-    'WebhookFilterSet',
 )
 )
 
 
 EXACT_FILTER_TYPES = (
 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):
 class CustomFieldFilter(django_filters.Filter):
     """
     """
     Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.
     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.type not in EXACT_FILTER_TYPES:
             if custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_LOOSE:
             if custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_LOOSE:
                 self.lookup_expr = 'icontains'
                 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 dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
 from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
 from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
-from extras.filters import *
+from extras.filtersets import *
 from extras.models import *
 from extras.models import *
 from ipam.models import IPAddress
 from ipam.models import IPAddress
 from tenancy.models import Tenant, TenantGroup
 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.tables import paginate_table
 from utilities.utils import copy_safe_request, count_related, shallow_compare_dict
 from utilities.utils import copy_safe_request, count_related, shallow_compare_dict
 from utilities.views import ContentTypePermissionRequiredMixin
 from utilities.views import ContentTypePermissionRequiredMixin
-from . import filters, forms, tables
+from . import filtersets, forms, tables
 from .choices import JobResultStatusChoices
 from .choices import JobResultStatusChoices
 from .models import ConfigContext, ImageAttachment, JournalEntry, ObjectChange, JobResult, Tag, TaggedItem
 from .models import ConfigContext, ImageAttachment, JournalEntry, ObjectChange, JobResult, Tag, TaggedItem
 from .reports import get_report, get_reports, run_report
 from .reports import get_report, get_reports, run_report
@@ -28,7 +28,7 @@ class TagListView(generic.ObjectListView):
     queryset = Tag.objects.annotate(
     queryset = Tag.objects.annotate(
         items=count_related(TaggedItem, 'tag')
         items=count_related(TaggedItem, 'tag')
     )
     )
-    filterset = filters.TagFilterSet
+    filterset = filtersets.TagFilterSet
     filterset_form = forms.TagFilterForm
     filterset_form = forms.TagFilterForm
     table = tables.TagTable
     table = tables.TagTable
 
 
@@ -94,7 +94,7 @@ class TagBulkDeleteView(generic.BulkDeleteView):
 
 
 class ConfigContextListView(generic.ObjectListView):
 class ConfigContextListView(generic.ObjectListView):
     queryset = ConfigContext.objects.all()
     queryset = ConfigContext.objects.all()
-    filterset = filters.ConfigContextFilterSet
+    filterset = filtersets.ConfigContextFilterSet
     filterset_form = forms.ConfigContextFilterForm
     filterset_form = forms.ConfigContextFilterForm
     table = tables.ConfigContextTable
     table = tables.ConfigContextTable
     action_buttons = ('add',)
     action_buttons = ('add',)
@@ -127,7 +127,7 @@ class ConfigContextEditView(generic.ObjectEditView):
 
 
 class ConfigContextBulkEditView(generic.BulkEditView):
 class ConfigContextBulkEditView(generic.BulkEditView):
     queryset = ConfigContext.objects.all()
     queryset = ConfigContext.objects.all()
-    filterset = filters.ConfigContextFilterSet
+    filterset = filtersets.ConfigContextFilterSet
     table = tables.ConfigContextTable
     table = tables.ConfigContextTable
     form = forms.ConfigContextBulkEditForm
     form = forms.ConfigContextBulkEditForm
 
 
@@ -173,7 +173,7 @@ class ObjectConfigContextView(generic.ObjectView):
 
 
 class ObjectChangeListView(generic.ObjectListView):
 class ObjectChangeListView(generic.ObjectListView):
     queryset = ObjectChange.objects.all()
     queryset = ObjectChange.objects.all()
-    filterset = filters.ObjectChangeFilterSet
+    filterset = filtersets.ObjectChangeFilterSet
     filterset_form = forms.ObjectChangeFilterForm
     filterset_form = forms.ObjectChangeFilterForm
     table = tables.ObjectChangeTable
     table = tables.ObjectChangeTable
     template_name = 'extras/objectchange_list.html'
     template_name = 'extras/objectchange_list.html'
@@ -300,7 +300,7 @@ class ImageAttachmentDeleteView(generic.ObjectDeleteView):
 
 
 class JournalEntryListView(generic.ObjectListView):
 class JournalEntryListView(generic.ObjectListView):
     queryset = JournalEntry.objects.all()
     queryset = JournalEntry.objects.all()
-    filterset = filters.JournalEntryFilterSet
+    filterset = filtersets.JournalEntryFilterSet
     filterset_form = forms.JournalEntryFilterForm
     filterset_form = forms.JournalEntryFilterForm
     table = tables.JournalEntryTable
     table = tables.JournalEntryTable
     action_buttons = ('export',)
     action_buttons = ('export',)
@@ -338,14 +338,14 @@ class JournalEntryDeleteView(generic.ObjectDeleteView):
 
 
 class JournalEntryBulkEditView(generic.BulkEditView):
 class JournalEntryBulkEditView(generic.BulkEditView):
     queryset = JournalEntry.objects.prefetch_related('created_by')
     queryset = JournalEntry.objects.prefetch_related('created_by')
-    filterset = filters.JournalEntryFilterSet
+    filterset = filtersets.JournalEntryFilterSet
     table = tables.JournalEntryTable
     table = tables.JournalEntryTable
     form = forms.JournalEntryBulkEditForm
     form = forms.JournalEntryBulkEditForm
 
 
 
 
 class JournalEntryBulkDeleteView(generic.BulkDeleteView):
 class JournalEntryBulkDeleteView(generic.BulkDeleteView):
     queryset = JournalEntry.objects.prefetch_related('created_by')
     queryset = JournalEntry.objects.prefetch_related('created_by')
-    filterset = filters.JournalEntryFilterSet
+    filterset = filtersets.JournalEntryFilterSet
     table = tables.JournalEntryTable
     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 netaddr.core import AddrFormatError
 
 
 from dcim.models import Device, Interface, Region, Site, SiteGroup
 from dcim.models import Device, Interface, Region, Site, SiteGroup
-from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from tenancy.filters import TenancyFilterSet
 from tenancy.filters import TenancyFilterSet
 from utilities.filters import (
 from utilities.filters import (
     ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TagFilter,
     ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TagFilter,
     TreeNodeMultipleChoiceFilter,
     TreeNodeMultipleChoiceFilter,
 )
 )
-from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
+from utilities.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
 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
@@ -32,7 +31,7 @@ __all__ = (
 )
 )
 
 
 
 
-class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -75,7 +74,7 @@ class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, C
         fields = ['id', 'name', 'rd', 'enforce_unique']
         fields = ['id', 'name', 'rd', 'enforce_unique']
 
 
 
 
-class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -117,14 +116,14 @@ class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilt
         fields = ['id', 'name']
         fields = ['id', 'name']
 
 
 
 
-class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class RIRFilterSet(OrganizationalModelFilterSet):
 
 
     class Meta:
     class Meta:
         model = RIR
         model = RIR
         fields = ['id', 'name', 'slug', 'is_private', 'description']
         fields = ['id', 'name', 'slug', 'is_private', 'description']
 
 
 
 
-class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -174,7 +173,7 @@ class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
             return queryset.none()
             return queryset.none()
 
 
 
 
-class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class RoleFilterSet(OrganizationalModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -185,7 +184,7 @@ class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilter
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='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(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -536,7 +535,7 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
         return queryset.exclude(assigned_object_id__isnull=value)
         return queryset.exclude(assigned_object_id__isnull=value)
 
 
 
 
-class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class VLANGroupFilterSet(OrganizationalModelFilterSet):
     scope_type = ContentTypeFilter()
     scope_type = ContentTypeFilter()
     region = django_filters.NumberFilter(
     region = django_filters.NumberFilter(
         method='filter_scope'
         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(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -667,7 +666,7 @@ class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
         return queryset.get_for_virtualmachine(value)
         return queryset.get_for_virtualmachine(value)
 
 
 
 
-class ServiceFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
+class ServiceFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',

+ 3 - 4
netbox/secrets/filters.py

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

+ 3 - 4
netbox/tenancy/filters.py

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

+ 50 - 2
netbox/utilities/filtersets.py

@@ -1,9 +1,13 @@
 import django_filters
 import django_filters
 from copy import deepcopy
 from copy import deepcopy
-from dcim.forms import MACAddressField
+from django.contrib.contenttypes.models import ContentType
 from django.db import models
 from django.db import models
 from django_filters.utils import get_model_field, resolve_field
 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 (
 from utilities.constants import (
     FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
     FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
     FILTER_NUMERIC_BASED_LOOKUP_MAP
     FILTER_NUMERIC_BASED_LOOKUP_MAP
@@ -11,6 +15,14 @@ from utilities.constants import (
 from utilities import filters
 from utilities import filters
 
 
 
 
+__all__ = (
+    'BaseFilterSet',
+    'ChangeLoggedModelFilterSet',
+    'OrganizationalModelFilterSet',
+    'PrimaryModelFilterSet',
+)
+
+
 #
 #
 # FilterSets
 # FilterSets
 #
 #
@@ -172,7 +184,43 @@ class BaseFilterSet(django_filters.FilterSet):
         return filters
         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
     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 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.filtersets import LocalConfigContextFilterSet
 from tenancy.filters import TenancyFilterSet
 from tenancy.filters import TenancyFilterSet
 from utilities.filters import MultiValueMACAddressFilter, TagFilter, TreeNodeMultipleChoiceFilter
 from utilities.filters import MultiValueMACAddressFilter, TagFilter, TreeNodeMultipleChoiceFilter
-from utilities.filtersets import BaseFilterSet, NameSlugSearchFilterSet
+from utilities.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
 from .choices import *
 from .choices import *
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 
 
@@ -18,21 +18,21 @@ __all__ = (
 )
 )
 
 
 
 
-class ClusterTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class ClusterTypeFilterSet(OrganizationalModelFilterSet):
 
 
     class Meta:
     class Meta:
         model = ClusterType
         model = ClusterType
         fields = ['id', 'name', 'slug', 'description']
         fields = ['id', 'name', 'slug', 'description']
 
 
 
 
-class ClusterGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
+class ClusterGroupFilterSet(OrganizationalModelFilterSet):
 
 
     class Meta:
     class Meta:
         model = ClusterGroup
         model = ClusterGroup
         fields = ['id', 'name', 'slug', 'description']
         fields = ['id', 'name', 'slug', 'description']
 
 
 
 
-class ClusterFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='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(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -235,7 +229,7 @@ class VirtualMachineFilterSet(
         return queryset.exclude(params)
         return queryset.exclude(params)
 
 
 
 
-class VMInterfaceFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
+class VMInterfaceFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',