Add contenttype model to API (#7079)

* Add contenttype APIs

* add plugin and id identifier

* resolve via modelname

* bump API version

* differentiate model view ids

* add API test

* remove unneeded response
This commit is contained in:
Matthias Mair 2024-04-23 00:05:43 +02:00 committed by GitHub
parent 3f80a45cb5
commit b02b6b2bba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 120 additions and 1 deletions

View File

@ -1,11 +1,14 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # 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.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ 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 v190 - 2024-04-19 : https://github.com/inventree/InvenTree/pull/7024
- Adds "active" field to the Company API endpoints - Adds "active" field to the Company API endpoints
- Allow company list to be filtered by "active" status - Allow company list to be filtered by "active" status

View File

@ -280,6 +280,8 @@ class InvenTreeMetadata(SimpleMetadata):
# Special case for 'user' model # Special case for 'user' model
if field_info['model'] == 'user': if field_info['model'] == 'user':
field_info['api_url'] = '/api/user/' field_info['api_url'] = '/api/user/'
elif field_info['model'] == 'contenttype':
field_info['api_url'] = '/api/contenttype/'
else: else:
field_info['api_url'] = model.get_api_url() field_info['api_url'] = model.get_api_url()

View File

@ -3,6 +3,7 @@
import json import json
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.urls import include, path, re_path from django.urls import include, path, re_path
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -619,6 +620,38 @@ class FlagDetail(RetrieveAPI):
return {key: value} 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 = [ settings_api_urls = [
# User settings # User settings
path( path(
@ -799,6 +832,21 @@ common_api_urls = [
path('', AllStatusViews.as_view(), name='api-status-all'), path('', AllStatusViews.as_view(), name='api-status-all'),
]), ]),
), ),
# Contenttype
path(
'contenttype/',
include([
path(
'<int:pk>/', ContentTypeDetail.as_view(), name='api-contenttype-detail'
),
path(
'<str:model>/',
ContentTypeModelDetail.as_view(),
name='api-contenttype-detail-modelname',
),
path('', ContentTypeList.as_view(), name='api-contenttype-list'),
]),
),
] ]
admin_api_urls = [ admin_api_urls = [

View File

@ -1,5 +1,6 @@
"""JSON serializers for common components.""" """JSON serializers for common components."""
from django.contrib.contenttypes.models import ContentType
from django.db.models import OuterRef, Subquery from django.db.models import OuterRef, Subquery
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -16,6 +17,7 @@ from InvenTree.serializers import (
InvenTreeImageSerializerField, InvenTreeImageSerializerField,
InvenTreeModelSerializer, InvenTreeModelSerializer,
) )
from plugin import registry as plugin_registry
from users.serializers import OwnerSerializer from users.serializers import OwnerSerializer
@ -303,6 +305,26 @@ class FlagSerializer(serializers.Serializer):
return data 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): class CustomUnitSerializer(InvenTreeModelSerializer):
"""DRF serializer for CustomUnit model.""" """DRF serializer for CustomUnit model."""

View File

@ -8,6 +8,7 @@ from http import HTTPStatus
from unittest import mock from unittest import mock
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
@ -1339,3 +1340,46 @@ class CustomUnitAPITest(InvenTreeAPITestCase):
for name in invalid_name_values: for name in invalid_name_values:
self.patch(url, {'name': name}, expected_code=400) 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,
)