Coverage for backend/django/Economics/studies/viewsets.py: 100%

137 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-07-22 05:22 +0000

1from django.db.models import Prefetch 

2from drf_spectacular.utils import OpenApiParameter, OpenApiTypes, extend_schema 

3from rest_framework import status 

4from rest_framework.decorators import action 

5from rest_framework.response import Response 

6 

7from core.viewset import ModelViewSet 

8from Economics.costing.costable_items.serializers import CostableItemSerializer 

9from Economics.costing.models import CapitalCostLine, CostableItem, OperatingCostLine 

10from Economics.costing.operating.serializers import ( 

11 OperatingCostLineSerializer, 

12 OperatingLinesFromPropertiesRequestSerializer, 

13 OperatingLineFromPropertyRequestSerializer, 

14 OperatingStreamPropertyOptionSerializer, 

15) 

16from Economics.results.serializers import ( 

17 EconomicsChartDatasetSerializer, 

18 EconomicsResultRunSerializer, 

19 RecalculateRequestSerializer, 

20) 

21from Economics.results.models import EconomicsResultLine, EconomicsResultRun 

22from Economics.results.services.comparison.contracts import ( 

23 ComparisonPayload, 

24 EconomicsStudyComparisonRequest, 

25) 

26from Economics.scheduling.composite.contracts import CompositeSchedulePlanPayload, CompositeSchedulePlanRequest 

27from Economics.scheduling.serializers import ScheduleScenarioOptionSerializer 

28from Economics.settings_profiles.serializers import EconomicsAssumptionsSerializer, EconomicsBaselineSerializer 

29from Economics.studies.models import EconomicsStudy 

30from Economics.studies.serializers import DuplicateStudyRequestSerializer, EconomicsStudyFullSerializer, EconomicsStudySerializer, EnableCostingRequestSerializer 

31from Economics.studies.services.baseline_access import require_readable_baseline_studies_for_user 

32from Economics.studies.services.configuration_api import ( 

33 assumptions_data, 

34 baseline_data, 

35 comparison_data, 

36 schedule_options_data, 

37 schedule_plan_data, 

38 schedule_plan_preview_data, 

39) 

40from Economics.studies.services.mutation_api import ( 

41 create_operating_line_from_property_response, 

42 create_operating_lines_from_properties_response, 

43 duplicate_study_instance, 

44 enable_costing_response, 

45 operating_property_options_data, 

46 recalculate_data, 

47) 

48from Economics.studies.services.read_api import chart_datasets_data, current_result_data, full_study_instance 

49from Economics.costing.operating.stream_properties import sync_project_default_operating_line_rates_for_study 

50from Economics.studies.services.project_scope import ( 

51 current_project_from_flowsheet_context, 

52 study_queryset_for_project, 

53) 

54from Economics.studies.services.result_staleness import mark_study_stale 

55 

56 

57class EconomicsStudyViewSet(ModelViewSet): 

58 """CRUD and workflow actions for v1 economics studies.""" 

59 

60 serializer_class = EconomicsStudySerializer 

61 

62 def get_queryset(self): 

63 return EconomicsStudy.objects.select_related( 

64 "flowsheet_state__flowsheet", 

65 "settings_profile", 

66 "schedule_scenario", 

67 ).prefetch_related( 

68 Prefetch( 

69 "costable_items", 

70 queryset=CostableItem.objects.select_related( 

71 "study__settings_profile", 

72 "simulation_object", 

73 "cost_driver", 

74 "cost_driver__property_info", 

75 "cost_driver__manual_property_info", 

76 "equipment_mapping", 

77 "equipment_mapping__cost_curve", 

78 ), 

79 ), 

80 Prefetch( 

81 "capital_lines", 

82 queryset=CapitalCostLine.objects.select_related("costable_item", "cost_curve"), 

83 ), 

84 Prefetch( 

85 "operating_lines", 

86 queryset=OperatingCostLine.objects.select_related( 

87 "study", 

88 "costable_item", 

89 "source_property_info", 

90 "source_property_info__set__simulationObject", 

91 "source_default_rate", 

92 ).prefetch_related("source_property_info__values"), 

93 ), 

94 Prefetch( 

95 "result_runs", 

96 queryset=EconomicsResultRun.objects.order_by("-created_at", "-pk") 

97 .prefetch_related("dependencies")[:1], 

98 to_attr="_economics_latest_result_runs", 

99 ), 

100 Prefetch( 

101 "result_runs", 

102 queryset=EconomicsResultRun.objects.filter(status="current") 

103 .order_by("-created_at", "-pk") 

104 .prefetch_related( 

105 "dependencies", 

106 Prefetch( 

107 "lines", 

108 queryset=EconomicsResultLine.objects.select_related( 

109 "source_operating_line__source_default_rate" 

110 ), 

111 ), 

112 "chart_datasets", 

113 )[:1], 

114 to_attr="_economics_current_result_runs", 

115 ), 

116 ) 

117 

118 def get_serializer_class(self): 

119 if self.action in {"retrieve", "full", "recalculate", "duplicate"}: 

120 return EconomicsStudyFullSerializer 

