diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 94aa80c38e..0b50283160 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 190 +INVENTREE_API_VERSION = 191 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v191 - 2024-04-22 : https://github.com/inventree/InvenTree/pull/7079 + - Adds API endpoints for Contenttype model + v190 - 2024-04-19 : https://github.com/inventree/InvenTree/pull/7024 - Adds "active" field to the Company API endpoints - Allow company list to be filtered by "active" status diff --git a/src/backend/InvenTree/InvenTree/metadata.py b/src/backend/InvenTree/InvenTree/metadata.py index 65097ed758..9811a41d7c 100644 --- a/src/backend/InvenTree/InvenTree/metadata.py +++ b/src/backend/InvenTree/InvenTree/metadata.py @@ -280,6 +280,8 @@ class InvenTreeMetadata(SimpleMetadata): # Special case for 'user' model if field_info['model'] == 'user': field_info['api_url'] = '/api/user/' + elif field_info['model'] == 'contenttype': + field_info['api_url'] = '/api/contenttype/' else: field_info['api_url'] = model.get_api_url() diff --git a/src/backend/InvenTree/common/api.py b/src/backend/InvenTree/common/api.py index 50f6361e0b..bba5ac766b 100644 --- a/src/backend/InvenTree/common/api.py +++ b/src/backend/InvenTree/common/api.py @@ -3,6 +3,7 @@ import json from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponse from django.urls import include, path, re_path from django.utils.decorators import method_decorator @@ -619,6 +620,38 @@ class FlagDetail(RetrieveAPI): return {key: value} +class ContentTypeList(ListAPI): + """List view for ContentTypes.""" + + queryset = ContentType.objects.all() + serializer_class = common.serializers.ContentTypeSerializer + permission_classes = [permissions.IsAuthenticated] + + +class ContentTypeDetail(RetrieveAPI): + """Detail view for a ContentType model.""" + + queryset = ContentType.objects.all() + serializer_class = common.serializers.ContentTypeSerializer + permission_classes = [permissions.IsAuthenticated] + + +@extend_schema(operation_id='contenttype_retrieve_model') +class ContentTypeModelDetail(ContentTypeDetail): + """Detail view for a ContentType model.""" + + def get_object(self): + """Attempt to find a ContentType object with the provided key.""" + model_ref = self.kwargs.get('model', None) + if model_ref: + qs = self.filter_queryset(self.get_queryset()) + try: + return qs.get(model=model_ref) + except ContentType.DoesNotExist: + raise NotFound() + raise NotFound() + + settings_api_urls = [ # User settings path( @@ -799,6 +832,21 @@ common_api_urls = [ path('', AllStatusViews.as_view(), name='api-status-all'), ]), ), + # Contenttype + path( + 'contenttype/', + include([ + path( + '/', ContentTypeDetail.as_view(), name='api-contenttype-detail' + ), + path( + '/', + ContentTypeModelDetail.as_view(), + name='api-contenttype-detail-modelname', + ), + path('', ContentTypeList.as_view(), name='api-contenttype-list'), + ]), + ), ] admin_api_urls = [ diff --git a/src/backend/InvenTree/common/serializers.py b/src/backend/InvenTree/common/serializers.py index 360794d8a5..5bc1759d9a 100644 --- a/src/backend/InvenTree/common/serializers.py +++ b/src/backend/InvenTree/common/serializers.py @@ -1,5 +1,6 @@ """JSON serializers for common components.""" +from django.contrib.contenttypes.models import ContentType from django.db.models import OuterRef, Subquery from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -16,6 +17,7 @@ from InvenTree.serializers import ( InvenTreeImageSerializerField, InvenTreeModelSerializer, ) +from plugin import registry as plugin_registry from users.serializers import OwnerSerializer @@ -303,6 +305,26 @@ class FlagSerializer(serializers.Serializer): return data +class ContentTypeSerializer(serializers.Serializer): + """Serializer for ContentType models.""" + + pk = serializers.IntegerField(read_only=True) + app_label = serializers.CharField(read_only=True) + model = serializers.CharField(read_only=True) + app_labeled_name = serializers.CharField(read_only=True) + is_plugin = serializers.SerializerMethodField('get_is_plugin', read_only=True) + + class Meta: + """Meta options for ContentTypeSerializer.""" + + model = ContentType + fields = ['pk', 'app_label', 'model', 'app_labeled_name', 'is_plugin'] + + def get_is_plugin(self, obj) -> bool: + """Return True if the model is a plugin model.""" + return obj.app_label in plugin_registry.installed_apps + + class CustomUnitSerializer(InvenTreeModelSerializer): """DRF serializer for CustomUnit model.""" diff --git a/src/backend/InvenTree/common/tests.py b/src/backend/InvenTree/common/tests.py index a45a7ad1f6..6a5b26766d 100644 --- a/src/backend/InvenTree/common/tests.py +++ b/src/backend/InvenTree/common/tests.py @@ -8,6 +8,7 @@ from http import HTTPStatus from unittest import mock from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType from django.core.cache import cache from django.core.exceptions import ValidationError from django.core.files.uploadedfile import SimpleUploadedFile @@ -1339,3 +1340,46 @@ class CustomUnitAPITest(InvenTreeAPITestCase): for name in invalid_name_values: self.patch(url, {'name': name}, expected_code=400) + + +class ContentTypeAPITest(InvenTreeAPITestCase): + """Unit tests for the ContentType API.""" + + def test_list(self): + """Test API list functionality.""" + response = self.get(reverse('api-contenttype-list'), expected_code=200) + self.assertEqual(len(response.data), ContentType.objects.count()) + + def test_detail(self): + """Test API detail functionality.""" + ct = ContentType.objects.first() + assert ct + + response = self.get( + reverse('api-contenttype-detail', kwargs={'pk': ct.pk}), expected_code=200 + ) + + self.assertEqual(response.data['app_label'], ct.app_label) + self.assertEqual(response.data['model'], ct.model) + + # Test with model name + response = self.get( + reverse('api-contenttype-detail-modelname', kwargs={'model': ct.model}), + expected_code=200, + ) + self.assertEqual(response.data['app_label'], ct.app_label) + self.assertEqual(response.data['model'], ct.model) + + # Test non-existent model + self.get( + reverse( + 'api-contenttype-detail-modelname', kwargs={'model': 'nonexistent'} + ), + expected_code=404, + ) + + # PK should not work on model name endpoint + self.get( + reverse('api-contenttype-detail-modelname', kwargs={'model': None}), + expected_code=404, + )