diff --git a/app/all_questions/metadata_utils.py b/app/all_questions/metadata_utils.py index f3656de6..3ec28f55 100644 --- a/app/all_questions/metadata_utils.py +++ b/app/all_questions/metadata_utils.py @@ -368,17 +368,22 @@ def build_components_from_page( condition_name = next_config["condition"] condition_config = next(fc for fc in form_conditions if fc["name"] == condition_name) destination = index_of_printed_headers[next_config["path"]]["heading_number"] - condition_value = next( + text_with_coordinators = "" + for condition in [ cc for cc in condition_config["value"]["conditions"] if cc["field"]["name"] == c["name"] - )["value"]["value"] - condition_text = determine_display_value_for_condition( - condition_value, - list_name=c["list"] if "list" in c else None, - form_lists=form_lists, - lang=lang, - ) + ]: + condition_value = condition["value"]["value"] + condition_text = determine_display_value_for_condition( + condition_value, + list_name=c["list"] if "list" in c else None, + form_lists=form_lists, + lang=lang, + ) + if condition.get("coordinator"): + text_with_coordinators += f" {condition.get('coordinator')} " + text_with_coordinators += f"'{condition_text}'" text.append( - f"If '{condition_text}', go to {destination}" + f"If {text_with_coordinators}, go to {destination}" if lang == "en" else (f"Os '{condition_text}', ewch i {destination}") ) diff --git a/app/db/queries/application.py b/app/db/queries/application.py index 3bcfde6e..cbe7496d 100644 --- a/app/db/queries/application.py +++ b/app/db/queries/application.py @@ -32,16 +32,6 @@ def get_form_for_component(component: Component) -> Form: return form -def get_template_page_by_display_path(display_path: str) -> Page: - page = ( - db.session.query(Page) - .where(Page.display_path == display_path) - .where(Page.is_template == True) # noqa:E712 - .one_or_none() - ) - return page - - def get_form_by_id(form_id: str) -> Form: form = db.session.query(Form).where(Form.form_id == form_id).one_or_none() return form @@ -62,6 +52,11 @@ def get_list_by_id(list_id: str) -> Lizt: return lizt +def get_list_by_name(list_name: str) -> Lizt: + lizt = db.session.query(Lizt).filter_by(name=list_name).first() + return lizt + + def _initiate_cloned_component(to_clone: Component, new_page_id=None, new_theme_id=None): clone = Component(**to_clone.as_dict()) @@ -746,3 +741,22 @@ def move_form_up(section_id, form_index_to_move_up: int): section.forms = swap_elements_in_list(section.forms, list_index_to_move_up, list_index_to_move_up - 1) db.session.commit() + + +def insert_list(list_config: dict, do_commit: bool = True) -> Lizt: + new_list = Lizt( + is_template=True, + name=list_config.get("name"), + title=list_config.get("title"), + type=list_config.get("type"), + items=list_config.get("items"), + ) + try: + db.session.add(new_list) + except Exception as e: + print(e) + raise e + if do_commit: + db.session.commit() + db.session.flush() # flush to get the list id + return new_list diff --git a/app/export_config/generate_form.py b/app/export_config/generate_form.py index b08e0036..31bc9d34 100644 --- a/app/export_config/generate_form.py +++ b/app/export_config/generate_form.py @@ -1,4 +1,5 @@ import copy +from dataclasses import asdict from app.db.models import Component from app.db.models import Form @@ -6,7 +7,7 @@ from app.db.models.application_config import READ_ONLY_COMPONENTS from app.db.models.application_config import ComponentType from app.db.queries.application import get_list_by_id -from app.db.queries.application import get_template_page_by_display_path +from app.shared.data_classes import ConditionValue BASIC_FORM_STRUCTURE = { "startPage": None, @@ -43,34 +44,26 @@ def build_conditions(component: Component) -> list: """ results = [] for condition in component.conditions: - condition_entry = { - "field": { - "name": component.runner_component_name, - "type": component.type.value, - "display": component.title, - }, - "operator": condition["operator"], - "value": condition["value"], - } - - # Add 'coordinator' only if it exists - if condition.get("coordinator"): - condition_entry["coordinator"] = condition.get("coordinator") - - if condition["name"] in [c["name"] for c in results]: - # If this condition already exists, add it to the existing condition - existing_condition = next(c for c in results if c["name"] == condition["name"]) - existing_condition["value"]["conditions"].append(condition_entry) - continue - result = { "displayName": condition["display_name"], "name": condition["name"], - "value": { - "name": condition["display_name"], - "conditions": [condition_entry], - }, + "value": asdict( + ConditionValue( + name=condition["value"]["name"], + conditions=[], + ) + ), } + for sc in condition["value"]["conditions"]: + sub_condition = { + "field": sc["field"], + "operator": sc["operator"], + "value": sc["value"], + } + # only add coordinator if it exists + if "coordinator" in sc and sc.get("coordinator") is not None: + sub_condition["coordinator"] = sc.get("coordinator", None) + result["value"]["conditions"].append(sub_condition) results.append(result) @@ -118,19 +111,12 @@ def build_component(component: Component) -> dict: return built_component -def build_page(page: Page = None, page_display_path: str = None) -> dict: +def build_page(page: Page = None) -> dict: """ - Builds the form runner JSON structure for the supplied page. If that page is None, retrieves a template - page with the display_path matching page_display_path. - - This accounts for conditional logic where the destination target will be the display path of a template - page, but that page does not actually live in the main hierarchy as branching logic uses a fixed set of - conditions at this stage. + Builds the form runner JSON structure for the supplied page. Then builds all the components on this page and adds them to the page json structure """ - if not page: - page = get_template_page_by_display_path(page_display_path) built_page = copy.deepcopy(BASIC_PAGE_STRUCTURE) built_page.update( { @@ -173,26 +159,12 @@ def build_navigation(partial_form_json: dict, input_pages: list[Page]) -> dict: for component in page.components: if not component.conditions: continue - form_json_conditions = build_conditions(component) has_conditions = True + form_json_conditions = build_conditions(component) partial_form_json["conditions"].extend(form_json_conditions) for condition in component.conditions: - if condition["destination_page_path"] == "CONTINUE": - destination_path = f"/{next_path}" - else: - destination_path = f"/{condition['destination_page_path'].lstrip('/')}" - # TODO No longer needed since db schema change? - # If this points to a pre-built page flow, add that in now (it won't be in the input) - # if ( - # destination_path not in [page["path"] for page in partial_form_json["pages"]] - # and not destination_path == "/summary" - # ): - # sub_page = build_page(page_display_path=destination_path[1:]) - # if not sub_page.get("next", None): - # sub_page["next"] = [{"path": f"/{next_path}"}] - - # partial_form_json["pages"].append(sub_page) + destination_path = f"/{condition['destination_page_path'].lstrip('/')}" this_page_in_results["next"].append( { @@ -245,20 +217,26 @@ def build_start_page(content: str, form: Form) -> dict: "title": form.name_in_apply_json["en"], "path": f"/intro-{human_to_kebab_case(form.name_in_apply_json['en'])}", "controller": "./pages/start.js", - "next": [{"path": f"/{form.pages[0].display_path}"}], } ) - ask_about = "

We will ask you about:

" + ask_about = None + if len(form.pages) > 0: + ask_about = '

We will ask you about:

" + start_page.update( + { + "next": [{"path": f"/{form.pages[0].display_path}"}], + } + ) start_page["components"].append( { "name": "start-page-content", "options": {}, "type": "Html", - "content": f"

{content}

{ask_about}", + "content": f'

{content or ""}

{ask_about or ""}', "schema": {}, } ) diff --git a/app/import_config/files_to_import/coinditions.json b/app/import_config/files_to_import/coinditions.json new file mode 100644 index 00000000..c476eb9d --- /dev/null +++ b/app/import_config/files_to_import/coinditions.json @@ -0,0 +1,183 @@ +{ + "metadata": {}, + "startPage": "/first-page", + "pages": [ + { + "title": "First page", + "path": "/first-page", + "components": [ + { + "name": "YFTKhr", + "options": {}, + "type": "RadiosField", + "title": "org type", + "list": "PaUvtG" + } + ], + "next": [ + { + "path": "/org-type-a", + "condition": "GDqNNS" + }, + { + "path": "/org-type-b", + "condition": "aTqAcd" + }, + { + "path": "/org-type-c", + "condition": "LQOPpJ" + } + ] + }, + { + "title": "Summary", + "path": "/summary", + "controller": "./pages/summary.js", + "components": [] + }, + { + "path": "/org-type-a", + "title": "Org Type A", + "components": [], + "next": [ + { + "path": "/summary" + } + ] + }, + { + "path": "/org-type-b", + "title": "Org type B", + "components": [], + "next": [ + { + "path": "/summary" + } + ] + }, + { + "path": "/org-type-c", + "title": "Org type C", + "components": [], + "next": [ + { + "path": "/summary" + } + ] + } + ], + "lists": [ + { + "title": "org type", + "name": "PaUvtG", + "type": "string", + "items": [ + { + "text": "A", + "value": "A" + }, + { + "text": "B", + "value": "B" + }, + { + "text": "C1", + "value": "C1" + }, + { + "text": "C2", + "value": "C2" + } + ] + } + ], + "sections": [], + "conditions": [ + { + "displayName": "org type a", + "name": "GDqNNS", + "value": { + "name": "org type a", + "conditions": [ + { + "field": { + "name": "YFTKhr", + "type": "RadiosField", + "display": "org type" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "A", + "display": "A" + } + } + ] + } + }, + { + "displayName": "org type b", + "name": "aTqAcd", + "value": { + "name": "org type b", + "conditions": [ + { + "field": { + "name": "YFTKhr", + "type": "RadiosField", + "display": "org type" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "B", + "display": "B" + } + } + ] + } + }, + { + "displayName": "org type c", + "name": "LQOPpJ", + "value": { + "name": "org type c", + "conditions": [ + { + "field": { + "name": "YFTKhr", + "type": "RadiosField", + "display": "org type" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "C1", + "display": "C1" + } + }, + { + "coordinator": "or", + "field": { + "name": "YFTKhr", + "type": "RadiosField", + "display": "org type" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "C2", + "display": "C2" + } + } + ] + } + } + ], + "fees": [], + "outputs": [], + "version": 2, + "skipSummary": false, + "feeOptions": {}, + "markAsComplete": false +} diff --git a/app/import_config/files_to_import/organisation-information-cof.json b/app/import_config/files_to_import/organisation-information-cof.json new file mode 100644 index 00000000..98ca88c7 --- /dev/null +++ b/app/import_config/files_to_import/organisation-information-cof.json @@ -0,0 +1,1143 @@ +{ + "metadata": {}, + "startPage": "/organisation-information", + "backLinkText": "Go back to application overview", + "pages": [ + { + "title": "Organisation information", + "path": "/organisation-information", + "components": [ + { + "name": "orUXQc", + "options": {}, + "type": "Para", + "content": "\n\n
\n\n
", + "schema": {} + } + ], + "next": [ + { + "path": "/organisation-names" + } + ], + "controller": "./pages/start.js" + }, + { + "path": "/organisation-names", + "title": "Organisation names", + "components": [ + { + "name": "WWWWxy", + "options": { + "classes": "govuk-!-width-full" + }, + "type": "TextField", + "title": "Your expression of interest (EOI) application reference", + "hint": "This was included in the email we sent you to confirm your successful EOI.\n

For example, 'COF-EOI-##-######'

", + "schema": {} + }, + { + "name": "YdtlQZ", + "options": { + "classes": "govuk-!-width-full" + }, + "type": "TextField", + "title": "Organisation name", + "hint": "This must match your registered legal organisation name", + "schema": {} + }, + { + "name": "iBCGxY", + "options": {}, + "type": "YesNoField", + "title": "Does your organisation use any other names?", + "schema": {} + } + ], + "next": [ + { + "path": "/alternative-names-of-your-organisation", + "condition": "RzJfsD" + }, + { + "path": "/purpose-and-activities", + "condition": "IshRJh" + } + ], + "section": "JBqDtK" + }, + { + "path": "/alternative-names-of-your-organisation", + "title": "Alternative names of your organisation", + "components": [ + { + "name": "XFvKwZ", + "options": {}, + "type": "Html", + "content": "
Alternative names of your organisation", + "schema": {} + }, + { + "name": "PHFkCs", + "options": { + "classes": "govuk-!-width-full", + "hideTitle": true + }, + "type": "TextField", + "title": "Alternative names of your organisation", + "hint": "", + "schema": {} + }, + { + "name": "QgNhXX", + "options": { + "hideTitle": true, + "required": false, + "classes": "govuk-!-width-full" + }, + "type": "TextField", + "title": "Alternative names of your organisation - Alternative name 2 ", + "hint": "", + "schema": {} + }, + { + "name": "XCcqae", + "options": { + "hideTitle": true, + "required": false, + "classes": "govuk-!-width-full" + }, + "type": "TextField", + "title": "Alternative names of your organisation - Alternative name 3 ", + "hint": "", + "schema": {} + }, + { + "name": "yiAMdU", + "options": {}, + "type": "Html", + "content": "
", + "schema": {} + } + ], + "next": [ + { + "path": "/purpose-and-activities" + } + ], + "section": "JBqDtK" + }, + { + "path": "/purpose-and-activities", + "title": "Purpose and activities", + "components": [ + { + "name": "emVGxS", + "options": { + "maxWords": "500" + }, + "type": "FreeTextField", + "title": "What is your organisation's main purpose?", + "schema": {}, + "hint": "This is what the organisation was set up to achieve." + }, + { + "name": "tgnZof", + "options": {}, + "type": "Html", + "content": "
Tell us about your organisation's main activities\n\n
Include any activities you undertake in order to achieve the organisation's purpose.
\n\n

You must list at least one, and can include up to three.

", + "schema": {} + }, + { + "name": "btTtIb", + "options": { + "maxWords": "500", + "hideTitle": true + }, + "type": "FreeTextField", + "title": "Tell us about your organisation's main activities", + "hint": "", + "schema": {} + }, + { + "name": "SkocDi", + "options": { + "hideTitle": true, + "required": false, + "optionalText": false, + "maxWords": "500" + }, + "type": "FreeTextField", + "title": "Tell us about your organisation's main activities - Activity 2 ", + "hint": "", + "schema": {} + }, + { + "name": "CNeeiC", + "options": { + "hideTitle": true, + "maxWords": "500", + "required": false + }, + "type": "FreeTextField", + "title": "Tell us about your organisation's main activities - Activity 3 ", + "hint": "", + "schema": {} + }, + { + "name": "DhpcVW", + "options": {}, + "type": "Html", + "content": "
", + "schema": {} + }, + { + "name": "BBlCko", + "options": {}, + "type": "YesNoField", + "title": "Have you delivered projects like this before?", + "schema": {} + } + ], + "next": [ + { + "path": "/previous-projects-similar-to-this-one", + "condition": "iUHNKI" + }, + { + "path": "/how-your-organisation-is-classified", + "condition": "RzrXoE" + } + ], + "section": "JBqDtK" + }, + { + "path": "/previous-projects-similar-to-this-one", + "title": "Previous projects similar to this one", + "components": [ + { + "name": "SXfYwl", + "options": {}, + "type": "Html", + "content": "
Describe your previous projects", + "schema": {} + }, + { + "name": "wxCszQ", + "options": { + "maxWords": "500", + "hideTitle": true + }, + "type": "FreeTextField", + "title": "Describe your previous projects", + "hint": "", + "schema": {} + }, + { + "name": "QJFQgi", + "options": { + "maxWords": "500", + "hideTitle": true, + "required": false + }, + "type": "FreeTextField", + "title": "Describe your previous projects - Project 2 ", + "hint": "", + "schema": {} + }, + { + "name": "DGNWqE", + "options": { + "maxWords": "500", + "hideTitle": true, + "required": false + }, + "type": "FreeTextField", + "title": "Describe your previous projects - Project 3 ", + "hint": "", + "schema": {} + }, + { + "name": "lsKMnV", + "options": {}, + "type": "Html", + "content": "
", + "schema": {} + } + ], + "next": [ + { + "path": "/how-your-organisation-is-classified" + } + ], + "section": "JBqDtK" + }, + { + "path": "/how-your-organisation-is-classified", + "title": "How your organisation is classified", + "components": [ + { + "name": "lajFtB", + "options": {}, + "type": "RadiosField", + "title": "Type of organisation", + "hint": "Select one option", + "list": "ATpRJn", + "values": { + "type": "listRef" + }, + "schema": {} + } + ], + "next": [ + { + "path": "/how-your-organisation-is-classified-other", + "condition": "HkIMoI" + }, + { + "path": "/company-registration-details", + "condition": "mNExjy" + }, + { + "path": "/charity-registration-details", + "condition": "EfGsOh" + }, + { + "path": "/trading-subsidiaries", + "condition": "zZSerd" + } + ], + "section": "JBqDtK" + }, + { + "path": "/how-your-organisation-is-classified-other", + "title": "How your organisation is classified", + "components": [ + { + "name": "plmwJv", + "options": { + "classes": "govuk-input--width-20" + }, + "type": "TextField", + "title": "Type of organisation (Other)", + "schema": {} + } + ], + "next": [ + { + "path": "/registration-details" + } + ], + "section": "JBqDtK" + }, + { + "path": "/registration-details", + "title": "Registration details", + "components": [ + { + "name": "GvPSna", + "options": {}, + "type": "RadiosField", + "title": "Which regulatory body is your company registered with?", + "hint": "Select one option", + "list": "jcNxEX", + "values": { + "type": "listRef" + }, + "schema": {} + } + ], + "next": [ + { + "path": "/about-your-organisation-eBkQGy", + "condition": "BiTnsS" + }, + { + "path": "/trading-subsidiaries" + } + ], + "section": "JBqDtK" + }, + { + "path": "/charity-registration-details", + "title": "Charity registration details", + "components": [ + { + "name": "aHIGbK", + "options": { + "classes": "govuk-!-width-two-thirds" + }, + "type": "NumberField", + "title": "Charity number ", + "schema": {} + } + ], + "next": [ + { + "path": "/trading-subsidiaries" + } + ], + "section": "JBqDtK" + }, + { + "path": "/trading-subsidiaries", + "title": "Trading subsidiaries", + "components": [ + { + "name": "DwfHtk", + "options": {}, + "type": "YesNoField", + "title": "Is your organisation a trading subsidiary of a parent company?", + "schema": {} + } + ], + "next": [ + { + "path": "/parent-organisation-details", + "condition": "UlBxQv" + }, + { + "path": "/organisation-address", + "condition": "kcWadF" + } + ], + "section": "JBqDtK" + }, + { + "path": "/parent-organisation-details", + "title": "Parent organisation details", + "components": [ + { + "name": "MPNlZx", + "options": { + "classes": "govuk-!-width-full" + }, + "type": "TextField", + "title": "Name of parent organisation", + "schema": {} + }, + { + "name": "MyiYMw", + "options": {}, + "type": "DatePartsField", + "title": "Date parent organisation was established", + "hint": "For example, 27 3 2007", + "schema": {} + } + ], + "next": [ + { + "path": "/organisation-address" + } + ], + "section": "JBqDtK" + }, + { + "path": "/organisation-address", + "title": "Organisation address", + "components": [ + { + "name": "ZQolYb", + "options": {}, + "type": "UkAddressField", + "title": "Organisation address", + "schema": {} + }, + { + "name": "zsoLdf", + "options": {}, + "type": "YesNoField", + "title": "Is your correspondence address different to the organisation address?", + "schema": {} + }, + { + "name": "zyWOmY", + "options": {}, + "type": "Html", + "content": "
Website and social media
For example, your company's Facebook, Instagram or Twitter accounts (if applicable)

", + "schema": {} + }, + { + "name": "FhbaEy", + "options": { + "classes": "govuk-!-width-full", + "hideTitle": true + }, + "type": "WebsiteField", + "title": "Website and social media", + "hint": "\n", + "schema": {} + }, + { + "name": "FcdKlB", + "options": { + "hideTitle": true, + "required": false, + "classes": "govuk-!-width-full" + }, + "type": "WebsiteField", + "title": "Website and social media - Link or username 2", + "hint": "", + "schema": {} + }, + { + "name": "BzxgDA", + "options": { + "hideTitle": true, + "required": false, + "classes": "govuk-!-width-full" + }, + "type": "WebsiteField", + "title": "Website and social media - Link or username 3", + "hint": "", + "schema": {} + }, + { + "name": "vuutkz", + "options": {}, + "type": "Html", + "content": "
", + "schema": {} + } + ], + "next": [ + { + "path": "/correspondence-address", + "condition": "fvQqsO" + }, + { + "path": "/joint-applications", + "condition": "MnYndb" + } + ], + "section": "JBqDtK" + }, + { + "path": "/correspondence-address", + "title": "Correspondence address", + "components": [ + { + "name": "VhkCbM", + "options": {}, + "type": "UkAddressField", + "title": "Correspondence address", + "schema": {} + } + ], + "next": [ + { + "path": "/joint-applications" + } + ], + "section": "JBqDtK" + }, + { + "path": "/joint-applications", + "title": "Joint applications", + "components": [ + { + "name": "hnLurH", + "options": {}, + "type": "YesNoField", + "title": "Is your application a joint bid in partnership with other organisations?", + "schema": {}, + "values": { + "type": "listRef" + } + } + ], + "next": [ + { + "path": "/partner-organisation-details", + "condition": "gCTswV" + }, + { + "path": "/summary", + "condition": "erqBEX" + } + ], + "section": "JBqDtK" + }, + { + "path": "/partner-organisation-details", + "title": "Partner organisation details", + "components": [ + { + "name": "APSjeB", + "options": { + "classes": "govuk-!-width-full" + }, + "type": "TextField", + "title": "Partner organisation name", + "schema": {} + }, + { + "name": "biTJjF", + "options": {}, + "type": "UkAddressField", + "title": "Partner organisation address", + "schema": {} + }, + { + "name": "IkmvEt", + "options": { + "maxWords": "500", + "hideTitle": true + }, + "type": "FreeTextField", + "title": "Tell us about your partnership and how you plan to work together", + "schema": {}, + "hint": "Tell us about your partnership and how you plan to work togetherIf you are working in partnership with more than one organisation, include their details here" + } + ], + "next": [ + { + "path": "/summary" + } + ], + "section": "JBqDtK" + }, + { + "path": "/summary", + "title": "Check your answers", + "components": [], + "next": [], + "controller": "./pages/summary.js" + }, + { + "path": "/company-registration-details", + "title": "Company registration details", + "components": [ + { + "name": "GlPmCX", + "options": { + "classes": "govuk-!-width-two-thirds" + }, + "type": "TextField", + "title": "Company registration number", + "schema": {} + } + ], + "next": [ + { + "path": "/registration-details" + } + ], + "section": "JBqDtK" + }, + { + "path": "/about-your-organisation-eBkQGy", + "title": "About your organisation", + "components": [ + { + "name": "nITyAP", + "options": {}, + "type": "Html", + "content": "

Registration details

", + "schema": {} + }, + { + "name": "zsbmRx", + "options": { + "classes": "govuk-input--width-20" + }, + "type": "TextField", + "title": "Which regulatory body is your company registered with? (Other)", + "schema": {} + } + ], + "next": [ + { + "path": "/trading-subsidiaries" + } + ], + "section": "JBqDtK" + } + ], + "lists": [ + { + "title": "Type of organisation", + "name": "ATpRJn", + "type": "string", + "items": [ + { + "text": "Charitable incorporated organisation (CIO)", + "value": "CIO" + }, + { + "text": "Co-operative, such as a community benefit society", + "value": "COOP" + }, + { + "text": "Community interest company (CIC)", + "value": "CIC" + }, + { + "text": "Company limited by guarantee", + "value": "CLG" + }, + { + "text": "Scottish charitable incorporated organisation (SCIO)", + "value": "SCIO" + }, + { + "text": "Parish, town or community council", + "value": "PTC" + }, + { + "text": "Trust port", + "value": "Trust port" + }, + { + "text": "Other", + "value": "Other" + } + ] + }, + { + "title": "Which regulatory body is your company registered with", + "name": "jcNxEX", + "type": "string", + "items": [ + { + "text": "Companies House", + "value": "Companies House" + }, + { + "text": "Other", + "value": "Other" + } + ] + }, + { + "title": "Section as Complete", + "name": "dwPYuR", + "type": "string", + "items": [ + { + "text": "Yes, I’ve completed this section", + "value": "Yes" + }, + { + "text": "No, I’ll come back to it later", + "value": "No" + } + ] + } + ], + "sections": [ + { + "name": "JBqDtK", + "title": "Organisation information" + } + ], + "conditions": [ + { + "displayName": "Does your organisation use any other names-yes", + "name": "RzJfsD", + "value": { + "name": "Does your organisation use any other names-yes", + "conditions": [ + { + "field": { + "name": "iBCGxY", + "type": "YesNoField", + "display": "Does your organisation use any other names?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "Does your organisation use any other names-no", + "name": "IshRJh", + "value": { + "name": "Does your organisation use any other names-no", + "conditions": [ + { + "field": { + "name": "iBCGxY", + "type": "YesNoField", + "display": "Does your organisation use any other names?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "Have you delivered projects like this before-main-purpose-yes", + "name": "iUHNKI", + "value": { + "name": "Have you delivered projects like this before-main-purpose-yes", + "conditions": [ + { + "field": { + "name": "BBlCko", + "type": "YesNoField", + "display": "Have you delivered projects like this before?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "Have you delivered projects like this before-main-purpose-no", + "name": "RzrXoE", + "value": { + "name": "Have you delivered projects like this before-main-purpose-no", + "conditions": [ + { + "field": { + "name": "BBlCko", + "type": "YesNoField", + "display": "Have you delivered projects like this before?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "Type of organisation-other", + "name": "HkIMoI", + "value": { + "name": "Type of organisation-other", + "conditions": [ + { + "field": { + "name": "lajFtB", + "type": "RadiosField", + "display": "Type of organisation" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "Other", + "display": "Other" + } + } + ] + } + }, + { + "displayName": "Is your organisation a trading subsidiary of a parent company-yes", + "name": "UlBxQv", + "value": { + "name": "Is your organisation a trading subsidiary of a parent company-yes", + "conditions": [ + { + "field": { + "name": "DwfHtk", + "type": "YesNoField", + "display": "Is your organisation a trading subsidiary of a parent company?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "Is your correspondence address different to the organisation address-yes", + "name": "fvQqsO", + "value": { + "name": "Is your correspondence address different to the organisation address-yes", + "conditions": [ + { + "field": { + "name": "zsoLdf", + "type": "YesNoField", + "display": "Is your correspondence address different to the organisation address?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "Is your correspondence address different to the organisation address-no", + "name": "MnYndb", + "value": { + "name": "Is your correspondence address different to the organisation address-no", + "conditions": [ + { + "field": { + "name": "zsoLdf", + "type": "YesNoField", + "display": "Is your correspondence address different to the organisation address?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "Is your application a joint bid in partnership with another organisation-yes", + "name": "gCTswV", + "value": { + "name": "Is your application a joint bid in partnership with another organisation-yes", + "conditions": [ + { + "field": { + "name": "hnLurH", + "type": "YesNoField", + "display": "Is your application a joint bid in partnership with another organisation?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "Is your application a joint bid in partnership with another organisation-no", + "name": "erqBEX", + "value": { + "name": "Is your application a joint bid in partnership with another organisation-no", + "conditions": [ + { + "field": { + "name": "hnLurH", + "type": "YesNoField", + "display": "Is your application a joint bid in partnership with another organisation?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "Is your organisation a trading subsidiary of a parent company-no", + "name": "kcWadF", + "value": { + "name": "Is your organisation a trading subsidiary of a parent company-no", + "conditions": [ + { + "field": { + "name": "DwfHtk", + "type": "YesNoField", + "display": "Is your organisation a trading subsidiary of a parent company?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "Type of organisation- option(two, three, four)", + "name": "mNExjy", + "value": { + "name": "Type of organisation- option(two, three, four)", + "conditions": [ + { + "field": { + "name": "lajFtB", + "type": "RadiosField", + "display": "Type of organisation" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "COOP", + "display": "COOP" + } + }, + { + "coordinator": "or", + "field": { + "name": "lajFtB", + "type": "RadiosField", + "display": "Type of organisation" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "CIC", + "display": "CIC" + } + }, + { + "coordinator": "or", + "field": { + "name": "lajFtB", + "type": "RadiosField", + "display": "Type of organisation" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "CLG", + "display": "CLG" + } + }, + { + "coordinator": "or", + "field": { + "name": "lajFtB", + "type": "RadiosField", + "display": "Type of organisation" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "Trust port", + "display": "Trust port" + } + } + ] + } + }, + { + "displayName": "Type of organisation-option(one, five)", + "name": "EfGsOh", + "value": { + "name": "Type of organisation-option(one, five)", + "conditions": [ + { + "field": { + "name": "lajFtB", + "type": "RadiosField", + "display": "Type of organisation" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "CIO", + "display": "CIO" + } + }, + { + "coordinator": "or", + "field": { + "name": "lajFtB", + "type": "RadiosField", + "display": "Type of organisation" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "SCIO", + "display": "SCIO" + } + } + ] + } + }, + { + "displayName": "Which regulatory body is your company registered with-other", + "name": "BiTnsS", + "value": { + "name": "Which regulatory body is your company registered with-other", + "conditions": [ + { + "field": { + "name": "GvPSna", + "type": "RadiosField", + "display": "Which regulatory body is your company registered with?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "Other", + "display": "Other" + } + } + ] + } + }, + { + "displayName": "Type of organisation-parish", + "name": "zZSerd", + "value": { + "name": "Type of organisation-parish", + "conditions": [ + { + "field": { + "name": "lajFtB", + "type": "RadiosField", + "display": "Type of organisation" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "PTC", + "display": "PTC" + } + } + ] + } + } + ], + "fees": [], + "outputs": [ + { + "name": "update-form", + "title": "Update form in application store", + "type": "savePerPage", + "outputConfiguration": { + "savePerPageUrl": "True" + } + } + ], + "version": 2, + "skipSummary": false, + "name": "Apply for funding to save an asset in your community", + "feedback": { + "feedbackForm": false, + "url": "" + }, + "phaseBanner": { + "phase": "beta" + } +} diff --git a/app/import_config/load_form_json.py b/app/import_config/load_form_json.py index b3d47966..f5f768f7 100644 --- a/app/import_config/load_form_json.py +++ b/app/import_config/load_form_json.py @@ -3,6 +3,10 @@ import json import os import sys +from uuid import UUID + +from app.db.queries.application import get_list_by_name +from app.db.queries.application import insert_list sys.path.insert(1, ".") from dataclasses import asdict # noqa:E402 @@ -12,50 +16,64 @@ from app.db.models import Component # noqa:E402 from app.db.models import ComponentType # noqa:E402 from app.db.models import Form # noqa:E402 -from app.db.models import Lizt # noqa:E402 from app.db.models import Page # noqa:E402 from app.shared.data_classes import Condition # noqa:E402 +from app.shared.data_classes import ConditionValue # noqa:E402 from app.shared.helpers import find_enum # noqa:E402 -def add_conditions_to_components(db, page, conditions): +def _build_condition(condition_data, destination_page_path) -> Condition: + sub_conditions = [] + for c in condition_data["value"]["conditions"]: + sc = { + "field": c["field"], + "value": c["value"], + "operator": c["operator"], + } + if "coordinator" in c and c.get("coordinator"): + sc["coordinator"] = c.get("coordinator") + sub_conditions.append(sc) + condition_value = ConditionValue(name=condition_data["displayName"], conditions=sub_conditions) + result = Condition( + name=condition_data["name"], + display_name=condition_data["displayName"], + value=condition_value, + destination_page_path=destination_page_path, + ) + return result + + +def _get_component_by_runner_name(db, runner_component_name): + + return db.session.query(Component).filter(Component.runner_component_name == runner_component_name).first() + + +def add_conditions_to_components(db, page: dict, conditions: dict): # Convert conditions list to a dictionary for faster lookup conditions_dict = {cond["name"]: cond for cond in conditions} # Initialize a cache for components to reduce database queries components_cache = {} - for path in page["next"]: - if "condition" in path: - target_condition_name = path["condition"] - # Use the conditions dictionary for faster lookup - if target_condition_name in conditions_dict: - condition_data = conditions_dict[target_condition_name] - for condition in condition_data["value"]["conditions"]: - condition_name = condition_data["name"] - condition_display_name = condition_data["displayName"] - runner_component_name = condition["field"]["name"] + if "next" in page: + for path in page["next"]: + if "condition" in path: + target_condition_name = path["condition"] + # Use the conditions dictionary for faster lookup + if target_condition_name in conditions_dict: + condition_data = conditions_dict[target_condition_name] + # for condition in condition_data["value"]["conditions"]: + runner_component_name = condition_data["value"]["conditions"][0]["field"]["name"] # Use the cache to reduce database queries if runner_component_name not in components_cache: - component_to_update = ( - db.session.query(Component) - .filter(Component.runner_component_name == runner_component_name) - .first() - ) + component_to_update = _get_component_by_runner_name(db, runner_component_name) components_cache[runner_component_name] = component_to_update else: component_to_update = components_cache[runner_component_name] # Create a new Condition instance with a different variable name - new_condition = Condition( - name=condition_name, - display_name=condition_display_name, - value=condition["value"], - coordinator=condition.get("coordinator", None), - operator=condition.get("operator", None), - destination_page_path=path["path"], - ) + new_condition = _build_condition(condition_data, destination_page_path=path["path"]) # Add the new condition to the conditions list of the component to update if component_to_update.conditions: @@ -64,34 +82,27 @@ def add_conditions_to_components(db, page, conditions): component_to_update.conditions = [asdict(new_condition)] +def _find_list_and_create_if_not_existing(list_name: str, all_lists_in_form: list[dict]) -> UUID: + list_from_form = next(li for li in all_lists_in_form if li["name"] == list_name) + + # Check if this list already exists in the database + existing_list = get_list_by_name(list_name=list_name) + if existing_list: + return existing_list.list_id + + # If it doesn't, insert new list + new_list = insert_list(do_commit=False, list_config={"is_template": True, **list_from_form}) + return new_list.list_id + + def insert_component_as_template(component, page_id, page_index, lizts): # if component has a list, insert the list into the database list_id = None component_list = component.get("list", None) if component_list: - for li in lizts: - if li["name"] == component_list: - # Check if the list already exists - existing_list = db.session.query(Lizt).filter_by(name=li.get("name")).first() - if existing_list is None: - new_list = Lizt( - is_template=True, - name=li.get("name"), - title=li.get("title"), - type=li.get("type"), - items=li.get("items"), - ) - try: - db.session.add(new_list) - except Exception as e: - print(e) - raise e - db.session.flush() # flush to get the list id - list_id = new_list.list_id - else: - # If the list already exists, you can use its ID or handle it as needed - list_id = existing_list.list_id - break + list_id = _find_list_and_create_if_not_existing(list_name=component_list, all_lists_in_form=lizts) + + # establish component type component_type = component.get("type", None) if component_type is None or find_enum(ComponentType, component_type) is None: raise ValueError(f"Component type not found: {component_type}") @@ -184,9 +195,8 @@ def insert_form_config(form_config, form_id): def insert_form_as_template(form): - form_name = next(p for p in form["pages"] if p.get("controller") and p.get("controller").endswith("start.js"))[ - "title" - ] + start_page_path = form.get("startPage") + form_name = next(p for p in form["pages"] if p["path"] == start_page_path)["title"] new_form = Form( section_id=None, name_in_apply_json={"en": form_name}, diff --git a/app/shared/data_classes.py b/app/shared/data_classes.py index c58b151d..400c9b6a 100644 --- a/app/shared/data_classes.py +++ b/app/shared/data_classes.py @@ -4,14 +4,26 @@ from typing import Optional +@dataclass +class SubCondition: + field: dict + operator: str + value: dict + coordinator: Optional[str] + + +@dataclass +class ConditionValue: + name: str + conditions: list[SubCondition] + + @dataclass class Condition: name: str display_name: str - value: str - operator: str + value: ConditionValue destination_page_path: str - coordinator: Optional[str] = None @dataclass diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index d9382c85..56b2ead3 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -25,7 +25,7 @@ services: image: postgres container_name: fab-db-dc ports: - - 5432:5432 + - 5435:5432 volumes: - ~/apps/postgres:/var/lib/postgresql/data environment: diff --git a/tests/test_config_export.py b/tests/test_config_export.py index dca3a4b1..ee3f2e42 100644 --- a/tests/test_config_export.py +++ b/tests/test_config_export.py @@ -184,7 +184,7 @@ def test_generate_form_jsons_for_round_valid_input(seed_dynamic_data): "name": "start-page-content", "options": {}, "type": "Html", - "content": "

None

" + "content": '

' "We will ask you about:

", "schema": {}, } diff --git a/tests/test_db.py b/tests/test_db.py index 4979482b..ea6671e9 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -7,13 +7,11 @@ from app.db.models import Form from app.db.models import Fund from app.db.models import Organisation -from app.db.models import Page from app.db.models import Round from app.db.models import Section from app.db.queries.application import delete_form_from_section from app.db.queries.application import delete_section_from_round from app.db.queries.application import get_section_by_id -from app.db.queries.application import get_template_page_by_display_path from app.db.queries.application import move_form_down from app.db.queries.application import move_form_up from app.db.queries.application import move_section_down @@ -216,35 +214,6 @@ def test_get_round_by_id(seed_dynamic_data): assert result.title_json["en"] == "round the first" -@pytest.mark.seed_config( - { - "pages": [ - Page( - page_id=uuid4(), - form_id=None, - display_path="testing_templates_path", - is_template=True, - name_in_apply_json={"en": "Template Path"}, - form_index=0, - ), - Page( - page_id=uuid4(), - form_id=None, - display_path="testing_templates_path", - is_template=False, - name_in_apply_json={"en": "Not Template Path"}, - form_index=0, - ), - ] - } -) -def test_get_template_page_by_display_path(seed_dynamic_data): - - result = get_template_page_by_display_path("testing_templates_path") - assert result - assert result.page_id == seed_dynamic_data["pages"][0].page_id - - section_id = uuid4() diff --git a/tests/test_form_import.py b/tests/test_form_import.py new file mode 100644 index 00000000..78f7ca98 --- /dev/null +++ b/tests/test_form_import.py @@ -0,0 +1,130 @@ +from dataclasses import asdict +from unittest import mock +from uuid import uuid4 + +import pytest + +from app.db.models.application_config import Component +from app.db.models.application_config import Lizt +from app.import_config.load_form_json import _build_condition +from app.import_config.load_form_json import _find_list_and_create_if_not_existing +from app.import_config.load_form_json import add_conditions_to_components +from app.shared.data_classes import Condition +from app.shared.data_classes import ConditionValue +from app.shared.data_classes import SubCondition +from tests.unit_test_data import test_condition_org_type_a +from tests.unit_test_data import test_condition_org_type_c +from tests.unit_test_data import test_form_json_condition_org_type_a +from tests.unit_test_data import test_form_json_condition_org_type_c + + +@pytest.mark.parametrize( + "input_condition,exp_result", + [ + ( + test_form_json_condition_org_type_a, + test_condition_org_type_a, + ), + ( + test_form_json_condition_org_type_c, + test_condition_org_type_c, + ), + ], +) +def test_build_conditions(input_condition, exp_result): + result = _build_condition(condition_data=input_condition, destination_page_path=exp_result.destination_page_path) + assert result == exp_result + + +@pytest.mark.parametrize( + "input_page, input_conditions, exp_condition_count", + [ + ({"next": [{"path": "default-next"}]}, [], 0), + ( + {"next": [{"path": "next-a", "condition": "condition-a"}]}, + [ + asdict( + Condition( + name="condition-a", + display_name="condition a", + destination_page_path="page-b", + value=ConditionValue( + name="condition a", + conditions=[SubCondition(field={"name": "c1"}, operator="is", value={}, coordinator=None)], + ), + ) + ) + ], + 1, + ), + ( + {"next": [{"path": "next-a", "condition": "condition-a"}]}, + [ + asdict( + Condition( + name="condition-a", + display_name="condition a", + destination_page_path="page-b", + value=ConditionValue( + name="condition a", + conditions=[ + SubCondition(field={"name": "c1"}, operator="is", value={}, coordinator=None), + SubCondition(field={"name": "c1"}, operator="is", value={}, coordinator="or"), + ], + ), + ) + ) + ], + 1, + ), + ], +) +def test_add_conditions_to_components(mocker, input_page, input_conditions, exp_condition_count): + mock_component = Component() + mocker.patch("app.import_config.load_form_json._get_component_by_runner_name", return_value=mock_component) + with mock.patch( + "app.import_config.load_form_json._build_condition", + return_value=Condition(name=None, display_name=None, destination_page_path=None, value=None), + ) as mock_build_condition: + add_conditions_to_components(None, input_page, input_conditions) + if exp_condition_count > 0: + assert mock_component.conditions + assert len(mock_component.conditions) == exp_condition_count + assert mock_build_condition.call_count == len(input_conditions) + + +@pytest.mark.parametrize( + "input_list_name,input_all_lists, existing_list", + [ + ("existing-list", [{"name": "existing-list"}], Lizt(list_id=uuid4())), + ], +) +def test_find_list_and_create_existing(mocker, input_list_name, input_all_lists, existing_list): + with ( + mock.patch( + "app.import_config.load_form_json.get_list_by_name", return_value=existing_list + ) as get_list_by_name_mock, + mock.patch( + "app.import_config.load_form_json.insert_list", return_value=Lizt(list_id="new-list-id") + ) as insert_list_mock, + ): + result = _find_list_and_create_if_not_existing(list_name=input_list_name, all_lists_in_form=input_all_lists) + assert result == existing_list.list_id + assert get_list_by_name_mock.called_once_with(list_name=input_list_name) + insert_list_mock.assert_not_called() + + +@pytest.mark.parametrize("input_list_name,input_all_lists, existing_list", [("new-list", [{"name": "new-list"}], None)]) +def test_find_list_and_create_not_existing(mocker, input_list_name, input_all_lists, existing_list): + with ( + mock.patch( + "app.import_config.load_form_json.get_list_by_name", return_value=existing_list + ) as get_list_by_name_mock, + mock.patch( + "app.import_config.load_form_json.insert_list", return_value=Lizt(list_id="new-list-id") + ) as insert_list_mock, + ): + result = _find_list_and_create_if_not_existing(list_name=input_list_name, all_lists_in_form=input_all_lists) + assert result == "new-list-id" + assert get_list_by_name_mock.called_once_with(list_name=input_list_name) + insert_list_mock.called_once() diff --git a/tests/test_generate_form.py b/tests/test_generate_form.py index 72bcb87e..5e00ed4f 100644 --- a/tests/test_generate_form.py +++ b/tests/test_generate_form.py @@ -1,3 +1,6 @@ +from copy import deepcopy +from dataclasses import asdict +from unittest import mock from uuid import uuid4 import pytest @@ -6,14 +9,30 @@ from app.db.models import ComponentType from app.db.models import Lizt from app.db.models import Page +from app.db.models.application_config import Form +from app.export_config.generate_form import build_component from app.export_config.generate_form import build_conditions from app.export_config.generate_form import build_form_json from app.export_config.generate_form import build_lists from app.export_config.generate_form import build_navigation from app.export_config.generate_form import build_page +from app.export_config.generate_form import build_start_page from app.export_config.generate_form import human_to_kebab_case from tests.unit_test_data import mock_c_1 +from tests.unit_test_data import mock_c_2 from tests.unit_test_data import mock_form_1 +from tests.unit_test_data import test_condition_org_type_a +from tests.unit_test_data import test_condition_org_type_b +from tests.unit_test_data import test_condition_org_type_c +from tests.unit_test_data import test_form_json_condition_org_type_a +from tests.unit_test_data import test_form_json_condition_org_type_b +from tests.unit_test_data import test_form_json_condition_org_type_c +from tests.unit_test_data import test_form_json_page_org_type_a +from tests.unit_test_data import test_form_json_page_org_type_b +from tests.unit_test_data import test_form_json_page_org_type_c +from tests.unit_test_data import test_page_object_org_type_a +from tests.unit_test_data import test_page_object_org_type_b +from tests.unit_test_data import test_page_object_org_type_c @pytest.mark.parametrize( @@ -80,122 +99,6 @@ def test_build_lists(mocker, pages, exp_result): assert results[0]["name"] == "greetings_list" -mock_lookups = { - "organisation-name": "Organisation Name", - "organisation-single-name": "Organisation Name", -} -mock_components = [ - { - "id": "reuse-charitable-objects", - "json_snippet": { - "options": {"hideTitle": True, "maxWords": "500"}, - "type": "FreeTextField", - "title": "What are your organisation’s charitable objects?", - "hint": "You can find this in your organisation's governing document.", - }, - }, - { - "id": "reuse-organisation-name", - "json_snippet": { - "options": {"hideTitle": False, "classes": "govuk-!-width-full"}, - "type": "TextField", - "title": "Organisation name", - "hint": "This must match your registered legal organisation name", - "schema": {}, - }, - }, - { - "id": "reuse_organisation_other_names_yes_no", - "json_snippet": { - "options": {}, - "type": "YesNoField", - "title": "Does your organisation use any other names?", - "schema": {}, - }, - "conditions": [ - { - "name": "organisation_other_names_no", - "value": "false", - "operator": "is", - "destination_page": "CONTINUE", - }, - { - "name": "organisation_other_names_yes", - "value": "true", - "operator": "is", - "destination_page": "alternative-organisation-name", - }, - ], - }, - { - "id": "reuse-alt-org-name-1", - "json_snippet": { - "options": {"classes": "govuk-input"}, - "type": "TextField", - "title": "Alternative name 1", - "schema": {}, - }, - }, - { - "id": "reuse-alt-org-name-2", - "json_snippet": { - "options": {"required": False, "classes": "govuk-input"}, - "type": "TextField", - "title": "Alternative name 2", - "schema": {}, - }, - }, - { - "id": "reuse-alt-org-name-3", - "json_snippet": { - "options": {"required": False, "classes": "govuk-input"}, - "type": "TextField", - "title": "Alternative name 3", - "schema": {}, - }, - }, -] -mock_pages = [ - { - "id": "organisation-single-name", - "builder_display_name": "Single Organisation Name", - "form_display_name": "Organisation Name", - "component_names": [ - "reuse-organisation-name", - ], - "show_in_builder": True, - }, - { - "id": "organisation-name", - "builder_display_name": "Organisation Name, with Alternatives", - "form_display_name": "Organisation Name", - "component_names": [ - "reuse-organisation-name", - "reuse_organisation_other_names_yes_no", - ], - "show_in_builder": True, - }, - { - "id": "organisation-charitable-objects", - "builder_display_name": "Organisation Charitable objects", - "form_display_name": "Organisation charitable objects", - "component_names": ["reuse-charitable-objects"], - "show_in_builder": True, - }, - { - "id": "alternative-organisation-name", - "builder_display_name": "Alternative Organisation Names", - "form_display_name": "Alternative names of your organisation", - "component_names": [ - "reuse-alt-org-name-1", - "reuse-alt-org-name-2", - "reuse-alt-org-name-3", - ], - "show_in_builder": False, - }, -] - - @pytest.mark.parametrize( "input_page, exp_result", [ @@ -227,11 +130,68 @@ def test_build_lists(mocker, pages, exp_result): ) ], ) -def test_build_page(mocker, input_page, exp_result): +def test_build_page_and_components(input_page, exp_result): result = build_page(input_page) assert result == exp_result +def test_build_page_controller_specified(): + input_page: Page = Page(name_in_apply_json={"en": "Name in json"}, controller="startPageController") + result_page = build_page(page=input_page) + assert result_page + assert result_page["controller"] == "startPageController" + + +def test_build_page_controller_not_specified(): + input_page: Page = Page(name_in_apply_json={"en": "Name in json"}, controller=None) + result_page = build_page(page=input_page) + assert result_page + assert ("controller" in result_page) is False + + +@pytest.mark.parametrize( + "input_page", + [ + ( + Page( + page_id=uuid4(), + form_id=uuid4(), + display_path="organisation-single-name", + name_in_apply_json={"en": "Organisation Name"}, + form_index=1, + components=[mock_c_1], + ) + ), + ( + Page( + page_id=uuid4(), + form_id=uuid4(), + display_path="organisation-single-name", + name_in_apply_json={"en": "Organisation Name"}, + form_index=1, + components=[mock_c_1, mock_c_2], + ) + ), + ( + Page( + page_id=uuid4(), + form_id=uuid4(), + display_path="organisation-single-name", + name_in_apply_json={"en": "Organisation Name"}, + form_index=1, + components=[], + ) + ), + ], +) +def test_build_page(input_page): + with mock.patch("app.export_config.generate_form.build_component", new_value=lambda c: c) as mock_build_component: + result_page = build_page(input_page) + assert result_page + assert mock_build_component.call_count == len(input_page.components) + assert len(result_page["components"]) == len(input_page.components) + + id = uuid4() id2 = uuid4() @@ -239,104 +199,129 @@ def test_build_page(mocker, input_page, exp_result): @pytest.mark.parametrize( "input_component, exp_results", [ + # single condition ( Component( component_id=id, - title="test_title", + title="org type", type=ComponentType.TEXT_FIELD, conditions=[ - { - "name": "test_condition", - "display_name": "display name", - "operator": "is", - "value": {"type": "Value", "value": "yes", "display": "yes"}, - "destination_page_path": "./who-knows", - "coordinator": None, - }, + asdict(test_condition_org_type_a), + ], + runner_component_name="org_type", + ), + [test_form_json_condition_org_type_a], + ), + # 2 conditions + ( + Component( + component_id=id2, + title="test_title_2", + type=ComponentType.TEXT_FIELD, + conditions=[ + asdict(test_condition_org_type_a), + asdict(test_condition_org_type_b), ], runner_component_name="test_name", ), [ - { - "displayName": "display name", - "name": "test_condition", - "value": { - "name": "display name", - "conditions": [ - { - "field": {"name": "test_name", "type": "TextField", "display": "test_title"}, - "operator": "is", - "value": {"type": "Value", "value": "yes", "display": "yes"}, - } - ], - }, - } + test_form_json_condition_org_type_a, + test_form_json_condition_org_type_b, ], ), + # single complex condition ( Component( component_id=id2, title="test_title_2", type=ComponentType.TEXT_FIELD, - conditions=[ - { - "name": "test_condition", - "display_name": "display name", - "operator": "is", - "value": {"type": "Value", "value": "yes", "display": "yes"}, - "destination_page_path": "./who-knows", - }, - { - "name": "test_condition2", - "display_name": "display name", - "operator": "is", - "value": {"type": "Value", "value": "no", "display": "no"}, - "destination_page_path": "./who-knows2", - }, - ], + conditions=[asdict(test_condition_org_type_c)], runner_component_name="test_name", ), [ - { - "displayName": "display name", - "name": "test_condition", - "value": { - "name": "display name", - "conditions": [ - { - "field": {"name": "test_name", "type": "TextField", "display": "test_title_2"}, - "operator": "is", - "value": {"type": "Value", "value": "yes", "display": "yes"}, - } - ], - }, - }, - { - "displayName": "display name", - "name": "test_condition2", - "value": { - "name": "display name", - "conditions": [ - { - "field": {"name": "test_name", "type": "TextField", "display": "test_title_2"}, - "operator": "is", - "value": {"type": "Value", "value": "no", "display": "no"}, - } - ], - }, - }, + test_form_json_condition_org_type_c, ], ), ], ) -def test_build_conditions(input_component, exp_results): +def test_build_conditions( + input_component, exp_results, ids=["single condition", "2 conditions", "single condition with coordinator"] +): results = build_conditions(input_component) assert results == exp_results +list_id = uuid4() + + +@pytest.mark.parametrize( + "component_to_build, exp_result", + [ + ( + Component( + component_id=uuid4(), + type=ComponentType.TEXT_FIELD, + title="Test Title", + hint_text="This must be a hint", + page_id=None, + page_index=1, + theme_id=None, + runner_component_name="test-name", + options={ + "hideTitle": False, + "classes": "govuk-!-width-full", + }, + ), + { + "name": "test-name", + "options": { + "hideTitle": False, + "classes": "govuk-!-width-full", + }, + "type": "TextField", + "title": "Test Title", + "hint": "This must be a hint", + "schema": {}, + "metadata": {}, + }, + ), + ( + Component( + component_id=uuid4(), + type=ComponentType.LIST_FIELD, + title="Test Title", + hint_text="This must be a hint", + page_id=None, + page_index=1, + theme_id=None, + runner_component_name="test-name", + options={}, + lizt=Lizt(name="test-list", list_id=list_id), + list_id=list_id, + ), + { + "name": "test-name", + "options": {}, + "type": "List", + "title": "Test Title", + "hint": "This must be a hint", + "schema": {}, + "metadata": {"fund_builder_list_id": str(list_id)}, + "list": "test-list", + "values": {"type": "listRef"}, + }, + ), + ], +) +def test_build_component(component_to_build, exp_result): + result = build_component(component=component_to_build) + assert result == exp_result + + @pytest.mark.parametrize( "input_pages,input_partial_json, exp_next", [ + # Simple flow of 1 page then summary (summary not in input pages) ( [ Page( @@ -375,6 +360,64 @@ def test_build_conditions(input_component, exp_results): "/organisation-single-name": [{"path": "/summary"}], }, ), + # 1 page then summary (summary is in input pages) + ( + [ + Page( + page_id=uuid4(), + form_id=uuid4(), + display_path="organisation-single-name", + name_in_apply_json={"en": "Organisation Name"}, + form_index=1, + default_next_page_id="summary-id", + ), + Page( + page_id="summary-id", + form_id=uuid4(), + display_path="summary-page", + name_in_apply_json={"en": "Summary Page"}, + form_index=1, + controller="summary.js", + ), + ], + { + "conditions": [], + "pages": [ + { + "path": "/organisation-single-name", + "title": "Organisation Name", + "components": [ + { + "name": "reuse-organisation-name", + "options": { + "hideTitle": False, + "classes": "govuk-!-width-full", + }, + "type": "TextField", + "title": "Organisation name", + "hint": "This must match your registered legal organisation name", + "schema": {}, + } + ], + "next": [], + "options": {}, + }, + { + "path": "/summary-page", + "title": "Summary Page", + "components": [], + "next": [], + "options": {}, + "controller": "summary.js", + }, + ], + }, + { + "/organisation-single-name": [{"path": "/summary-page"}], + "/summary-page": [], + }, + ), + # Simple flow of 2 pages then summary ( [ Page( @@ -437,9 +480,37 @@ def test_build_conditions(input_component, exp_results): "/organisation-charitable-objects": [{"path": "/summary"}], }, ), + # Just a summary page + ( + [ + Page( + page_id=uuid4(), + form_id=uuid4(), + display_path="summary", + name_in_apply_json={"en": "Summary"}, + form_index=1, + controller="summary.js", + ) + ], + { + "conditions": [], + "pages": [ + { + "path": "/summary", + "title": "Summary", + "components": [], + "next": [], + "options": {}, + }, + ], + }, + { + "/summary": [], + }, + ), ], ) -def test_build_navigation_no_conditions(mocker, input_partial_json, input_pages, exp_next): +def test_build_navigation_no_conditions(input_partial_json, input_pages, exp_next): results = build_navigation(partial_form_json=input_partial_json, input_pages=input_pages) for page in results["pages"]: @@ -449,161 +520,268 @@ def test_build_navigation_no_conditions(mocker, input_partial_json, input_pages, @pytest.mark.parametrize( - "input_pages,input_partial_json ,exp_next, exp_conditions", + "input_pages,input_partial_json ,exp_next", [ + # One page, 2 possible nexts, both based on defined conditions ( [ Page( page_id=uuid4(), form_id=uuid4(), - display_path="organisation-name", - name_in_apply_json={"en": "Organisation Name"}, + display_path="organisation-type", + name_in_apply_json={"en": "Organisation Type"}, form_index=1, components=[ Component( component_id=id2, - title="test_title_2", - type=ComponentType.TEXT_FIELD, + title="org_type", + type=ComponentType.RADIOS_FIELD, conditions=[ - { - "name": "orgno", - "display_name": "organisation_other_names_no", - "value": {"type": "Value", "value": "no", "display": "no"}, - "destination_page_path": "summary", - "operator": "is", - "coordinator": None, - }, - { - "name": "orgyes", - "display_name": "organisation_other_names_yes", - "operator": "is", - "value": {"type": "Value", "value": "yes", "display": "yes"}, - "destination_page_path": "organisation-alternative-names", - "coordinator": None, - }, + asdict(test_condition_org_type_c), + asdict(test_condition_org_type_b), ], runner_component_name="test_c_1", ) ], ), + test_page_object_org_type_b, + test_page_object_org_type_c, + ], + { + "conditions": [], + "pages": [ + { + "path": "/organisation-type", + "title": "Organisation Type", + "components": [], + "next": [], + "options": {}, + }, + deepcopy(test_form_json_page_org_type_b), + deepcopy(test_form_json_page_org_type_c), + ], + }, + { + "/organisation-type": [ + { + "path": "/org-type-c", + "condition": "org_type_c", + }, + { + "path": "/org-type-b", + "condition": "org_type_b", + }, + ], + "/org-type-b": [{"path": "/summary"}], + "/org-type-c": [{"path": "/summary"}], + }, + ), + # One page, 2 possible nexts, based on a condition and a default (summary) + ( + [ Page( page_id=uuid4(), form_id=uuid4(), - display_path="organisation-alternative-names", - name_in_apply_json={"en": "Organisation Alternative Names"}, + display_path="organisation-type", + name_in_apply_json={"en": "Organisation Type"}, + form_index=1, + default_next_page_id="summary-id", + components=[ + Component( + component_id=id2, + title="org_type", + type=ComponentType.RADIOS_FIELD, + conditions=[ + asdict(test_condition_org_type_b), + ], + runner_component_name="test_c_1", + ) + ], + ), + test_page_object_org_type_b, + Page( + page_id="summary-id", + form_id=uuid4(), + display_path="summary", + name_in_apply_json={"en": "Summary"}, form_index=2, + controller="summary.js", ), ], { "conditions": [], "pages": [ { - "path": "/organisation-name", - "title": "Organisation Name", - "components": [ - { - # "name": "reuse-organisation-name", - # "options": { - # "hideTitle": False, - # "classes": "govuk-!-width-full", - # }, - # "type": "TextField", - # "title": "Organisation name", - # "hint": "This must match your registered legal organisation name", - # "schema": {}, - }, - { - # "name": "reuse_organisation_other_names_yes_no", - # "options": {}, - # "type": "YesNoField", - # "title": "Does your organisation use any other names?", - # "schema": {}, - }, - ], + "path": "/organisation-type", + "title": "Organisation Type", + "components": [], "next": [], "options": {}, }, + deepcopy(test_form_json_page_org_type_b), { - "path": "/organisation-alternative-names", - "title": "Organisation Alternative Names", + "path": "/summary", + "title": "Summary", "components": [], "next": [], "options": {}, + "controller": "summary.js", }, ], }, { - "/organisation-name": [ + "/organisation-type": [ { "path": "/summary", - "condition": "orgno", }, { - "path": "/organisation-alternative-names", - "condition": "orgyes", + "path": "/org-type-b", + "condition": "org_type_b", }, ], - "/organisation-alternative-names": [{"path": "/summary"}], + "/org-type-b": [{"path": "/summary"}], + "/summary": [], }, + ), # One page, 2 possible nexts, based on a condition and a default + ( [ - { - "displayName": "organisation_other_names_no", - "name": "orgno", - "value": { - "name": "organisation_other_names_no", - "conditions": [ - { - "field": { - "name": "test_c_1", - "type": "TextField", - "display": "test_title_2", - }, - "operator": "is", - "value": { - "type": "Value", - "value": "no", - "display": "no", - }, - } - ], + Page( + page_id=uuid4(), + form_id=uuid4(), + display_path="organisation-type", + name_in_apply_json={"en": "Organisation Type"}, + form_index=1, + default_next_page_id="page-2", + components=[ + Component( + component_id=id2, + title="org_type", + type=ComponentType.RADIOS_FIELD, + conditions=[ + asdict(test_condition_org_type_b), + ], + runner_component_name="test_c_1", + ) + ], + ), + test_page_object_org_type_b, + Page( + page_id="page-2", + form_id=uuid4(), + display_path="page_2", + name_in_apply_json={"en": "Page 2"}, + form_index=2, + ), + ], + { + "conditions": [], + "pages": [ + { + "path": "/organisation-type", + "title": "Organisation Type", + "components": [], + "next": [], + "options": {}, }, - }, - { - "displayName": "organisation_other_names_yes", - "name": "orgyes", - "value": { - "name": "organisation_other_names_yes", - "conditions": [ - { - "field": { - "name": "test_c_1", - "type": "TextField", - "display": "test_title_2", - }, - "operator": "is", - "value": { - "type": "Value", - "value": "yes", - "display": "yes", - }, - } - ], + deepcopy(test_form_json_page_org_type_b), + { + "path": "/page_2", + "title": "Page 2", + "components": [], + "next": [], + "options": {}, }, - }, + ], + }, + { + "/organisation-type": [ + { + "path": "/page_2", + }, + { + "path": "/org-type-b", + "condition": "org_type_b", + }, + ], + "/org-type-b": [{"path": "/summary"}], + "/page_2": [{"path": "/summary"}], + "/summary": [], + }, + ), + # # One page, 3 possible nexts based on complex conditions (coordinators) + ( + [ + Page( + page_id=uuid4(), + form_id=uuid4(), + display_path="organisation-type", + name_in_apply_json={"en": "Organisation Type"}, + form_index=1, + components=[ + Component( + component_id=id2, + title="org_type", + type=ComponentType.RADIOS_FIELD, + conditions=[ + asdict(test_condition_org_type_a), + asdict(test_condition_org_type_b), + asdict(test_condition_org_type_c), + ], + runner_component_name="org_type_component", + ) + ], + ), + test_page_object_org_type_a, + test_page_object_org_type_b, + test_page_object_org_type_c, ], - ) + { + "conditions": [], + "pages": [ + { + "path": "/organisation-type", + "title": "Organisation Type", + "components": [ + {}, # don't care about these right now... + {}, + ], + "next": [], + "options": {}, + }, + deepcopy(test_form_json_page_org_type_a), + deepcopy(test_form_json_page_org_type_b), + deepcopy(test_form_json_page_org_type_c), + ], + }, + { + "/organisation-type": [ + { + "path": "/org-type-a", + "condition": "org_type_a", + }, + { + "path": "/org-type-b", + "condition": "org_type_b", + }, + { + "path": "/org-type-c", + "condition": "org_type_c", + }, + ], + "/org-type-a": [{"path": "/summary"}], + "/org-type-b": [{"path": "/summary"}], + "/org-type-c": [{"path": "/summary"}], + }, + ), ], ) -def test_build_navigation_with_conditions(mocker, input_pages, input_partial_json, exp_next, exp_conditions): - mocker.patch( - "app.export_config.generate_form.build_page", - return_value={"path": "/organisation-alternative-names", "next": []}, - ) +def test_build_navigation_with_conditions(mocker, input_pages, input_partial_json, exp_next): + mocker.patch("app.export_config.generate_form.build_conditions", return_value=["mock list"]) results = build_navigation(partial_form_json=input_partial_json, input_pages=input_pages) for page in results["pages"]: exp_next_this_page = exp_next[page["path"]] - assert page["next"] == exp_next_this_page - assert results["conditions"] == exp_conditions + assert page["next"] == exp_next_this_page, f"next for page {page['path']} does not match expected" + assert results["conditions"] == ["mock list"] @pytest.mark.parametrize( @@ -656,3 +834,60 @@ def test_build_form(input_form, exp_results): assert exp_next["path"] in [next["path"] for next in result_page["next"]] if "condition" in exp_next: assert exp_next["condition"] in [next["condition"] for next in result_page["next"]] + + +@pytest.mark.parametrize( + "input_content, input_form, expected_title, expected_path, expected_next, expected_content", + [ + ( + "2 pages", + Form( + name_in_apply_json={"en": "Test Form"}, + pages=[ + Page(name_in_apply_json={"en": "Page 1"}, display_path="page-1"), + Page(name_in_apply_json={"en": "Page 2"}, display_path="page-2"), + ], + ), + "Test Form", + "/intro-test-form", + [{"path": "/page-1"}], + ( + '

2 pages

' + '

We will ask you about:

" + ), + ), + ( + "Single page", + Form( + name_in_apply_json={"en": "Another Form"}, + pages=[Page(name_in_apply_json={"en": "Details Page"}, display_path="details-page")], + ), + "Another Form", + "/intro-another-form", + [{"path": "/details-page"}], + ( + '

Single page

' + '

We will ask you about:

" + ), + ), + ( + "Form with no pages", + Form(name_in_apply_json={"en": "Another Form"}, pages=[]), + "Another Form", + "/intro-another-form", + [], + ('

Form with no pages

'), + ), + ], +) +def test_build_start_page(input_content, input_form, expected_title, expected_path, expected_next, expected_content): + result = build_start_page(input_content, input_form) + + # Assert + assert result["title"] == expected_title + assert result["path"] == expected_path + assert result["controller"] == "./pages/start.js" + assert result["next"] == expected_next + assert result["components"][0]["content"] == expected_content diff --git a/tests/test_integration.py b/tests/test_integration.py index 686a7a98..497b1578 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,6 +1,7 @@ import json import os import shutil +from dataclasses import asdict from pathlib import Path from uuid import uuid4 @@ -22,6 +23,8 @@ generate_form_jsons_for_round, ) from app.import_config.load_form_json import load_form_jsons +from app.shared.data_classes import Condition +from app.shared.data_classes import ConditionValue from tasks.test_data import BASIC_FUND_INFO from tasks.test_data import BASIC_ROUND_INFO @@ -134,20 +137,50 @@ def test_build_form_json_no_conditions(seed_dynamic_data): runner_component_name="does_your_organisation_use_other_names", is_template=True, conditions=[ - { - "name": "organisation_other_names_no", - "value": "false", # this must be lowercaes or the navigation doesn't work - "operator": "is", - "destination_page_path": "CONTINUE", - "display_name": "Other Name No", - }, - { - "name": "organisation_other_names_yes", - "value": "true", # this must be lowercaes or the navigation doesn't work - "operator": "is", - "destination_page_path": "organisation-alternative-names", - "display_name": "Other Name Yes", - }, + asdict( + Condition( + name="organisation_other_names_no", + display_name="org other names no", + destination_page_path="/summary", + value=ConditionValue( + name="org other names no", + conditions=[ + { + "field": { + "name": "org_other_names", + "type": "YesNoField", + "display": "org other names", + }, + "operator": "is", + "value": {"type": "Value", "value": "false", "display": "false"}, + "coordinator": None, + }, + ], + ), + ), + ), + asdict( + Condition( + name="organisation_other_names_yes", + display_name="org other names yes", + destination_page_path="/organisation-alternative-names", + value=ConditionValue( + name="org other names yes", + conditions=[ + { + "field": { + "name": "org_other_names", + "type": "YesNoField", + "display": "org other names", + }, + "operator": "is", + "value": {"type": "Value", "value": "true", "display": "false"}, + "coordinator": None, + }, + ], + ), + ), + ), ], ), Component( diff --git a/tests/unit_test_data.py b/tests/unit_test_data.py index 57219af1..80024886 100644 --- a/tests/unit_test_data.py +++ b/tests/unit_test_data.py @@ -9,6 +9,8 @@ from app.db.models import Section from app.db.models import Subcriteria from app.db.models import Theme +from app.shared.data_classes import Condition +from app.shared.data_classes import ConditionValue form_1_id = uuid4() page_1_id = uuid4() @@ -93,3 +95,188 @@ components=[component_with_list], form_id=None, ) + + +test_condition_org_type_a = Condition( + name="org_type_a", + display_name="org type a", + destination_page_path="/org-type-a", + value=ConditionValue( + name="org type a", + conditions=[ + { + "field": { + "name": "org_type", + "type": "RadiosField", + "display": "org type", + }, + "operator": "is", + "value": {"type": "Value", "value": "A", "display": "A"}, + } + ], + ), +) +test_condition_org_type_b = Condition( + name="org_type_b", + display_name="org type b", + destination_page_path="/org-type-b", + value=ConditionValue( + name="org type b", + conditions=[ + { + "field": { + "name": "org_type", + "type": "RadiosField", + "display": "org type", + }, + "operator": "is", + "value": {"type": "Value", "value": "B", "display": "B"}, + } + ], + ), +) + + +test_condition_org_type_c = Condition( + name="org_type_c", + display_name="org type c", + destination_page_path="/org-type-c", + value=ConditionValue( + name="org type c", + conditions=[ + { + "field": { + "name": "org_type", + "type": "RadiosField", + "display": "org type", + }, + "operator": "is", + "value": {"type": "Value", "value": "C1", "display": "C1"}, + }, + { + "field": { + "name": "org_type", + "type": "RadiosField", + "display": "org type", + }, + "operator": "is", + "value": {"type": "Value", "value": "C2", "display": "C2"}, + "coordinator": "or", + }, + ], + ), +) + + +test_page_object_org_type_a = Page( + page_id=uuid4(), + form_id=uuid4(), + display_path="org-type-a", + name_in_apply_json={"en": "Organisation Type A"}, + form_index=2, +) + +test_page_object_org_type_b = Page( + page_id=uuid4(), + form_id=uuid4(), + display_path="org-type-b", + name_in_apply_json={"en": "Organisation Type B"}, + form_index=2, +) +test_page_object_org_type_c = Page( + page_id=uuid4(), + form_id=uuid4(), + display_path="org-type-c", + name_in_apply_json={"en": "Organisation Type C"}, + form_index=2, +) + +test_form_json_page_org_type_a = { + "path": "/org-type-a", + "title": "org-type-a", + "components": [], + "next": [], + "options": {}, +} +test_form_json_page_org_type_b = { + "path": "/org-type-b", + "title": "org-type-b", + "components": [], + "next": [], + "options": {}, +} +test_form_json_page_org_type_c = { + "path": "/org-type-c", + "title": "org-type-c", + "components": [], + "next": [], + "options": {}, +} +test_form_json_condition_org_type_c = { + "displayName": "org type c", + "name": "org_type_c", + "value": { + "name": "org type c", + "conditions": [ + { + "field": {"name": "org_type", "type": "RadiosField", "display": "org type"}, + "operator": "is", + "value": {"type": "Value", "value": "C1", "display": "C1"}, + }, + { + "field": {"name": "org_type", "type": "RadiosField", "display": "org type"}, + "operator": "is", + "value": {"type": "Value", "value": "C2", "display": "C2"}, + "coordinator": "or", + }, + ], + }, +} +test_form_json_condition_org_type_a = { + "displayName": "org type a", + "name": "org_type_a", + "value": { + "name": "org type a", + "conditions": [ + { + "field": {"name": "org_type", "type": "RadiosField", "display": "org type"}, + "operator": "is", + "value": {"type": "Value", "value": "A", "display": "A"}, + } + ], + }, +} +test_form_json_condition_org_type_b = { + "displayName": "org type b", + "name": "org_type_b", + "value": { + "name": "org type b", + "conditions": [ + { + "field": {"name": "org_type", "type": "RadiosField", "display": "org type"}, + "operator": "is", + "value": {"type": "Value", "value": "B", "display": "B"}, + } + ], + }, +} +test_form_json_condition_org_type_c = { + "displayName": "org type c", + "name": "org_type_c", + "value": { + "name": "org type c", + "conditions": [ + { + "field": {"name": "org_type", "type": "RadiosField", "display": "org type"}, + "operator": "is", + "value": {"type": "Value", "value": "C1", "display": "C1"}, + }, + { + "coordinator": "or", + "field": {"name": "org_type", "type": "RadiosField", "display": "org type"}, + "operator": "is", + "value": {"type": "Value", "value": "C2", "display": "C2"}, + }, + ], + }, +}