121 return EconomicsStudySerializer 

122 

123 def get_serializer(self, *args, **kwargs): 

124 if args and "data" not in kwargs: 

125 require_readable_baseline_studies_for_user( 

126 args[0], 

127 user=self.request.user, 

128 ) 

129 return super().get_serializer(*args, **kwargs) 

130 

131 def update(self, request, *args, **kwargs): 

132 """Treat study detail writes as configuration patches, including PUT.""" 

133 

134 kwargs["partial"] = True 

135 return super().update(request, *args, **kwargs) 

136 

137 @extend_schema(parameters=[OpenApiParameter(name="flowsheet", required=True, type=OpenApiTypes.INT)]) 

138 def list(self, request): 

139 return super().list(request) 

140 

141 @extend_schema( 

142 parameters=[OpenApiParameter(name="flowsheet", required=True, type=OpenApiTypes.INT)], 

143 responses=EconomicsStudySerializer(many=True), 

144 ) 

145 @action(detail=False, methods=["get"], url_path="project") 

146 def project(self, request): 

147 project = current_project_from_flowsheet_context(request.user) 

148 studies = study_queryset_for_project(project, request.user) 

149 serializer = self.get_serializer(studies, many=True) 

150 return Response(serializer.data) 

151 

152 @extend_schema(parameters=[OpenApiParameter(name="flowsheet", required=True, type=OpenApiTypes.INT)]) 

153 def create(self, request, *args, **kwargs): 

154 return super().create(request, *args, **kwargs) 

155 

156 def perform_update(self, serializer): 

157 previous_profile_id = serializer.instance.settings_profile_id 

158 previous_schedule_mode = serializer.instance.schedule_mode 

159 previous_schedule_scenario_id = serializer.instance.schedule_scenario_id 

160 previous_baseline_mode = serializer.instance.baseline_mode 

161 previous_baseline_reference = ( 

162 serializer.instance.baseline_flowsheet_id, 

163 serializer.instance.baseline_study_lineage_id, 

164 ) 

165 study = serializer.save() 

166 if previous_profile_id != study.settings_profile_id: 

167 sync_project_default_operating_line_rates_for_study(study, reason="economics_settings_profile_selected") 

168 mark_study_stale(study, reason="economics_settings_profile_selected") 

169 if ( 

170 previous_schedule_mode != study.schedule_mode 

171 or previous_schedule_scenario_id != study.schedule_scenario_id 

172 ): 

173 mark_study_stale(study, reason="economics_schedule_selected") 

174 if ( 

175 previous_baseline_mode != study.baseline_mode 

176 or previous_baseline_reference 

177 != (study.baseline_flowsheet_id, study.baseline_study_lineage_id) 

178 ): 

179 mark_study_stale(study, reason="economics_baseline_selected") 

180 

181 @extend_schema(responses=EconomicsStudyFullSerializer) 

182 @action(detail=True, methods=["get"]) 

183 def full(self, request, pk=None): 

184 refreshed_study = full_study_instance( 

185 study=self.get_object(), 

186 user=request.user, 

187 queryset=self.get_queryset(), 

188 ) 

189 return Response(self.get_serializer(refreshed_study).data) 

190 

191 @extend_schema(responses=OperatingStreamPropertyOptionSerializer(many=True)) 

192 @action(detail=True, methods=["get"], url_path="operating-property-options") 

193 def operating_property_options(self, request, pk=None): 

194 return Response(operating_property_options_data(self.get_object())) 

195 

196 @extend_schema(responses=ScheduleScenarioOptionSerializer(many=True)) 

197 @action(detail=True, methods=["get"], url_path="schedule-options") 

198 def schedule_options(self, request, pk=None): 

199 return Response(schedule_options_data(self.get_object())) 

200 

201 @extend_schema(methods=["GET"], responses=CompositeSchedulePlanPayload) 

202 @extend_schema( 

203 methods=["PUT"], 

204 request=CompositeSchedulePlanRequest, 

205 responses=CompositeSchedulePlanPayload, 

206 ) 

207 @action(detail=True, methods=["get", "put"], url_path="schedule-plan") 

208 def schedule_plan(self, request, pk=None): 

209 return Response( 

210 schedule_plan_data( 

211 study=self.get_object(), 

212 user=request.user, 

213 method=request.method, 

214 data=request.data, 

215 ) 

216 ) 

217 

218 @extend_schema( 

219 request=CompositeSchedulePlanRequest, 

220 responses=CompositeSchedulePlanPayload, 

221 ) 

222 @action(detail=True, methods=["post"], url_path="schedule-plan/preview") 

223 def schedule_plan_preview(self, request, pk=None): 

224 return Response(schedule_plan_preview_data(study=self.get_object(), data=request.data)) 

225 

226 @extend_schema( 

227 request=OperatingLineFromPropertyRequestSerializer, 

228 responses={status.HTTP_201_CREATED: OperatingCostLineSerializer}, 

229 ) 

230 @action(detail=True, methods=["post"], url_path="operating-lines/from-property") 

231 def create_operating_line_from_property(self, request, pk=None): 

