22from typing import Any
33
44from pyxform import constants as const
5- from pyxform .errors import PyXFormError
5+ from pyxform .errors import Detail , PyXFormError
66from pyxform .parsing .expression import is_xml_tag
7+ from pyxform .utils import PYXFORM_REFERENCE_REGEX
78
89EC = const .EntityColumns
10+ ENTITY001 = Detail (
11+ name = "Invalid entity repeat reference" ,
12+ msg = (
13+ "[row : 2] On the 'entities' sheet, the 'repeat' value '{value}' is invalid. "
14+ "The 'repeat' column, if specified, must contain only a single reference variable "
15+ "(like '${{q1}}'), and the reference variable must contain a valid name."
16+ ),
17+ )
18+ ENTITY002 = Detail (
19+ name = "Invalid entity repeat: target not found" ,
20+ msg = (
21+ "[row : 2] On the 'entities' sheet, the 'repeat' value '{value}' is invalid. "
22+ "The entity repeat target was not found in the 'survey' sheet."
23+ ),
24+ )
25+ ENTITY003 = Detail (
26+ name = "Invalid entity repeat: target is not a repeat" ,
27+ msg = (
28+ "[row : 2] On the 'entities' sheet, the 'repeat' value '{value}' is invalid. "
29+ "The entity repeat target is not a repeat."
30+ ),
31+ )
32+ ENTITY004 = Detail (
33+ name = "Invalid entity repeat: target is in a repeat" ,
34+ msg = (
35+ "[row : 2] On the 'entities' sheet, the 'repeat' value '{value}' is invalid. "
36+ "The entity repeat target is inside a repeat."
37+ ),
38+ )
39+ ENTITY005 = Detail (
40+ name = "Invalid entity repeat save_to: question in nested repeat" ,
41+ msg = (
42+ "[row : {row}] On the 'survey' sheet, the 'save_to' value '{value}' is invalid. "
43+ "The entity properties populated with 'save_to' must not be inside of a nested "
44+ "repeat within the entity repeat."
45+ ),
46+ )
47+ ENTITY006 = Detail (
48+ name = "Invalid entity repeat save_to: question not in entity repeat" ,
49+ msg = (
50+ "[row : {row}] On the 'survey' sheet, the 'save_to' value '{value}' is invalid. "
51+ "The entity properties populated with 'save_to' must be inside of the entity "
52+ "repeat."
53+ ),
54+ )
955
1056
1157def get_entity_declaration (
@@ -24,6 +70,7 @@ def get_entity_declaration(
2470 create_condition = entity_row .get (EC .CREATE_IF , None )
2571 update_condition = entity_row .get (EC .UPDATE_IF , None )
2672 entity_label = entity_row .get (EC .LABEL , None )
73+ entity_repeat = get_validated_repeat_name (entity_row )
2774
2875 if update_condition and not entity_id :
2976 raise PyXFormError (
@@ -49,6 +96,7 @@ def get_entity_declaration(
4996 EC .CREATE_IF .value : create_condition ,
5097 EC .UPDATE_IF .value : update_condition ,
5198 EC .LABEL .value : entity_label ,
99+ EC .REPEAT .value : entity_repeat ,
52100 },
53101 }
54102
@@ -77,11 +125,29 @@ def get_validated_dataset_name(entity):
77125 return dataset
78126
79127
128+ def get_validated_repeat_name (entity ) -> str | None :
129+ if EC .REPEAT .value not in entity :
130+ return None
131+
132+ value = entity [EC .REPEAT ]
133+ if not (value and len (value ) > 3 ):
134+ raise PyXFormError (ENTITY001 .format (value = value ))
135+ matches = PYXFORM_REFERENCE_REGEX .fullmatch (value )
136+ if matches is None :
137+ raise PyXFormError (ENTITY001 .format (value = value ))
138+ else :
139+ match = matches .group (1 )
140+ if not is_xml_tag (match ):
141+ raise PyXFormError (ENTITY001 .format (value = value ))
142+ else :
143+ return match
144+
145+
80146def validate_entity_saveto (
81147 row : dict ,
82148 row_number : int ,
83- in_repeat : bool ,
84149 entity_declaration : dict [str , Any ] | None = None ,
150+ stack : Sequence [dict [str , Any ]] | None = None ,
85151):
86152 save_to = row .get (const .BIND , {}).get ("entities:saveto" , "" )
87153 if not save_to :
@@ -97,10 +163,24 @@ def validate_entity_saveto(
97163 f"{ const .ROW_FORMAT_STRING % row_number } Groups and repeats can't be saved as entity properties."
98164 )
99165
100- if in_repeat :
101- raise PyXFormError (
102- f"{ const .ROW_FORMAT_STRING % row_number } Currently, you can't create entities from repeats. You may only specify save_to values for form fields outside of repeats."
103- )
166+ entity_repeat = entity_declaration [const .PARAMETERS ].get (EC .REPEAT , None )
167+ if entity_repeat and stack :
168+ # Error: saveto in nested repeat inside entity repeat.
169+ in_repeat = False
170+ located = False
171+ for i in reversed (stack ):
172+ if not i ["control_name" ] or not i ["control_type" ]:
173+ break
174+ elif i ["control_type" ] == const .REPEAT :
175+ if in_repeat :
176+ raise PyXFormError (ENTITY005 .format (row = row_number , value = save_to ))
177+ elif i ["control_name" ] == entity_repeat :
178+ located = True
179+ in_repeat = True
180+
181+ # Error: saveto not in entity repeat
182+ if not located :
183+ raise PyXFormError (ENTITY006 .format (row = row_number , value = save_to ))
104184
105185 error_start = f"{ const .ROW_FORMAT_STRING % row_number } Invalid save_to name:"
106186
@@ -134,3 +214,56 @@ def validate_entities_columns(row: dict):
134214 f"pyxform."
135215 )
136216 raise PyXFormError (msg )
217+
218+
219+ def validate_entity_repeat_target (
220+ entity_declaration : dict [str , Any ] | None ,
221+ stack : Sequence [dict [str , Any ]] | None = None ,
222+ ) -> bool :
223+ """
224+ Check if the entity repeat target exists, is a repeat, and is a name match.
225+
226+ Raises an error if the control type or name is None (such as for the Survey), or if
227+ the control type is not a repeat.
228+
229+ :param entity_declaration:
230+ :param stack: The control stack from workbook_to_json.
231+ :return:
232+ """
233+ # Ignore: entity already processed.
234+ if not entity_declaration :
235+ return False
236+
237+ entity_repeat = entity_declaration [const .PARAMETERS ].get (EC .REPEAT , None )
238+
239+ # Ignore: no repeat declared for the entity.
240+ if not entity_repeat :
241+ return False
242+
243+ # Error: repeat not found while processing survey sheet.
244+ if not stack :
245+ raise PyXFormError (ENTITY002 .format (value = entity_repeat ))
246+
247+ control_name = stack [- 1 ]["control_name" ]
248+ control_type = stack [- 1 ]["control_type" ]
249+
250+ # Ignore: current control is not the target.
251+ if control_name and control_name != entity_repeat :
252+ return False
253+
254+ # Error: target is not a repeat.
255+ if control_type and control_type != const .REPEAT :
256+ raise PyXFormError (ENTITY003 .format (value = entity_repeat ))
257+
258+ # Error: repeat is in nested repeat.
259+ located = False
260+ for i in reversed (stack ):
261+ if not i ["control_name" ] or not i ["control_type" ]:
262+ break
263+ elif i ["control_type" ] == const .REPEAT :
264+ if located :
265+ raise PyXFormError (ENTITY004 .format (value = entity_repeat ))
266+ elif i ["control_name" ] == entity_repeat :
267+ located = True
268+
269+ return entity_repeat == control_name
0 commit comments