Coverage for src/backend/InvenTree/generic/states/api.py: 77%
89 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Generic implementation of status api functions for InvenTree models."""
3import inspect
5from django.urls import include, path
7from drf_spectacular.types import OpenApiTypes
8from drf_spectacular.utils import OpenApiResponse, extend_schema
9from rest_framework import serializers
10from rest_framework.generics import GenericAPIView
11from rest_framework.response import Response
13import common.models
14import common.serializers
15import InvenTree.permissions
16from data_exporter.mixins import DataExportViewMixin
17from InvenTree.filters import SEARCH_ORDER_FILTER
18from InvenTree.helpers import inheritors
19from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI
20from InvenTree.serializers import EmptySerializer
22from .serializers import GenericStateClassSerializer
23from .states import StatusCode
26class StatusViewSerializer(serializers.Serializer):
27 """Serializer for the StatusView responses."""
29 class_name = serializers.CharField()
30 values = serializers.DictField()
33class StatusView(GenericAPIView):
34 """Generic API endpoint for discovering information on 'status codes' for a particular model.
36 This class should be implemented as a subclass for each type of status.
37 For example, the API endpoint /stock/status/ will have information about
38 all available 'StockStatus' codes
39 """
41 permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
42 serializer_class = GenericStateClassSerializer
44 # Override status_class for implementing subclass
45 MODEL_REF = 'statusmodel'
47 def get_status_model(self, *args, **kwargs):
48 """Return the StatusCode model based on extra parameters passed to the view."""
49 status_model = self.kwargs.get(self.MODEL_REF, None)
51 if status_model is None: 51 ↛ 52line 51 didn't jump to line 52 because the condition on line 51 was never true
52 raise serializers.ValidationError(
53 f"StatusView view called without '{self.MODEL_REF}' parameter"
54 )
56 return status_model
58 @extend_schema(
59 description='Retrieve information about a specific status code',
60 responses={
61 200: GenericStateClassSerializer,
62 400: OpenApiResponse(description='Invalid request'),
63 },
64 )
65 def get(self, request, *args, **kwargs):
66 """Perform a GET request to learn information about status codes."""
67 status_class = self.get_status_model()
69 if isinstance(status_class, str):
70 # Attempt to convert string to class
71 status_classes = inheritors(StatusCode)
73 for cls in status_classes:
74 if cls.__name__ == status_class: 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true
75 status_class = cls
76 break
78 if not inspect.isclass(status_class):
79 raise NotImplementedError(f'`{status_class}` not a class')
81 if not issubclass(status_class, StatusCode): 81 ↛ 82line 81 didn't jump to line 82 because the condition on line 81 was never true
82 raise NotImplementedError(f'`{status_class}` not a valid StatusCode class')
84 data = {'status_class': status_class.__name__, 'values': status_class.dict()}
86 # Extend with custom values
87 try:
88 custom_values = status_class.custom_values()
89 for item in custom_values: 89 ↛ 90line 89 didn't jump to line 90 because the loop on line 89 never started
90 if item.name not in data['values']:
91 data['values'][item.name] = {
92 'color': item.color,
93 'logical_key': item.logical_key,
94 'key': item.key,
95 'label': item.label,
96 'name': item.name,
97 'custom': True,
98 }
99 except Exception:
100 pass
102 serializer = GenericStateClassSerializer(data, many=False)
104 return Response(serializer.data)
107class AllStatusViews(StatusView):
108 """Endpoint for listing all defined status models."""
110 permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
111 serializer_class = EmptySerializer
113 # Specifically disable pagination for this view
114 pagination_class = None
116 @extend_schema(
117 operation_id='generic_status_retrieve_all',
118 responses={
119 200: OpenApiResponse(
120 description='Mapping from class name to GenericStateClass data',
121 response=OpenApiTypes.OBJECT,
122 )
123 },
124 )
125 def get(self, request, *args, **kwargs):
126 """Perform a GET request to learn information about status codes."""
127 data = {}
129 # Find all inherited status classes
130 status_classes = inheritors(StatusCode)
132 # Pre-fetch all custom values from the database
133 # This reduces the number of queries required
134 from common.models import InvenTreeCustomUserStateModel
136 custom_states_map = {}
137 for state in InvenTreeCustomUserStateModel.objects.all(): 137 ↛ 138line 137 didn't jump to line 138 because the loop on line 137 never started
138 key = state.reference_status
139 if key not in custom_states_map:
140 custom_states_map[key] = []
141 custom_states_map[key].append(state)
143 for cls in status_classes:
144 cls_data = {'status_class': cls.__name__, 'values': cls.dict(custom=False)}
146 # Extend with custom values (from pre-cached queryset)
147 custom_states = custom_states_map.get(cls.__name__, [])
149 for item in custom_states: 149 ↛ 150line 149 didn't jump to line 150 because the loop on line 149 never started
150 label = str(item.name)
151 if label not in cls_data['values']:
152 cls_data['values'][label] = {
153 'color': item.color,
154 'logical_key': item.logical_key,
155 'key': item.key,
156 'label': item.label,
157 'name': item.name,
158 'custom': True,
159 }
161 data[cls.__name__] = GenericStateClassSerializer(cls_data, many=False).data
163 return Response(data)
166# Custom states
167class CustomStateList(DataExportViewMixin, ListCreateAPI):
168 """List view for all custom states."""
170 queryset = common.models.InvenTreeCustomUserStateModel.objects.all()
171 serializer_class = common.serializers.CustomStateSerializer
172 permission_classes = [InvenTree.permissions.IsStaffOrReadOnlyScope]
173 filter_backends = SEARCH_ORDER_FILTER
174 ordering_fields = ['key']
175 search_fields = ['key', 'name', 'label', 'reference_status']
176 filterset_fields = ['model', 'reference_status']
179class CustomStateDetail(RetrieveUpdateDestroyAPI):
180 """Detail view for a particular custom states."""
182 queryset = common.models.InvenTreeCustomUserStateModel.objects.all()
183 serializer_class = common.serializers.CustomStateSerializer
184 permission_classes = [InvenTree.permissions.IsStaffOrReadOnlyScope]
187urlpattern = [
188 # Custom state
189 path(
190 'custom/',
191 include([
192 path(
193 '<int:pk>/', CustomStateDetail.as_view(), name='api-custom-state-detail'
194 ),
195 path('', CustomStateList.as_view(), name='api-custom-state-list'),
196 ]),
197 ),
198 # Generic status views
199 path(
200 '',
201 include([
202 path(
203 f'<str:{StatusView.MODEL_REF}>/',
204 include([path('', StatusView.as_view(), name='api-status')]),
205 ),
206 path('', AllStatusViews.as_view(), name='api-status-all'),
207 ]),
208 ),
209]