Skip to content

Commit d7c6899

Browse files
fix: repeat entity label binding path too low
- the bind for the entity attributes was one path too low to find the correct target, because the `insert_xpaths` context was the entity rather than the attribute. In order to use existing insert_xpaths functionality that navigates the form structure using .parent/.children, the entity attributes must be SurveyElements so a new class is created for it. - overall it's moving from procedural generation in entity_declaration to a declarative generation in entities_parsing.py, and attributes which are the target of bind/actions/etc are responsible for the generation of those nodes. - also simplify xls2json.py entities sheet processing since EntityColumns contains all the valid column names, and the slot names add a lot of column names which should not be user-specified.
1 parent 026bfe0 commit d7c6899

12 files changed

Lines changed: 213 additions & 159 deletions

File tree

pyxform/entities/entities_parsing.py

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Any
33

44
from pyxform import constants as const
5+
from pyxform.elements import action
56
from pyxform.errors import Detail, PyXFormError
67
from pyxform.parsing.expression import is_xml_tag
78
from pyxform.validators.pyxform.pyxform_reference import parse_pyxform_references
@@ -90,25 +91,112 @@ def get_entity_declaration(
9091
entity = {
9192
const.NAME: const.ENTITY,
9293
const.TYPE: const.ENTITY,
93-
const.PARAMETERS: {
94-
EC.DATASET.value: dataset_name,
95-
EC.ENTITY_ID.value: entity_id,
96-
EC.CREATE_IF.value: create_condition,
97-
EC.UPDATE_IF.value: update_condition,
98-
EC.REPEAT.value: entity_repeat,
94+
EC.REPEAT.value: entity_repeat,
95+
const.CHILDREN: [
96+
{
97+
const.NAME: "dataset",
98+
const.TYPE: "attribute",
99+
"value": dataset_name,
100+
},
101+
],
102+
}
103+
104+
if create_condition or (not update_condition and not entity_id):
105+
create = {const.NAME: "create", const.TYPE: "attribute", "value": "1"}
106+
if create_condition:
107+
create[const.BIND] = {
108+
"calculate": create_condition,
109+
"readonly": "true()",
110+
"type": "string",
111+
}
112+
entity[const.CHILDREN].append(create)
113+
114+
if update_condition or entity_id:
115+
update_attr = {
116+
const.NAME: "update",
117+
const.TYPE: "attribute",
118+
"value": "1",
119+
}
120+
121+
if update_condition:
122+
update_attr[const.BIND] = {
123+
"calculate": update_condition,
124+
"readonly": "true()",
125+
"type": "string",
126+
}
127+
128+
entity[const.CHILDREN].append(update_attr)
129+
130+
id_attr = {
131+
const.NAME: "id",
132+
const.TYPE: "attribute",
133+
const.BIND: {
134+
"readonly": "true()",
135+
"type": "string",
99136
},
137+
"actions": [],
100138
}
139+
140+
if create_condition or not entity_id:
141+
first_load = action.ActionLibrary.setvalue_first_load.value.to_dict()
142+
first_load["value"] = "uuid()"
143+
id_attr["actions"].append(first_load)
144+
145+
if entity_repeat:
146+
new_repeat = action.ActionLibrary.setvalue_new_repeat.value.to_dict()
147+
new_repeat["value"] = "uuid()"
148+
id_attr["actions"].append(new_repeat)
149+
150+
if entity_id:
151+
id_attr[const.BIND]["calculate"] = entity_id
152+
153+
entity_id_expression = f"instance('{dataset_name}')/root/item[name={entity_id}]"
154+
entity[const.CHILDREN].extend(
155+
[
156+
{
157+
const.NAME: "baseVersion",
158+
const.TYPE: "attribute",
159+
const.BIND: {
160+
"calculate": f"{entity_id_expression}/__version",
161+
"readonly": "true()",
162+
"type": "string",
163+
},
164+
},
165+
{
166+
const.NAME: "trunkVersion",
167+
const.TYPE: "attribute",
168+
const.BIND: {
169+
"calculate": f"{entity_id_expression}/__trunkVersion",
170+
"readonly": "true()",
171+
"type": "string",
172+
},
173+
},
174+
{
175+
const.NAME: "branchId",
176+
const.TYPE: "attribute",
177+
const.BIND: {
178+
"calculate": f"{entity_id_expression}/__branchId",
179+
"readonly": "true()",
180+
"type": "string",
181+
},
182+
},
183+
]
184+
)
185+
186+
entity[const.CHILDREN].append(id_attr)
187+
101188
if entity_label:
102-
entity[const.CHILDREN] = [
189+
entity[const.CHILDREN].append(
103190
{
104191
const.TYPE: "label",
192+
const.NAME: "label",
105193
const.BIND: {
106194
"calculate": entity_label,
107195
"readonly": "true()",
108196
"type": "string",
109197
},
110-
}
111-
]
198+
},
199+
)
112200

113201
return entity
114202

@@ -173,7 +261,7 @@ def validate_entity_saveto(
173261
f"{const.ROW_FORMAT_STRING % row_number} Groups and repeats can't be saved as entity properties."
174262
)
175263

176-
entity_repeat = entity_declaration[const.PARAMETERS].get(EC.REPEAT, None)
264+
entity_repeat = entity_declaration.get(EC.REPEAT, None)
177265
if entity_repeat and stack:
178266
# Error: saveto in nested repeat inside entity repeat.
179267
in_repeat = False
@@ -244,7 +332,7 @@ def validate_entity_repeat_target(
244332
if not entity_declaration:
245333
return False
246334

247-
entity_repeat = entity_declaration[const.PARAMETERS].get(EC.REPEAT, None)
335+
entity_repeat = entity_declaration.get(EC.REPEAT, None)
248336

249337
# Ignore: no repeat declared for the entity.
250338
if not entity_repeat:
Lines changed: 10 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
1-
from typing import TYPE_CHECKING
2-
31
from pyxform import constants as const
4-
from pyxform.elements import action
5-
from pyxform.entities.label import Label
62
from pyxform.section import Section
73
from pyxform.survey_element import SURVEY_ELEMENT_FIELDS
8-
from pyxform.utils import combine_lists, node
9-
10-
if TYPE_CHECKING:
11-
from pyxform.survey import Survey
12-
4+
from pyxform.survey_elements.attribute import Attribute
5+
from pyxform.survey_elements.label import Label
6+
from pyxform.utils import combine_lists
137

148
EC = const.EntityColumns
15-
ENTITY_EXTRA_FIELDS = (const.PARAMETERS,)
9+
ENTITY_EXTRA_FIELDS = (const.CHILDREN,)
1610
ENTITY_FIELDS = (*SURVEY_ELEMENT_FIELDS, *ENTITY_EXTRA_FIELDS)
11+
CHILD_TYPES = {"label": Label, "attribute": Attribute}
1712

1813

1914
class EntityDeclaration(Section):
@@ -40,109 +35,13 @@ class EntityDeclaration(Section):
4035
def get_slot_names() -> tuple[str, ...]:
4136
return ENTITY_FIELDS
4237

43-
def __init__(self, name: str, type: str, parameters: dict, **kwargs):
38+
def __init__(self, name: str, type: str, **kwargs):
4439
super().__init__(name=name, type=type, **kwargs)
45-
self.parameters: dict = parameters
46-
self.children: list[Label] | None = None
40+
self.children: list[Label | Attribute] = []
4741

48-
choices = combine_lists(
42+
children = combine_lists(
4943
a=kwargs.pop("children", None), b=kwargs.pop(const.CHILDREN, None)
5044
)
51-
if choices:
52-
self.children = [Label(name="label", **c) for c in choices]
45+
if children:
46+
self.children = [CHILD_TYPES[c["type"]](**c) for c in children]
5347
self._link_children()
54-
else:
55-
self.children = []
56-
57-
def xml_instance(self, survey: "Survey", **kwargs):
58-
parameters = self.parameters
59-
60-
attributes = {
61-
EC.DATASET.value: parameters.get(EC.DATASET, ""),
62-
"id": "",
63-
}
64-
65-
entity_id_expression = parameters.get(EC.ENTITY_ID, None)
66-
create_condition = parameters.get(EC.CREATE_IF, None)
67-
update_condition = parameters.get(EC.UPDATE_IF, None)
68-
69-
if entity_id_expression:
70-
attributes["update"] = "1"
71-
attributes["baseVersion"] = ""
72-
attributes["trunkVersion"] = ""
73-
attributes["branchId"] = ""
74-
75-
if create_condition or (not update_condition and not entity_id_expression):
76-
attributes["create"] = "1"
77-
78-
return super().xml_instance(survey=survey, **attributes)
79-
80-
def xml_bindings(self, survey: "Survey"):
81-
"""
82-
See the class comment for an explanation of the logic for generating bindings.
83-
"""
84-
parameters = self.parameters
85-
entity_id_expression = parameters.get(EC.ENTITY_ID, None)
86-
create_condition = parameters.get(EC.CREATE_IF, None)
87-
update_condition = parameters.get(EC.UPDATE_IF, None)
88-
89-
if create_condition:
90-
yield self._get_bind_node(survey, create_condition, "/@create")
91-
92-
yield self._get_id_bind_node(survey, entity_id_expression)
93-
94-
if update_condition:
95-
yield self._get_bind_node(survey, update_condition, "/@update")
96-
97-
if entity_id_expression:
98-
dataset_name = parameters.get(EC.DATASET, "")
99-
entity = f"instance('{dataset_name}')/root/item[name={entity_id_expression}]"
100-
yield self._get_bind_node(survey, f"{entity}/__version", "/@baseVersion")
101-
yield self._get_bind_node(
102-
survey, f"{entity}/__trunkVersion", "/@trunkVersion"
103-
)
104-
yield self._get_bind_node(survey, f"{entity}/__branchId", "/@branchId")
105-
106-
for e in self.iter_descendants():
107-
if e is not self:
108-
yield from e.xml_bindings(survey=survey)
109-
110-
def _get_id_bind_node(self, survey, entity_id_expression):
111-
extra_attrs = {}
112-
113-
if entity_id_expression:
114-
extra_attrs["calculate"] = survey.insert_xpaths(
115-
text=entity_id_expression, context=self
116-
)
117-
118-
return node(
119-
const.BIND,
120-
nodeset=f"{self.get_xpath()}/@id",
121-
type="string",
122-
readonly="true()",
123-
**extra_attrs,
124-
)
125-
126-
def xml_actions(self, survey: "Survey", in_repeat: bool = False):
127-
if self.parameters.get(EC.CREATE_IF, None) or not self.parameters.get(
128-
EC.ENTITY_ID, None
129-
):
130-
if in_repeat:
131-
setvalue_action = action.ActionLibrary.setvalue_new_repeat.value
132-
else:
133-
setvalue_action = action.ActionLibrary.setvalue_first_load.value
134-
135-
yield action.ACTION_CLASSES[setvalue_action.name](
136-
ref=f"{self.get_xpath()}/@id",
137-
event=action.Event(setvalue_action.event),
138-
value="uuid()",
139-
).node()
140-
141-
def _get_bind_node(self, survey, expression, destination):
142-
return node(
143-
const.BIND,
144-
nodeset=f"{self.get_xpath()}{destination}",
145-
calculate=survey.insert_xpaths(text=expression, context=self),
146-
type="string",
147-
readonly="true()",
148-
)

pyxform/question.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,12 @@ def xml_actions(self, survey: "Survey", in_repeat: bool = False):
198198
action_fields = {"name", "event", "value"}
199199
if self.actions:
200200
for _action in self.actions:
201-
cls = action.ACTION_CLASSES[_action["name"]]
201+
event = action.Event(_action["event"])
202+
if (not in_repeat and event == action.Event.ODK_NEW_REPEAT.value) or (
203+
in_repeat and event != action.Event.ODK_NEW_REPEAT
204+
):
205+
continue
206+
202207
if self._dynamic_default is True or (
203208
self.default and default_is_dynamic(self.default, self.type)
204209
):
@@ -208,13 +213,7 @@ def xml_actions(self, survey: "Survey", in_repeat: bool = False):
208213
else:
209214
value = _action.get("value")
210215

211-
event = action.Event(_action["event"])
212-
if (not in_repeat and event == action.Event.ODK_NEW_REPEAT.value) or (
213-
in_repeat and event != action.Event.ODK_NEW_REPEAT
214-
):
215-
continue
216-
217-
yield cls(
216+
yield action.ACTION_CLASSES[_action["name"]](
218217
ref=self.get_xpath(),
219218
event=event,
220219
value=value,

pyxform/section.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from collections.abc import Callable, Generator, Iterable
66
from itertools import chain
77
from typing import TYPE_CHECKING
8+
from xml.dom.minidom import Attr
89

910
from pyxform import constants
1011
from pyxform.external_instance import ExternalInstance
@@ -133,9 +134,13 @@ def xml_instance(self, survey: "Survey", **kwargs):
133134
if isinstance(child, RepeatingSection) and not append_template:
134135
append_template = not append_template
135136
repeating_template = child.generate_repeating_template(survey=survey)
136-
result.appendChild(
137-
child.xml_instance(survey=survey, append_template=append_template)
137+
child_instance = child.xml_instance(
138+
survey=survey, append_template=append_template
138139
)
140+
if isinstance(child_instance, Attr):
141+
result.setAttributeNode(child_instance)
142+
else:
143+
result.appendChild(child_instance)
139144
if append_template and repeating_template:
140145
append_template = not append_template
141146
result.insertBefore(repeating_template, result._get_lastChild())
@@ -232,11 +237,11 @@ def xml_control(self, survey: "Survey"):
232237

233238
# Get setvalue nodes for all descendants of this repeat that have dynamic defaults
234239
# and aren't nested in other repeats. Let nested repeats handle their own defaults
235-
from pyxform.entities.entity_declaration import EntityDeclaration
236240
from pyxform.question import Question
241+
from pyxform.survey_elements.attribute import Attribute
237242

238243
def condition(i, parent=self):
239-
return isinstance(i, EntityDeclaration | Question) and (
244+
return isinstance(i, Attribute | Question) and (
240245
i.parent is self
241246
or parent
242247
== next(

pyxform/survey.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414

1515
from pyxform import aliases, constants
1616
from pyxform.constants import EXTERNAL_INSTANCE_EXTENSIONS, NSMAP
17-
from pyxform.entities.entity_declaration import EntityDeclaration
18-
from pyxform.entities.label import Label
1917
from pyxform.errors import PyXFormError, ValidationError
2018
from pyxform.external_instance import ExternalInstance
2119
from pyxform.instance import SurveyInstance
@@ -24,6 +22,7 @@
2422
from pyxform.question import Itemset, MultipleChoiceQuestion, Option, Question, Tag
2523
from pyxform.section import SECTION_EXTRA_FIELDS, RepeatingSection, Section
2624
from pyxform.survey_element import _GET_SENTINEL, SURVEY_ELEMENT_FIELDS, SurveyElement
25+
from pyxform.survey_elements.attribute import Attribute
2726
from pyxform.utils import (
2827
LAST_SAVED_INSTANCE_NAME,
2928
DetachableElement,
@@ -600,11 +599,11 @@ def xml_model_bindings(self) -> Generator[DetachableElement | None, None, None]:
600599
Yield bindings (bind or action elements) for this node and all its descendants.
601600
"""
602601
for e in self.iter_descendants(
603-
condition=lambda i: not isinstance(i, Option | Tag | Label)
602+
condition=lambda i: not isinstance(i, Option | Tag)
604603
):
605604
yield from e.xml_bindings(survey=self)
606605

607-
if isinstance(e, EntityDeclaration | Question):
606+
if isinstance(e, Attribute | Question):
608607
yield from e.xml_actions(survey=self, in_repeat=False)
609608

610609
def xml_model(self):

0 commit comments

Comments
 (0)