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

Merge pull request #5372 from netbox-community/5274-custom-fields-api

Closes #5274: Enable REST API support for custom fields
Jeremy Stretch пре 5 година
родитељ
комит
8e0a6479ca

+ 3 - 3
netbox/circuits/filters.py

@@ -3,7 +3,7 @@ from django.db.models import Q
 
 
 from dcim.filters import CableTerminationFilterSet, PathEndpointFilterSet
 from dcim.filters import CableTerminationFilterSet, PathEndpointFilterSet
 from dcim.models import Region, Site
 from dcim.models import Region, Site
-from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
+from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from tenancy.filters import TenancyFilterSet
 from tenancy.filters import TenancyFilterSet
 from utilities.filters import (
 from utilities.filters import (
     BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
     BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
@@ -19,7 +19,7 @@ __all__ = (
 )
 )
 
 
 
 
-class ProviderFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -73,7 +73,7 @@ class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
+class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',

+ 6 - 6
netbox/dcim/filters.py

@@ -2,7 +2,7 @@ import django_filters
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.db.models import Count
 from django.db.models import Count
 
 
-from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
+from extras.filters import CustomFieldModelFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
 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
@@ -80,7 +80,7 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug', 'description']
         fields = ['id', 'name', 'slug', 'description']
 
 
 
 
-class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -179,7 +179,7 @@ class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug', 'color']
         fields = ['id', 'name', 'slug', 'color']
 
 
 
 
-class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -325,7 +325,7 @@ class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug', 'description']
         fields = ['id', 'name', 'slug', 'description']
 
 
 
 
-class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -504,7 +504,7 @@ class DeviceFilterSet(
     BaseFilterSet,
     BaseFilterSet,
     TenancyFilterSet,
     TenancyFilterSet,
     LocalConfigContextFilterSet,
     LocalConfigContextFilterSet,
-    CustomFieldFilterSet,
+    CustomFieldModelFilterSet,
     CreatedUpdatedFilterSet
     CreatedUpdatedFilterSet
 ):
 ):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
