Coverage for backend/django/flowsheetInternals/graphicData/viewsets/GroupingViewSet.py: 78%
225 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
1import traceback
2from core.viewset import ModelViewSet
3from rest_framework import serializers
4from rest_framework.response import Response
5from flowsheetInternals.graphicData.models.groupingModel import (
6 Breadcrumbs,
7 Connection,
8 Grouping,
9 GraphicObject,
10 AbstractionType,
11)
12from flowsheetInternals.graphicData.serializers.groupingSerializer import (
13 GroupingSerializer,
14)
15from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
16from rest_framework.decorators import action
17from flowsheetInternals.unitops.models.SimulationObject import SimulationObject
18from core.auxiliary.models import PropertyInfo # Import at top of file
19from flowsheetInternals.unitops.models.delete_factory import DeleteFactory
20from pydantic import RootModel
21from typing import List
22from flowsheetInternals.graphicData.logic.make_group import make_group
23from flowsheetInternals.graphicData.logic.ungroup import ungroup
24from core.auxiliary.models.Flowsheet import Flowsheet
25from flowsheetInternals.unitops.models.FlowsheetEditOperation import (
26 FlowsheetEditOperation,
27)
28from flowsheetInternals.unitops.services.edit_operations.mutation import flowsheet_edit
29from flowsheetInternals.unitops.services.edit_operations.recorder import (
30 tracked_bulk_update,
31)
34class MakeGroupSerializer(serializers.Serializer):
35 containedObjects = serializers.ListField(child=serializers.IntegerField())
38class DeleteSelectedObjects(serializers.Serializer):
39 containedObjects = serializers.ListField(child=serializers.IntegerField())
42class SelectionRectangleSerializer(serializers.Serializer):
43 containedObjects = serializers.ListField(child=serializers.IntegerField())
44 deltaX = serializers.FloatField()
45 deltaY = serializers.FloatField()
48class UngroupSerializer(serializers.Serializer):
49 parentGroup = serializers.IntegerField()
52# Workaround for DRF Spectacular not supporting lists: https://github.com/tfranzel/drf-spectacular/issues/1232
53class BreadcrumbsList(RootModel):
54 root: List[Breadcrumbs]
57class GetConnectionsList(RootModel):
58 root: List[Connection]
61class CustomGroupingSerializer(serializers.Serializer):
62 group = serializers.CharField(required=True)
63 componentName = serializers.CharField(required=True)
64 abstractionType = serializers.CharField(required=False, allow_blank=True)
67class GroupingViewSet(ModelViewSet):
68 serializer_class = GroupingSerializer
70 def get_queryset(self):
71 queryset = Grouping.objects.filter(
72 simulationObject__is_deleted=False
73 ).prefetch_related("propertyInfos")
74 flowsheetId = self.request.query_params.get("flowsheet")
75 if flowsheetId is not None:
76 queryset = queryset.filter(
77 simulationObject__flowsheet_state__flowsheet_id=flowsheetId
78 )
79 return queryset
81 @extend_schema(
82 parameters=[
83 OpenApiParameter(name="flowsheet", required=True, type=OpenApiTypes.INT),
84 ]
85 )
86 def list(self, request):
87 return super().list(request)
89 def create(self, request):
90 flowsheetId = request.data.get("_flowsheet")
91 if flowsheetId is None:
92 return Response({"error": "flowsheet is required"}, status=400)
94 flowsheet = Flowsheet.objects.select_related("current_state").get(
95 pk=flowsheetId
96 )
97 request_data = request.data.copy()
98 request_data.pop("_flowsheet", None)
99 serializer = GroupingSerializer(data=request_data)
100 serializer.is_valid(raise_exception=True)
101 with flowsheet_edit(
102 flowsheet=flowsheet,
103 user=request.user,
104 kind=FlowsheetEditOperation.Kind.Add,
105 label_key="Add group",
106 ) as mutation:
107 group = serializer.save(flowsheet_state=mutation.flowsheet.current_state)
108 return Response(
109 mutation.add_to_data(dict(GroupingSerializer(group).data)),
110 status=201,
111 )
113 @extend_schema(request=CustomGroupingSerializer, responses=None)
114 @action(detail=False, methods=["post"], url_path="create-custom-group")
115 def create_custom_group(self, request):
116 serializer = CustomGroupingSerializer(data=request.data)
117 serializer.is_valid(raise_exception=True)
118 flowsheetId = request.query_params.get("flowsheet")
119 parentGroupName = serializer.validated_data.get("group")
120 abstractionType = serializer.validated_data.get(
121 "abstractionType", AbstractionType.Zone
122 )
123 flowsheet = Flowsheet.objects.select_related("current_state").get(
124 pk=flowsheetId
125 )
126 with flowsheet_edit(
127 flowsheet=flowsheet,
128 user=request.user,
129 kind=FlowsheetEditOperation.Kind.Add,
130 label_key="Add custom group",
131 ) as mutation:
132 group = Grouping.create(
133 flowsheet_state=mutation.flowsheet.current_state,
134 group=Grouping.objects.get(
135 simulationObject__componentName=parentGroupName
136 ),
137 componentName=serializer.validated_data.get("componentName"),
138 visible=False,
139 )
140 group.abstractionType = abstractionType
141 group.save(update_fields=["abstractionType"])
143 return Response(
144 mutation.add_to_data(dict(GroupingSerializer(group).data)),
145 status=201,
146 )
148 def partial_update(self, request, pk=None):
149 # This means that offsets for x and y are required
150 offsetX = request.data.pop("offsetX", None)
151 offsetY = request.data.pop("offsetY", None)
152 x = request.data.pop("x", None)
153 y = request.data.pop("y", None)
154 width = request.data.pop("width", None)
155 height = request.data.pop("height", None)
156 containedObjects = request.data.pop("containedObjects", None)
157 propertyInfos = request.data.pop("propertyInfos", None)
158 abstractionType = request.data.pop("abstractionType", None)
160 try:
161 instance: Grouping = self.get_object()
162 except Grouping.DoesNotExist:
163 return Response({"error": "Object not found"}, status=404)
165 serializer = GroupingSerializer(instance, data=request.data, partial=True)
166 if not serializer.is_valid(): 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true
167 return Response(serializer.errors, status=400)
168 with flowsheet_edit(
169 flowsheet=instance.flowsheet_state.flowsheet,
170 user=request.user,
171 kind=FlowsheetEditOperation.Kind.Move,
172 label_key="Move group",
173 ) as mutation:
174 if containedObjects is not None: 174 ↛ 175line 174 didn't jump to line 175 because the condition on line 174 was never true
175 instance.containedObjects.set(
176 SimulationObject.objects.filter(pk__in=containedObjects)
177 )
178 if propertyInfos is not None: 178 ↛ 182line 178 didn't jump to line 182 because the condition on line 178 was always true
179 instance.propertyInfos.set(
180 PropertyInfo.objects.filter(pk__in=propertyInfos)
181 )
182 if abstractionType is not None: 182 ↛ 185line 182 didn't jump to line 185 because the condition on line 182 was always true
183 instance.abstractionType = abstractionType
184 instance.save(update_fields=["abstractionType"])
185 serializer.save()
187 graphic_object = instance.get_graphic_object()
188 if x is not None and y is not None: 188 ↛ 189line 188 didn't jump to line 189 because the condition on line 188 was never true
189 if containedObjects is None:
190 offsetX = float(x) - float(graphic_object.x)
191 offsetY = float(y) - float(graphic_object.y)
192 else:
193 graphic_object.x = x
194 graphic_object.y = y
195 if width is not None and height is not None: 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true
196 graphic_object.width = width
197 graphic_object.height = height
198 if offsetX is not None and offsetY is not None: 198 ↛ 199line 198 didn't jump to line 199 because the condition on line 198 was never true
199 graphic_object.x = float(offsetX) + float(graphic_object.x)
200 graphic_object.y = float(offsetY) + float(graphic_object.y)
201 tracked_bulk_update(
202 GraphicObject.objects,
203 [graphic_object],
204 ["x", "y", "width", "height"],
205 )
207 return Response(mutation.add_to_data(dict(serializer.data)), status=206)
209 def destroy(self, request, *args, **kwargs):
210 """
211 override the destroy method to also delete all the unit operations and material streams
212 """
213 instance: Grouping = self.get_object()
214 flowsheet = instance.flowsheet_state.flowsheet
215 with flowsheet_edit(
216 flowsheet=flowsheet,
217 user=request.user,
218 kind=FlowsheetEditOperation.Kind.Delete,
219 label_key="Delete group",
220 ) as mutation:
221 instance.clear_group()
222 return Response(mutation.add_to_data({}), status=200)
224 @extend_schema(request=MakeGroupSerializer, responses=None)
225 @action(detail=False, methods=["post"], url_path="make-group")
226 def make_group(self, request):
227 try:
228 serializer = MakeGroupSerializer(data=request.data)
229 serializer.is_valid(raise_exception=True)
230 validated_data = serializer.validated_data
231 contained_objects_id = validated_data.get("containedObjects")
232 first_object = (
233 SimulationObject.objects.filter(pk__in=contained_objects_id)
234 .select_related("flowsheet_state__flowsheet")
235 .first()
236 )
237 if first_object is None: 237 ↛ 238line 237 didn't jump to line 238 because the condition on line 237 was never true
238 raise ValueError("No active objects were provided.")
239 with flowsheet_edit(
240 flowsheet=first_object.flowsheet_state.flowsheet,
241 user=request.user,
242 kind=FlowsheetEditOperation.Kind.Group,
243 label_key="Group objects",
244 ) as mutation:
245 new_group = make_group(contained_objects_id)
247 return Response(
248 mutation.add_to_data(
249 {
250 "status": "success",
251 "group_id": new_group.pk,
252 }
253 ),
254 status=200,
255 )
256 except Exception as e:
257 print(traceback.format_exc())
258 return Response({"status": "error", "message": str(e)}, status=400)
260 @extend_schema(request=SelectionRectangleSerializer, responses=None)
261 @action(detail=False, methods=["post"], url_path="move-selection")
262 def move_selection(self, request):
263 try:
264 serializer = SelectionRectangleSerializer(data=request.data)
265 serializer.is_valid(raise_exception=True)
266 validated_data = serializer.validated_data
268 contained_objects_id = validated_data.get("containedObjects")
269 first_object = (
270 SimulationObject.objects.filter(pk__in=contained_objects_id)
271 .select_related("flowsheet_state__flowsheet")
272 .first()
273 )
274 if first_object is None: 274 ↛ 275line 274 didn't jump to line 275 because the condition on line 274 was never true
275 raise ValueError("No active objects were provided.")
276 with flowsheet_edit(
277 flowsheet=first_object.flowsheet_state.flowsheet,
278 user=request.user,
279 kind=FlowsheetEditOperation.Kind.Move,
280 label_key="Move selection",
281 ) as mutation:
282 graphic_objects = list(
283 GraphicObject.objects.filter(
284 simulationObject__id__in=contained_objects_id,
285 simulationObject__in=SimulationObject.objects.all(),
286 )
287 )
289 deltaX = validated_data.get("deltaX")
290 deltaY = validated_data.get("deltaY")
292 for graphic_object in graphic_objects:
293 graphic_object.x = float(graphic_object.x) + deltaX
294 graphic_object.y = float(graphic_object.y) + deltaY
296 tracked_bulk_update(
297 GraphicObject.objects,
298 graphic_objects,
299 ["x", "y"],
300 )
301 return Response(
302 mutation.add_to_data({"status": "success"}),
303 status=200,
304 )
305 except Exception as e:
306 return Response({"status": "error", "message": str(e)}, status=400)
308 @extend_schema(request=DeleteSelectedObjects, responses=None)
309 @action(detail=False, methods=["post"], url_path="delete-selected-objects")
310 def delete_selected_objects(self, request):
311 try:
312 serializer = DeleteSelectedObjects(data=request.data)
313 serializer.is_valid(raise_exception=True)
314 validated_data = serializer.validated_data
316 contained_objects_id = validated_data.get("containedObjects")
317 first_object = (
318 SimulationObject.objects.filter(pk__in=contained_objects_id)
319 .select_related("flowsheet_state__flowsheet")
320 .first()
321 )
322 if first_object is None: 322 ↛ 323line 322 didn't jump to line 323 because the condition on line 322 was never true
323 raise ValueError("No active objects were provided.")
324 with flowsheet_edit(
325 flowsheet=first_object.flowsheet_state.flowsheet,
326 user=request.user,
327 kind=FlowsheetEditOperation.Kind.Delete,
328 label_key="Delete objects",
329 ) as mutation:
330 DeleteFactory.delete_multiple_objects(
331 SimulationObject.objects.filter(pk__in=contained_objects_id)
332 )
334 return Response(
335 mutation.add_to_data({"status": "success"}),
336 status=200,
337 )
338 except Exception as e:
339 return Response({"status": "error", "message": str(e)}, status=400)
341 # ungoruop method is passed in the id of the parent group ansd all objects within this gorup have thiert parent group set to none assigned
343 @extend_schema(request=UngroupSerializer, responses=None)
344 @action(detail=False, methods=["post"], url_path="ungroup")
345 def ungroup(self, request):
346 try:
347 serializer = UngroupSerializer(data=request.data)
348 serializer.is_valid(raise_exception=True)
349 validated_data = serializer.validated_data
350 group_id = validated_data.get("parentGroup")
351 group = Grouping.objects.get(pk=group_id)
352 flowsheet = group.flowsheet_state.flowsheet
353 with flowsheet_edit(
354 flowsheet=flowsheet,
355 user=request.user,
356 kind=FlowsheetEditOperation.Kind.Ungroup,
357 label_key="Ungroup objects",
358 ) as mutation:
359 ungroup(group)
361 return Response(
362 mutation.add_to_data(
363 {
364 "status": "success",
365 "group_id": group.pk,
366 }
367 ),
368 status=200,
369 )
370 except Exception as e:
371 traceback.print_exc()
372 return Response({"status": "error", "message": str(e)}, status=400)
374 @extend_schema(
375 request=None,
376 responses=BreadcrumbsList,
377 parameters=[
378 OpenApiParameter(name="group", required=True, type=OpenApiTypes.INT),
379 ],
380 )
381 @action(detail=False, methods=["get"], url_path="breadcrumbs")
382 def breadcrumbs(self, request):
383 """
384 This method will return the bread crumbs for the group that the user is currently in
385 """
386 try:
387 group_id = request.query_params.get("group")
388 # If group_id is -1, return empty breadcrumbs array
389 # -1 is the id of the root group
390 if group_id == "-1": 390 ↛ 391line 390 didn't jump to line 391 because the condition on line 390 was never true
391 return Response([], status=200)
392 group = Grouping.objects.get(pk=group_id)
393 breadcrumbs = group.get_breadcrumbs_trail()
394 breadcrumbs_data = [crumb.model_dump() for crumb in breadcrumbs]
395 return Response(breadcrumbs_data, status=200)
396 except Exception as e:
397 return Response({"status": "error", "message": str(e)}, status=400)
399 @extend_schema(
400 request=None,
401 responses=GetConnectionsList,
402 parameters=[
403 OpenApiParameter(name="group", required=True, type=OpenApiTypes.INT),
404 ],
405 )
406 @action(detail=False, methods=["get"], url_path="connections")
407 def get_connections(self, request):
408 """
409 This method will return the connections for the group that the user is currently in
410 """
411 try:
412 group_id = request.query_params.get("group")
413 group = Grouping.objects.get(pk=group_id)
414 connections = group.get_connections()
415 connections_data = [connection.model_dump() for connection in connections]
416 return Response(connections_data, status=200)
417 except Exception as e:
418 return Response({"status": "error", "message": str(e)}, status=400)
420 @extend_schema(
421 request=None,
422 responses=List[OpenApiTypes.STR],
423 )
424 @action(detail=False, methods=["get"], url_path="zones")
425 def zones(self, request):
426 """
427 This method will return the group simulation object name for the flowsheet
428 """
429 try:
430 groups = Grouping.objects.filter(simulationObject__is_deleted=False)
431 zones = [
432 group.simulationObject.componentName
433 for group in groups
434 if group.simulationObject
435 ]
436 return Response(zones, status=200)
437 except Exception as e:
438 return Response({"status": "error", "message": str(e)}, status=400)