Coverage for backend/django/Economics/costing/cost_curves/viewsets.py: 100%
99 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 import transaction
2from drf_spectacular.utils import OpenApiParameter, 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 core.validation import flowsheet_context, get_current_flowsheet
9from Economics.costing.models import CapitalCostLine, CostCurve, CostCurveTemplate, EquipmentMapping
10from Economics.costing.cost_curves.catalog import cost_curve_equipment_categories
11from Economics.costing.cost_curves.serializers import (
12 CostCurveAuthoringSerializer,
13 CostCurveEquipmentCategorySerializer,
14 CostCurveSerializer,
15)
16from Economics.costing.cost_curves.library import save_private_template_from_curve
17from Economics.costing.capital.generated_lines import sync_generated_capital_lines
18from Economics.shared.access import require_write_access
19from Economics.studies.services.result_staleness import mark_study_stale
20from Economics.studies.models import EconomicsStudy
23class CostCurveViewSet(ModelViewSet):
24 serializer_class = CostCurveSerializer
26 def get_serializer_class(self):
27 if self.action in {"create", "update", "partial_update"}:
28 return CostCurveAuthoringSerializer
29 return CostCurveSerializer
31 @extend_schema(
32 parameters=[
33 OpenApiParameter("flowsheet", int, OpenApiParameter.QUERY, required=True),
34 OpenApiParameter("active", bool, OpenApiParameter.QUERY, required=False),
35 ],
36 responses=CostCurveSerializer(many=True),
37 )
38 def list(self, request, *args, **kwargs):
39 return super().list(request, *args, **kwargs)
41 @extend_schema(request=CostCurveAuthoringSerializer, responses={status.HTTP_201_CREATED: CostCurveSerializer})
42 def create(self, request, *args, **kwargs):
43 require_write_access(request.user)
44 with transaction.atomic():
45 serializer = self.get_serializer(data=request.data)
46 serializer.is_valid(raise_exception=True)
47 self.perform_create(serializer)
48 instance = serializer.instance
49 headers = self.get_success_headers(serializer.data)
50 self._save_private_template_if_requested(instance)
51 response_data = CostCurveSerializer(instance, context=self.get_serializer_context()).data
52 return Response(response_data, status=status.HTTP_201_CREATED, headers=headers)
54 @extend_schema(request=CostCurveAuthoringSerializer, responses=CostCurveSerializer)
55 def update(self, request, *args, **kwargs):
56 require_write_access(request.user)
57 with transaction.atomic():
58 response = super().update(request, *args, **kwargs)
59 instance = self.get_object()
60 self._save_private_template_if_requested(instance)
61 _refresh_dependent_studies_for_curve(instance)
62 response.data = CostCurveSerializer(instance, context=self.get_serializer_context()).data
63 return response
65 @extend_schema(request=CostCurveAuthoringSerializer(partial=True), responses=CostCurveSerializer)
66 def partial_update(self, request, *args, **kwargs):
67 require_write_access(request.user)
68 with transaction.atomic():
69 response = super().partial_update(request, *args, **kwargs)
70 instance = self.get_object()
71 self._save_private_template_if_requested(instance)
72 _refresh_dependent_studies_for_curve(instance)
73 response.data = CostCurveSerializer(instance, context=self.get_serializer_context()).data
74 return response
76 def destroy(self, request, *args, **kwargs):
77 require_write_access(request.user)
78 instance = self.get_object()
79 affected_study_ids = set(
80 EquipmentMapping._base_manager.filter(cost_curve=instance).values_list(
81 "costable_item__study_id", flat=True
82 )
83 )
84 affected_study_ids.update(
85 CapitalCostLine._base_manager.filter(cost_curve=instance).values_list("study_id", flat=True)
86 )
87 affected_studies = list(
88 EconomicsStudy._base_manager.filter(pk__in=affected_study_ids).select_related(
89 "flowsheet_state__flowsheet__project"
90 )
91 )
92 with transaction.atomic():
93 response = super().destroy(request, *args, **kwargs)
94 for study in affected_studies:
95 _refresh_dependent_study(study)
96 return response
98 @extend_schema(
99 parameters=[
100 OpenApiParameter("flowsheet", int, OpenApiParameter.QUERY, required=True),
101 OpenApiParameter("active", bool, OpenApiParameter.QUERY, required=False),
102 ],
103 responses=CostCurveEquipmentCategorySerializer(many=True),
104 )
105 @action(detail=False, methods=["get"], url_path="equipment-options")
106 def equipment_options(self, request):
107 options = cost_curve_equipment_categories(
108 self.get_queryset(),
109 user_templates=CostCurveTemplate.objects.all(),
110 )
111 serializer = CostCurveEquipmentCategorySerializer(options, many=True)
112 return Response(serializer.data)
114 def get_queryset(self):
115 queryset = CostCurve.objects.all()
116 active = self.request.query_params.get("active")
117 return queryset.filter(active=active.lower() == "true") if active is not None else queryset
119 def perform_create(self, serializer):
120 self._save_as_template_requested = serializer.validated_data.pop("save_as_template", False)
121 serializer.save()
123 def perform_update(self, serializer):
124 self._save_as_template_requested = serializer.validated_data.pop("save_as_template", False)
125 serializer.save()
127 def _save_private_template_if_requested(self, curve: CostCurve) -> None:
128 if getattr(self, "_save_as_template_requested", False):
129 save_private_template_from_curve(curve=curve, owner=self.request.user)
132def _dependent_studies_for_curve(curve: CostCurve) -> list[EconomicsStudy]:
133 affected_study_ids = set(
134 EquipmentMapping._base_manager.filter(cost_curve=curve).values_list(
135 "costable_item__study_id", flat=True
136 )
137 )
138 affected_study_ids.update(
139 CapitalCostLine._base_manager.filter(cost_curve=curve).values_list("study_id", flat=True)
140 )
141 return list(
142 EconomicsStudy._base_manager.filter(pk__in=affected_study_ids)
143 .select_related("flowsheet_state__flowsheet__project")
144 .order_by("pk")
145 )
148def _refresh_dependent_studies_for_curve(curve: CostCurve) -> None:
149 for study in _dependent_studies_for_curve(curve):
150 _refresh_dependent_study(study)
153def _refresh_dependent_study(study: EconomicsStudy) -> None:
154 """Refresh a referenced study under its own flowsheet access context."""
155 flowsheet = study.flowsheet_state.flowsheet
156 with flowsheet_context(flowsheet.pk, flowsheet.project.owner):
157 get_current_flowsheet()["bypass_write_checks"] = True
158 sync_generated_capital_lines(study)
159 mark_study_stale(study, reason="cost_curve_changed")