From a350074ea82b9780019e0abaf3f1773dbe1f1fea Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Wed, 6 Dec 2023 16:25:26 -0500 Subject: [PATCH 1/5] Start working WIP on #905 --- .../ALWeaver/data/questions/assembly_line.yml | 23 +++- .../ALWeaver/data/templates/output.mako | 4 + docassemble/ALWeaver/interview_generator.py | 114 +++++++++++++++++- 3 files changed, 138 insertions(+), 3 deletions(-) diff --git a/docassemble/ALWeaver/data/questions/assembly_line.yml b/docassemble/ALWeaver/data/questions/assembly_line.yml index aea9f0a1..3dbc5f66 100644 --- a/docassemble/ALWeaver/data/questions/assembly_line.yml +++ b/docassemble/ALWeaver/data/questions/assembly_line.yml @@ -319,6 +319,18 @@ fields: datatype: files accept: | "application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document" + - Upload JSON file with draft screen arrangement (advanced): interview.start_with_json + help: | + If you have a JSON file that has a draft of the screens and their order, you can upload + it now. This is intended to make it possible to draft the screens in another tool, such + as with the help of GPT-4. + datatype: yesno + - JSON file: interview.uploaded_json + datatype: file + file css class: None + show if: interview.start_with_json + accept: | + "application/json" validation code: | # HACK Litcon 2023: remove these confusing checkboxes for now yes_normalize_fields = False @@ -1970,8 +1982,15 @@ code: | yes_recognize_form_fields = False interview_type = "regular" if not has_safe_pdf_in_url: - interview.auto_assign_attributes() - interview_label_draft = interview.short_filename + if hasattr(interview, "start_with_json") and interview.start_with_json: + try: + interview.parsed_json = json.loads(interview.uploaded_json.path()) + interview.auto_assign_attributes(screens=interview.parsed_json["screens"], interview_logic = interview.parsed_json["interview_logic"]) + except: + interview.auto_assign_attributes() + else: + interview.auto_assign_attributes() + interview_label_draft = varname(interview.title) # TODO: refactor this at some point, this is a shim to create objects block but we # shouldn't need it forever. diff --git a/docassemble/ALWeaver/data/templates/output.mako b/docassemble/ALWeaver/data/templates/output.mako index 6dfb6fdd..c3bf59ee 100644 --- a/docassemble/ALWeaver/data/templates/output.mako +++ b/docassemble/ALWeaver/data/templates/output.mako @@ -181,7 +181,11 @@ ${ field_entry_yaml(field) }\ % endfor % endif % if question.needs_continue_button_field: + % if hasattr(question, "continue_button_field"): +continue button field: ${ question.continue_button_field } + % else: continue button field: ${ varname(question.question_text) } + % endif % endif % endfor <%doc> diff --git a/docassemble/ALWeaver/interview_generator.py b/docassemble/ALWeaver/interview_generator.py index 95320764..adc639f5 100644 --- a/docassemble/ALWeaver/interview_generator.py +++ b/docassemble/ALWeaver/interview_generator.py @@ -97,6 +97,7 @@ def formfyxer_available(): "to_yaml_file", "using_string", "varname", + "logic_to_code_block", ] always_defined = set( @@ -204,6 +205,37 @@ def varname(var_name: str) -> str: return var_name +def logic_to_code_block(items: List[Union[Dict, str]], indent_level=0) -> str: + """Converts a list of logic items to a code block with the given indentation level + + Args: + items (list): A list of logic items, of the form ['var0', {'condition': '...', 'children': ['var1']}, 'var2', 'var3', ...] + indent_level (int, optional): The indentation level to use. Defaults to 0. Used for recursion. + + Returns: + str: The code block, as a string + """ + code_lines = [] + indent = ' ' * indent_level # Define the indentation (e.g., 2 spaces per level) + for item in items: + if isinstance(item, str): # If the item is a string, it's a variable + code_lines.append(f"{indent}{item}") + elif isinstance(item, dict): # If the item is a dictionary, it's a condition + # Add the condition line with the current indentation + condition_line = item['condition'] + if not condition_line.startswith('if '): + condition_line = 'if ' + condition_line # Add 'if' if it's not already there + if not condition_line.endswith(':'): + condition_line += ':' + code_lines.append(f"{indent}{condition_line}") + + # Recursively process the children with increased indentation + children_code = logic_to_code_block(item['children'], indent_level + 1) + code_lines.append(children_code) + + return '\n'.join(code_lines) + + class DAFieldGroup(Enum): RESERVED = "reserved" BUILT_IN = "built in" @@ -1486,11 +1518,24 @@ def auto_assign_attributes( jurisdiction: Optional[str] = None, categories: Optional[str] = None, default_country_code: str = "US", + interview_logic: Optional[List[Union[Dict, str]]] = None, + screens: Optional[List[Dict]] = None, ): """ Automatically assign interview attributes based on the template assigned to the interview object. To assist with "I'm feeling lucky" button. + + Args: + url (Optional[str]): URL to a template file + input_file (Optional[Union[DAFileList, DAFile, DAStaticFile]]): A file + object + title (Optional[str]): Title of the interview + jurisdiction (Optional[str]): Jurisdiction of the interview + categories (Optional[str]): Categories of the interview + default_country_code (str): Default country code for the interview. Defaults to "US". + interview_logic (Optional[List[Union[Dict, str]]]): Interview logic, represented as a tree + screens (Optional[DAList[Dict]]): Interview screens, represented in the same structure as Docassemble's dictionary for a question block """ try: if user_logged_in(): @@ -1540,7 +1585,12 @@ def auto_assign_attributes( self._auto_load_fields() self.all_fields.auto_label_fields() self.all_fields.auto_mark_people_as_builtins() - self.auto_group_fields() + if interview_logic: + self.interview_logic = interview_logic + if screens: + self.create_questions_from_screen_list(screens) + else: + self.auto_group_fields() def _set_title(self, url=None, input_file=None): if url: @@ -1849,6 +1899,68 @@ def _guess_categories(self, title) -> List[str]: def _null_group_fields(self): return {"Screen 1": [field.variable for field in self.all_fields.custom()]} + def create_questions_from_screen_list(self, screen_list: List[Dict]): + """ + Create a question for each screen in the screen list. This is an alternative to + allow an author to upload a list of fields and then create a question for each + without using FormFyxer's auto field creation. + + Args: + screen_list (list): A list of dictionaries, each representing a screen + """ + self.questions.auto_gather = False + for screen in screen_list: + new_screen = self.questions.appendObject() + if screen.get("continue button field"): + new_screen.continue_button_field = screen.get("continue_button_field") + new_screen.is_informational = True + else: + new_screen.is_informational = False + new_screen.question_text = screen.get("question", "") + new_screen.subquestion_text = screen.get("subquestion", "") + new_screen.field_list = [] + for field in screen.get("fields", []): + new_field = new_screen.field_list.appendObject() + + if field.get("label") and field.get("field"): + new_field.variable = field.get("field") + new_field.label = field.get("label") + else: + first_item = next(iter(field.items())) + new_field.variable = first_item[1] + new_field.label = first_item[0] + # For some reason we made the field_type not exactly the same as the datatype in Docassemble + #TODO: consider refactoring this + if field.get("datatype") or field.get("input type"): + if field.get("datatype", "") == "radio": + new_field.field_type = "multiple choice radio" + elif field.get("datatype", "") == "checkboxes": + new_field.field_type = "multiple choice checkboxes" + elif field.get("datatype", "") == "dropdown": + new_field.field_type = "multiple choice dropdown" + elif field.get("datatype", "") == "combobox": + new_field.field_type = "multiple choice combobox" + else: + new_field.field_type = field.get("datatype", field.get("input type", "text")) + else: + new_field.field_type = "text" + if field.get("maxlength"): + new_field.maxlength = field.get("maxlength", None) + if field.get("choices"): + # We turn choices into a newline separated string + new_field.choices = "\n".join(field.get("choices", [])) + if field.get("min"): + new_field.range_min = field.get("min", None) + if field.get("max"): + new_field.range_max = field.get("max", None) + if field.get("step"): + new_field.range_step = field.get("step", None) + if field.get("required") == False: + new_field.is_optional = True + + self.questions.gathered = True + + def auto_group_fields(self): """ Use FormFyxer to assign fields to screens. From bef766042da56f1339184fd2a14d51272d5987bb Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 12 Dec 2023 16:56:45 -0500 Subject: [PATCH 2/5] Use proper JSON loading method --- .../ALWeaver/data/questions/assembly_line.yml | 10 +++--- docassemble/ALWeaver/interview_generator.py | 31 ++++++++++++++++--- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/docassemble/ALWeaver/data/questions/assembly_line.yml b/docassemble/ALWeaver/data/questions/assembly_line.yml index 3dbc5f66..855b0a78 100644 --- a/docassemble/ALWeaver/data/questions/assembly_line.yml +++ b/docassemble/ALWeaver/data/questions/assembly_line.yml @@ -1983,11 +1983,11 @@ code: | interview_type = "regular" if not has_safe_pdf_in_url: if hasattr(interview, "start_with_json") and interview.start_with_json: - try: - interview.parsed_json = json.loads(interview.uploaded_json.path()) - interview.auto_assign_attributes(screens=interview.parsed_json["screens"], interview_logic = interview.parsed_json["interview_logic"]) - except: - interview.auto_assign_attributes() + # try + interview.parsed_json = json.loads(interview.uploaded_json.slurp()) + interview.auto_assign_attributes(screens=interview.parsed_json["questions"], interview_logic = interview.parsed_json["interview order"]) + # except + # interview.auto_assign_attributes() else: interview.auto_assign_attributes() interview_label_draft = varname(interview.title) diff --git a/docassemble/ALWeaver/interview_generator.py b/docassemble/ALWeaver/interview_generator.py index adc639f5..670519e8 100644 --- a/docassemble/ALWeaver/interview_generator.py +++ b/docassemble/ALWeaver/interview_generator.py @@ -47,6 +47,7 @@ import uuid import zipfile import spacy +from dataclasses import dataclass mako.runtime.UNDEFINED = DAEmpty() @@ -1242,7 +1243,7 @@ class DAQuestion(DAObject): def init(self, *pargs, **kwargs): super().init(*pargs, **kwargs) - self.field_list = DAFieldList() + self.initializeAttribute("field_list", DAFieldList) @property def complete(self) -> bool: @@ -1376,6 +1377,27 @@ def interview_order_list( return list(more_itertools.unique_everseen(logic_list)) +@dataclass +class Field: + label: Optional[str] = None + field: Optional[str] = None + datatype: Optional[str] = None + input_type: Optional[str] = None + maxlength: Optional[int] = None + choices: Optional[List[str]] = None + min: Optional[int] = None + max: Optional[int] = None + step: Optional[int] = None + required: Optional[bool] = None + +@dataclass +class Screen: + continue_button_field: Optional[str] = None + question: Optional[str] = None + subquestion: Optional[str] = None + fields: List[Field] = None + + class DAInterview(DAObject): """ This class is a container for the various questions and metadata @@ -1535,7 +1557,7 @@ def auto_assign_attributes( categories (Optional[str]): Categories of the interview default_country_code (str): Default country code for the interview. Defaults to "US". interview_logic (Optional[List[Union[Dict, str]]]): Interview logic, represented as a tree - screens (Optional[DAList[Dict]]): Interview screens, represented in the same structure as Docassemble's dictionary for a question block + screens (Optional[List[Dict]]): Interview screens, represented in the same structure as Docassemble's dictionary for a question block """ try: if user_logged_in(): @@ -1899,7 +1921,7 @@ def _guess_categories(self, title) -> List[str]: def _null_group_fields(self): return {"Screen 1": [field.variable for field in self.all_fields.custom()]} - def create_questions_from_screen_list(self, screen_list: List[Dict]): + def create_questions_from_screen_list(self, screen_list: List[Screen]): """ Create a question for each screen in the screen list. This is an alternative to allow an author to upload a list of fields and then create a question for each @@ -1918,7 +1940,6 @@ def create_questions_from_screen_list(self, screen_list: List[Dict]): new_screen.is_informational = False new_screen.question_text = screen.get("question", "") new_screen.subquestion_text = screen.get("subquestion", "") - new_screen.field_list = [] for field in screen.get("fields", []): new_field = new_screen.field_list.appendObject() @@ -1957,7 +1978,7 @@ def create_questions_from_screen_list(self, screen_list: List[Dict]): new_field.range_step = field.get("step", None) if field.get("required") == False: new_field.is_optional = True - + new_screen.field_list.gathered = True self.questions.gathered = True From b5b1d616a66f9568f179c0b9052eb2cc97110cf8 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Thu, 14 Dec 2023 10:09:56 -0500 Subject: [PATCH 3/5] Add a little more typing; get_question_file_variables could also address #898 --- docassemble/ALWeaver/interview_generator.py | 42 ++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/docassemble/ALWeaver/interview_generator.py b/docassemble/ALWeaver/interview_generator.py index 670519e8..52de9405 100644 --- a/docassemble/ALWeaver/interview_generator.py +++ b/docassemble/ALWeaver/interview_generator.py @@ -1376,12 +1376,30 @@ def interview_order_list( return list(more_itertools.unique_everseen(logic_list)) +class DADataType(Enum): + TEXT = "text" + AREA = "area" + YESNO = "yesno" + NOYES = "noyes" + YESNORADIO = "yesnoradio" + NOYESRADIO = "noyesradio" + YESNOWIDE = "yesnowide" + NOYESWIDE = "noyeswide" + NUMBER = "number" + INTEGER = "integer" + CURRENCY = "currency" + EMAIL = "email" + DATE = "date" + FILE = "file" + RADIO = "radio" + COMBOBOX = "combobox" + CHECKBOXES = "checkboxes" @dataclass class Field: label: Optional[str] = None field: Optional[str] = None - datatype: Optional[str] = None + datatype: Optional[DADataType] = None input_type: Optional[str] = None maxlength: Optional[int] = None choices: Optional[List[str]] = None @@ -2135,6 +2153,28 @@ def get_fields(document: Union[DAFile, DAFileList]) -> Iterable: text = docx_data.text return get_docx_variables(text) +def get_question_file_variables(screens:List[Screen]) -> Set[str]: + """Extract the fields from a list of screens representing a Docassemble interview, + such as might be supplied as an input to the Weaver in JSON format. + + Args: + screens (List[Screen]): A list of screens, each represented as a dictionary + + Returns: + List[str]: A list of variables + """ + fields = set() + for screen in screens: + if screen.get("continue button field"): + fields.add(screen.get("continue button field")) + if screen.get("fields"): + for field in screen.get("fields"): + if field.get("field"): + fields.add(field.get("field")) + else: + fields.add(next(iter(field.values()))) + return fields + def get_docx_variables(text: str) -> set: """ From 24a183f461ffdc7911ca980de7cda75593ab6081 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Thu, 14 Dec 2023 13:47:10 -0500 Subject: [PATCH 4/5] Add some better flexibility to #905 --- .../ALWeaver/data/questions/assembly_line.yml | 63 +++++++++++++------ docassemble/ALWeaver/interview_generator.py | 22 ++++--- 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/docassemble/ALWeaver/data/questions/assembly_line.yml b/docassemble/ALWeaver/data/questions/assembly_line.yml index 855b0a78..79e868f8 100644 --- a/docassemble/ALWeaver/data/questions/assembly_line.yml +++ b/docassemble/ALWeaver/data/questions/assembly_line.yml @@ -319,18 +319,6 @@ fields: datatype: files accept: | "application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document" - - Upload JSON file with draft screen arrangement (advanced): interview.start_with_json - help: | - If you have a JSON file that has a draft of the screens and their order, you can upload - it now. This is intended to make it possible to draft the screens in another tool, such - as with the help of GPT-4. - datatype: yesno - - JSON file: interview.uploaded_json - datatype: file - file css class: None - show if: interview.start_with_json - accept: | - "application/json" validation code: | # HACK Litcon 2023: remove these confusing checkboxes for now yes_normalize_fields = False @@ -1905,6 +1893,29 @@ fields: choices: - Build step by step: False - Use auto-drafting mode: True + - Upload JSON file with draft screen arrangement (advanced): interview.start_with_json + help: | + If you have a JSON file that has a draft of the screens and their order, you can upload + it now. This is intended to make it possible to draft the screens in another tool, such + as with the help of GPT-4. You still get the benefit of the Weaver's templates + and pre-built questions. + + Your uploaded JSON file should be a dictionary with two keys: + + 1. `questions` should be a list of dictionaries, each of which represents a screen with + "custom" fields. + 2. `interview order` should be a list of strings, each of which is the name of a screen. It + can include "built-in" screens you want to let the Weaver control, such as users.gather(). + datatype: yesno + show if: + variable: im_feeling_lucky + is: True + - JSON file: interview.uploaded_json + datatype: file + file css class: None + show if: interview.start_with_json + accept: | + "application/json" - Try to install the `en_core_web_lg` package before using auto drafting mode: install_en_core_web_lg help: | Installing `en_core_web_lg` will allow you to use automatic field @@ -1982,15 +1993,29 @@ code: | yes_recognize_form_fields = False interview_type = "regular" if not has_safe_pdf_in_url: - if hasattr(interview, "start_with_json") and interview.start_with_json: - # try - interview.parsed_json = json.loads(interview.uploaded_json.slurp()) - interview.auto_assign_attributes(screens=interview.parsed_json["questions"], interview_logic = interview.parsed_json["interview order"]) - # except - # interview.auto_assign_attributes() + if hasattr(interview, "start_with_json") and interview.start_with_json: + #try: # Don't know yet why this always raises an exception. A name error that DA silently fixes? + interview.parsed_json = json.loads(interview.uploaded_json.slurp()) + #except: + # log("Unable to parse JSON file with json.loads", "error") + # interview.parsed_json = None + if isinstance(interview.parsed_json, dict) and "questions" in interview.parsed_json and "interview order" in interview.parsed_json: + interview.auto_assign_attributes( + screens=interview.parsed_json["questions"], + interview_logic = interview.parsed_json["interview order"] + ) + if isinstance(interview.parsed_json, dict) and "questions" in interview.parsed_json: + # Let author upload just the "questions" dict without "interview order" + interview.auto_assign_attributes(screens=interview.parsed_json["questions"]) + elif isinstance(interview.parsed_json, list) and next(iter(interview.parsed_json), None) and isinstance(next(iter(interview.parsed_json)), dict): + # Assume the author uploaded a list of screens, without interview order + interview.auto_assign_attributes(screens=interview.parsed_json) else: + log("Not using JSON file because it has an unexpected format: should be a dict with 'questions' and 'interview order' keys, or a list of question dicts", "error") interview.auto_assign_attributes() - interview_label_draft = varname(interview.title) + else: + interview.auto_assign_attributes() + interview_label_draft = varname(interview.title) # TODO: refactor this at some point, this is a shim to create objects block but we # shouldn't need it forever. diff --git a/docassemble/ALWeaver/interview_generator.py b/docassemble/ALWeaver/interview_generator.py index 52de9405..ce74131c 100644 --- a/docassemble/ALWeaver/interview_generator.py +++ b/docassemble/ALWeaver/interview_generator.py @@ -80,6 +80,7 @@ def formfyxer_available(): "get_docx_validation_errors", "get_docx_variables", "get_fields", + "get_question_file_variables", "get_pdf_validation_errors", "get_pdf_variable_name_matches", "get_variable_name_warnings", @@ -98,7 +99,7 @@ def formfyxer_available(): "to_yaml_file", "using_string", "varname", - "logic_to_code_block", + "logic_to_code_block", ] always_defined = set( @@ -1628,6 +1629,8 @@ def auto_assign_attributes( if interview_logic: self.interview_logic = interview_logic if screens: + if not interview_logic: + self.interview_logic = get_question_file_variables(screens) self.create_questions_from_screen_list(screens) else: self.auto_group_fields() @@ -1950,9 +1953,11 @@ def create_questions_from_screen_list(self, screen_list: List[Screen]): """ self.questions.auto_gather = False for screen in screen_list: + if not screen.get("question"): + continue new_screen = self.questions.appendObject() if screen.get("continue button field"): - new_screen.continue_button_field = screen.get("continue_button_field") + new_screen.continue_button_field = screen.get("continue button field") new_screen.is_informational = True else: new_screen.is_informational = False @@ -2153,7 +2158,7 @@ def get_fields(document: Union[DAFile, DAFileList]) -> Iterable: text = docx_data.text return get_docx_variables(text) -def get_question_file_variables(screens:List[Screen]) -> Set[str]: +def get_question_file_variables(screens:List[Screen]) -> List[str]: """Extract the fields from a list of screens representing a Docassemble interview, such as might be supplied as an input to the Weaver in JSON format. @@ -2163,17 +2168,18 @@ def get_question_file_variables(screens:List[Screen]) -> Set[str]: Returns: List[str]: A list of variables """ - fields = set() + fields = [] for screen in screens: if screen.get("continue button field"): - fields.add(screen.get("continue button field")) + fields.append(screen.get("continue button field")) if screen.get("fields"): for field in screen.get("fields"): if field.get("field"): - fields.add(field.get("field")) + fields.append(field.get("field")) else: - fields.add(next(iter(field.values()))) - return fields + fields.append(next(iter(field.values()))) + # remove duplicates without changing order + return list(dict.fromkeys(fields)) def get_docx_variables(text: str) -> set: From c1d31ed777a000fbca6c547b0d6009638a58ac5f Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Thu, 14 Dec 2023 13:55:23 -0500 Subject: [PATCH 5/5] Format with Black --- docassemble/ALWeaver/interview_generator.py | 39 ++++++++++++--------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/docassemble/ALWeaver/interview_generator.py b/docassemble/ALWeaver/interview_generator.py index ce74131c..681db182 100644 --- a/docassemble/ALWeaver/interview_generator.py +++ b/docassemble/ALWeaver/interview_generator.py @@ -209,7 +209,7 @@ def varname(var_name: str) -> str: def logic_to_code_block(items: List[Union[Dict, str]], indent_level=0) -> str: """Converts a list of logic items to a code block with the given indentation level - + Args: items (list): A list of logic items, of the form ['var0', {'condition': '...', 'children': ['var1']}, 'var2', 'var3', ...] indent_level (int, optional): The indentation level to use. Defaults to 0. Used for recursion. @@ -218,24 +218,26 @@ def logic_to_code_block(items: List[Union[Dict, str]], indent_level=0) -> str: str: The code block, as a string """ code_lines = [] - indent = ' ' * indent_level # Define the indentation (e.g., 2 spaces per level) + indent = " " * indent_level # Define the indentation (e.g., 2 spaces per level) for item in items: if isinstance(item, str): # If the item is a string, it's a variable - code_lines.append(f"{indent}{item}") + code_lines.append(f"{indent}{item}") elif isinstance(item, dict): # If the item is a dictionary, it's a condition # Add the condition line with the current indentation - condition_line = item['condition'] - if not condition_line.startswith('if '): - condition_line = 'if ' + condition_line # Add 'if' if it's not already there - if not condition_line.endswith(':'): - condition_line += ':' + condition_line = item["condition"] + if not condition_line.startswith("if "): + condition_line = ( + "if " + condition_line + ) # Add 'if' if it's not already there + if not condition_line.endswith(":"): + condition_line += ":" code_lines.append(f"{indent}{condition_line}") # Recursively process the children with increased indentation - children_code = logic_to_code_block(item['children'], indent_level + 1) + children_code = logic_to_code_block(item["children"], indent_level + 1) code_lines.append(children_code) - return '\n'.join(code_lines) + return "\n".join(code_lines) class DAFieldGroup(Enum): @@ -1377,6 +1379,7 @@ def interview_order_list( return list(more_itertools.unique_everseen(logic_list)) + class DADataType(Enum): TEXT = "text" AREA = "area" @@ -1396,6 +1399,7 @@ class DADataType(Enum): COMBOBOX = "combobox" CHECKBOXES = "checkboxes" + @dataclass class Field: label: Optional[str] = None @@ -1409,6 +1413,7 @@ class Field: step: Optional[int] = None required: Optional[bool] = None + @dataclass class Screen: continue_button_field: Optional[str] = None @@ -1965,7 +1970,7 @@ def create_questions_from_screen_list(self, screen_list: List[Screen]): new_screen.subquestion_text = screen.get("subquestion", "") for field in screen.get("fields", []): new_field = new_screen.field_list.appendObject() - + if field.get("label") and field.get("field"): new_field.variable = field.get("field") new_field.label = field.get("label") @@ -1974,7 +1979,7 @@ def create_questions_from_screen_list(self, screen_list: List[Screen]): new_field.variable = first_item[1] new_field.label = first_item[0] # For some reason we made the field_type not exactly the same as the datatype in Docassemble - #TODO: consider refactoring this + # TODO: consider refactoring this if field.get("datatype") or field.get("input type"): if field.get("datatype", "") == "radio": new_field.field_type = "multiple choice radio" @@ -1985,7 +1990,9 @@ def create_questions_from_screen_list(self, screen_list: List[Screen]): elif field.get("datatype", "") == "combobox": new_field.field_type = "multiple choice combobox" else: - new_field.field_type = field.get("datatype", field.get("input type", "text")) + new_field.field_type = field.get( + "datatype", field.get("input type", "text") + ) else: new_field.field_type = "text" if field.get("maxlength"): @@ -2004,7 +2011,6 @@ def create_questions_from_screen_list(self, screen_list: List[Screen]): new_screen.field_list.gathered = True self.questions.gathered = True - def auto_group_fields(self): """ Use FormFyxer to assign fields to screens. @@ -2158,10 +2164,11 @@ def get_fields(document: Union[DAFile, DAFileList]) -> Iterable: text = docx_data.text return get_docx_variables(text) -def get_question_file_variables(screens:List[Screen]) -> List[str]: + +def get_question_file_variables(screens: List[Screen]) -> List[str]: """Extract the fields from a list of screens representing a Docassemble interview, such as might be supplied as an input to the Weaver in JSON format. - + Args: screens (List[Screen]): A list of screens, each represented as a dictionary