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

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) 

32 

33 

34class MakeGroupSerializer(serializers.Serializer): 

35 containedObjects = serializers.ListField(child=serializers.IntegerField()) 

36 

37 

38class DeleteSelectedObjects(serializers.Serializer): 

39 containedObjects = serializers.ListField(child=serializers.IntegerField()) 

40 

41 

42class SelectionRectangleSerializer(serializers.Serializer): 

43 containedObjects = serializers.ListField(child=serializers.IntegerField()) 

44 deltaX = serializers.FloatField() 

45 deltaY = serializers.FloatField() 

46 

47 

48class UngroupSerializer(serializers.Serializer): 

49 parentGroup = serializers.IntegerField() 

50 

51 

52# Workaround for DRF Spectacular not supporting lists: https://github.com/tfranzel/drf-spectacular/issues/1232 

53class BreadcrumbsList(RootModel): 

54 root: List[Breadcrumbs] 

55 

56 

57class GetConnectionsList(RootModel): 

58 root: List[Connection] 

59 

60 

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) 

65 

66 

67class GroupingViewSet(ModelViewSet): 

68 serializer_class = GroupingSerializer 

69 

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 

80 

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) 

88 

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) 

93 

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 ) 

112 

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

142 

143 return Response( 

144 mutation.add_to_data(dict(GroupingSerializer(group).data)), 

145 status=201, 

146 ) 

147 

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) 

159 

160 try: 

161 instance: Grouping = self.get_object() 

162 except Grouping.DoesNotExist: 

163 return Response({"error": "Object not found"}, status=404) 

164 

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

186 

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 ) 

206 

207 return Response(mutation.add_to_data(dict(serializer.data)), status=206) 

208 

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) 

223 

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) 

246 

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) 

259 

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 

267 

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 ) 

288 

289 deltaX = validated_data.get("deltaX") 

290 deltaY = validated_data.get("deltaY") 

291 

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 

295 

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) 

307 

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 

315 

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 ) 

333 

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) 

340 

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 

342 

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) 

360 

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) 

373 

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) 

398 

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) 

419 

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)