Coverage for backend/django/Economics/studies/services/baseline_access.py: 97%
57 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"""Access helpers for study-baseline references."""
3from collections.abc import Iterable
5from rest_framework.exceptions import PermissionDenied
7from authentication.user.models import User
8from core.managers import has_flowsheet_read_access
9from Economics.studies.models import EconomicsStudy
10from Economics.studies.services.project_scope import study_is_in_current_project
13def stable_flowsheet_id(study: EconomicsStudy) -> int:
14 """Return the stable flowsheet identity that owns a physical study row."""
16 return study.flowsheet_state.flowsheet_id
19def resolve_study_reference(
20 *,
21 owner_state_id: int,
22 target_flowsheet_id: int | None,
23 target_lineage_id,
24) -> EconomicsStudy | None:
25 """Resolve a logical study reference in the appropriate live state.
27 Same-flowsheet references resolve in the owning state so revision clones stay
28 self-contained. Cross-flowsheet references follow the target stable
29 flowsheet's current state.
30 """
32 if target_flowsheet_id is None or target_lineage_id is None:
33 return None
35 from core.auxiliary.models.FlowsheetState import FlowsheetState
36 from core.auxiliary.models.Flowsheet import Flowsheet
38 owner_flowsheet_id = (
39 FlowsheetState._base_manager.filter(pk=owner_state_id)
40 .values_list("flowsheet_id", flat=True)
41 .first()
42 )
43 if owner_flowsheet_id == target_flowsheet_id:
44 target_state_id = owner_state_id
45 else:
46 target_state_id = (
47 Flowsheet.objects.filter(pk=target_flowsheet_id)
48 .values_list("current_state_id", flat=True)
49 .first()
50 )
51 if target_state_id is None:
52 return None
53 return EconomicsStudy._base_manager.filter(
54 flowsheet_state_id=target_state_id,
55 lineage_id=target_lineage_id,
56 ).first()
59def resolve_baseline_study(study: EconomicsStudy) -> EconomicsStudy | None:
60 """Resolve a study's logical baseline reference to its current physical row."""
62 if study.flowsheet_state_id is None: 62 ↛ 63line 62 didn't jump to line 63 because the condition on line 62 was never true
63 return None
64 return resolve_study_reference(
65 owner_state_id=study.flowsheet_state_id,
66 target_flowsheet_id=study.baseline_flowsheet_id,
67 target_lineage_id=study.baseline_study_lineage_id,
68 )
71def set_baseline_study_reference(
72 study: EconomicsStudy,
73 baseline_study: EconomicsStudy | None,
74) -> None:
75 """Store or clear the logical identity of a selected baseline study."""
77 if baseline_study is None:
78 study.baseline_flowsheet_id = None
79 study.baseline_study_lineage_id = None
80 return
81 study.baseline_flowsheet_id = stable_flowsheet_id(baseline_study)
82 study.baseline_study_lineage_id = baseline_study.lineage_id
85def baseline_study_is_readable_by_user(user: User, baseline_study: EconomicsStudy) -> bool:
86 """Return whether the user can read a selected baseline study."""
87 flowsheet_id = stable_flowsheet_id(baseline_study)
88 return flowsheet_id in _readable_flowsheet_ids(
89 user=user,
90 flowsheet_ids={flowsheet_id},
91 )
94def require_readable_baseline_studies_for_user(
95 instance_or_instances: EconomicsStudy | Iterable[EconomicsStudy],
96 *,
97 user: User,
98) -> None:
99 """Raise 403 when a selected study baseline points at an unreadable flowsheet."""
100 if isinstance(instance_or_instances, EconomicsStudy):
101 _require_readable_baseline_studies([instance_or_instances], user=user)
102 return
103 instances = list(instance_or_instances)
104 _require_readable_baseline_studies(instances, user=user)
107def _require_readable_baseline_studies(studies: list[EconomicsStudy], *, user: User) -> None:
108 baseline_flowsheet_ids = {
109 study.baseline_flowsheet_id
110 for study in studies
111 if study.baseline_flowsheet_id is not None
112 }
113 readable_flowsheet_ids = _readable_flowsheet_ids(
114 user=user,
115 flowsheet_ids=baseline_flowsheet_ids,
116 )
117 for study in studies:
118 if study.baseline_flowsheet_id is None:
119 continue
120 baseline_study = resolve_baseline_study(study)
121 baseline_is_invalid = not baseline_study_matches_target_study(study, baseline_study)
122 baseline_is_unreadable = study.baseline_flowsheet_id not in readable_flowsheet_ids
123 if baseline_is_invalid or baseline_is_unreadable:
124 raise PermissionDenied("Selected baseline study is not accessible.")
127def _readable_flowsheet_ids(*, user: User, flowsheet_ids: set[int]) -> set[int]:
128 if not flowsheet_ids:
129 return set()
130 return {
131 flowsheet_id
132 for flowsheet_id in flowsheet_ids
133 if has_flowsheet_read_access(user, flowsheet_id)
134 }
137def baseline_study_matches_target_study(
138 target_study: EconomicsStudy,
139 baseline_study: EconomicsStudy | None,
140) -> bool:
141 """Return whether a baseline points at a non-template sibling flowsheet."""
143 return (
144 baseline_study is not None
145 and target_study.flowsheet_state_id is not None
146 and study_is_in_current_project(
147 baseline_study,
148 stable_flowsheet_id(target_study),
149 )
150 )