From a0a3685fa87d79eb454bf03c83161144c57c83ef Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 22 Aug 2024 17:05:33 +0100 Subject: [PATCH] refactor --- .gitignore | 3 + app/all_questions/metadata_utils.py | 6 +- app/blueprints/fund_builder/routes.py | 38 + .../templates/view_application_config.html | 6 + .../self_serve/templates/index.html | 4 +- .../~2024_08_19_1303-4ec629449867_.py | 34 + .../~2024_08_21_1123-ed84bb152ee3_.py | 36 + .../~2024_08_21_1528-117417bed885_.py | 32 + app/db/models/application_config.py | 7 + app/export/config_generator/generate_form.py | 83 +- .../generate_fund_round_config.py | 2 +- app/export/config_generator/helpers.py | 7 +- app/import/asset-information-cof-r3-w2.json | 1003 +++++++++++++++++ app/import/load_form_json copy.py | 187 +++ app/import/load_form_json.py | 215 ++++ app/shared/data_classes.py | 8 + app/shared/helpers.py | 7 + tasks/test_data.py | 40 +- tests/test_config_generators.py | 24 +- tests/test_generate_form.py | 44 +- tests/test_integration.py | 5 +- 21 files changed, 1717 insertions(+), 74 deletions(-) create mode 100644 app/db/migrations/versions/~2024_08_19_1303-4ec629449867_.py create mode 100644 app/db/migrations/versions/~2024_08_21_1123-ed84bb152ee3_.py create mode 100644 app/db/migrations/versions/~2024_08_21_1528-117417bed885_.py create mode 100644 app/import/asset-information-cof-r3-w2.json create mode 100644 app/import/load_form_json copy.py create mode 100644 app/import/load_form_json.py diff --git a/.gitignore b/.gitignore index 82f92755..a4cbc1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# Script output directory +app/export/config_generator/output/ diff --git a/app/all_questions/metadata_utils.py b/app/all_questions/metadata_utils.py index dfdf6601..08f1c15c 100644 --- a/app/all_questions/metadata_utils.py +++ b/app/all_questions/metadata_utils.py @@ -300,7 +300,11 @@ def determine_title_and_text_for_component( extract_from_html(soup, text) update_wording_for_multi_input_fields(text) - if component["type"].casefold() in FIELD_TYPES_WITH_MAX_WORDS and not is_child: + if ( + component["type"].casefold() in FIELD_TYPES_WITH_MAX_WORDS + and not is_child + and "maxWords" in component["options"] + ): text.append(f"(Max {component['options']['maxWords']} words)") if "list" in component: diff --git a/app/blueprints/fund_builder/routes.py b/app/blueprints/fund_builder/routes.py index b5a79e47..713d0159 100644 --- a/app/blueprints/fund_builder/routes.py +++ b/app/blueprints/fund_builder/routes.py @@ -1,14 +1,18 @@ import json +import os +import shutil from datetime import datetime from random import randint import requests from flask import Blueprint from flask import Response +from flask import after_this_request from flask import flash from flask import redirect from flask import render_template from flask import request +from flask import send_file from flask import url_for from app.all_questions.metadata_utils import generate_print_data_for_sections @@ -25,6 +29,14 @@ from app.db.queries.round import get_round_by_id from app.export.config_generator.generate_all_questions import print_html from app.export.config_generator.generate_form import build_form_json +from app.export.config_generator.generate_fund_round_config import ( + generate_application_display_config, +) +from app.export.config_generator.generate_fund_round_config import generate_fund_config +from app.export.config_generator.generate_fund_round_form_jsons import ( + generate_form_jsons_for_round, +) +from app.export.config_generator.generate_fund_round_html import generate_all_round_html from config import Config # Blueprint for routes used by v1 of FAB - using the DB @@ -207,3 +219,29 @@ def view_all_questions(round_id): ) html = print_html(print_data) return render_template("view_questions.html", round=round, fund=fund, question_html=html) + + +@build_fund_bp.route("/create_export_files/", methods=["GET"]) +def create_export_files(round_id): + generate_form_jsons_for_round(round_id) + generate_all_round_html(round_id) + generate_application_display_config(round_id) + generate_fund_config(round_id) + round_short_name = get_round_by_id(round_id).short_name + + # Directory to zip + directory_to_zip = f"app/export/config_generator/output/{round_short_name}/" + # Output zip file path (temporary) + output_zip_path = f"app/export/config_generator/output/{round_short_name}.zip" + + # Create a zip archive of the directory + shutil.make_archive(output_zip_path.replace(".zip", ""), "zip", directory_to_zip) + + # Ensure the file is removed after sending it + @after_this_request + def remove_file(response): + os.remove(output_zip_path) + return response + + # Return the zipped folder for the user to download + return send_file(output_zip_path, as_attachment=True, download_name=f"{round_short_name}.zip") diff --git a/app/blueprints/fund_builder/templates/view_application_config.html b/app/blueprints/fund_builder/templates/view_application_config.html index 3075cc89..5d7e8435 100644 --- a/app/blueprints/fund_builder/templates/view_application_config.html +++ b/app/blueprints/fund_builder/templates/view_application_config.html @@ -16,6 +16,12 @@

{{ fund.short_name }} - {{ round.short_name }}

"classes": "govuk-button--secondary" }) }} + {{ govukButton({ + "text": "Create export files for round", + "href": url_for("build_fund_bp.create_export_files", round_id=round.round_id), + "classes": "govuk-button--secondary" + }) + }} {% for section in round.sections %}
diff --git a/app/blueprints/self_serve/templates/index.html b/app/blueprints/self_serve/templates/index.html index 1c831d47..1ad2c37e 100644 --- a/app/blueprints/self_serve/templates/index.html +++ b/app/blueprints/self_serve/templates/index.html @@ -19,7 +19,7 @@

What do you want to do?

-

Template Setup

+

Template Administration

-

Fund Metadata

+

