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

1"""Generic implementation of status api functions for InvenTree models.""" 

2 

3import inspect 

4 

5from django.urls import include, path 

6 

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 

12 

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 

21 

22from .serializers import GenericStateClassSerializer 

23from .states import StatusCode 

24 

25 

26class StatusViewSerializer(serializers.Serializer): 

27 """Serializer for the StatusView responses.""" 

28 

29 class_name = serializers.CharField() 

30 values = serializers.DictField() 

31 

32 

33class StatusView(GenericAPIView): 

34 """Generic API endpoint for discovering information on 'status codes' for a particular model. 

35 

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 """ 

40 

41 permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope] 

42 serializer_class = GenericStateClassSerializer 

43 

44 # Override status_class for implementing subclass 

45 MODEL_REF = 'statusmodel' 

46 

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) 

50 

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 ) 

55 

56 return status_model 

57 

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() 

68 

69 if isinstance(status_class, str): 

70 # Attempt to convert string to class 

71 status_classes = inheritors(StatusCode) 

72 

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 

77 

78 if not inspect.isclass(status_class): 

79 raise NotImplementedError(f'`{status_class}` not a class') 

80 

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') 

83 

84 data = {'status_class': status_class.__name__, 'values': status_class.dict()} 

85 

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 

101 

102 serializer = GenericStateClassSerializer(data, many=False) 

103 

104 return Response(serializer.data) 

105 

106 

107class AllStatusViews(StatusView): 

108 """Endpoint for listing all defined status models.""" 

109 

110 permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope] 

111 serializer_class = EmptySerializer 

112 

113 # Specifically disable pagination for this view 

114 pagination_class = None 

115 

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 = {} 

128 

129 # Find all inherited status classes 

130 status_classes = inheritors(StatusCode) 

131 

132 # Pre-fetch all custom values from the database 

133 # This reduces the number of queries required 

134 from common.models import InvenTreeCustomUserStateModel 

135 

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) 

142 

143 for cls in status_classes: 

144 cls_data = {'status_class': cls.__name__, 'values': cls.dict(custom=False)} 

145 

146 # Extend with custom values (from pre-cached queryset) 

147 custom_states = custom_states_map.get(cls.__name__, []) 

148 

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 } 

160 

161 data[cls.__name__] = GenericStateClassSerializer(cls_data, many=False).data 

162 

163 return Response(data) 

164 

165 

166# Custom states 

167class CustomStateList(DataExportViewMixin, ListCreateAPI): 

168 """List view for all custom states.""" 

169 

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'] 

177 

178 

179class CustomStateDetail(RetrieveUpdateDestroyAPI): 

180 """Detail view for a particular custom states.""" 

181 

182 queryset = common.models.InvenTreeCustomUserStateModel.objects.all() 

183 serializer_class = common.serializers.CustomStateSerializer 

184 permission_classes = [InvenTree.permissions.IsStaffOrReadOnlyScope] 

185 

186 

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]