@@ -1246,7 +1246,7 @@ class PowerFeedFilterSet(
     BaseFilterSet,
     BaseFilterSet,
     CableTerminationFilterSet,
     CableTerminationFilterSet,
     PathEndpointFilterSet,
     PathEndpointFilterSet,
-    CustomFieldFilterSet,
+    CustomFieldModelFilterSet,
     CreatedUpdatedFilterSet
     CreatedUpdatedFilterSet
 ):
 ):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(

+ 9 - 0
netbox/extras/api/nested_serializers.py

@@ -6,6 +6,7 @@ from users.api.nested_serializers import NestedUserSerializer
 
 
 __all__ = [
 __all__ = [
     'NestedConfigContextSerializer',
     'NestedConfigContextSerializer',
+    'NestedCustomFieldSerializer',
     'NestedExportTemplateSerializer',
     'NestedExportTemplateSerializer',
     'NestedImageAttachmentSerializer',
     'NestedImageAttachmentSerializer',
     'NestedJobResultSerializer',
     'NestedJobResultSerializer',
@@ -13,6 +14,14 @@ __all__ = [
 ]
 ]
 
 
 
 
+class NestedCustomFieldSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
+
+    class Meta:
+        model = models.CustomField
+        fields = ['id', 'url', 'name']
+
+
 class NestedConfigContextSerializer(WritableNestedSerializer):
 class NestedConfigContextSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail')
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail')
 
 

+ 22 - 1
netbox/extras/api/serializers.py

@@ -10,7 +10,7 @@ from dcim.api.nested_serializers import (
 from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
 from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
 from extras.choices import *
 from extras.choices import *
 from extras.models import (
 from extras.models import (
-    ConfigContext, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag,
+    ConfigContext, CustomField, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag,
 )
 )
 from extras.utils import FeatureQuery
 from extras.utils import FeatureQuery
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer
@@ -24,6 +24,27 @@ from virtualization.models import Cluster, ClusterGroup
 from .nested_serializers import *
 from .nested_serializers import *
 
 
 
 
+#
+# Custom fields
+#
+
+class CustomFieldSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
+    content_types = ContentTypeField(
+        queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
+        many=True
+    )
+    type = ChoiceField(choices=CustomFieldTypeChoices)
+    filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
+
+    class Meta:
+        model = CustomField
+        fields = [
+            'id', 'url', 'content_types', 'type', 'name', 'label', 'description', 'required', 'filter_logic',
+            'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices',
+        ]
+
+
 #
 #
 # Export templates
 # Export templates
 #
 #

+ 3 - 0
netbox/extras/api/urls.py

@@ -5,6 +5,9 @@ from . import views
 router = OrderedDefaultRouter()
 router = OrderedDefaultRouter()
 router.APIRootView = views.ExtrasRootView
 router.APIRootView = views.ExtrasRootView
 
 
+# Custom fields
+router.register('custom-fields', views.CustomFieldViewSet)
+
 # Export templates
 # Export templates
 router.register('export-templates', views.ExportTemplateViewSet)
 router.register('export-templates', views.ExportTemplateViewSet)
 
 

+ 17 - 9
netbox/extras/api/views.py

@@ -12,17 +12,26 @@ from rq import Worker
 
 
 from extras import filters
 from extras import filters
 from extras.choices import JobResultStatusChoices
 from extras.choices import JobResultStatusChoices
-from extras.models import ConfigContext, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag
+from extras.models import ConfigContext, CustomField, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag
 from extras.reports import get_report, get_reports, run_report
 from extras.reports import get_report, get_reports, run_report
 from extras.scripts import get_script, get_scripts, run_script
 from extras.scripts import get_script, get_scripts, run_script
 from netbox.api.views import ModelViewSet
 from netbox.api.views import ModelViewSet
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.metadata import ContentTypeMetadata
 from utilities.exceptions import RQWorkerNotRunningException
 from utilities.exceptions import RQWorkerNotRunningException
+from utilities.querysets import RestrictedQuerySet
 from utilities.utils import copy_safe_request
 from utilities.utils import copy_safe_request
 from . import serializers
 from . import serializers
 
 
 
 
+class ExtrasRootView(APIRootView):
+    """
+    Extras API root view
+    """
+    def get_view_name(self):
+        return 'Extras'
+
+
 class ConfigContextQuerySetMixin:
 class ConfigContextQuerySetMixin:
     """
     """
     Used by views that work with config context models (device and virtual machine).
     Used by views that work with config context models (device and virtual machine).
@@ -46,18 +55,17 @@ class ConfigContextQuerySetMixin:
         return self.queryset.annotate_config_context_data()
         return self.queryset.annotate_config_context_data()
 
 
 
 
-class ExtrasRootView(APIRootView):
-    """
-    Extras API root view
-    """
-    def get_view_name(self):
-        return 'Extras'
-
-
 #
 #
 # Custom fields
 # Custom fields
 #
 #
 
 
+class CustomFieldViewSet(ModelViewSet):
+    metadata_class = ContentTypeMetadata
+    queryset = CustomField.objects.all()
+    serializer_class = serializers.CustomFieldSerializer
+    filterset_class = filters.CustomFieldFilterSet
+
+
 class CustomFieldModelViewSet(ModelViewSet):
 class CustomFieldModelViewSet(ModelViewSet):
     """
     """
     Include the applicable set of CustomFields in the ModelViewSet context.
     Include the applicable set of CustomFields in the ModelViewSet context.

+ 9 - 2
netbox/extras/filters.py

@@ -16,7 +16,7 @@ __all__ = (
     'ContentTypeFilterSet',
     'ContentTypeFilterSet',
     'CreatedUpdatedFilterSet',
     'CreatedUpdatedFilterSet',
     'CustomFieldFilter',
     'CustomFieldFilter',
-    'CustomFieldFilterSet',
+    'CustomFieldModelFilterSet',
     'ExportTemplateFilterSet',
     'ExportTemplateFilterSet',
     'ImageAttachmentFilterSet',
     'ImageAttachmentFilterSet',
     'LocalConfigContextFilterSet',
     'LocalConfigContextFilterSet',
@@ -58,7 +58,7 @@ class CustomFieldFilter(django_filters.Filter):
         return queryset.filter(**kwargs)
         return queryset.filter(**kwargs)
 
 
 
 
-class CustomFieldFilterSet(django_filters.FilterSet):
+class CustomFieldModelFilterSet(django_filters.FilterSet):
     """
     """
     Dynamically add a Filter for each CustomField applicable to the parent model.
     Dynamically add a Filter for each CustomField applicable to the parent model.
     """
     """
@@ -74,6 +74,13 @@ class CustomFieldFilterSet(django_filters.FilterSet):
             self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
             self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
 
 
 
 
+class CustomFieldFilterSet(django_filters.FilterSet):
+
+    class Meta:
+        model = CustomField
+        fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'default', 'weight']
+
+
 class ExportTemplateFilterSet(BaseFilterSet):
 class ExportTemplateFilterSet(BaseFilterSet):
 
 
     class Meta:
     class Meta:

+ 3 - 2
netbox/extras/models/customfields.py

@@ -13,6 +13,7 @@ from django.utils.safestring import mark_safe
 from extras.choices import *
 from extras.choices import *
 from extras.utils import FeatureQuery
 from extras.utils import FeatureQuery
 from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2, add_blank_choice
 from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2, add_blank_choice
+from utilities.querysets import RestrictedQuerySet
 from utilities.validators import validate_regex
 from utilities.validators import validate_regex
 
 
 
 
@@ -63,7 +64,7 @@ class CustomFieldModel(models.Model):
                 raise ValidationError(f"Missing required custom field '{cf.name}'.")
                 raise ValidationError(f"Missing required custom field '{cf.name}'.")
 
 
 
 
-class CustomFieldManager(models.Manager):
+class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
     use_in_migrations = True
     use_in_migrations = True
 
 
     def get_for_model(self, model):
     def get_for_model(self, model):
@@ -193,7 +194,7 @@ class CustomField(models.Model):
             })
             })
 
 
         # A selection field must have at least two choices defined
         # A selection field must have at least two choices defined
