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

1from __future__ import annotations 

2 

3from typing import TYPE_CHECKING, Any, Literal 

4 

5from rest_framework import serializers 

6 

7if TYPE_CHECKING: 

8 from core.auxiliary.models.PropertyInfo import PropertyInfo 

9 from core.auxiliary.models.PropertyValue import PropertyValue 

10 

11 

12PropertyUpdateAction = Literal["property", "value", "formula", "delete", "auto_replace"] 

13 

14 

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.""" 

22 

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 

25 

26 actions = {action} if action is not None else _actions_for_payload(payload) 

27 if not actions: 

28 return 

29 

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.") 

40 

41 

42def validate_objective_property(property_info: "PropertyInfo | None") -> None: 

43 """Reject incomplete formula properties as optimisation objectives.""" 

44 

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) 

52 

53 

54def validate_optimization_dof_property_value(property_value: "PropertyValue | None") -> None: 

55 """Reject formula-backed values as optimisation degrees of freedom. 

56 

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 """ 

61 

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 ) 

67 

68 

69def is_incomplete_formula_property(property_info: "PropertyInfo | None") -> bool: 

70 return bool(property_info is not None and property_info.formula_incomplete) 

71 

72 

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() 

77 

78 

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