From 7ca72ff2621c548b37dd2c5c7a25bd43640fdc24 Mon Sep 17 00:00:00 2001 From: Joe Rogers <1337joe@users.noreply.github.com> Date: Wed, 8 Oct 2025 03:39:30 +0200 Subject: [PATCH] Generic status endpoint fixes (#10530) * Allow querying for generic status by class name, add schema return type for AllStatusViews * Bump version api * Fix tests --- .../InvenTree/InvenTree/api_version.py | 6 +- src/backend/InvenTree/generic/states/api.py | 30 ++++++++-- src/backend/InvenTree/generic/states/tests.py | 59 ++++++++++--------- 3 files changed, 61 insertions(+), 34 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 623f334fab..70912f4f31 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,12 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 404 +INVENTREE_API_VERSION = 405 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v405 -> 2025-10-07: https://github.com/inventree/InvenTree/pull/10530 + - Add response to generic/status endpoint + - Fix logic for generic status model lookup to allow searching by class name string + v404 -> 2025-10-06: https://github.com/inventree/InvenTree/pull/10497 - Add minimum_stock to PartBrief api response diff --git a/src/backend/InvenTree/generic/states/api.py b/src/backend/InvenTree/generic/states/api.py index 23964c2a5d..925fc772ce 100644 --- a/src/backend/InvenTree/generic/states/api.py +++ b/src/backend/InvenTree/generic/states/api.py @@ -4,6 +4,7 @@ import inspect from django.urls import include, path +from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework import serializers from rest_framework.generics import GenericAPIView @@ -14,6 +15,7 @@ import common.serializers import InvenTree.permissions from data_exporter.mixins import DataExportViewMixin from InvenTree.filters import SEARCH_ORDER_FILTER +from InvenTree.helpers import inheritors from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI from InvenTree.serializers import EmptySerializer @@ -64,11 +66,20 @@ class StatusView(GenericAPIView): """Perform a GET request to learn information about status codes.""" status_class = self.get_status_model() + if isinstance(status_class, str): + # Attempt to convert string to class + status_classes = inheritors(StatusCode) + + for cls in status_classes: + if cls.__name__ == status_class: + status_class = cls + break + if not inspect.isclass(status_class): - raise NotImplementedError('`status_class` not a class') + raise NotImplementedError(f'`{status_class}` not a class') if not issubclass(status_class, StatusCode): - raise NotImplementedError('`status_class` not a valid StatusCode class') + raise NotImplementedError(f'`{status_class}` not a valid StatusCode class') data = {'status_class': status_class.__name__, 'values': status_class.dict()} @@ -99,11 +110,20 @@ class AllStatusViews(StatusView): permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope] serializer_class = EmptySerializer - @extend_schema(operation_id='generic_status_retrieve_all') + # Specifically disable pagination for this view + pagination_class = None + + @extend_schema( + operation_id='generic_status_retrieve_all', + responses={ + 200: OpenApiResponse( + description='Mapping from class name to GenericStateClass data', + response=OpenApiTypes.OBJECT, + ) + }, + ) def get(self, request, *args, **kwargs): """Perform a GET request to learn information about status codes.""" - from InvenTree.helpers import inheritors - data = {} # Find all inherited status classes diff --git a/src/backend/InvenTree/generic/states/tests.py b/src/backend/InvenTree/generic/states/tests.py index 30b45feb82..17a2221d1f 100644 --- a/src/backend/InvenTree/generic/states/tests.py +++ b/src/backend/InvenTree/generic/states/tests.py @@ -172,34 +172,37 @@ class GeneralStateTest(InvenTreeTestCase): rqst = RequestFactory().get('status/') force_authenticate(rqst, user=self.user) - # Correct call - resp = view(rqst, **{StatusView.MODEL_REF: GeneralStatus}) - self.assertDictEqual( - resp.data, - { - 'status_class': 'GeneralStatus', - 'values': { - 'COMPLETE': { - 'key': 30, - 'name': 'COMPLETE', - 'label': 'Complete', - 'color': 'success', - }, - 'PENDING': { - 'key': 10, - 'name': 'PENDING', - 'label': 'Pending', - 'color': 'secondary', - }, - 'PLACED': { - 'key': 20, - 'name': 'PLACED', - 'label': 'Placed', - 'color': 'primary', - }, + expected = { + 'status_class': 'GeneralStatus', + 'values': { + 'COMPLETE': { + 'key': 30, + 'name': 'COMPLETE', + 'label': 'Complete', + 'color': 'success', + }, + 'PENDING': { + 'key': 10, + 'name': 'PENDING', + 'label': 'Pending', + 'color': 'secondary', + }, + 'PLACED': { + 'key': 20, + 'name': 'PLACED', + 'label': 'Placed', + 'color': 'primary', }, }, - ) + } + + # Correct call (class) + resp = view(rqst, **{StatusView.MODEL_REF: GeneralStatus}) + self.assertDictEqual(resp.data, expected) + + # Correct call (name) + resp = view(rqst, **{StatusView.MODEL_REF: 'GeneralStatus'}) + self.assertDictEqual(resp.data, expected) # No status defined resp = view(rqst, **{StatusView.MODEL_REF: None}) @@ -212,13 +215,13 @@ class GeneralStateTest(InvenTreeTestCase): # Invalid call - not a class with self.assertRaises(NotImplementedError) as e: resp = view(rqst, **{StatusView.MODEL_REF: 'invalid'}) - self.assertEqual(str(e.exception), '`status_class` not a class') + self.assertEqual(str(e.exception), '`invalid` not a class') # Invalid call - not the right class with self.assertRaises(NotImplementedError) as e: resp = view(rqst, **{StatusView.MODEL_REF: object}) self.assertEqual( - str(e.exception), '`status_class` not a valid StatusCode class' + str(e.exception), "`` not a valid StatusCode class" )