Coverage for backend/django/flowsheetInternals/formula_templates/add_template.py: 86%
56 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 typing import Tuple, Dict
2from flowsheetInternals.unitops.models import SimulationObject
3from .template_schema import TemplateSchema
4from .formula_templates import templates
5from core.auxiliary.formula_limits import validate_formula_length
6from core.auxiliary.models.PropertyInfo import PropertyInfo
7from core.auxiliary.models.PropertyValue import PropertyValue
9ID_MAPPING = dict[
10 str, Tuple[PropertyInfo, PropertyValue]
11] # Mapping from property key to the info/value tuple persisted in the DB
14def add_predefined_template(operation: SimulationObject, key: str):
15 """Attach a property template and initialise its custom formulas.
17 The template describes one or more synthetic properties (for example cost
18 expressions) that should be created on ``object``. Required properties are
19 validated up-front; once the new ``PropertyInfo`` and ``PropertyValue``
20 records are persisted, each configured formula is rewritten to point at the
21 database identifiers of the participating properties.
23 :param object: Simulation object that receives the template-driven properties.
24 :param key: Template key, as defined in ``formula_templates``.
25 :raises ValueError: If the template does not exist or required properties
26 are missing on the simulation object.
27 """
28 template: TemplateSchema = templates.get(key)
29 if not template: 29 ↛ 30line 29 didn't jump to line 30 because the condition on line 29 was never true
30 raise ValueError(f"Template with key '{key}' does not exist.")
31 add_template(operation, template)
33def add_template(operation: SimulationObject, template: TemplateSchema):
34 """Attach a property template and initialise its custom formulas.
35 """
37 property_keys = [prop.key for prop in operation.properties.containedProperties.all()]
38 for required_property in template.required_properties:
39 if required_property not in property_keys: 39 ↛ 40line 39 didn't jump to line 40 because the condition on line 39 was never true
40 raise ValueError(
41 f"Template requires property '{required_property}' which is not present in the object."
42 )
44 property_infos = []
45 property_values = []
47 for field in template.fields:
48 property_info = PropertyInfo(
49 flowsheet=operation.flowsheet,
50 displayName=field.name,
51 key=field.key,
52 set=operation.properties,
53 )
54 property_value = PropertyValue(
55 property=property_info,
56 flowsheet=operation.flowsheet,
57 value=None, # Default value can be set later
58 )
59 property_infos.append(property_info)
60 property_values.append(property_value)
62 PropertyInfo.objects.bulk_create(property_infos)
63 PropertyValue.objects.bulk_create(property_values)
65 # Now all properties are created, we can set the formula for each property value.
66 # This must be done later, because we need the IDs of the new objects in the database.
67 # We now create a mapping so that we can set the formula information correctly.
68 id_mapping: ID_MAPPING = {
69 field.key: (property_info, property_value)
70 for field, property_info, property_value in zip(
71 template.fields, property_infos, property_values
72 )
73 }
74 # Also add all the properties that already exist on the object
75 for property_info in operation.properties.containedProperties.all():
76 if property_info.key not in id_mapping:
77 property_value = (
78 property_info.values.first()
79 ) # TODO: Support indexed properties
80 if property_value: 80 ↛ 75line 80 didn't jump to line 75 because the condition on line 80 was always true
81 id_mapping[property_info.key] = (property_info, property_value)
82 # add all the properties in the inlet and outlet ports with a port_name.property_name convention
83 for port in operation.ports.all():
84 stream = port.stream
85 if stream is None: 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true
86 continue
87 if not operation.schema.ports: 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true
88 continue
89 port_config = operation.schema.ports[port.key]
90 if not port_config: 90 ↛ 91line 90 didn't jump to line 91 because the condition on line 90 was never true
91 continue
92 if port_config.many:
93 port_name = f"{port.key}_{port.index + 1}"
94 else:
95 port_name = port.key
96 for property_info in stream.properties.containedProperties.all():
97 property_value = (
98 property_info.values.first()
99 ) # TODO: Support indexed properties
100 if property_value: 100 ↛ 96line 100 didn't jump to line 96 because the condition on line 100 was always true
101 id_mapping[f"{port_name}.{property_info.key}"] = (property_info, property_value)
105 for field, property_value, property_info in zip(
106 template.fields, property_values, property_infos
107 ):
108 property_value.formula = validate_formula_length(
109 replace_with_ids(field.formula, id_mapping)
110 )
112 PropertyValue.objects.bulk_update(property_values, ["formula"])
115def replace_with_ids(formula: str, id_mapping: ID_MAPPING) -> str:
116 """Swap template placeholders with the persisted property identifiers.
118 ``formula`` uses the template convention ``[key]``. This helper updates the
119 formula with the ``@[property display name](prop<ID>)`` syntax expected by the front-end using
120 the mapping collected during template application.
122 :param formula: Raw template formula string.
123 :param id_mapping: Mapping from property keys to the ``PropertyInfo`` and
124 ``PropertyValue`` instances created or found on the simulation object.
125 :return: Updated formula string with property references rewritten.
126 """
127 for key, (property_info, property_value) in id_mapping.items():
128 formula = formula.replace(
129 f"[{key}]", f"@[{property_info.displayName}](prop{property_value.id})"
130 )
131 return validate_formula_length(formula)