Coverage for backend/django/flowsheetInternals/unitops/viewsets/SimulationObjectViewSet.py: 82%

208 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-02-11 21:43 +0000

1import json 

2 

3from core.auxiliary.models.PropertyValue import PropertyValue 

4from core.viewset import ModelViewSet 

5from rest_framework.response import Response 

6from flowsheetInternals.unitops.viewsets.DuplicateSimulationObject import DuplicateSimulationObject 

7from flowsheetInternals.unitops.models.compound_propogation import update_compounds_on_set 

8from flowsheetInternals.unitops.serializers.SimulationObjectSerializer import SimulationObjectRetrieveSerializer 

9from flowsheetInternals.unitops.models import SimulationObject 

10from flowsheetInternals.unitops.models.delete_factory import DeleteFactory 

11from flowsheetInternals.unitops.serializers import * 

12from core.auxiliary.models.PropertySet import PropertySet 

13from core.auxiliary.models.PropertyInfo import PropertyInfo 

14from flowsheetInternals.unitops.models.summary_table_factory import get_composition_summary_table_data, get_stream_summary_table_data, get_unitops_summary_table_data 

15from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes 

16from rest_framework import serializers 

17from rest_framework.decorators import action 

18from rest_framework import status 

19import traceback 

20from django.db.models import Prefetch 

21from common.config_types import PropertyType as PropertyTypeObj 

22from flowsheetInternals.formula_templates.add_template import add_template 

23from flowsheetInternals.unitops.methods.add_expression import add_expression 

24from flowsheetInternals.graphicData.models.groupingModel import Grouping 

25 

26class UpdateCompoundSerializer(serializers.Serializer): 

27 """Payload schema for updating compound selections on a stream.""" 

28 

29 compounds = serializers.ListField(child=serializers.CharField()) 

30 simulationObject = serializers.IntegerField() 

31 

32class AddPropertyTemplateSerializer(serializers.Serializer): 

33 """Payload schema for adding a property template by key to a simulation object.""" 

34 

35 templateKey = serializers.CharField() 

36 

37class DuplicateSimulationObjectSerializer(serializers.Serializer): 

38 """Payload schema for duplicating one or more simulation objects.""" 

39 

40 objectIDs = serializers.ListField(child=serializers.IntegerField()) 

41 x = serializers.FloatField() 

42 y = serializers.FloatField() 

43 

44class AddPortSerializer(serializers.Serializer): 

45 """Payload schema for attaching a new port to a simulation object.""" 

46 

47 simulationObjectId = serializers.IntegerField() 

48 key = serializers.CharField() 

49 stream = serializers.IntegerField(required=False, allow_null=True) #optional if existing stream isnt passed in  

50 

51class MergeDecisionNodesSerializer(serializers.Serializer): 

52 """Payload schema for merging two decision nodes.""" 

53 

54 decisionNodeActive = serializers.IntegerField(required=True) 

55 decisionNodeOver = serializers.IntegerField(required=True) 

56 

57class RestoreObjectsSerializer(serializers.Serializer): 

58 """Payload schema for restoring previously deleted simulation objects.""" 

59 

60 object_ids = serializers.ListField( 

61 child=serializers.IntegerField(), 

62 help_text="Array of object IDs to restore", 

63 min_length=1 

64 ) 

65 

66 def validate(self, data): 

67 """Ensure we have at least one valid ID""" 

68 if not data.get('object_ids'): 68 ↛ 69line 68 didn't jump to line 69 because the condition on line 68 was never true

69 raise serializers.ValidationError("At least one object ID must be provided") 

70 return data 

71 

72class SimulationObjectViewSet(ModelViewSet): 

73 """API endpoints for interacting with flowsheet simulation objects.""" 

74 

75 serializer_class = SimulationObjectSerializer 

76 

77 def get_queryset(self): 

78 """Return simulation objects with related property metadata eagerly loaded.""" 

79 # Prefetch related entities to avoid N+1 queries when serializing. 

80 queryset = SimulationObject.objects.all().select_related( 

81 "properties", 

82 "grouping" 

83 ).prefetch_related( 

84 Prefetch( 

85 "properties__ContainedProperties", 

86 queryset=PropertyInfo.objects 

87 .select_related("recycleConnection") 

88 .prefetch_related( 

89 Prefetch("values", queryset=PropertyValue.objects 

90 .select_related("controlManipulated", "controlSetPoint") 

91 .prefetch_related("indexedItems")) 

92 ) 

93 ), 

94 "connectedPorts__unitOp" 

95 ) 

