11import csv
22from pathlib import Path
3+ import logging
34
45import requests
56
89from .models import Activity , Item , Protocol , ResponseOption
910from .utils import start_server , stop_server
1011
12+ logger = logging .getLogger (__name__ )
13+
1114
1215def fetch_choices_from_url (url ):
1316 try :
@@ -37,6 +40,17 @@ def fetch_choices_from_url(url):
3740
3841
3942def find_Ftype_and_colH (item , row_data , response_options ):
43+ """
44+ Determine field type and column H value.
45+
46+ Args:
47+ item: Item object containing UI information
48+ row_data: Dictionary to store field data
49+ response_options: Response options object
50+
51+ Returns:
52+ dict: Updated row_data with field type and validation info
53+ """
4054 # Extract the input type from the item_json
4155 f_type = item .ui .inputType
4256 col_h = ""
@@ -58,16 +72,15 @@ def find_Ftype_and_colH(item, row_data, response_options):
5872 f_type = "text"
5973 col_h = "date_mdy"
6074 elif f_type == "select" :
61- multiple_choice = response_options . multipleChoice
62- print ( "mult" , multiple_choice )
75+ multiple_choice = getattr ( response_options , ' multipleChoice' , False )
76+ logger . debug ( f"Multiple choice setting for { item . id } : { multiple_choice } " )
6377 f_type = "checkbox" if multiple_choice else "dropdown"
6478 elif f_type == "radio" :
65- if response_options . multipleChoice :
79+ if getattr ( response_options , ' multipleChoice' , False ) :
6680 f_type = "checkbox"
67- elif f_type .startswith ("select" ): # TODO: this should be reviewed
68- # Adjusting for selectCountry, selectLanguage, selectState types
81+ elif f_type .startswith ("select" ):
6982 f_type = "radio"
70- choices_url = response_options . choices
83+ choices_url = getattr ( response_options , ' choices' , None )
7184 if choices_url and isinstance (choices_url , str ):
7285 choices_data = fetch_choices_from_url (choices_url )
7386 if choices_data :
@@ -78,7 +91,6 @@ def find_Ftype_and_colH(item, row_data, response_options):
7891 f_type = "text"
7992
8093 row_data ["field_type" ] = f_type .lower ()
81-
8294 if col_h :
8395 row_data ["val_type_OR_slider" ] = col_h .lower ()
8496
@@ -139,39 +151,67 @@ def process_item(
139151 choices = response_options .choices
140152 if choices and not isinstance (choices , str ):
141153 if isinstance (choices , list ):
142- item_choices = [
143- f"{ ch .value } , { ch .name .get ('en' , '' )} "
144- for ch in choices
145- if ch .value is not None
146- ]
154+ # Handle the case where choices is a list
155+ item_choices = []
156+ for ch in choices :
157+ if hasattr (ch , 'value' ) and ch .value is not None :
158+ name = ch .name .get ('en' , '' ) if hasattr (ch , 'name' ) else ''
159+ item_choices .append (f"{ ch .value } , { name } " )
147160 if item_choices :
148161 row_data ["choices" ] = " | " .join (item_choices )
149162
150163 # Add valueRequired if explicitly True
151164 if (
152165 item_properties
153- and "valueRequired" in item_properties
154- and item_properties [ "valueRequired" ] is True
166+ and isinstance ( item_properties , dict ) # Ensure it's a dictionary
167+ and item_properties . get ( "valueRequired" ) is True
155168 ):
156169 row_data ["required" ] = "y"
157170
158171 var_name = str (item .id ).split ("/" )[- 1 ] # Get the last part of the id path
172+
173+ # Handle compute items
174+ if compute_item and compute_expr :
175+ logger .debug (f"Processing compute item: { var_name } " )
176+ logger .debug (f"Compute expression: { compute_expr } " )
177+ row_data ["choices" ] = compute_expr
178+ row_data ["field_type" ] = "calc"
179+ # For computed fields, we may need to set visibility to false by default
180+ if any (score_type in var_name for score_type in ["_score" , "_total" ]):
181+ row_data ["isVis_logic" ] = False
182+ else :
183+ # Use find_Ftype_and_colH but only add non-empty values
184+ field_info = find_Ftype_and_colH (item , {}, response_options )
185+ if field_info .get ("field_type" ):
186+ row_data ["field_type" ] = field_info ["field_type" ]
187+ if field_info .get ("val_type_OR_slider" ):
188+ row_data ["val_type_OR_slider" ] = field_info ["val_type_OR_slider" ]
189+
190+ # Handle visibility
159191 if var_name .endswith ("_total_score" ):
160- row_data ["isVis_logic" ] = False # This will make the field hidden
161- # Regular isVis handling for other fields
162- elif "isVis" in item_properties and item_properties ["isVis" ] is not True :
192+ row_data ["isVis_logic" ] = False
193+ elif (
194+ item_properties
195+ and isinstance (item_properties , dict ) # Ensure it's a dictionary
196+ and "isVis" in item_properties
197+ and item_properties ["isVis" ] is not True
198+ ):
163199 row_data ["isVis_logic" ] = item_properties ["isVis" ]
164200
165201 # Handle description
166202 if (
167- item . description
168- and "en" in item .description
169- and item .description [ "en" ]
203+ hasattr ( item , ' description' )
204+ and isinstance ( item .description , dict )
205+ and item .description . get ( "en" )
170206 ):
171207 row_data ["field_notes" ] = item .description ["en" ]
172208
173209 # Handle preamble
174- if item .preamble and "en" in item .preamble and item .preamble ["en" ]:
210+ if (
211+ hasattr (item , 'preamble' )
212+ and isinstance (item .preamble , dict )
213+ and item .preamble .get ("en" )
214+ ):
175215 row_data ["preamble" ] = item .preamble ["en" ]
176216 elif activity_preamble :
177217 row_data ["preamble" ] = activity_preamble
@@ -180,44 +220,23 @@ def process_item(
180220 if compute_item :
181221 question = item .description
182222 else :
183- question = item .question
223+ question = item .question if hasattr ( item , 'question' ) else None
184224
185- if isinstance (question , dict ) and "en" in question and question [ "en" ] :
225+ if isinstance (question , dict ) and question . get ( "en" ) :
186226 row_data ["field_label" ] = question ["en" ]
187227 elif isinstance (question , str ) and question :
188228 row_data ["field_label" ] = question
189229
190- # Handle compute items
191- if compute_item and compute_expr :
192- print (f"\n Debug - Compute Item: { var_name } " )
193- print (f"Compute Expression: { compute_expr } " )
194- row_data ["choices" ] = compute_expr
195- row_data ["field_type" ] = "calc"
196- # For computed fields, we may need to set visibility to false by default
197- if any (score_type in var_name for score_type in ["_score" , "_total" ]):
198- row_data ["isVis_logic" ] = False
199- else :
200- # Use find_Ftype_and_colH but only add non-empty values
201- field_info = find_Ftype_and_colH (item , {}, response_options )
202- if field_info .get ("field_type" ):
203- row_data ["field_type" ] = field_info ["field_type" ]
204- if field_info .get ("val_type_OR_slider" ):
205- row_data ["val_type_OR_slider" ] = field_info ["val_type_OR_slider" ]
206-
207230 return row_data
208231
209232
210233def get_csv_data (dir_path , contextfile , http_kwargs ):
211234 csv_data = []
212235
213- # Iterate over directories in dir_path
214236 for protocol_dir in dir_path .iterdir ():
215237 if protocol_dir .is_dir ():
216- # Check for a _schema file in each directory
217238 schema_file = next (protocol_dir .glob ("*_schema" ), None )
218- print (f"Found schema file: { schema_file } " )
219239 if schema_file :
220- # Process the found _schema file
221240 parsed_protocol_json = load_file (
222241 schema_file ,
223242 started = True ,
@@ -234,6 +253,7 @@ def get_csv_data(dir_path, contextfile, http_kwargs):
234253 for activity_path in activity_order :
235254 if not _is_url (activity_path ):
236255 activity_path = protocol_dir / activity_path
256+
237257 parsed_activity_json = load_file (
238258 activity_path ,
239259 started = True ,
@@ -244,112 +264,73 @@ def get_csv_data(dir_path, contextfile, http_kwargs):
244264 )
245265 del parsed_activity_json ["@context" ]
246266 act = Activity (** parsed_activity_json )
247- items_properties = {
248- el ["variableName" ]: el
249- for el in parsed_activity_json ["ui" ]["addProperties" ]
250- }
251267
252- # Get activity name without adding extra _schema
268+ # Get activity name
253269 activity_name = act .id .split ("/" )[- 1 ]
254270 if activity_name .endswith ("_schema.jsonld" ):
255- activity_name = activity_name [
256- :- 12
257- ] # Remove _schema.jsonld
271+ activity_name = activity_name [:- 12 ]
258272 elif activity_name .endswith (".jsonld" ):
259- activity_name = activity_name [:- 7 ] # Remove .jsonld
260-
261- items_properties . update (
262- {
263- el [ "isAbout" ]: el
264- for el in parsed_activity_json [ "ui" ][
265- "addProperties"
266- ]
273+ activity_name = activity_name [:- 7 ]
274+
275+ # Create a map of computed items
276+ compute_map = {}
277+ if hasattr ( act , 'compute' ):
278+ compute_map = {
279+ comp . variableName : comp . jsExpression
280+ for comp in act . compute
267281 }
268- )
269282
270- if parsed_activity_json :
271- item_order = [("ord" , el ) for el in act .ui .order ]
272- item_calc = [("calc" , el ) for el in act .compute ]
283+ # Process each item defined in addProperties
284+ for item_def in parsed_activity_json ["ui" ]["addProperties" ]:
285+ item_path = item_def ["isAbout" ]
286+ var_name = item_def ["variableName" ]
287+
288+ # Get the item file path
289+ if not _is_url (item_path ):
290+ full_item_path = Path (activity_path ).parent / item_path
291+ else :
292+ full_item_path = item_path
293+
294+ try :
295+ item_json = load_file (
296+ full_item_path ,
297+ started = True ,
298+ http_kwargs = http_kwargs ,
299+ fixoldschema = True ,
300+ compact = True ,
301+ compact_context = contextfile ,
302+ )
303+ item_json .pop ("@context" , "" )
304+ item = Item (** item_json )
273305
274- computed_fields = {
275- calc_item .variableName
276- for _ , calc_item in item_calc
277- }
306+ activity_preamble = (
307+ act .preamble .get ("en" , "" ).strip ()
308+ if hasattr (act , "preamble" )
309+ else ""
310+ )
278311
279- for tp , item in item_order + item_calc :
280- try :
281- if tp == "calc" :
282- js_expr = item .jsExpression
283- var_name = item .variableName
284-
285- # Find the corresponding item properties
286- if var_name in items_properties :
287- item = items_properties [var_name ][
288- "isAbout"
289- ]
290- # Ensure computed fields are marked as hidden
291- items_properties [var_name ][
292- "isVis"
293- ] = False
294- else :
295- print (
296- f"WARNING: no item properties found for computed field { var_name } in { activity_name } "
297- )
298- continue
299- item_calc = True
300- else :
301- item_calc = False
302- js_expr = None
303- it_prop = items_properties .get (item )
304- if not _is_url (item ):
305- item = Path (activity_path ).parent / item
306-
307- try :
308- item_json = load_file (
309- item ,
310- started = True ,
311- http_kwargs = http_kwargs ,
312- fixoldschema = True ,
313- compact = True ,
314- compact_context = contextfile ,
315- )
316- item_json .pop ("@context" , "" )
317- itm = Item (** item_json )
318- except Exception as e :
319- print (f"Error loading item: { item } " )
320- print (f"Error details: { str (e )} " )
321- continue
322-
323- activity_name = act .id .split ("/" )[- 1 ].split (
324- "."
325- )[0 ]
326- activity_preamble = (
327- act .preamble .get ("en" , "" ).strip ()
328- if hasattr (act , "preamble" )
329- else ""
330- )
331-
332- row_data = process_item (
333- itm ,
334- it_prop ,
335- activity_name ,
336- activity_preamble ,
337- contextfile ,
338- http_kwargs ,
339- item_calc ,
340- js_expr ,
341- )
342- csv_data .append (row_data )
343-
344- except Exception as e :
345- print (
346- f"Error processing item { item } : { str (e )} "
347- )
348- continue
349- # Break after finding the first _schema file
350- break
351- return csv_data
312+ # Check if this is a computed item
313+ compute_expr = compute_map .get (var_name )
314+ is_computed = compute_expr is not None
315+
316+ row_data = process_item (
317+ item ,
318+ item_def ,
319+ activity_name ,
320+ activity_preamble ,
321+ contextfile ,
322+ http_kwargs ,
323+ is_computed ,
324+ compute_expr
325+ )
326+ csv_data .append (row_data )
327+
328+ except Exception as e :
329+ print (f"Error processing item { item_path } for activity { activity_name } " )
330+ print (f"Error details: { str (e )} " )
331+ continue
352332
333+ return csv_data
353334
354335def write_to_csv (csv_data , output_csv_filename ):
355336 # REDCap-specific headers
0 commit comments