XLSForm / pyxform

A Python package to create XForms for ODK Collect.
BSD 2-Clause "Simplified" License
77 stars 134 forks source link

Change the parameters field/column default type to type dict #605

Closed ukanga closed 2 years ago

ukanga commented 2 years ago

Software and hardware versions

pyxform v1.9

Problem description

When you process an XLSForm, in whatever format (markdown, spreadsheet file, e.t.c.), and get the JSON survey form via survey.to_json() command. If you create a new survey object using this JSON representation, which is possible via the SurveyElementBuilder().create_survey_element_from_json(json_survey_dict), the parameters field for a select one question that results in itemsets will be set to an empty string due to the str default type of the parameters field here . This is the case for all XLSForms that do not have a parameters values set for any field in the form. As such, the empty str default will be set here and as a result fail with the exception AttributeError: 'str' object has no attribute 'get' when generating the XForm XML via SurveyElementBuilder().create_survey_element_from_json(survey.to_json()).to_xml().

Traceback (most recent call last):
 ...
    json_survey = xls2json.parse_file_to_json(xlsform_path, warnings=warnings)
    survey = builder.create_survey_element_from_dict(json_survey)
    SurveyElementBuilder().create_survey_element_from_json(survey.to_json()).to_xml()
  File "/.../pyxform/pyxform/survey.py", line 1117, in to_xml
    self.print_xform_to_file(
  File "/.../pyxform/pyxform/survey.py", line 1082, in print_xform_to_file
    raise error
  File "/.../pyxform/pyxform/survey.py", line 1076, in print_xform_to_file
    file_obj.write(self._to_pretty_xml())
  File "/.../pyxform/pyxform/survey.py", line 866, in _to_pretty_xml
    xml_with_linebreaks = self.xml().toprettyxml(indent="  ")
  File "/.../code/pyxform/pyxform/survey.py", line 254, in xml
    node("h:body", *self.xml_control(), **body_kwargs),
  File "/.../pyxform/pyxform/section.py", line 91, in xml_control
    control = e.xml_control()
  File "/.../pyxform/pyxform/question.py", line 55, in xml_control
    xml_node = self.build_xml()
  File "/.../pyxform/pyxform/question.py", line 210, in build_xml
    itemset_value_ref = self.parameters.get(
AttributeError: 'str' object has no attribute 'get'

Steps to reproduce the problem

  # create a survey object from an XLSForm File
  json_survey = xls2json.parse_file_to_json(xlsform_path, warnings=warnings)
  survey = builder.create_survey_element_from_dict(json_survey)
  # test that you can successfully create the XForm XML of the survey object
  survey.to_xml()   # will not raise an exception

  # creating survey from the JSON for the same survey even with no modifications will raise the AttributeError exception
  SurveyElementBuilder().create_survey_element_from_json(survey.to_json()).to_xml()  # AttributeError exception will be raised.

Expected behaviour

The JSON Survey representation should be able to create a Survey object that can generate the XForm XML without any exception being thrown.

Other information

I noticed that, when processing the XLSForm, or its JSON dict equivalent which is processed by workbook_to_json() always results in the parameters field that has a dict empty or with key-value parameters. Hence the recommended fix is making the default value of type dict which will not result in an exception. There seem to be no other scenarios, as far as I can tell, where the parameters field value is a string when dealing with a SurveyElement.

Why would you want to recreate a Survey object from the "XForm JSON representation"?

It is a cheap way to add some extra fields or metadata like setting the XForm version post processing the XLSForm file among other tasks an application could do for the user without the user making those changes themselves and if those changes are relevant to the application and not necessarily the user. Any other option better than this approach for this use case?

ukanga commented 2 years ago

Before the change to set the parameters default type to dict.

======================================================================
ERROR: Re-using the same xml external data source id and URI is OK.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/.../pyxform/tests/test_external_instances.py", line 393, in test_can__reuse_xml__external_then_selects
    SurveyElementBuilder().create_survey_element_from_json(survey.to_json()).to_xml()
  File "/.../pyxform/pyxform/survey.py", line 1117, in to_xml
    self.print_xform_to_file(
  File "/.../pyxform/pyxform/survey.py", line 1082, in print_xform_to_file
    raise error
  File "/.../pyxform/pyxform/survey.py", line 1076, in print_xform_to_file
    file_obj.write(self._to_pretty_xml())
  File "/.../pyxform/pyxform/survey.py", line 866, in _to_pretty_xml
    xml_with_linebreaks = self.xml().toprettyxml(indent="  ")
  File "/.../pyxform/pyxform/survey.py", line 254, in xml
    node("h:body", *self.xml_control(), **body_kwargs),
  File "/.../pyxform/pyxform/section.py", line 91, in xml_control
    control = e.xml_control()
  File "/.../pyxform/pyxform/question.py", line 55, in xml_control
    xml_node = self.build_xml()
  File "/.../pyxform/pyxform/question.py", line 210, in build_xml
    itemset_value_ref = self.parameters.get(
AttributeError: 'str' object has no attribute 'get'
lognaturel commented 2 years ago

Thanks for describing why you use this, that's really helpful!