-        if self.type == CustomFieldTypeChoices.TYPE_SELECT and len(self.choices) < 2:
+        if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.choices and len(self.choices) < 2:
             raise ValidationError({
             raise ValidationError({
                 'choices': "Selection fields must specify at least two choices."
                 'choices': "Selection fields must specify at least two choices."
             })
             })

+ 48 - 1
netbox/extras/tests/test_api.py

@@ -11,7 +11,7 @@ from rq import Worker
 
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site
 from extras.api.views import ReportViewSet, ScriptViewSet
 from extras.api.views import ReportViewSet, ScriptViewSet
-from extras.models import ConfigContext, ExportTemplate, ImageAttachment, Tag
+from extras.models import ConfigContext, CustomField, ExportTemplate, ImageAttachment, Tag
 from extras.reports import Report
 from extras.reports import Report
 from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
 from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
 from utilities.testing import APITestCase, APIViewTestCases
 from utilities.testing import APITestCase, APIViewTestCases
@@ -30,6 +30,53 @@ class AppTest(APITestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
 
 
+class CustomFieldTest(APIViewTestCases.APIViewTestCase):
+    model = CustomField
+    brief_fields = ['id', 'name', 'url']
+    create_data = [
+        {
+            'content_types': ['dcim.site'],
+            'name': 'cf4',
+            'type': 'date',
+        },
+        {
+            'content_types': ['dcim.site'],
+            'name': 'cf5',
+            'type': 'url',
+        },
+        {
+            'content_types': ['dcim.site'],
+            'name': 'cf6',
+            'type': 'select',
+        },
+    ]
+    bulk_update_data = {
+        'description': 'New description',
+    }
+
+    @classmethod
+    def setUpTestData(cls):
+        site_ct = ContentType.objects.get_for_model(Site)
+
+        custom_fields = (
+            CustomField(
+                name='cf1',
+                type='text'
+            ),
+            CustomField(
+                name='cf2',
+                type='integer'
+            ),
+            CustomField(
+                name='cf3',
+                type='boolean'
+            ),
+        )
+        CustomField.objects.bulk_create(custom_fields)
+        for cf in custom_fields:
+            cf.content_types.add(site_ct)
+
+
 class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
 class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
     model = ExportTemplate
     model = ExportTemplate
     brief_fields = ['id', 'name', 'url']
     brief_fields = ['id', 'name', 'url']

+ 7 - 7
netbox/ipam/filters.py

@@ -5,7 +5,7 @@ from django.db.models import Q
 from netaddr.core import AddrFormatError
 from netaddr.core import AddrFormatError
 
 
 from dcim.models import Device, Interface, Region, Site
 from dcim.models import Device, Interface, Region, Site
-from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
+from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from tenancy.filters import TenancyFilterSet
 from tenancy.filters import TenancyFilterSet
 from utilities.filters import (
 from utilities.filters import (
     BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericArrayFilter, TagFilter,
     BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericArrayFilter, TagFilter,
@@ -30,7 +30,7 @@ __all__ = (
 )
 )
 
 
 
 
-class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -73,7 +73,7 @@ class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Create
         fields = ['id', 'name', 'rd', 'enforce_unique']
         fields = ['id', 'name', 'rd', 'enforce_unique']
 
 
 
 
-class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -122,7 +122,7 @@ class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug', 'is_private', 'description']
         fields = ['id', 'name', 'slug', 'is_private', 'description']
 
 
 
 
-class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -183,7 +183,7 @@ class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -355,7 +355,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Cre
         )
         )
 
 
 
 