232 data, response_status = create_operating_line_from_property_response( 

233 study=self.get_object(), 

234 user=request.user, 

235 data=request.data, 

236 serializer_context=self.get_serializer_context(), 

237 ) 

238 return Response(data, status=response_status) 

239 

240 @extend_schema( 

241 request=OperatingLinesFromPropertiesRequestSerializer, 

242 responses={ 

243 status.HTTP_201_CREATED: OperatingCostLineSerializer(many=True) 

244 }, 

245 ) 

246 @action(detail=True, methods=["post"], url_path="operating-lines/from-properties") 

247 def create_operating_lines_from_properties(self, request, pk=None): 

248 data, response_status = create_operating_lines_from_properties_response( 

249 study=self.get_object(), 

250 user=request.user, 

251 data=request.data, 

252 serializer_context=self.get_serializer_context(), 

253 ) 

254 return Response(data, status=response_status) 

255 

256 @extend_schema(methods=["GET"], responses=EconomicsAssumptionsSerializer) 

257 @extend_schema(methods=["PUT"], request=EconomicsAssumptionsSerializer, responses=EconomicsAssumptionsSerializer) 

258 @extend_schema(methods=["PATCH"], request=EconomicsAssumptionsSerializer(partial=True), responses=EconomicsAssumptionsSerializer) 

259 @action(detail=True, methods=["get", "put", "patch"]) 

260 def assumptions(self, request, pk=None): 

261 return Response( 

262 assumptions_data( 

263 study=self.get_object(), 

264 method=request.method, 

265 data=request.data, 

266 user=request.user, 

267 serializer_context=self.get_serializer_context(), 

268 ) 

269 ) 

270 

271 @extend_schema(methods=["GET"], responses=EconomicsBaselineSerializer) 

272 @extend_schema(methods=["PUT"], request=EconomicsBaselineSerializer, responses=EconomicsBaselineSerializer) 

273 @extend_schema(methods=["PATCH"], request=EconomicsBaselineSerializer(partial=True), responses=EconomicsBaselineSerializer) 

274 @action(detail=True, methods=["get", "put", "patch"]) 

275 def baseline(self, request, pk=None): 

276 return Response( 

277 baseline_data( 

278 study=self.get_object(), 

279 method=request.method, 

280 data=request.data, 

281 user=request.user, 

282 serializer_context=self.get_serializer_context(), 

283 ) 

284 ) 

285 

286 @extend_schema(request=EnableCostingRequestSerializer, responses=CostableItemSerializer) 

287 @action(detail=True, methods=["post"], url_path="enable-costing") 

288 def enable_costing(self, request, pk=None): 

289 data, response_status = enable_costing_response( 

290 study=self.get_object(), 

291 user=request.user, 

292 data=request.data, 

293 serializer_context=self.get_serializer_context(), 

294 ) 

295 return Response(data, status=response_status) 

296 

297 @extend_schema(request=RecalculateRequestSerializer, responses=EconomicsResultRunSerializer) 

298 @action(detail=True, methods=["post"]) 

299 def recalculate(self, request, pk=None): 

300 return Response( 

301 recalculate_data( 

302 study=self.get_object(), 

303 user=request.user, 

304 data=request.data, 

305 serializer_context=self.get_serializer_context(), 

306 ) 

307 ) 

308 

309 @extend_schema(responses=EconomicsResultRunSerializer) 

310 @action(detail=True, methods=["get"], url_path="current-result") 

311 def current_result(self, request, pk=None): 

312 return Response( 

313 current_result_data( 

314 study=self.get_object(), 

315 user=request.user, 

316 serializer_context=self.get_serializer_context(), 

317 ) 

318 ) 

319 

320 @extend_schema(responses=EconomicsChartDatasetSerializer(many=True)) 

321 @action(detail=True, methods=["get"], url_path="chart-datasets") 

322 def chart_datasets(self, request, pk=None): 

323 return Response( 

324 chart_datasets_data( 

325 study=self.get_object(), 

326 user=request.user, 

327 serializer_context=self.get_serializer_context(), 

328 ) 

329 ) 

330 

331 @extend_schema(methods=["GET"], responses=ComparisonPayload) 

332 @extend_schema( 

333 methods=["PUT"], 

334 request=EconomicsStudyComparisonRequest, 

335 responses=ComparisonPayload, 

336 ) 

337 @action(detail=True, methods=["get", "put"]) 

338 def comparison(self, request, pk=None): 

339 return Response( 

340 comparison_data( 

341 study=self.get_object(), 

342 user=request.user, 

343 method=request.method, 

344 data=request.data, 

345 ) 

346 ) 

347 

348 @extend_schema(request=DuplicateStudyRequestSerializer, responses={status.HTTP_201_CREATED: EconomicsStudyFullSerializer}) 

349 @action(detail=True, methods=["post"]) 

350 def duplicate(self, request, pk=None): 

351 study = duplicate_study_instance( 

352 source=self.get_object(), 

353 user=request.user, 

354 data=request.data, 

355 ) 

356 return Response(self.get_serializer(study).data, status=status.HTTP_201_CREATED)