Coverage for backend/django/core/auxiliary/property_state.py: 86%
49 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-06-23 21:51 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-06-23 21:51 +0000
1from __future__ import annotations
3from typing import TYPE_CHECKING, Any, Literal
5from rest_framework import serializers
7if TYPE_CHECKING:
8 from core.auxiliary.models.PropertyInfo import PropertyInfo
9 from core.auxiliary.models.PropertyValue import PropertyValue
12PropertyUpdateAction = Literal["property", "value", "formula", "delete", "auto_replace"]
15def reject_property_update(
16 property_info: "PropertyInfo | None",
17 payload: dict[str, Any],
18 *,
19 action: PropertyUpdateAction | None = None,
20) -> None:
21 """Reject direct writes that conflict with generic PropertyInfo ownership state."""
23 if property_info is None: 23 ↛ 24line 23 didn't jump to line 24 because the condition on line 23 was never true
24 return
26 actions = {action} if action is not None else _actions_for_payload(payload)
27 if not actions:
28 return
30 if "delete" in actions and not property_info.can_delete:
31 raise serializers.ValidationError("This property is generated and cannot be deleted directly.")
32 if "auto_replace" in actions and not property_info.can_edit:
33 raise serializers.ValidationError("This property is generated and cannot be edited directly.")
34 if "property" in actions and not property_info.can_edit:
35 raise serializers.ValidationError("This property is generated and cannot be edited directly.")
36 if "formula" in actions and not property_info.can_edit_formula:
37 raise serializers.ValidationError("This property formula is generated and cannot be edited directly.")
38 if "value" in actions and not property_info.can_edit:
39 raise serializers.ValidationError("This property is generated and cannot be edited directly.")
42def validate_objective_property(property_info: "PropertyInfo | None") -> None:
43 """Reject incomplete formula properties as optimisation objectives."""
45 if property_info is None or not property_info.formula_incomplete:
46 return
47 reason = property_info.formula_incomplete_reason.strip()
48 message = "This property is incomplete and cannot currently be used as an optimisation objective."
49 if reason: 49 ↛ 51line 49 didn't jump to line 51 because the condition on line 49 was always true
50 message = f"{message} {reason}"
51 raise serializers.ValidationError(message)
54def validate_optimization_dof_property_value(property_value: "PropertyValue | None") -> None:
55 """Reject formula-backed values as optimisation degrees of freedom.
57 A degree of freedom unfixed by the optimiser must be an existing solver
58 variable. Formula-backed values are expressions over other variables, so
59 they can be objectives but not variables that IDAES should unfix.
60 """
62 if property_value is None or property_value.formula in (None, ""):
63 return
64 raise serializers.ValidationError(
65 "Formula properties can be objectives, but not optimisation degrees of freedom."
66 )
69def is_incomplete_formula_property(property_info: "PropertyInfo | None") -> bool:
70 return bool(property_info is not None and property_info.formula_incomplete)
73def property_incomplete_reason(property_info: "PropertyInfo | None") -> str:
74 if property_info is None:
75 return ""
76 return property_info.formula_incomplete_reason.strip()
79def _actions_for_payload(payload: dict[str, Any]) -> set[PropertyUpdateAction]:
80 actions: set[PropertyUpdateAction] = set()
81 if not payload: 81 ↛ 82line 81 didn't jump to line 82 because the condition on line 81 was never true
82 return actions
83 if {"displayName", "unit", "unitType", "type", "key", "index"} & payload.keys():
84 actions.add("property")
85 if "formula" in payload:
86 actions.add("formula")
87 if {"value", "displayValue"} & payload.keys():
88 actions.add("value")
89 return actions