From a05fe690436ff465135090354d6c346fe38a056c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 23 Nov 2020 15:25:20 -0500 Subject: [PATCH] Add REST API endpoint for custom fields --- netbox/extras/api/nested_serializers.py | 9 +++++++++ netbox/extras/api/serializers.py | 23 +++++++++++++++++++++- netbox/extras/api/urls.py | 3 +++ netbox/extras/api/views.py | 26 ++++++++++++++++--------- netbox/extras/filters.py | 7 +++++++ netbox/extras/models/customfields.py | 5 +++-- 6 files changed, 61 insertions(+), 12 deletions(-) diff --git a/netbox/extras/api/nested_serializers.py b/netbox/extras/api/nested_serializers.py index 762bfb0d9..5635f401b 100644 --- a/netbox/extras/api/nested_serializers.py +++ b/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') diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 268a5d7c0..8476ea862 100644 --- a/netbox/extras/api/serializers.py +++ b/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 # diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index 917aedca5..da62b3d72 100644 --- a/netbox/extras/api/urls.py +++ b/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) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index e06f4dece..6bf78bf6a 100644 --- a/netbox/extras/api/views.py +++ b/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. diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 0d3c58d2b..54bf8103c 100644 --- a/netbox/extras/filters.py +++ b/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: diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 504de0953..039d6fc44 100644 --- a/netbox/extras/models/customfields.py +++ b/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." })