From 3df3706f277c7c046d844d8079d21b261dbdbf80 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 9 Oct 2020 15:08:29 -0400 Subject: [PATCH] Closes #5190: Add a REST API endpoint for content types --- docs/release-notes/version-2.10.md | 2 ++ netbox/extras/api/serializers.py | 17 +++++++++++++++++ netbox/extras/api/urls.py | 3 +++ netbox/extras/api/views.py | 17 +++++++++++++++-- netbox/extras/filters.py | 12 ++++++++++++ netbox/extras/forms.py | 11 +++++++---- netbox/extras/tests/test_api.py | 19 +++++++++++++++++++ 7 files changed, 75 insertions(+), 6 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 26b13477a..69df8c15d 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -49,6 +49,7 @@ All end-to-end cable paths are now cached using the new CablePath model. This al * [#4956](https://github.com/netbox-community/netbox/issues/4956) - Include inventory items on primary device view * [#5003](https://github.com/netbox-community/netbox/issues/5003) - CSV import now accepts slug values for choice fields * [#5146](https://github.com/netbox-community/netbox/issues/5146) - Add custom fields support for cables, power panels, rack reservations, and virtual chassis +* [#5190](https://github.com/netbox-community/netbox/issues/5190) - Add a REST API endpoint for content types ### Other Changes @@ -62,6 +63,7 @@ All end-to-end cable paths are now cached using the new CablePath model. This al ### REST API Changes * Added support for `PUT`, `PATCH`, and `DELETE` operations on list endpoints (bulk update and delete) +* Added `/extras/content-types/` endpoint for Django ContentTypes * circuits.CircuitTermination: * Added the `/trace/` endpoint * Replaced `connection_status` with `connected_endpoint_reachable` (boolean) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index c06d4b32d..0071791fa 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -339,3 +339,20 @@ class ObjectChangeSerializer(serializers.ModelSerializer): data = serializer(obj.changed_object, context=context).data return data + + +# +# ContentTypes +# + +class ContentTypeSerializer(serializers.ModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail') + display_name = serializers.SerializerMethodField() + + class Meta: + model = ContentType + fields = ['id', 'url', 'app_label', 'model', 'display_name'] + + @swagger_serializer_method(serializer_or_field=serializers.CharField) + def get_display_name(self, obj): + return obj.app_labeled_name diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index 20d0f8d17..d5d7e15b6 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -29,5 +29,8 @@ router.register('object-changes', views.ObjectChangeViewSet) # Job Results router.register('job-results', views.JobResultViewSet) +# ContentTypes +router.register('content-types', views.ContentTypeViewSet) + app_name = 'extras-api' urlpatterns = router.urls diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 940cc7912..96dcb4d8f 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from django.contrib.contenttypes.models import ContentType from django.db.models import Count from django.http import Http404 @@ -311,3 +309,18 @@ class JobResultViewSet(ReadOnlyModelViewSet): queryset = JobResult.objects.prefetch_related('user') serializer_class = serializers.JobResultSerializer filterset_class = filters.JobResultFilterSet + + +# +# ContentTypes +# + +class ContentTypeViewSet(ReadOnlyModelViewSet): + """ + Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects. + """ + queryset = ContentType.objects.order_by('app_label', 'model').filter(app_label__in=( + 'circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization' + )) + serializer_class = serializers.ContentTypeSerializer + filterset_class = filters.ContentTypeFilterSet diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index ad5884b7a..91f4d333b 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -13,6 +13,7 @@ from .models import ConfigContext, CustomField, ExportTemplate, ImageAttachment, __all__ = ( 'ConfigContextFilterSet', + 'ContentTypeFilterSet', 'CreatedUpdatedFilterSet', 'CustomFieldFilter', 'CustomFieldFilterSet', @@ -313,3 +314,14 @@ class JobResultFilterSet(BaseFilterSet): return queryset.filter( Q(user__username__icontains=value) ) + + +# +# ContentTypes +# + +class ContentTypeFilterSet(django_filters.FilterSet): + + class Meta: + model = ContentType + fields = ['id', 'app_label', 'model'] diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 7e7587901..d09ff64fe 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -362,11 +362,14 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form): api_url='/api/users/users/', ) ) - changed_object_type_id = forms.ModelChoiceField( - queryset=ContentType.objects.order_by('app_label', 'model'), + changed_object_type_id = DynamicModelMultipleChoiceField( + queryset=ContentType.objects.all(), required=False, - widget=ContentTypeSelect(), - label='Object Type' + display_field='display_name', + label='Object Type', + widget=APISelectMultiple( + api_url='/api/extras/content-types/', + ) ) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 860aed56f..66acb0741 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -2,6 +2,7 @@ import datetime from unittest import skipIf from django.contrib.contenttypes.models import ContentType +from django.test import override_settings from django.urls import reverse from django.utils import timezone from django_rq.queues import get_connection @@ -396,3 +397,21 @@ class CreatedUpdatedFilterTest(APITestCase): self.assertEqual(response.data['count'], 1) self.assertEqual(response.data['results'][0]['id'], self.rack2.pk) + + +class ContentTypeTest(APITestCase): + + @override_settings(EXEMPT_VIEW_PERMISSIONS=['contenttypes.contenttype']) + def test_list_objects(self): + contenttype_count = ContentType.objects.count() + + response = self.client.get(reverse('extras-api:contenttype-list'), **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data['count'], contenttype_count) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=['contenttypes.contenttype']) + def test_get_object(self): + contenttype = ContentType.objects.first() + + url = reverse('extras-api:contenttype-detail', kwargs={'pk': contenttype.pk}) + self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_200_OK)