96 

97 return queryset 

98 

99 def get_serializer_class(self): 

100 """Use the retrieve serializer when properties must be serialized.""" 

101 if self.action == "retrieve": 

102 # for the retrieve action, we want to include the properties 

103 # of the simulation object (so a separate serializer is needed) 

104 return SimulationObjectRetrieveSerializer 

105 return SimulationObjectSerializer 

106 

107 @extend_schema( 

108 parameters=[ 

109 OpenApiParameter(name="flowsheet", required=True, type=OpenApiTypes.INT), 

110 ] 

111 ) 

112 def list(self, request): 

113 """List simulation objects filtered by the provided flowsheet ID.""" 

114 return super().list(request) 

115 

116 def destroy(self, request, *args, **kwargs): 

117 """Delete the simulation object via the shared DeleteFactory.""" 

118 instance = self.get_object() 

119 DeleteFactory.delete_object(instance) 

120 

121 return Response(status=status.HTTP_204_NO_CONTENT) 

122 #maybe do local storage and its request it in here 

123 

124 

125 def get_summary_queryset(self): 

126 queryset = SimulationObject.objects.all().select_related( 

127 "properties", 

128 ).prefetch_related( 

129 Prefetch( 

130 "properties__ContainedProperties", 

131 queryset=PropertyInfo.objects 

132 .prefetch_related( 

133 Prefetch("values", queryset=PropertyValue.objects 

134 .prefetch_related("indexedItems")) 

135 ) 

136 ) 

137 ) 

138 

139 return queryset 

140 

141 @extend_schema( 

142 parameters=[ 

143 OpenApiParameter(name="unit_map", type=OpenApiTypes.STR, required=True), 

144 OpenApiParameter(name="groups", type=OpenApiTypes.ANY, required=True) 

145 ], 

146 responses=serializers.DictField() 

147 ) 

148 @action(detail=False, methods=['get'], url_path='streams-summary') 

149 def summary_table_streams(self, request): 

150 unit_map = self.request.query_params.get("unit_map") 

151 groups = self.request.query_params.get("groups") 

152 

153 results = {} 

154 groups = Grouping.objects.filter(id__in=json.loads(groups)) 

155 for current_group in groups: 

156 query_set = self.get_summary_queryset().filter(graphicObject__in=current_group.graphicObjects.all()) 

157 

158 data = get_stream_summary_table_data(query_set, json.loads(unit_map)) 

159 

160 group_name = current_group.simulationObject.componentName 

161 results[group_name] = data 

162 

163 return Response(results, status=200) 

164 

165 @extend_schema( 

166 parameters=[ 

167 OpenApiParameter(name="unit_map", type=OpenApiTypes.STR, required=True), 

168 OpenApiParameter(name="groups", type=OpenApiTypes.ANY, required=True) 

169 ], 

170 responses=serializers.DictField() 

171 ) 

172 @action(detail=False, methods=['get'], url_path='unitops-summary') 

173 def summary_table_unitops(self, request): 

174 unit_map = self.request.query_params.get("unit_map") 

175 groups = self.request.query_params.get("groups") 

176 

177 results = {} 

178 groups = Grouping.objects.filter(id__in=json.loads(groups)) 

179 for current_group in groups: 

180 query_set = self.get_summary_queryset().filter(graphicObject__in=current_group.graphicObjects.all()) 

181 data = get_unitops_summary_table_data(query_set, json.loads(unit_map)) 

182 

183 group_name = current_group.simulationObject.componentName 

184 results[group_name] = data 

185 

186 return Response(results, status=200) 

187 

188 @extend_schema( 

189 parameters=[ 

190 OpenApiParameter(name="compound_mode", type=OpenApiTypes.STR, required=True), 

191 OpenApiParameter(name="measure_type", type=OpenApiTypes.STR, required=True), 

192 OpenApiParameter(name="groups", type=OpenApiTypes.ANY, required=True) 

193 ], 

194 responses=serializers.DictField() 

195 ) 

196 @action(detail=False, methods=['get'], url_path='compounds-summary') 

