Coverage for backend/django/flowsheetInternals/unitops/viewsets/SimulationObjectViewSet.py: 82%
208 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-12 01:47 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-12 01:47 +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'): 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
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: 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)
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: 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
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: 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 )
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)