Coverage for backend/flowsheetInternals/unitops/viewsets/SimulationObjectViewSet.py: 67%
208 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
1import json
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
26class UpdateCompoundSerializer(serializers.Serializer):
27 """Payload schema for updating compound selections on a stream."""
29 compounds = serializers.ListField(child=serializers.CharField())
30 simulationObject = serializers.IntegerField()
32class AddPropertyTemplateSerializer(serializers.Serializer):
33 """Payload schema for adding a property template by key to a simulation object."""
35 templateKey = serializers.CharField()
37class DuplicateSimulationObjectSerializer(serializers.Serializer):
38 """Payload schema for duplicating one or more simulation objects."""
40 objectIDs = serializers.ListField(child=serializers.IntegerField())
41 x = serializers.FloatField()
42 y = serializers.FloatField()
44class AddPortSerializer(serializers.Serializer):
45 """Payload schema for attaching a new port to a simulation object."""
47 simulationObjectId = serializers.IntegerField()
48 key = serializers.CharField()
49 stream = serializers.IntegerField(required=False, allow_null=True) #optional if existing stream isnt passed in
51class MergeDecisionNodesSerializer(serializers.Serializer):
52 """Payload schema for merging two decision nodes."""
54 decisionNodeActive = serializers.IntegerField(required=True)
55 decisionNodeOver = serializers.IntegerField(required=True)
57class RestoreObjectsSerializer(serializers.Serializer):
58 """Payload schema for restoring previously deleted simulation objects."""
60 object_ids = serializers.ListField(
61 child=serializers.IntegerField(),
62 help_text="Array of object IDs to restore",
63 min_length=1
64 )
66 def validate(self, data):
67 """Ensure we have at least one valid ID"""
68 if not data.get('object_ids'):
69 raise serializers.ValidationError("At least one object ID must be provided")
70 return data
72class SimulationObjectViewSet(ModelViewSet):
73 """API endpoints for interacting with flowsheet simulation objects."""
75 serializer_class = SimulationObjectSerializer
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 )
97 return queryset
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
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)
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)
121 return Response(status=status.HTTP_204_NO_CONTENT)
122 #maybe do local storage and its request it in here
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 )
139 return queryset
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")
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())
158 data = get_stream_summary_table_data(query_set, json.loads(unit_map))
160 group_name = current_group.simulationObject.componentName
161 results[group_name] = data
163 return Response(results, status=200)
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")
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))
183 group_name = current_group.simulationObject.componentName
184 results[group_name] = data
186 return Response(results, status=200)
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")
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)
208 group_name = current_group.simulationObject.componentName
209 results[group_name] = data
211 return Response(results, status=200)
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
222 simulation_object = SimulationObject.objects.get(
223 pk=validated_data.get("simulationObject")
224 )
226 expected_compounds = validated_data.get("compounds")
227 update_compounds_on_set(simulation_object, expected_compounds)
229 return Response({'status': 'success'}, status=200)
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)
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
248 simulation_object = SimulationObject.objects.get(
249 pk=validated_data.get("simulationObjectId")
250 )
252 key = validated_data.get("key")
254 streamId = validated_data.get("stream")
255 existing_stream = None
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)
264 return Response({'status': 'success'}, status=200)
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)
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
281 decision_node_active = SimulationObject.objects.get(
282 pk=validated_data.get("decisionNodeActive")
283 )
285 decision_node_over = SimulationObject.objects.get(
286 pk=validated_data.get("decisionNodeOver")
287 )
289 decision_node_active.merge_decision_nodes(decision_node_active, decision_node_over)
291 return Response({'status': 'success'}, status=200)
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)
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)
307 simulation_object = SimulationObject.objects.get(pk=pk)
308 prop_info = add_expression(simulation_object)
310 return Response(prop_info.id, status=200)
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:
317 return Response({'status': 'error', 'message': 'id is required'}, status=400)
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")
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)
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:
340 request_data = {'object_ids': request.data['ids']}
341 else:
342 request_data = request.data
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 )
358 if not object_ids:
359 return Response(
360 {'error': 'No objectIds provided'},
361 status=status.HTTP_400_BAD_REQUEST
362 )
364 DeleteFactory._restore_object_ids(object_ids)
365 return Response(status=status.HTTP_200_OK)
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)