Просмотр исходного кода

Closes #5190: Add a REST API endpoint for content types

Jeremy Stretch 5 лет назад
Родитель
Сommit
3df3706f27

+ 2 - 0
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)

+ 17 - 0
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

+ 3 - 0
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

+ 15 - 2
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

+ 12 - 0
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']

+ 7 - 4
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/',
+        )
     )
 
 

+ 19 - 0
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)