Coverage for backend/django/diagnostics/services.py: 14%
36 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-12 01:47 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-02-12 01:47 +0000
1"""
2Diagnostics service functions.
4This module contains the business logic for diagnostics validation, extracted
5from views.py to keep views thin and make the logic reusable/testable.
6"""
8from __future__ import annotations
10from typing import TYPE_CHECKING, TypeAlias
12from diagnostics.rules.engine import build_rule_context, evaluate_rules
14if TYPE_CHECKING:
15 from core.auxiliary.models.PropertyInfo import PropertyInfo
16 from core.auxiliary.models.PropertyValue import PropertyValue
17 from core.auxiliary.models.PropertySet import PropertySet
18 from flowsheetInternals.unitops.models.SimulationObject import SimulationObject
21# Type aliases for clarity
22FindingDict: TypeAlias = dict[str, str | int | float | None]
23ErrorDict: TypeAlias = dict[str, str]
24ValidationResult: TypeAlias = tuple[list[FindingDict] | None, ErrorDict | None]
27def validate_property_change(
28 obj: "SimulationObject",
29 property_id: int | None,
30 property_key: str | None,
31 property_value_id: int | None,
32 new_value: float | str | None,
33) -> ValidationResult:
34 """
35 Validate a proposed property change against diagnostic rules.
37 This is a pure "what-if" validation that runs rules against a proposed value
38 without making any database changes or in-memory mutations. Perfect for:
39 - Frontend inline validation (user typing in a field)
40 - LLM-suggested values (test suggestions before applying them)
41 - Pre-solve validation (check if a change would violate constraints)
43 Args:
44 obj: The SimulationObject with properties prefetched.
45 property_id: ID of the property to validate (optional if property_key given).
46 property_key: Key of the property to validate (optional if property_id given).
47 property_value_id: ID of the specific value (required for indexed properties).
48 new_value: The proposed new value to validate.
50 Returns:
51 ValidationResult tuple of (findings, error):
52 - On success: (list of FindingDict, None)
53 - On error: (None, ErrorDict with "error" key)
55 Example:
56 # Validate a user-entered value
57 findings, error = validate_property_change(obj, property_id=123, new_value=1.5)
58 if error:
59 return Response(error, status=400)
60 return Response({"findings": findings})
62 # Validate an LLM suggestion
63 llm_suggested_value = 0.85
64 findings, error = validate_property_change(obj, property_key="efficiency",
65 new_value=llm_suggested_value)
66 if not error and not findings:
67 # Safe to apply - no rule violations
68 apply_llm_suggestion(llm_suggested_value)
69 """
71 # Property values live under: SimulationObject -> PropertySet -> PropertyInfo -> PropertyValue.
72 # If the object has no PropertySet attached, there's nothing to validate.
73 from core.auxiliary.models.PropertySet import PropertySet
75 try:
76 prop_set: PropertySet = obj.properties
77 except PropertySet.DoesNotExist:
78 return None, {"error": "Property set not found for object"}
80 # Resolve the target property (serializer guarantees at least one of id/key is provided).
81 if property_id is not None:
82 target_prop: PropertyInfo | None = prop_set.ContainedProperties.filter(id=property_id).first()
83 else:
84 target_prop = prop_set.ContainedProperties.filter(key=property_key).first()
86 if not target_prop:
87 return None, {"error": "Property not found for object"}
89 # Resolve the target value (needed for indexed properties).
90 target_value: PropertyValue | None = None
91 values: list[PropertyValue] = list(target_prop.values.all())
93 if property_value_id is not None:
94 # Explicit value ID provided
95 target_value = next((v for v in values if v.id == property_value_id), None)
96 if not target_value:
97 return None, {"error": "Property value not found for property"}
99 elif len(values) == 1 and not values[0].indexedItems.exists():
100 # Single non-indexed value - auto-select it
101 target_value = values[0]
103 # Check for ambiguity: new_value provided but we don't know which value to update
104 if new_value is not None and target_value is None:
105 return None, {"error": "Ambiguous property value; provide property_value_id for indexed properties"}
107 # Validate the proposed value without any DB writes or in-memory mutations.
108 # build_rule_context takes numeric_value directly as a parameter.
109 if new_value is None:
110 return [], None
112 try:
113 numeric_value = float(new_value)
114 except (TypeError, ValueError):
115 return None, {"error": "new_value must be a number"}
117 # Run the diagnostic rules with the proposed value
118 context = build_rule_context(obj, target_prop, numeric_value)
119 findings: list[FindingDict] = [f.to_dict() for f in evaluate_rules(context)]
120 return findings, None