Coverage for backend/django/Economics/results/services/comparison/config.py: 100%
51 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
1"""Persistence for saved Economics study comparison configurations."""
3from __future__ import annotations
5from django.core.exceptions import PermissionDenied
6from django.db import transaction
8from authentication.user.models import User
9from core.managers import has_flowsheet_write_access
10from Economics.shared.choices import EconomicsBaselineMode
11from Economics.studies.models import (
12 EconomicsStudy,
13 EconomicsStudyComparisonConfig,
14 EconomicsStudyComparisonSelection,
15)
16from Economics.studies.services.baseline_access import (
17 resolve_study_reference,
18 stable_flowsheet_id,
19)
21from .contracts import ComparisonConfigPayload
22from .targets import validate_comparison_targets
25def save_comparison_config(
26 *,
27 active_study: EconomicsStudy,
28 user: User,
29 comparison_study_ids: list[int],
30 baseline_study_id: int | None,
31 baseline_mode: EconomicsBaselineMode = EconomicsBaselineMode.STUDY,
32) -> EconomicsStudyComparisonConfig:
33 """Replace the saved comparison configuration for an active study."""
34 active_flowsheet_id = stable_flowsheet_id(active_study)
35 if not has_flowsheet_write_access(user, active_flowsheet_id):
36 raise PermissionDenied("User does not have write access to this flowsheet.")
37 selected_baseline_id = _selected_baseline_study_id(
38 active_study=active_study,
39 baseline_mode=baseline_mode,
40 baseline_study_id=baseline_study_id,
41 )
42 targets = validate_comparison_targets(
43 active_study=active_study,
44 user=user,
45 comparison_study_ids=comparison_study_ids,
46 baseline_mode=baseline_mode,
47 baseline_study_id=selected_baseline_id,
48 )
49 studies_by_id = {active_study.pk: active_study, **{target.pk: target for target in targets}}
50 selected_baseline = studies_by_id[selected_baseline_id]
51 with transaction.atomic():
52 EconomicsStudy.objects.select_for_update().only("pk").get(
53 flowsheet_state=active_study.flowsheet_state,
54 pk=active_study.pk,
55 )
56 config = (
57 EconomicsStudyComparisonConfig.objects.select_for_update()
58 .filter(
59 flowsheet_state=active_study.flowsheet_state,
60 active_study=active_study,
61 )
62 .first()
63 )
64 if config is None:
65 config = EconomicsStudyComparisonConfig.objects.create(
66 flowsheet_state=active_study.flowsheet_state,
67 active_study=active_study,
68 baseline_mode=baseline_mode,
69 baseline_flowsheet_id=stable_flowsheet_id(selected_baseline),
70 baseline_study_lineage_id=selected_baseline.lineage_id,
71 )
72 else:
73 config.baseline_mode = baseline_mode
74 config.baseline_flowsheet_id = stable_flowsheet_id(selected_baseline)
75 config.baseline_study_lineage_id = selected_baseline.lineage_id
76 config.save(
77 update_fields=[
78 "baseline_mode",
79 "baseline_flowsheet_id",
80 "baseline_study_lineage_id",
81 "updated_at",
82 ]
83 )
85 EconomicsStudyComparisonSelection.objects.filter(
86 flowsheet_state=active_study.flowsheet_state,
87 config=config,
88 ).delete()
89 EconomicsStudyComparisonSelection.objects.bulk_create(
90 [
91 EconomicsStudyComparisonSelection(
92 flowsheet_state=active_study.flowsheet_state,
93 config=config,
94 target_study_lineage_id=target.lineage_id,
95 target_flowsheet_id=stable_flowsheet_id(target),
96 study_name_snapshot=target.name,
97 flowsheet_name_snapshot=target.flowsheet_state.flowsheet.name,
98 sort_order=index,
99 )
100 for index, target in enumerate(targets, start=1)
101 ]
102 )
103 return config
106def _selected_baseline_study_id(
107 *,
108 active_study: EconomicsStudy,
109 baseline_mode: EconomicsBaselineMode,
110 baseline_study_id: int | None,
111) -> int:
112 """Normalize saved comparison config so manual mode never depends on another study id."""
113 if baseline_mode == EconomicsBaselineMode.MANUAL:
114 return active_study.pk
115 return baseline_study_id or active_study.pk
118def comparison_config(active_study: EconomicsStudy) -> EconomicsStudyComparisonConfig | None:
119 """Load the saved comparison config, including ordered selections."""
120 try:
121 return (
122 EconomicsStudyComparisonConfig.objects.prefetch_related("selections")
123 .select_related("active_study")
124 .get(
125 flowsheet_state=active_study.flowsheet_state,
126 active_study=active_study,
127 )
128 )
129 except EconomicsStudyComparisonConfig.DoesNotExist:
130 return None
133def baseline_study_id(*, active_study: EconomicsStudy, config: EconomicsStudyComparisonConfig | None) -> int:
134 """Return the saved baseline, defaulting to the active study."""
135 if config is None:
136 return active_study.pk
137 baseline = resolve_study_reference(
138 owner_state_id=config.flowsheet_state_id,
139 target_flowsheet_id=config.baseline_flowsheet_id,
140 target_lineage_id=config.baseline_study_lineage_id,
141 )
142 return baseline.pk if baseline is not None else active_study.pk
145def baseline_mode(*, config: EconomicsStudyComparisonConfig | None) -> EconomicsBaselineMode:
146 """Return the saved comparison baseline mode, defaulting to study mode."""
147 if config is None:
148 return EconomicsBaselineMode.STUDY
149 return EconomicsBaselineMode(config.baseline_mode)
152def config_payload(
153 *,
154 active_study: EconomicsStudy,
155 config: EconomicsStudyComparisonConfig | None,
156 selections: list[EconomicsStudyComparisonSelection],
157) -> ComparisonConfigPayload:
158 """Serialize the saved config for the comparison API payload."""
159 baseline_id = baseline_study_id(active_study=active_study, config=config)
160 return ComparisonConfigPayload(
161 id=config.pk if config is not None else None,
162 active_study=active_study.pk,
163 baseline_mode=baseline_mode(config=config),
164 baseline_study_id=baseline_id,
165 comparison_study_ids=[
166 target.pk
167 for selection in selections
168 if (
169 target := resolve_study_reference(
170 owner_state_id=selection.flowsheet_state_id,
171 target_flowsheet_id=selection.target_flowsheet_id,
172 target_lineage_id=selection.target_study_lineage_id,
173 )
174 )
175 is not None
176 ],
177 updated_at=config.updated_at if config is not None else None,
178 )