Fund Configuration

  • Add a Fund
  • Add a Round
  • diff --git a/app/db/migrations/versions/~2024_08_19_1303-4ec629449867_.py b/app/db/migrations/versions/~2024_08_19_1303-4ec629449867_.py new file mode 100644 index 00000000..49be9c3f --- /dev/null +++ b/app/db/migrations/versions/~2024_08_19_1303-4ec629449867_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 4ec629449867 +Revises: +Create Date: 2024-08-19 13:03:32.320669 + +""" + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "4ec629449867" +down_revision = "ab7d40d652d5" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.execute("ALTER TYPE componenttype ADD VALUE 'PARA';") + op.execute("ALTER TYPE componenttype ADD VALUE 'DATE_PARTS_FIELD';") + op.execute("ALTER TYPE componenttype ADD VALUE 'CHECKBOXES_FIELD';") + op.execute("ALTER TYPE componenttype ADD VALUE 'CLIENT_SIDE_FILE_UPLOAD_FIELD';") + op.execute("ALTER TYPE componenttype ADD VALUE 'WEBSITE_FIELD';") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + # Note: PostgreSQL does not support removing values from enums directly. + # To fully support downgrade, consider creating a new enum without the values and swapping them, + # or use a workaround that suits your database version and requirements. + # ### end Alembic commands ### + pass diff --git a/app/db/migrations/versions/~2024_08_21_1123-ed84bb152ee3_.py b/app/db/migrations/versions/~2024_08_21_1123-ed84bb152ee3_.py new file mode 100644 index 00000000..2804fd28 --- /dev/null +++ b/app/db/migrations/versions/~2024_08_21_1123-ed84bb152ee3_.py @@ -0,0 +1,36 @@ +"""empty message + +Revision ID: ed84bb152ee3 +Revises: 4ec629449867 +Create Date: 2024-08-21 11:23:11.330243 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "ed84bb152ee3" +down_revision = "4ec629449867" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("page", schema=None) as batch_op: + batch_op.add_column(sa.Column("default_next_page_id", sa.UUID(), nullable=True)) + batch_op.create_foreign_key( + batch_op.f("fk_page_default_next_page_id_page"), "page", ["default_next_page_id"], ["page_id"] + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("page", schema=None) as batch_op: + batch_op.drop_constraint(batch_op.f("fk_page_default_next_page_id_page"), type_="foreignkey") + batch_op.drop_column("default_next_page_id") + + # ### end Alembic commands ### diff --git a/app/db/migrations/versions/~2024_08_21_1528-117417bed885_.py b/app/db/migrations/versions/~2024_08_21_1528-117417bed885_.py new file mode 100644 index 00000000..6da30464 --- /dev/null +++ b/app/db/migrations/versions/~2024_08_21_1528-117417bed885_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 117417bed885 +Revises: ed84bb152ee3 +Create Date: 2024-08-21 15:28:58.583369 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "117417bed885" +down_revision = "ed84bb152ee3" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("lizt", schema=None) as batch_op: + batch_op.add_column(sa.Column("title", sa.String(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("lizt", schema=None) as batch_op: + batch_op.drop_column("title") + + # ### end Alembic commands ### diff --git a/app/db/models/application_config.py b/app/db/models/application_config.py index 3bf547fe..8ce4d6d7 100644 --- a/app/db/models/application_config.py +++ b/app/db/models/application_config.py @@ -34,6 +34,11 @@ class ComponentType(Enum): HTML = "Html" YES_NO_FIELD = "YesNoField" RADIOS_FIELD = "RadiosField" + PARA = "Para" + DATE_PARTS_FIELD = "DatePartsField" + CHECKBOXES_FIELD = "CheckboxesField" + CLIENT_SIDE_FILE_UPLOAD_FIELD = "ClientSideFileUploadField" + WEBSITE_FIELD = "WebsiteField" @dataclass @@ -122,6 +127,7 @@ class Page(BaseModel): audit_info = Column(JSON(none_as_null=True)) form_index = Column(Integer()) display_path = Column(String()) + default_next_page_id = Column(UUID(as_uuid=True), ForeignKey("page.page_id"), nullable=True) components: Mapped[List["Component"]] = relationship( "Component", order_by="Component.page_index", @@ -154,6 +160,7 @@ class Lizt(BaseModel): default=uuid.uuid4, ) name = Column(String()) + title = Column(String()) type = Column(String()) items = Column(JSON()) is_template = Column(Boolean, default=False, nullable=False) diff --git a/app/export/config_generator/generate_form.py b/app/export/config_generator/generate_form.py index c16f3c4e..6fb7aaff 100644 --- a/app/export/config_generator/generate_form.py +++ b/app/export/config_generator/generate_form.py @@ -32,7 +32,6 @@ "title": None, "components": [], "next": [], - "options": {}, } @@ -90,7 +89,9 @@ def build_component(component: Component) -> dict: "hint": component.hint_text or "", "schema": {}, "name": component.runner_component_name, - "metadata": {"fund_builder_id": str(component.component_id)}, + "metadata": { + # "fund_builder_id": str(component.component_id) TODO why do we need this? + }, } # add a reference to the relevant list if this component use a list if component.lizt: @@ -133,22 +134,23 @@ def build_page(page: Page = None, page_display_path: str = None) -> dict: # Goes through the set of pages and updates the conditions and next properties to account for branching def build_navigation(partial_form_json: dict, input_pages: list[Page]) -> dict: - # TODO order by index not order in list - # Think this is sorted now that the collection is sorted by index, but needs testing - for i in range(0, len(input_pages)): - if i < len(input_pages) - 1: - next_path = input_pages[i + 1].display_path - elif i == len(input_pages) - 1: - next_path = "summary" + for page in input_pages: + if page.controller and page.controller.endswith("summary.js"): + continue + next_page_id = page.default_next_page_id + if next_page_id: + find_next_page = lambda id: next(p for p in input_pages if p.page_id == id) # noqa:E731 + next_page = find_next_page(next_page_id) + next_path = next_page.display_path else: + # all page paths are conditionals which will be processed later next_path = None - this_page = input_pages[i] - this_page_in_results = next(p for p in partial_form_json["pages"] if p["path"] == f"/{this_page.display_path}") + # find page in prepared output results + this_page_in_results = next(p for p in partial_form_json["pages"] if p["path"] == f"/{page.display_path}") has_conditions = False - - for component in this_page.components: + for component in page.components: if not component.conditions: continue form_json_conditions = build_conditions(component) @@ -159,18 +161,18 @@ def build_navigation(partial_form_json: dict, input_pages: list[Page]) -> dict: if condition["destination_page_path"] == "CONTINUE": destination_path = f"/{next_path}" else: - destination_path = f"/{condition['destination_page_path']}" - + 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}"}] + # 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) + # partial_form_json["pages"].append(sub_page) this_page_in_results["next"].append( { @@ -180,8 +182,10 @@ def build_navigation(partial_form_json: dict, input_pages: list[Page]) -> dict: ) # If there were no conditions we just continue to the next page - if not has_conditions: + if not has_conditions and next_path: this_page_in_results["next"].append({"path": f"/{next_path}"}) + if not has_conditions and not next_path: + this_page_in_results["next"].append({"path": "/summary"}) return partial_form_json @@ -193,12 +197,24 @@ def build_lists(pages: list[dict]) -> list: for component in page["components"]: if component.get("list"): list_from_db = get_list_by_id(component["metadata"]["fund_builder_list_id"]) - list = {"type": list_from_db.type, "items": list_from_db.items, "name": list_from_db.name} + list = { + "type": list_from_db.type, + "items": list_from_db.items, + "name": list_from_db.name, + "title": list_from_db.title, + } lists.append(list) + # Remove the metadata key from built_component (no longer needed) + component.pop("metadata", None) # The second argument prevents KeyError if 'metadata' is not found return lists +def _find_page_by_controller(pages, controller_name) -> dict: + + return next((p for p in pages if p.controller and p.controller.endswith(controller_name)), None) + + def build_start_page(content: str, form: Form) -> dict: """ Builds the start page which contains just an html component comprising a bullet @@ -252,10 +268,15 @@ def build_form_json(form: Form) -> dict: for page in form.pages: results["pages"].append(build_page(page=page)) - # Create the start page - start_page = build_start_page(content=None, form=form) - results["pages"].append(start_page) - results["startPage"] = start_page["path"] + # start page is the page with the controller ending start.js + start_page = _find_page_by_controller(form.pages, "start.js") + if start_page: + results["startPage"] = f"/{start_page.display_path}" + else: + # Create the start page + start_page = build_start_page(content=None, form=form) + results["pages"].append(start_page) + results["startPage"] = start_page["path"] # Build navigation and add any pages from branching logic results = build_navigation(results, form.pages) @@ -264,6 +285,8 @@ def build_form_json(form: Form) -> dict: results["lists"] = build_lists(results["pages"]) # Add on the summary page - results["pages"].append(SUMMARY_PAGE) + summary_page = _find_page_by_controller(form.pages, "summary.js") + if not summary_page: + results["pages"].append(SUMMARY_PAGE) return results diff --git a/app/export/config_generator/generate_fund_round_config.py b/app/export/config_generator/generate_fund_round_config.py index 06ce7b5c..2f85dfc0 100644 --- a/app/export/config_generator/generate_fund_round_config.py +++ b/app/export/config_generator/generate_fund_round_config.py @@ -18,6 +18,7 @@ ROUND_BASE_PATHS = { # Should increment for each new round, anything that shares the same base path will also share # the child tree path config. + "TEST": 0, "COF_R2_W2": 1, "COF_R2_W3": 1, "COF_R3_W1": 2, @@ -29,7 +30,6 @@ "COF_R4_W1": 9, "HSRA": 10, "COF_R4_W2": 11, - "R605": 12, } diff --git a/app/export/config_generator/helpers.py b/app/export/config_generator/helpers.py index 8f831594..b613bbc8 100644 --- a/app/export/config_generator/helpers.py +++ b/app/export/config_generator/helpers.py @@ -11,8 +11,11 @@ def write_config(config, filename, round_short_name, config_type): - # Determine the base output directory - base_output_dir = f"app/export/config_generator/output/{round_short_name}/" + # Get the directory of the current file + current_dir = os.path.dirname(__file__) + + # Construct the path to the output directory relative to this file's location + base_output_dir = os.path.join(current_dir, f"output/{round_short_name}/") if config_type == "form_json": output_dir = os.path.join(base_output_dir, "form_runner/") diff --git a/app/import/asset-information-cof-r3-w2.json b/app/import/asset-information-cof-r3-w2.json new file mode 100644 index 00000000..4f5ce545 --- /dev/null +++ b/app/import/asset-information-cof-r3-w2.json @@ -0,0 +1,1003 @@ +{ + "metadata": {}, + "startPage": "/asset-information", + "backLinkText": "Go back to application overview", + "pages": [ + { + "title": "Asset information", + "path": "/asset-information", + "components": [ + { + "name": "JGhnud", + "options": {}, + "type": "Para", + "content": "\n\n
      \n
    • asset type
    • \n
    • what you intend to do with the asset
    • \n
    • asset ownership status
    • \n
    • asset ownership details (if applicable)
    • \n
    • public ownership details (if applicable)
    • \n
    • reason asset is at risk
    • \n
    • Asset of Community Value details (if applicable)
    • \n
    • Community Asset Transfer details (if applicable)
    • \n
    " + } + ], + "next": [ + { + "path": "/how-the-asset-is-used-in-the-community" + } + ], + "controller": "./pages/start.js" + }, + { + "path": "/how-the-asset-is-used-in-the-community", + "title": "How the asset is used in the community", + "section": "wxYZcT", + "components": [ + { + "name": "oXGwlA", + "options": {}, + "type": "RadiosField", + "title": "Asset type", + "hint": "Select how the asset is mainly used. For example, if it is a theatre that also has a cafe, select 'Theatre'", + "list": "jWXaBR" + } + ], + "next": [ + { + "path": "/the-asset-in-community-ownership" + }, + { + "path": "/how-the-asset-is-used-in-the-community-BgwTSc", + "condition": "VxBLMN" + } + ] + }, + { + "title": "Check your answers", + "path": "/summary", + "controller": "./pages/summary.js", + "components": [], + "next": [], + "section": "wxYZcT" + }, + { + "path": "/the-asset-in-community-ownership", + "title": "The asset in community ownership", + "components": [ + { + "name": "LaxeJN", + "options": {}, + "type": "RadiosField", + "title": "How do you intend to take community ownership of the asset?", + "list": "grEEzr" + } + ], + "next": [ + { + "path": "/risk-of-closure-KVISjF", + "condition": "FwscXH" + }, + { + "path": "/upload-asset-valuation-or-lease-agreement" + }, + { + "path": "/upload-asset-valuation-or-lease-agreement-pJQtXk", + "condition": "hWsHyx" + } + ], + "section": "wxYZcT" + }, + { + "path": "/how-the-asset-is-used-in-the-community-BgwTSc", + "title": "How the asset is used in the community", + "components": [ + { + "name": "aJGyCR", + "options": { + "classes": "govuk-input--width-20" + }, + "type": "TextField", + "title": "Type of asset (other)" + } + ], + "next": [ + { + "path": "/the-asset-in-community-ownership" + } + ], + "section": "wxYZcT" + }, + { + "path": "/expected-terms-of-your-ownership-or-lease", + "title": "Expected terms of your ownership or lease", + "components": [ + { + "name": "XPcbJx", + "options": {}, + "type": "FreeTextField", + "title": "Describe the expected sale process, or the proposed terms of your lease if you are planning to rent the asset", + "hint": "" + }, + { + "name": "jGjScT", + "options": {}, + "type": "DatePartsField", + "title": "Expected date of sale or lease", + "hint": "For example, 27 3 2007\n" + } + ], + "next": [ + { + "path": "/public-ownership" + } + ], + "section": "wxYZcT" + }, + { + "path": "/public-ownership", + "title": "Public ownership", + "components": [ + { + "name": "VGXXyq", + "options": {}, + "type": "YesNoField", + "title": "Is your asset currently publicly owned?" + } + ], + "next": [ + { + "path": "/public-ownership-details-and-declarations", + "condition": "oHpvvn" + }, + { + "path": "/risk-of-closure-JncvIu", + "condition": "ujYYpF" + } + ], + "section": "wxYZcT" + }, + { + "path": "/public-ownership-details-and-declarations", + "title": "Public ownership details and declarations", + "components": [ + { + "name": "rNYmXq", + "options": { + "classes": "govuk-!-width-full" + }, + "type": "TextField", + "title": "Tell us about the person you have spoken to at the relevant public body about the asset", + "hint": "" + }, + { + "name": "SiOmZn", + "options": { + "hideTitle": true, + "classes": "govuk-!-width-full" + }, + "type": "TextField", + "hint": "Job title of contact", + "title": "Job title of contact" + }, + { + "name": "XXVXuj", + "options": { + "hideTitle": true, + "classes": "govuk-!-width-full" + }, + "type": "TextField", + "title": "Organisation name", + "hint": "Organisation name" + }, + { + "name": "MIqglh", + "options": {}, + "type": "CheckboxesField", + "title": "When you buy or lease a publicly owned asset, the public authority cannot transfer statutory services or duties to the community group.", + "hint": "This includes things like social care, waste collection and planning services.\n\n

    We do not define individual libraries as statutory services for this purpose.

    ", + "list": "OSzelY" + }, + { + "name": "JdEmqn", + "options": {}, + "type": "CheckboxesField", + "list": "OSzelY", + "title": "Grants from this fund cannot be used to buy the freehold or premium on the lease of a publicly owned asset. Money must only be used for renovation and refurbishment costs" + }, + { + "name": "RFTloT", + "options": { + "dropzoneConfig": { + "maxFiles": 1, + "parallelUploads": 1, + "maxFilesize": 10, + "acceptedFiles": "image/jpeg,image/png,application/pdf,text/plain,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.oasis.opendocument.text,text/csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.spreadsheet" + }, + "showNoScriptWarning": false, + "minimumRequiredFiles": 1 + }, + "type": "ClientSideFileUploadField", + "title": "Upload evidence to confirm the above information and that the asset is at risk", + "hint": "This could be from:\n

    \n

      \n
    • a letter from an appropriate public authority officer or cabinet member
    • \n
    • a published cabinet paper from a local authority
    • \n
    \n

    \n\n\n\n

    It should be a single file no bigger than 10MB in an accepted format (jpg, jpeg, png, pdf, txt, doc, docx, odt, csv, xls, xlsx, ods).

    " + } + ], + "next": [ + { + "path": "/risk-of-closure-JncvIu" + } + ], + "section": "wxYZcT" + }, + { + "path": "/who-owns-the-asset", + "title": "Who owns the asset", + "components": [ + { + "name": "wAUFqr", + "options": {}, + "type": "YesNoField", + "title": "Do you know who currently owns your asset?" + } + ], + "next": [ + { + "path": "/who-currently-owns-your-asset", + "condition": "YBZKoC" + }, + { + "path": "/current-ownership-status", + "condition": "oFfQbV" + } + ], + "section": "wxYZcT" + }, + { + "path": "/who-currently-owns-your-asset", + "title": "Who currently owns your asset", + "components": [ + { + "name": "FOURVe", + "options": {}, + "type": "TextField", + "title": "Name of current asset owner" + } + ], + "next": [ + { + "path": "/expected-terms-of-your-ownership-or-lease" + } + ], + "section": "wxYZcT" + }, + { + "path": "/current-ownership-status", + "title": "Current ownership status", + "components": [ + { + "name": "XiHjDO", + "options": {}, + "type": "TextField", + "title": "Tell us what you know about the sale or lease of the asset" + } + ], + "next": [ + { + "path": "/expected-terms-of-your-ownership-or-lease" + } + ], + "section": "wxYZcT" + }, + { + "path": "/risk-of-closure-JncvIu", + "title": "Risk of closure", + "components": [ + { + "name": "qlqyUq", + "options": {}, + "type": "CheckboxesField", + "title": "Why is the asset at risk of closure?", + "hint": "Select all that apply", + "list": "YNTnCC" + } + ], + "next": [ + { + "path": "/asset-listing-details", + "condition": "EQpfAl" + }, + { + "path": "/community-asset-transfer", + "condition": "rrSKPO" + }, + { + "path": "/assets-of-community-value" + } + ], + "section": "wxYZcT" + }, + { + "path": "/assets-of-community-value", + "title": "Assets of community value", + "components": [ + { + "name": "iqnlTk", + "options": {}, + "type": "YesNoField", + "title": "Is this a registered Asset of Community Value (ACV)?" + } + ], + "next": [ + { + "path": "/summary" + } + ], + "section": "wxYZcT" + }, + { + "path": "/risk-of-closure-KVISjF", + "title": "Risk of closure", + "components": [ + { + "name": "LmOXLT", + "options": {}, + "type": "CheckboxesField", + "title": "Why is the asset at risk of closure?", + "hint": "Select all that apply", + "list": "IZlNBH" + } + ], + "next": [ + { + "path": "/assets-of-community-value" + } + ], + "section": "wxYZcT" + }, + { + "path": "/upload-asset-valuation-or-lease-agreement", + "title": "Upload asset valuation or lease agreement", + "components": [ + { + "name": "tTOrEp", + "options": { + "dropzoneConfig": { + "maxFiles": 1, + "parallelUploads": 1, + "maxFilesize": 10, + "acceptedFiles": "image/jpeg,image/png,application/pdf,text/plain,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.oasis.opendocument.text,text/csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.spreadsheet" + }, + "showNoScriptWarning": false, + "minimumRequiredFiles": 1 + }, + "type": "ClientSideFileUploadField", + "title": "Please upload evidence that shows the asset valuation (if you are buying the asset) or the lease agreement (if you are leasing the asset).", + "hint": "" + } + ], + "next": [ + { + "path": "/terms-of-your-lease", + "condition": "hWsHyx" + }, + { + "path": "/who-owns-the-asset" + } + ], + "section": "wxYZcT" + }, + { + "path": "/terms-of-your-lease", + "title": "Terms of your lease", + "components": [ + { + "name": "apJIBm", + "options": {}, + "type": "FreeTextField", + "title": "Describe the terms of your lease if you have rented the asset", + "hint": "For example, length of lease, conditions of lease or break clauses" + } + ], + "next": [ + { + "path": "/public-ownership" + } + ], + "section": "wxYZcT" + }, + { + "path": "/asset-listing-details", + "title": "Asset listing details", + "components": [ + { + "name": "QPIPjx", + "options": {}, + "type": "DatePartsField", + "title": "When was the asset listed?", + "hint": "For example, 27 3 2007" + }, + { + "name": "OJWGGr", + "options": { + "classes": "govuk-!-width-full" + }, + "type": "WebsiteField", + "title": "Provide a link to the listing" + } + ], + "next": [ + { + "path": "/community-asset-transfer", + "condition": "rrSKPO" + }, + { + "path": "/assets-of-community-value" + } + ], + "section": "wxYZcT" + }, + { + "path": "/community-asset-transfer", + "title": "Community asset transfer", + "components": [ + { + "name": "WKIGQE", + "options": { + "maxWords": "500" + }, + "type": "FreeTextField", + "title": "Describe the current status of the Community Asset Transfer" + } + ], + "next": [ + { + "path": "/assets-of-community-value" + } + ], + "section": "wxYZcT" + }, + { + "path": "/upload-asset-valuation-or-lease-agreement-pJQtXk", + "title": "Upload asset valuation or lease agreement", + "components": [ + { + "name": "TtOReP", + "options": { + "dropzoneConfig": { + "maxFiles": 1, + "parallelUploads": 1, + "maxFilesize": 10, + "acceptedFiles": "image/jpeg,image/png,application/pdf,text/plain,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.oasis.opendocument.text,text/csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.spreadsheet" + }, + "showNoScriptWarning": false, + "minimumRequiredFiles": 1 + }, + "type": "ClientSideFileUploadField", + "title": "Please upload evidence that shows the asset is already leased by your organisation.", + "hint": "" + } + ], + "next": [ + { + "path": "/terms-of-your-lease" + } + ], + "section": "wxYZcT" + } + ], + "lists": [ + { + "title": "Asset type", + "name": "jWXaBR", + "type": "string", + "items": [ + { + "text": "Community centre", + "value": "community-centre" + }, + { + "text": "Cinema", + "value": "cinema" + }, + { + "text": "Gallery", + "value": "gallery" + }, + { + "text": "Museum", + "value": "museum" + }, + { + "text": "Music venue", + "value": "music-venue" + }, + { + "text": "Park", + "value": "park" + }, + { + "text": "Post office building", + "value": "post-office" + }, + { + "text": "Pub", + "value": "pub" + }, + { + "text": "Shop", + "value": "shop" + }, + { + "text": "Sporting or leisure facility", + "value": "sporting" + }, + { + "text": "Theatre", + "value": "theatre" + }, + { + "text": "Other", + "value": "other" + } + ] + }, + { + "title": "How do you intend to take community ownership of the asset?", + "name": "grEEzr", + "type": "string", + "items": [ + { + "text": "Buy the asset", + "value": "buy-the-asset" + }, + { + "text": "Lease the asset", + "value": "lease-the-asset" + }, + { + "text": "Already owned by organisation", + "value": "already-owned-by-organisation" + }, + { + "text": "Already leased by organisation", + "value": "already-leased-by-organisation" + } + ] + }, + { + "title": "I Confirm", + "name": "OSzelY", + "type": "string", + "items": [ + { + "text": "I confirm", + "value": "confirm" + } + ] + }, + { + "title": "Risk of closure", + "name": "RGVpbi", + "type": "string", + "items": [ + { + "text": "For sale or listed for disposal", + "value": "for-sale-or-listed-for-disposal" + }, + { + "text": "Future use not secured", + "value": "future-use-not-secured" + }, + { + "text": "Unprofitable under current business model", + "value": "unprofitable-under-current-business-model" + }, + { + "text": "Current ownership not tenable", + "value": "current-ownership-not-tenable" + } + ] + }, + { + "title": "Why is the asset at risk of closure?", + "name": "YNTnCC", + "type": "string", + "items": [ + { + "text": "Closure", + "value": "Closure" + }, + { + "text": "Sale", + "value": "Sale" + }, + { + "text": "Neglect or dereliction", + "value": "Neglect or dereliction" + }, + { + "text": "Unsustainable current business model", + "value": "Unsustainable current business model" + }, + { + "text": "Listed for disposal", + "value": "Listed for disposal" + }, + { + "text": "Part of a Community Asset Transfer", + "value": "Part of a Community Asset Transfer" + } + ] + }, + { + "title": "Why is the asset at risk of closure? 2", + "name": "IZlNBH", + "type": "string", + "items": [ + { + "text": "Closure", + "value": "Closure" + }, + { + "text": "Neglect or dereliction", + "value": "Neglect or dereliction" + }, + { + "text": "Unsustainable current business model", + "value": "Unsustainable current business model" + } + ] + } + ], + "sections": [ + { + "name": "wxYZcT", + "title": "Asset information" + } + ], + "conditions": [ + { + "displayName": "How the asset is used-other", + "name": "VxBLMN", + "value": { + "name": "How the asset is used-other", + "conditions": [ + { + "field": { + "name": "oXGwlA", + "type": "RadiosField", + "display": "Asset type" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "other", + "display": "other" + } + } + ] + } + }, + { + "displayName": "Do you know who currently owns your asset-yes", + "name": "KZLYTF", + "value": { + "name": "Do you know who currently owns your asset-yes", + "conditions": [ + { + "field": { + "name": "hdmYjg", + "type": "YesNoField", + "display": "Do you know who currently owns your asset?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "Do you know who currently owns your asset-no", + "name": "GRGSGT", + "value": { + "name": "Do you know who currently owns your asset-no", + "conditions": [ + { + "field": { + "name": "hdmYjg", + "type": "YesNoField", + "display": "Do you know who currently owns your asset?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "Is your asset currently publicly owned-yes", + "name": "oHpvvn", + "value": { + "name": "Is your asset currently publicly owned-yes", + "conditions": [ + { + "field": { + "name": "VGXXyq", + "type": "YesNoField", + "display": "Is your asset currently publicly owned?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "Is your asset currently publicly owned-no", + "name": "ujYYpF", + "value": { + "name": "Is your asset currently publicly owned-no", + "conditions": [ + { + "field": { + "name": "VGXXyq", + "type": "YesNoField", + "display": "Is your asset currently publicly owned?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "Is this a registered Asset of Community Value (ACV)-yes", + "name": "pErRiw", + "value": { + "name": "Is this a registered Asset of Community Value (ACV)-yes", + "conditions": [ + { + "field": { + "name": "wjBFTf", + "type": "YesNoField", + "display": "Is this a registered Asset of Community Value (ACV)?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "Is the asset listed for disposal, or part of a Community Asset Transfer-yes", + "name": "XVOzZh", + "value": { + "name": "Is the asset listed for disposal, or part of a Community Asset Transfer-yes", + "conditions": [ + { + "field": { + "name": "HyWPwE", + "type": "YesNoField", + "display": "Is the asset listed for disposal, or part of a Community Asset Transfer?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "Is this a registered Asset of Community Value (ACV)-no", + "name": "nxnCXz", + "value": { + "name": "Is this a registered Asset of Community Value (ACV)-no", + "conditions": [ + { + "field": { + "name": "wjBFTf", + "type": "YesNoField", + "display": "Is this a registered Asset of Community Value (ACV)?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "Is the asset listed for disposal, or part of a Community Asset Transfer-no", + "name": "lmmYts", + "value": { + "name": "Is the asset listed for disposal, or part of a Community Asset Transfer-no", + "conditions": [ + { + "field": { + "name": "HyWPwE", + "type": "YesNoField", + "display": "Is the asset listed for disposal, or part of a Community Asset Transfer?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "Do you know who owns the asset - yes", + "name": "YBZKoC", + "value": { + "name": "Do you know who owns the asset - yes", + "conditions": [ + { + "field": { + "name": "wAUFqr", + "type": "YesNoField", + "display": "Do you know who currently owns your asset?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "Do you know who owns the asset - no", + "name": "oFfQbV", + "value": { + "name": "Do you know who owns the asset - no", + "conditions": [ + { + "field": { + "name": "wAUFqr", + "type": "YesNoField", + "display": "Do you know who currently owns your asset?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "displayName": "already-owned-by-organisation-yes", + "name": "FwscXH", + "value": { + "name": "already-owned-by-organisation-yes", + "conditions": [ + { + "field": { + "name": "LaxeJN", + "type": "RadiosField", + "display": "How do you intend to take community ownership of the asset?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "already-owned-by-organisation", + "display": "already-owned-by-organisation" + } + } + ] + } + }, + { + "displayName": "already-leased-by-organisation-yes", + "name": "hWsHyx", + "value": { + "name": "already-leased-by-organisation-yes", + "conditions": [ + { + "field": { + "name": "LaxeJN", + "type": "RadiosField", + "display": "How do you intend to take community ownership of the asset?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "already-leased-by-organisation", + "display": "already-leased-by-organisation" + } + } + ] + } + }, + { + "displayName": "listed-for-disposal", + "name": "EQpfAl", + "value": { + "name": "listed-for-disposal", + "conditions": [ + { + "field": { + "name": "qlqyUq", + "type": "CheckboxesField", + "display": "Why is the asset at risk of closure?" + }, + "operator": "contains", + "value": { + "type": "Value", + "value": "Listed for disposal", + "display": "Listed for disposal" + } + } + ] + } + }, + { + "displayName": "part-of-a-community-asset-transfer", + "name": "rrSKPO", + "value": { + "name": "part-of-a-community-asset-transfer", + "conditions": [ + { + "field": { + "name": "qlqyUq", + "type": "CheckboxesField", + "display": "Why is the asset at risk of closure?" + }, + "operator": "contains", + "value": { + "type": "Value", + "value": "Part of a Community Asset Transfer", + "display": "Part of a Community Asset Transfer" + } + } + ] + } + } + ], + "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/load_form_json copy.py b/app/import/load_form_json copy.py new file mode 100644 index 00000000..cba98bc0 --- /dev/null +++ b/app/import/load_form_json copy.py @@ -0,0 +1,187 @@ +# pip install pandas openpyxl + +import json +import os +import sys + +sys.path.insert(1, ".") +from dataclasses import asdict # noqa:E402 + +from app.app import app # noqa:E402 +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.db.models import Section # noqa:E402 +from app.shared.data_classes import Condition # noqa:E402 +from app.shared.helpers import find_enum # noqa:E402 + + +def add_conditions_to_components(db, page, conditions): + # 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] + 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() + ) + 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_data["value"]["name"], + value=condition_data["value"]["conditions"][0]["value"]["value"], + operator=condition_data["value"]["conditions"][0]["operator"], + destination_page_path=path["path"], + ) + + # Add the new condition to the conditions list of the component to update + if component_to_update.conditions: + component_to_update.conditions.append(asdict(new_condition)) + else: + component_to_update.conditions = [asdict(new_condition)] + + +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: + new_list = Lizt( + is_template=True, + name=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 + break + + new_component = Component( + page_id=page_id, + theme_id=None, + title=component.get("title", ""), + hint_text=component.get("hint", None), + options=component.get("options", None), + type=find_enum(ComponentType, component.get("type", None)), + template_name=component.get("title"), + is_template=True, + page_index=page_index, + # theme_index=component.get('theme_index', None), TODO: add theme_index to json + runner_component_name=component.get("name", ""), + list_id=list_id, + ) + try: + db.session.add(new_component) + except Exception as e: + print(e) + raise e + return new_component + + +def insert_page_as_template(page, form_id, idx): + new_page = Page( + form_id=form_id, + display_path=page.get("path").lstrip("/"), + form_index=idx, + name_in_apply_json={"en": page.get("title")}, + controller=page.get("controller", None), + is_template=True, + template_name=page.get("title", None), + ) + try: + db.session.add(new_page) + except Exception as e: + print(e) + raise e + return new_page + + +def insert_form_config(form_config, form_id, db): + inserted_pages = [] + inserted_components = [] + for p_idx, page in enumerate(form_config.get("pages", [])): + inserted_page = insert_page_as_template(page, form_id, p_idx) + inserted_pages.append(inserted_page) + db.session.flush() # flush to get the page id + for c_idx, component in enumerate(page.get("components", [])): + inserted_component = insert_component_as_template( + component, inserted_page.page_id, c_idx, form_config["lists"] + ) + inserted_components.append(inserted_component) + db.session.flush() # flush to make components available for conditions + add_conditions_to_components(db, page, form_config["conditions"]) + return inserted_pages, inserted_components + + +def insert_form_as_template(form, db): + section = db.session.query(Section.section_id).first() + new_form = Form( + section_id=section.section_id, + name_in_apply_json={"en": form.get("name")}, + template_name=form.get("name"), + is_template=True, + audit_info=None, + section_index=None, + runner_publish_name=filename, + source_template_id=None, + ) + + try: + db.session.add(new_form) + except Exception as e: + print(e) + raise e + + return new_form + + +def read_json(filename): + script_dir = os.path.dirname(__file__) + # Construct the path to the Excel file relative to the script's location + file_name = os.path.join(script_dir, filename) + # Read json file into python dict + with open(file_name, "r") as json_file: + form = json.load(json_file) + return form + + +if __name__ == "__main__": + with app.app_context(): + try: + db = app.extensions["sqlalchemy"] + filename = "asset-information-cof-r3-w2.json" + form_config = read_json(filename) + # prepare all row commits + inserted_form = insert_form_as_template(form_config, db) + db.session.flush() # flush to get the form id + inserted_pages, inserted_components = insert_form_config(form_config, inserted_form.form_id, db) + db.session.commit() + except Exception as e: + print(e) + db.session.rollback() + raise e diff --git a/app/import/load_form_json.py b/app/import/load_form_json.py new file mode 100644 index 00000000..e4dc022c --- /dev/null +++ b/app/import/load_form_json.py @@ -0,0 +1,215 @@ +# pip install pandas openpyxl + +import json +import os +import sys + +sys.path.insert(1, ".") +from dataclasses import asdict # noqa:E402 + +from app.app import app # noqa:E402 +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.db.models import Section # noqa:E402 +from app.shared.data_classes import Condition # noqa:E402 +from app.shared.helpers import find_enum # noqa:E402 + + +def add_conditions_to_components(db, page, conditions): + # 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] + 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() + ) + 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_data["value"]["name"], + value=condition_data["value"]["conditions"][0]["value"]["value"], + operator=condition_data["value"]["conditions"][0]["operator"], + destination_page_path=path["path"], + ) + + # Add the new condition to the conditions list of the component to update + if component_to_update.conditions: + component_to_update.conditions.append(asdict(new_condition)) + else: + component_to_update.conditions = [asdict(new_condition)] + + +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: + 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 + break + + new_component = Component( + page_id=page_id, + theme_id=None, + title=component.get("title", ""), + hint_text=component.get("hint", None), + options=component.get("options", None), + type=find_enum(ComponentType, component.get("type", None)), + template_name=component.get("title"), + is_template=True, + page_index=page_index, + # theme_index=component.get('theme_index', None), TODO: add theme_index to json + runner_component_name=component.get("name", ""), + list_id=list_id, + ) + try: + db.session.add(new_component) + except Exception as e: + print(e) + raise e + return new_component + + +def insert_page_as_template(page, form_id): + new_page = Page( + form_id=form_id, + display_path=page.get("path").lstrip("/"), + form_index=None, + name_in_apply_json={"en": page.get("title")}, + controller=page.get("controller", None), + is_template=True, + template_name=page.get("title", None), + ) + try: + db.session.add(new_page) + except Exception as e: + print(e) + raise e + return new_page + + +def find_page_by_path(path): + page = db.session.query(Page).filter(Page.display_path == path.lstrip("/")).first() + return page + + +def insert_page_default_next_page(pages_config, db_pages): + for current_page_config in pages_config: + for db_page in db_pages: + if db_page.display_path == current_page_config.get("path").lstrip("/"): + current_db_page = db_page + page_nexts = current_page_config.get("next", []) + next_page_path_with_no_condition = next((p for p in page_nexts if not p.get("condition")), None) + if not next_page_path_with_no_condition: + # no default next page so move on (next page is based on conditions) + continue + + # set default next page id + for db_page in db_pages: + if db_page.display_path == next_page_path_with_no_condition.get("path").lstrip("/"): + current_db_page.default_next_page_id = db_page.page_id + # Update the page in the database + db.session.add(current_db_page) + db.session.flush() + + +def insert_form_config(form_config, form_id, db): + inserted_pages = [] + inserted_components = [] + for page in form_config.get("pages", []): + inserted_page = insert_page_as_template(page, form_id) + inserted_pages.append(inserted_page) + db.session.flush() # flush to get the page id + for c_idx, component in enumerate(page.get("components", [])): + inserted_component = insert_component_as_template( + component, inserted_page.page_id, (c_idx + 1), form_config["lists"] + ) + inserted_components.append(inserted_component) + db.session.flush() # flush to make components available for conditions + add_conditions_to_components(db, page, form_config["conditions"]) + insert_page_default_next_page(form_config.get("pages", None), inserted_pages) + db + return inserted_pages, inserted_components + + +def insert_form_as_template(form, db): + section = db.session.query(Section.section_id).first() + new_form = Form( + section_id=section.section_id, + name_in_apply_json={"en": form.get("name")}, + template_name=form.get("name"), + is_template=True, + audit_info=None, + section_index=None, + runner_publish_name=filename, + source_template_id=None, + ) + + try: + db.session.add(new_form) + except Exception as e: + print(e) + raise e + + return new_form + + +def read_json(filename): + script_dir = os.path.dirname(__file__) + # Construct the path to the Excel file relative to the script's location + file_name = os.path.join(script_dir, filename) + # Read json file into python dict + with open(file_name, "r") as json_file: + form = json.load(json_file) + return form + + +if __name__ == "__main__": + with app.app_context(): + try: + db = app.extensions["sqlalchemy"] + filename = "asset-information-cof-r3-w2.json" + form_config = read_json(filename) + # prepare all row commits + inserted_form = insert_form_as_template(form_config, db) + db.session.flush() # flush to get the form id + inserted_pages, inserted_components = insert_form_config(form_config, inserted_form.form_id, db) + db.session.commit() + except Exception as e: + print(e) + db.session.rollback() + raise e diff --git a/app/shared/data_classes.py b/app/shared/data_classes.py index 5c372c67..8bc4a3c1 100644 --- a/app/shared/data_classes.py +++ b/app/shared/data_classes.py @@ -4,6 +4,14 @@ from typing import Optional +@dataclass +class Condition: + name: str + value: str + operator: str + destination_page_path: str + + @dataclass class SectionName: en: str diff --git a/app/shared/helpers.py b/app/shared/helpers.py index e27ccc95..7637183f 100644 --- a/app/shared/helpers.py +++ b/app/shared/helpers.py @@ -9,3 +9,10 @@ def convert_to_dict(obj): return [asdict(item) if is_dataclass(item) else item for item in obj] else: return obj + + +def find_enum(enum_class, value): + for enum in enum_class: + if enum.value == value: + return enum + return None diff --git a/tasks/test_data.py b/tasks/test_data.py index 79162992..a3300aa7 100644 --- a/tasks/test_data.py +++ b/tasks/test_data.py @@ -34,6 +34,13 @@ "privacy_notice_link": "http://www.google.com", } +page_one_id = uuid4() +page_two_id = uuid4() +page_three_id = uuid4() +page_four_id = uuid4() +page_five_id = uuid4() +alt_page_id = uuid4() + def init_salmon_fishing_fund(): organisation_uuid = uuid4() @@ -104,36 +111,37 @@ def init_salmon_fishing_fund(): runner_publish_name="contact-details", ) p1: Page = Page( - page_id=uuid4(), + page_id=page_one_id, form_id=f1.form_id, display_path="organisation-name", name_in_apply_json={"en": "Organisation Name"}, form_index=1, ) p2: Page = Page( - page_id=uuid4(), + page_id=page_two_id, display_path="organisation-address", form_id=f1.form_id, name_in_apply_json={"en": "Organisation Address"}, form_index=3, ) p3: Page = Page( - page_id=uuid4(), + page_id=page_three_id, form_id=f2.form_id, display_path="lead-contact-details", name_in_apply_json={"en": "Lead Contact Details"}, form_index=1, ) p5: Page = Page( - page_id=uuid4(), + page_id=page_five_id, display_path="organisation-classification", form_id=f1.form_id, name_in_apply_json={"en": "Organisation Classification"}, form_index=4, + default_next_page_id=None, ) p_org_alt_names: Page = Page( - page_id=uuid4(), - form_id=None, + page_id=alt_page_id, + form_id=f1.form_id, display_path="organisation-alternative-names", name_in_apply_json={"en": "Alternative names of your organisation"}, form_index=2, @@ -205,13 +213,13 @@ def init_salmon_fishing_fund(): conditions=[ { "name": "organisation_other_names_no", - "value": "false", # this must be lowercaes or the navigation doesn't work + "value": "false", # this must be lowercase or the navigation doesn't work "operator": "is", - "destination_page_path": "CONTINUE", + "destination_page_path": "organisation-address", }, { "name": "organisation_other_names_yes", - "value": "true", # this must be lowercaes or the navigation doesn't work + "value": "true", # this must be lowercase or the navigation doesn't work "operator": "is", "destination_page_path": "organisation-alternative-names", }, @@ -266,6 +274,10 @@ def init_salmon_fishing_fund(): "sections": [s1], "forms": [f1, f2], "pages": [p1, p2, p3, p5, p_org_alt_names], + "default_next_pages": [ + {"page_id": alt_page_id, "default_next_page_id": page_two_id}, + {"page_id": page_two_id, "default_next_page_id": page_five_id}, + ], "components": [c1, c2, c4, c5, c6, c8, c3, c7], "criteria": [cri1], "subcriteria": [sc1], @@ -339,6 +351,7 @@ def init_unit_test_data() -> dict: display_path="organisation-name", name_in_apply_json={"en": "Organisation Name"}, form_index=1, + default_next_page_id=None, ) cri1: Criteria = Criteria(criteria_id=uuid4(), index=1, round_id=r.round_id, name="Unscored", weighting=0.0) @@ -396,6 +409,14 @@ def init_unit_test_data() -> dict: } +def add_default_page_paths(db, default_next_page_config): + # set up the default paths + for page_config in default_next_page_config: + page = Page.query.filter_by(page_id=page_config["page_id"]).first() + page.default_next_page_id = page_config["default_next_page_id"] + db.session.commit() + + def insert_test_data(db, test_data={}): db.session.bulk_save_objects(test_data.get("organisations", [])) db.session.commit() @@ -409,6 +430,7 @@ def insert_test_data(db, test_data={}): db.session.commit() db.session.bulk_save_objects(test_data.get("pages", [])) db.session.commit() + add_default_page_paths(db, test_data.get("default_next_pages", [])) db.session.bulk_save_objects(test_data.get("criteria", [])) db.session.commit() db.session.bulk_save_objects(test_data.get("subcriteria", [])) diff --git a/tests/test_config_generators.py b/tests/test_config_generators.py index 58b66415..9730487d 100644 --- a/tests/test_config_generators.py +++ b/tests/test_config_generators.py @@ -15,7 +15,7 @@ from app.export.config_generator.generate_fund_round_html import generate_all_round_html from app.export.config_generator.helpers import validate_json -output_base_path = Path("app") / "config_generator" / "output" +output_base_path = Path("app") / "export" / "config_generator" / "output" def test_generate_config_for_round_valid_input(seed_dynamic_data, monkeypatch): @@ -156,7 +156,27 @@ def test_generate_form_jsons_for_round_valid_input(seed_dynamic_data): expected_files = [ { "path": output_base_path / round_short_name / "form_runner" / f"{form_publish_name}.json", - "expected_output": '{"metadata": {}, "startPage": "/intro-about-your-organisation", "backLinkText": "Go back to application overview", "pages": [{"path": "/organisation-name", "title": "Organisation Name", "components": [{"options": {"hideTitle": false, "classes": ""}, "type": "TextField", "title": "What is your organisation\'s name?", "hint": "This must match the regsitered legal organisation name", "schema": {}, "name": "organisation_name"}, {"options": {"hideTitle": false, "classes": ""}, "type": "RadiosField", "title": "How is your organisation classified?", "hint": "", "schema": {}, "name": "organisation_classification", "list": "classifications_list"}], "next": [{"path": "/summary"}], "options": {}}, {"path": "/intro-about-your-organisation", "title": "About your organisation", "components": [{"name": "start-page-content", "options": {}, "type": "Html", "content": "

    None

    We will ask you about:

    • Organisation Name
    ", "schema": {}}], "next": [{"path": "/organisation-name"}], "options": {}, "controller": "./pages/start.js"}, {"path": "/summary", "title": "Check your answers", "components": [], "next": [], "section": "uLwBuz", "controller": "./pages/summary.js"}], "lists": [{"type": "string", "items": [{"text": "Charity", "value": "charity"}, {"text": "Public Limited Company", "value": "plc"}], "name": "classifications_list"}], "conditions": [], "fees": [], "sections": [], "outputs": [{"name": "update-form", "title": "Update form in application store", "type": "savePerPage", "outputConfiguration": {"savePerPageUrl": true}}], "skipSummary": false, "name": "About your organisation"}', # noqa: E501 + "expected_output": ( + '{"metadata": {}, "startPage": "/intro-about-your-organisation", "backLinkText": ' + '"Go back to application overview", "pages": [{"path": "/organisation-name", "title": ' + '"Organisation Name", "components": [{"options": {"hideTitle": false, "classes": ""}, ' + '"type": "TextField", "title": "What is your organisation\'s name?", "hint": "This ' + 'must match the regsitered legal organisation name", "schema": {}, "name": ' + '"organisation_name"}, {"options": {"hideTitle": false, "classes": ""}, "type": ' + '"RadiosField", "title": "How is your organisation classified?", "hint": "", "schema":' + ' {}, "name": "organisation_classification", "list": "classifications_list"}], "next":' + ' [{"path": "/summary"}]}, {"path": "/intro-about-your-organisation", "title": "About' + ' your organisation", "components": [{"name": "start-page-content", "options": {}, ' + '"type": "Html", "content": "

    None

    ' + 'We will ask you about:

    • Organisation Name
    ", "schema": {}}], "next":' + ' [{"path": "/organisation-name"}], "controller": "./pages/start.js"}, {"path": "/summary",' + ' "title": "Check your answers", "components": [], "next": [], "section": "uLwBuz", "controller":' + ' "./pages/summary.js"}], "lists": [{"type": "string", "items": [{"text": "Charity", "value":' + ' "charity"}, {"text": "Public Limited Company", "value": "plc"}], "name": "classifications_list",' + ' "title": null}], "conditions": [], "fees": [], "sections": [], "outputs": [{"name": "update-form",' + ' "title": "Update form in application store", "type": "savePerPage", "outputConfiguration":' + ' {"savePerPageUrl": true}}], "skipSummary": false, "name": "About your organisation"}' + ), } ] try: diff --git a/tests/test_generate_form.py b/tests/test_generate_form.py index 9e58f8a6..050b318d 100644 --- a/tests/test_generate_form.py +++ b/tests/test_generate_form.py @@ -224,11 +224,10 @@ def test_build_lists(mocker, pages, exp_result): "title": "Organisation name", "hint": "This must match your registered legal organisation name", "schema": {}, - "metadata": {"fund_builder_id": str(mock_c_1.component_id)}, + "metadata": {}, } ], "next": [], - "options": {}, }, ) ], @@ -394,9 +393,10 @@ def test_build_conditions(input_component, exp_results): display_path="organisation-single-name", name_in_apply_json={"en": "Organisation Name"}, form_index=1, + default_next_page_id=id2, ), Page( - page_id=uuid4(), + page_id=id2, form_id=uuid4(), display_path="organisation-charitable-objects", name_in_apply_json={"en": "What are your organisation's charitable objects?"}, @@ -479,7 +479,7 @@ def test_build_navigation_no_conditions(mocker, input_partial_json, input_pages, "name": "organisation_other_names_no", "operator": "is", "value": "no", - "destination_page_path": "CONTINUE", + "destination_page_path": "summary", }, { "name": "organisation_other_names_yes", @@ -492,13 +492,13 @@ def test_build_navigation_no_conditions(mocker, input_partial_json, input_pages, ) ], ), - # Page( - # page_id=uuid4(), - # form_id=uuid4(), - # display_path="organisation-alternative-names", - # name_in_apply_json={"en": "Organisation Alternative Names"}, - # form_index=2, - # ), + Page( + page_id=uuid4(), + form_id=uuid4(), + display_path="organisation-alternative-names", + name_in_apply_json={"en": "Organisation Alternative Names"}, + form_index=2, + ), ], { "conditions": [], @@ -529,21 +529,13 @@ def test_build_navigation_no_conditions(mocker, input_partial_json, input_pages, "next": [], "options": {}, }, - # { - # "path": "/organisation-alternative-names", - # "title": "Organisation Alternative Names", - # "components": [ - # # { - # # "name": "reuse-charitable-objects", - # # "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.", - # # }, - # ], - # "next": [], - # "options": {}, - # }, + { + "path": "/organisation-alternative-names", + "title": "Organisation Alternative Names", + "components": [], + "next": [], + "options": {}, + }, ], }, { diff --git a/tests/test_integration.py b/tests/test_integration.py index 116de14b..d7dedafe 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -95,13 +95,16 @@ def test_build_form_json_no_conditions(seed_dynamic_data): ), Page( page_id=page_2_id, - form_id=None, + form_id=form_id, display_path="organisation-alternative-names", name_in_apply_json={"en": "Alternative names of your organisation"}, form_index=2, is_template=True, ), ], + "default_next_pages": [ + {"page_id": page_1_id, "default_next_page_id": page_2_id}, + ], "components": [ Component( component_id=uuid4(),