197 def summary_table_compounds(self, request): 

198 compound_mode = self.request.query_params.get("compound_mode") 

199 measure_type = self.request.query_params.get("measure_type") 

200 groups = self.request.query_params.get("groups") 

201 

202 results = {} 

203 groups = Grouping.objects.filter(id__in=json.loads(groups)) 

204 for current_group in groups: 

205 query_set = self.get_summary_queryset().filter(graphicObject__in=current_group.graphicObjects.all()) 

206 data = get_composition_summary_table_data(queryset=query_set, target_compound_mode=compound_mode, measure_type=measure_type) 

207 

208 group_name = current_group.simulationObject.componentName 

209 results[group_name] = data 

210 

211 return Response(results, status=200) 

212 

213 @extend_schema(request=UpdateCompoundSerializer, responses=None) 

214 @action(detail=False, methods=['post'], url_path='update-compounds') 

215 def update_compounds(self, request): 

216 """Update the selected compounds for the specified simulation object.""" 

217 try: 

218 serializer = UpdateCompoundSerializer(data=request.data) 

219 serializer.is_valid(raise_exception=True) 

220 validated_data = serializer.validated_data 

221 

222 simulation_object = SimulationObject.objects.get( 

223 pk=validated_data.get("simulationObject") 

224 ) 

225 

226 expected_compounds = validated_data.get("compounds") 

227 update_compounds_on_set(simulation_object, expected_compounds) 

228 

229 return Response({'status': 'success'}, status=200) 

230 

231 except (SimulationObject.DoesNotExist, PropertySet.DoesNotExist) as e: 

232 return Response({'status': 'error', 'message': str(e)}, status=404) 

233 except Exception as e: 

234 print(traceback.format_exc()) 

235 return Response({'status': 'error', 'message': str(e),'traceback': traceback.format_exc()}, status=400) 

236 

237 

238 

239 @extend_schema(request=AddPortSerializer, responses=None) 

240 @action(detail=False, methods=['post'], url_path='add-port') 

241 def add_port(self, request): 

242 """Add a port to a simulation object, optionally reusing an existing stream.""" 

243 try: 

244 serializer = AddPortSerializer(data=request.data) 

245 serializer.is_valid(raise_exception=True) 

246 validated_data = serializer.validated_data 

247 

248 simulation_object = SimulationObject.objects.get( 

249 pk=validated_data.get("simulationObjectId") 

250 ) 

251 

252 key = validated_data.get("key") 

253 

254 streamId = validated_data.get("stream") 

255 existing_stream = None 

256 

257 if streamId is not None: 257 ↛ 258line 257 didn't jump to line 258 because the condition on line 257 was never true

258 existing_stream = SimulationObject.objects.get(pk=streamId) 

259 # Reuse the existing stream to keep compound propagation in sync. 

260 simulation_object.add_port(key, existing_stream) 

261 else: 

262 simulation_object.add_port(key) 

263 

264 return Response({'status': 'success'}, status=200) 

265 

266 except (SimulationObject.DoesNotExist, PropertySet.DoesNotExist) as e: 

267 return Response({'status': 'error', 'message': str(e)}, status=404) 

268 except Exception as e: 

269 return Response({'status': 'error', 'message': str(e),'traceback': traceback.format_exc()}, status=400) 

270 

271 

272 

273 @extend_schema(request=MergeDecisionNodesSerializer, responses=None) 

274 @action(detail=False, methods=['post'], url_path='merge-decision-nodes') 

275 def merge_decision_nodes(self, request): 

276 try: 

277 serializer = MergeDecisionNodesSerializer(data=request.data) 

278 serializer.is_valid(raise_exception=True) 

279 validated_data = serializer.validated_data 

280 

281 decision_node_active = SimulationObject.objects.get( 

282 pk=validated_data.get("decisionNodeActive") 

283 ) 

284 

285 decision_node_over = SimulationObject.objects.get( 

286 pk=validated_data.get("decisionNodeOver") 

287 ) 

288 

289 decision_node_active.merge_decision_nodes(decision_node_active, decision_node_over) 

290 

291 return Response({'status': 'success'}, status=200) 

292 

293 except (SimulationObject.DoesNotExist, PropertySet.DoesNotExist) as e: 

