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
« 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
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
57class EconomicsStudyViewSet(ModelViewSet):
58 """CRUD and workflow actions for v1 economics studies."""
60 serializer_class = EconomicsStudySerializer
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 )
118 def get_serializer_class(self):
119 if self.action in {"retrieve", "full", "recalculate", "duplicate"}:
120 return EconomicsStudyFullSerializer
121 return EconomicsStudySerializer
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)
131 def update(self, request, *args, **kwargs):
132 """Treat study detail writes as configuration patches, including PUT."""
134 kwargs["partial"] = True
135 return super().update(request, *args, **kwargs)
137 @extend_schema(parameters=[OpenApiParameter(name="flowsheet", required=True, type=OpenApiTypes.INT)])
138 def list(self, request):
139 return super().list(request)
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)
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)
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")
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)
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()))
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()))
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 )
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))
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)
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)
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 )
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 )
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)
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 )
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 )
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 )
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 )
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)