Coverage for backend/django/diagnostics/views.py: 38%
122 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 API views.
4These endpoints exist to support the diagnostics UI:
5- Retrieve persisted `DiagnosticRun`s (task-triggered, evented)
6- Run light-weight "what if?" validations used by the properties panel
8The goal is to keep views fairly thin: parse/validate the request, fetch the
9minimal DB state needed, then delegate to the diagnostics rules engine.
10"""
12from __future__ import annotations
14import logging
16from django.db.models import Prefetch
17from drf_spectacular.types import OpenApiTypes
18from drf_spectacular.utils import OpenApiParameter, extend_schema
19from rest_framework import status
20from rest_framework.decorators import api_view
21from rest_framework.response import Response
23from core.validation import api_view_validate
24from diagnostics.models import DiagnosticRun
25from diagnostics.rules.engine import build_rule_context, evaluate_rules, load_decision_model, evaluate_property_rules
26from diagnostics.services import validate_property_change
27from diagnostics.serializers import (
28 DiagnosticRunListSerializer,
29 DiagnosticRunSerializer,
30 PreSolveValidationSerializer,
31 PropertyRuleFindingsSerializer,
32 RuleValidateRequestSerializer,
33 RunEventsSerializer,
34 RunSummarySerializer,
35)
36from core.auxiliary.models.PropertyInfo import PropertyInfo
37from core.auxiliary.models.PropertySet import PropertySet
38from core.auxiliary.models.PropertyValue import PropertyValue
39from flowsheetInternals.unitops.models.SimulationObject import SimulationObject
41logger = logging.getLogger(__name__)
44def _get_simulation_object_for_rules(object_id: int, *, include_indexed_items: bool) -> SimulationObject:
45 """
46 Fetch a SimulationObject with properties eagerly loaded for rule evaluation.
48 I prefetch properties/values here to avoid N+1 queries when we walk through
49 all properties during rule evaluation. The `include_indexed_items` flag is
50 for pre-solve validation where we need to temporarily mutate indexed values.
51 """
52 qs = SimulationObject.objects.filter(id=object_id).select_related("properties")
53 values_qs = PropertyValue.objects.select_related(
54 "controlManipulated",
55 "controlSetPoint",
56 )
57 if include_indexed_items:
58 values_qs = values_qs.prefetch_related("indexedItems")
60 return qs.prefetch_related(
61 Prefetch(
62 "properties__ContainedProperties",
63 queryset=PropertyInfo.objects.select_related("recycleConnection").prefetch_related(
64 Prefetch("values", queryset=values_qs)
65 ),
66 )
67 ).get()
74# ============================================================================
75# Diagnostic Run Endpoints
76# ============================================================================
77# These endpoints serve the Diagnostics tab UI. They're intentionally thin:
78# I fetch the persisted DiagnosticRun rows and return them as-is. The heavy
79# lifting (rulesets, findings, events) happens in the orchestrator.
82@extend_schema(
83 operation_id="diagnostics_runs_list",
84 parameters=[
85 OpenApiParameter(
86 name="flowsheet",
87 type=OpenApiTypes.INT,
88 location=OpenApiParameter.QUERY,
89 required=True,
90 ),
91 ],
92 responses={200: DiagnosticRunListSerializer(many=True)},
93)
94@api_view_validate
95@api_view(["GET"])
96def list_runs(request) -> Response:
97 """List diagnostic runs for the current flowsheet."""
98 try:
99 flowsheet_id = int(request.query_params.get("flowsheet"))
100 except ValueError:
101 return Response({"error": "flowsheet id must be an integer"}, status=status.HTTP_400_BAD_REQUEST)
103 runs = DiagnosticRun.objects.filter(flowsheet_id=flowsheet_id).order_by("-created_at")[:50]
104 serializer = DiagnosticRunListSerializer(runs, many=True)
105 return Response(serializer.data)
108@extend_schema(
109 operation_id="diagnostics_runs_retrieve",
110 parameters=[
111 OpenApiParameter(name="run_id", type=OpenApiTypes.INT, location=OpenApiParameter.PATH)
112 ],
113 responses={200: DiagnosticRunSerializer},
114)
115@api_view_validate
116@api_view(["GET"])
117def retrieve_run(request, run_id: int) -> Response:
118 """Retrieve a single diagnostic run."""
121 try:
122 run = DiagnosticRun.objects.get(id=run_id)
123 except DiagnosticRun.DoesNotExist:
124 return Response(status=status.HTTP_404_NOT_FOUND)
126 serializer = DiagnosticRunSerializer(run)
127 return Response(serializer.data)
130# TODO: Frontend should derive latest run from list_runs response (runs[0]) instead of
131# making a separate API call. The get_latest_run endpoint was removed as redundant.
133@extend_schema(
134 operation_id="diagnostics_run_summary",
135 parameters=[
136 OpenApiParameter(name="run_id", type=OpenApiTypes.INT, location=OpenApiParameter.PATH)
137 ],
138 responses={200: RunSummarySerializer},
139)
140@api_view_validate
141@api_view(["GET"])
142def get_run_summary(request, run_id: int) -> Response:
143 """Get summary for a diagnostic run."""
144 try:
145 run = DiagnosticRun.objects.get(id=run_id)
146 except DiagnosticRun.DoesNotExist:
147 return Response(status=status.HTTP_404_NOT_FOUND)
148 # TODO: Frontend should extract findings from summary.findings instead of
149 # calling a separate endpoint. The summary already contains all findings.
150 return Response(
151 {
152 "run_id": run.id,
153 "status": run.diagnostic_status,
154 "summary": run.summary,
155 }
156 )
159@extend_schema(
160 operation_id="diagnostics_run_events",
161 parameters=[
162 OpenApiParameter(name="run_id", type=OpenApiTypes.INT, location=OpenApiParameter.PATH)
163 ],
164 responses={200: RunEventsSerializer},
165)
166@api_view_validate
167@api_view(["GET"])
168def get_run_events(request, run_id: int) -> Response:
169 """Get diagnostic events for a run."""
170 try:
171 run = DiagnosticRun.objects.get(id=run_id)
172 except DiagnosticRun.DoesNotExist:
173 return Response(status=status.HTTP_404_NOT_FOUND)
175 events = list(run.events.order_by("id").values("id", "ts", "type", "data"))
176 return Response({"run_id": run.id, "events": events})
182# ============================================================================
183# Property Rule Endpoints
184# ============================================================================
185# These are the validation endpoints used by the Properties panel.
189@extend_schema(
190 operation_id="diagnostics_evaluate_object_property_rules",
191 parameters=[
192 OpenApiParameter(name="object_id", type=OpenApiTypes.INT, location=OpenApiParameter.PATH),
193 OpenApiParameter(
194 name="flowsheet",
195 type=OpenApiTypes.INT,
196 location=OpenApiParameter.QUERY,
197 required=True,
198 ),
199 ],
200 responses={200: PropertyRuleFindingsSerializer},
201)
202@api_view_validate
203@api_view(["GET"])
204def evaluate_object_property_rules(request, object_id: int) -> Response:
205 """
206 Evaluate deterministic diagnostics rules against a unit op's current properties.
207 Intended for inline properties-panel warnings/suggestions/errors.
208 """
209 try:
210 obj = _get_simulation_object_for_rules(object_id, include_indexed_items=False)
211 except SimulationObject.DoesNotExist:
212 return Response(status=status.HTTP_404_NOT_FOUND)
214 findings = evaluate_property_rules(obj)
215 return Response({"object_id": obj.id, "findings": [f.to_dict() for f in findings]})
218@extend_schema(
219 operation_id="diagnostics_rules_get",
220 responses={200: OpenApiTypes.OBJECT},
221)
222@api_view_validate
223@api_view(["GET"])
224def get_rules_jdm(request) -> Response:
225 """Return the JSON Decision Model (JDM) for frontend rule evaluation."""
226 return Response({"jdm": load_decision_model()})
229@extend_schema(
230 operation_id="validate_value_against_rule",
231 parameters=[
232 OpenApiParameter(
233 name="flowsheet",
234 type=OpenApiTypes.INT,
235 location=OpenApiParameter.QUERY,
236 required=True,
237 ),
238 ],
239 request=RuleValidateRequestSerializer,
240 responses={200: PropertyRuleFindingsSerializer},
241)
242@api_view_validate
243@api_view(["POST"])
244def validate_value_against_rule(request) -> Response:
245 """
246 Validate a single property value against deterministic rules.
247 Intended for frontend inline validation without persisting the value.
249 This endpoint is simpler than pre_solve_validation: it only needs
250 object_id, property_key, and value. Use this for basic inline validation
251 where you don't need to handle indexed properties or property_value_id.
252 """
253 # This is a compute-only endpoint:
254 # - No DB writes
255 # - Used for inline validation while the user is typing
256 serializer = RuleValidateRequestSerializer(data=request.data)
257 if not serializer.is_valid():
258 return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
260 data = serializer.validated_data
261 object_id = data["object_id"]
262 property_key = data["property_key"]
263 value = data["value"]
265 try:
266 obj = _get_simulation_object_for_rules(object_id, include_indexed_items=False)
267 except SimulationObject.DoesNotExist:
268 return Response(status=status.HTTP_404_NOT_FOUND)
270 try:
271 prop_set = obj.properties
272 except PropertySet.DoesNotExist:
273 return Response({"object_id": obj.id, "findings": []})
275 prop = prop_set.ContainedProperties.filter(key=property_key).first()
276 if not prop:
277 # If the property isn't found, treat it as "no rules apply" rather than erroring.
278 return Response({"object_id": obj.id, "findings": []})
282 context = build_rule_context(obj, prop, value)
283 findings = [f.to_dict() for f in evaluate_rules(context)] #convert to dict for json serialization
284 return Response({"object_id": obj.id, "findings": findings})
287# ============================================================================
288# Pre-Solve Validation
289# ============================================================================
292@extend_schema(
293 operation_id="diagnostics_pre_solve_validation",
294 parameters=[
295 OpenApiParameter(name="flowsheet", type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, required=True),
296 ],
297 request=PreSolveValidationSerializer,
298 responses={200: PropertyRuleFindingsSerializer},
299)
300@api_view_validate
301@api_view(["POST"])
302def pre_solve_validation(request) -> Response:
303 """
304 Pre-solve validation for a *proposed* property change (compute-only).
306 This endpoint extends validate_value_against_rule with support for indexed
307 properties. It exists separately because:
308 1. It accepts property_id OR property_key (more flexible property resolution)
309 2. It handles property_value_id for indexed properties (e.g., mole_frac_comp[CO2])
310 3. It uses the service layer for "what-if" validation with full property resolution
312 Use this endpoint when validating changes to indexed properties or when you need
313 the full property resolution logic. Use validate_value_against_rule for simpler cases.
315 This endpoint is intentionally NOT persisted:
316 - It exists to support the UI while the user is editing a value.
317 - It should be safe to call frequently (e.g. on blur / debounce).
318 - It should not create DiagnosticRun rows or write events.
319 """
320 serializer = PreSolveValidationSerializer(data=request.data)
321 if not serializer.is_valid():
322 return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
324 data = serializer.validated_data
326 try:
327 obj = _get_simulation_object_for_rules(data["object_id"], include_indexed_items=True)
328 except SimulationObject.DoesNotExist:
329 return Response(status=status.HTTP_404_NOT_FOUND)
331 # Delegate to service function (see services.py for the logic)
332 findings, error = validate_property_change(
333 obj=obj,
334 property_id=data.get("property_id"),
335 property_key=data.get("property_key"),
336 property_value_id=data.get("property_value_id"),
337 new_value=data.get("new_value"),
338 )
340 if error:
341 # Return appropriate status based on error type
342 if "not found" in error.get("error", "").lower():
343 return Response(error, status=status.HTTP_404_NOT_FOUND)
344 return Response(error, status=status.HTTP_400_BAD_REQUEST)
346 return Response({"object_id": obj.id, "findings": findings})