294 return Response({'status': 'error', 'message': str(e)}, status=404) 

295 except Exception as e: 

296 return Response({'status': 'error', 'message': str(e),'traceback': traceback.format_exc()}, status=400) 

297 

298 

299 

300 @extend_schema(request=None,responses=int) 

301 @action(detail=True, methods=['POST']) 

302 def add_expression(self, request, pk=None): 

303 """Create a blank expression property on the simulation object.""" 

304 if pk is None: 304 ↛ 305line 304 didn't jump to line 305 because the condition on line 304 was never true

305 return Response({'status': 'error', 'message': 'id is required'}, status=400) 

306 

307 simulation_object = SimulationObject.objects.get(pk=pk) 

308 prop_info = add_expression(simulation_object) 

309 

310 return Response(prop_info.id, status=200) 

311 

312 @extend_schema(request=AddPropertyTemplateSerializer) 

313 @action(detail=True, methods=['POST']) 

314 def add_custom_property_template(self, request, pk=None): 

315 """Attach a custom propertytemplate to the simulation object using the supplied key.""" 

316 if pk is None: 316 ↛ 317line 316 didn't jump to line 317 because the condition on line 316 was never true

317 return Response({'status': 'error', 'message': 'id is required'}, status=400) 

318 

319 simulation_object = SimulationObject.objects.prefetch_related( 

320 "properties" 

321 ).get(pk=pk) 

322 serializer = AddPropertyTemplateSerializer(data=request.data) 

323 serializer.is_valid(raise_exception=True) 

324 template_key = serializer.validated_data.get("templateKey") 

325 

326 try: 

327 add_template(simulation_object, template_key) 

328 return Response({'status': 'success'}, status=200) 

329 except ValueError as e: 

330 return Response({'status': 'error', 'message': str(e),'traceback': traceback.format_exc()}, status=400) 

331 

332 

333 @extend_schema(request=RestoreObjectsSerializer, responses=None) 

334 @action(detail=False, methods=['post']) 

335 def restore(self, request): 

336 """Restore previously deleted simulation objects.""" 

337 try: 

338 # Handle both 'ids' and 'object_ids' in request data 

339 if 'ids' in request.data: 339 ↛ 340line 339 didn't jump to line 340 because the condition on line 339 was never true

340 request_data = {'object_ids': request.data['ids']} 

341 else: 

342 request_data = request.data 

343 

344 serializer = RestoreObjectsSerializer(data=request_data) 

345 serializer.is_valid(raise_exception=True) 

346 object_ids = serializer.validated_data['object_ids'] 

347 except Exception as e: 

348 return Response( 

349 { 

350 'error': 'Validation error', 

351 'details': str(e), 

352 'traceback': traceback.format_exc(), 

353 'received_data': request.data 

354 }, 

355 status=status.HTTP_400_BAD_REQUEST 

356 ) 

357 

358 if not object_ids: 358 ↛ 359line 358 didn't jump to line 359 because the condition on line 358 was never true

359 return Response( 

360 {'error': 'No objectIds provided'}, 

361 status=status.HTTP_400_BAD_REQUEST 

362 ) 

363 

364 DeleteFactory._restore_object_ids(object_ids) 

365 return Response(status=status.HTTP_200_OK) 

366 

367 

368 

369 @extend_schema(request=DuplicateSimulationObjectSerializer, responses=None) 

370 @action(detail=False, methods=['post'], url_path='duplicate-simulation-object') 

371 def duplicate_simulation_object(self, request): 

372 """Duplicate simulation objects and reposition them at the supplied coordinates.""" 

373 try: 

374 serializer = DuplicateSimulationObjectSerializer(data=request.data) 

375 serializer.is_valid(raise_exception=True) 

376 validated_data = serializer.validated_data 

377 duplicator = DuplicateSimulationObject() 

378 flowsheet = self.request.query_params.get("flowsheet") 

379 duplicator.handle_duplication_request( 

380 flowsheet, 

381 validated_data 

382 ) 

383 return Response(None, status=200) 

384 except SimulationObject.DoesNotExist as e: 

385 return Response({'status': 'error', 'message': str(e)}, status=400) 

386 except Exception as e: 

387 return Response({'status': 'error', 'message': str(e),'traceback': traceback.format_exc()}, status=400)