Преглед на файлове

Add REST API endpoint for custom fields

Jeremy Stretch преди 5 години
родител
ревизия
a05fe69043

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

@@ -6,6 +6,7 @@ from users.api.nested_serializers import NestedUserSerializer
 
 __all__ = [
     'NestedConfigContextSerializer',
+    'NestedCustomFieldSerializer',
     'NestedExportTemplateSerializer',
     'NestedImageAttachmentSerializer',
     '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):
     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 extras.choices import *
 from extras.models import (
-    ConfigContext, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag,
+    ConfigContext, CustomField, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag,
 )
 from extras.utils import FeatureQuery
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer
@@ -24,6 +24,27 @@ from virtualization.models import Cluster, ClusterGroup
 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
 #

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

@@ -5,6 +5,9 @@ from . import views
 router = OrderedDefaultRouter()
 router.APIRootView = views.ExtrasRootView
 
+# Custom fields
+router.register('custom-fields', views.CustomFieldViewSet)
+
 # Export templates
 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.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.scripts import get_script, get_scripts, run_script
 from netbox.api.views import ModelViewSet
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.metadata import ContentTypeMetadata
 from utilities.exceptions import RQWorkerNotRunningException
+from utilities.querysets import RestrictedQuerySet
 from utilities.utils import copy_safe_request
 from . import serializers
 
 
+class ExtrasRootView(APIRootView):
+    """
+    Extras API root view
+    """
+    def get_view_name(self):
+        return 'Extras'
+
+
 class ConfigContextQuerySetMixin:
     """
     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()
 
 
-class ExtrasRootView(APIRootView):
-    """
-    Extras API root view
-    """
-    def get_view_name(self):
-        return 'Extras'
-
-
 #
 # Custom fields
 #
 
+class CustomFieldViewSet(ModelViewSet):
+    metadata_class = ContentTypeMetadata
+    queryset = CustomField.objects.all()
+    serializer_class = serializers.CustomFieldSerializer
+    filterset_class = filters.CustomFieldFilterSet
+
+
 class CustomFieldModelViewSet(ModelViewSet):
     """
     Include the applicable set of CustomFields in the ModelViewSet context.

+ 7 - 0
netbox/extras/filters.py

@@ -74,6 +74,13 @@ class CustomFieldModelFilterSet(django_filters.FilterSet):
             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 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.utils import FeatureQuery
 from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2, add_blank_choice
+from utilities.querysets import RestrictedQuerySet
 from utilities.validators import validate_regex
 
 
@@ -63,7 +64,7 @@ class CustomFieldModel(models.Model):
                 raise ValidationError(f"Missing required custom field '{cf.name}'.")
 
 
-class CustomFieldManager(models.Manager):
+class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
     use_in_migrations = True
 
     def get_for_model(self, model):
@@ -193,7 +194,7 @@ class CustomField(models.Model):
             })
 
         # 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({
                 'choices': "Selection fields must specify at least two choices."
             })