-class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -551,7 +551,7 @@ class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug', 'description']
         fields = ['id', 'name', 'slug', 'description']
 
 
 
 
-class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',

+ 2 - 2
netbox/secrets/filters.py

@@ -2,7 +2,7 @@ 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 CustomFieldFilterSet, CreatedUpdatedFilterSet
+from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter
 from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
 from .models import Secret, SecretRole
 from .models import Secret, SecretRole
@@ -21,7 +21,7 @@ class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class SecretFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class SecretFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',

+ 2 - 2
netbox/tenancy/filters.py

@@ -1,7 +1,7 @@
 import django_filters
 import django_filters
 from django.db.models import Q
 from django.db.models import Q
 
 
-from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
+from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
 from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
 from .models import Tenant, TenantGroup
 from .models import Tenant, TenantGroup
 
 
@@ -30,7 +30,7 @@ class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug', 'description']
         fields = ['id', 'name', 'slug', 'description']
 
 
 
 
-class TenantFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class TenantFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',

+ 3 - 3
netbox/virtualization/filters.py

@@ -2,7 +2,7 @@ import django_filters
 from django.db.models import Q
 from django.db.models import Q
 
 
 from dcim.models import DeviceRole, Platform, Region, Site
 from dcim.models import DeviceRole, Platform, Region, Site
-from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet, LocalConfigContextFilterSet
+from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet, LocalConfigContextFilterSet
 from tenancy.filters import TenancyFilterSet
 from tenancy.filters import TenancyFilterSet
 from utilities.filters import (
 from utilities.filters import (
     BaseFilterSet, MultiValueMACAddressFilter, NameSlugSearchFilterSet, TagFilter,
     BaseFilterSet, MultiValueMACAddressFilter, NameSlugSearchFilterSet, TagFilter,
@@ -34,7 +34,7 @@ class ClusterGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug', 'description']
         fields = ['id', 'name', 'slug', 'description']
 
 
 
 
-class ClusterFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class ClusterFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -101,7 +101,7 @@ class VirtualMachineFilterSet(
     BaseFilterSet,
     BaseFilterSet,
     LocalConfigContextFilterSet,
     LocalConfigContextFilterSet,
     TenancyFilterSet,
     TenancyFilterSet,
-    CustomFieldFilterSet,
+    CustomFieldModelFilterSet,
     CreatedUpdatedFilterSet
     CreatedUpdatedFilterSet
 ):
 ):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(