Coverage for backend/django/flowsheetInternals/unitops/services/edit_operations/mutation.py: 88%
44 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 contextlib import contextmanager
2from dataclasses import dataclass
3from typing import Iterator
5from django.db import transaction
7from core.auxiliary.models.Flowsheet import Flowsheet
8from core.auxiliary.services.flowsheet_template_transitions import (
9 lock_flowsheet_content_mutation,
10)
11from flowsheetInternals.unitops.models.FlowsheetEditOperation import (
12 FlowsheetEditOperation,
13)
15from .context import active_recorder, get_active_recorder
16from .recorder import FlowsheetChangeRecorder
17from .service import operation_response, record_transition
20@dataclass
21class FlowsheetMutation:
22 """Result and locked context for one history-aware domain mutation."""
24 flowsheet: Flowsheet
25 operation: FlowsheetEditOperation | None = None
27 def add_to_response(self, response, *, header_only: bool = False):
28 """Attach operation metadata without changing an endpoint's data contract."""
29 if self.operation is None:
30 return response
31 if (
32 header_only
33 or response.status_code == 204
34 or not isinstance(response.data, dict)
35 ):
36 response["X-Flowsheet-Edit-Operation"] = self.operation.pk
37 else:
38 response.data.update(operation_response(self.operation))
39 return response
41 def add_to_data(self, data: dict) -> dict:
42 """Return response data enriched with operation metadata when present."""
43 if self.operation is None:
44 return data
45 return {**data, **operation_response(self.operation)}
48@contextmanager
49def flowsheet_edit(
50 *,
51 flowsheet: Flowsheet,
52 user,
53 kind: str,
54 label_key: str,
55) -> Iterator[FlowsheetMutation]:
56 """Record all scoped database writes made by one flowsheet user action.
58 Nested domain services join the active recorder. Only the outer boundary
59 allocates a revision and persists an operation.
60 """
61 existing_recorder = get_active_recorder()
62 if existing_recorder is not None: 62 ↛ 63line 62 didn't jump to line 63 because the condition on line 62 was never true
63 if (
64 existing_recorder.flowsheet_id != flowsheet.pk
65 or existing_recorder.flowsheet_state_id != flowsheet.current_state_id
66 ):
67 raise ValueError("A flowsheet edit cannot span multiple flowsheets.")
68 yield FlowsheetMutation(flowsheet=flowsheet)
69 return
71 with transaction.atomic():
72 locked_flowsheet = lock_flowsheet_content_mutation(candidate=flowsheet)
73 mutation = FlowsheetMutation(flowsheet=locked_flowsheet)
74 recorder = FlowsheetChangeRecorder(
75 locked_flowsheet.pk,
76 locked_flowsheet.current_state_id,
77 kind=kind,
78 )
79 token = active_recorder.set(recorder)
80 try:
81 yield mutation
82 transition = recorder.build_transition()
83 if transition is not None:
84 mutation.operation = record_transition(
85 flowsheet=locked_flowsheet,
86 user=user,
87 kind=kind,
88 label_key=label_key,
89 transition=transition,
90 )
91 finally:
92 active_recorder.reset(token)