From e7005e440a9e6be4157715e8b62858bf1e47061f Mon Sep 17 00:00:00 2001 From: NarenderRajuB Date: Fri, 18 Oct 2024 02:36:16 +0100 Subject: [PATCH 1/6] fix import json having condition in multiple pages --- app/import_config/load_form_json.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/import_config/load_form_json.py b/app/import_config/load_form_json.py index 5136c76..f6df4ba 100644 --- a/app/import_config/load_form_json.py +++ b/app/import_config/load_form_json.py @@ -49,7 +49,6 @@ def _get_component_by_runner_name(db, runner_component_name, page_id): return ( db.session.query(Component) .filter(Component.runner_component_name == runner_component_name) - .filter(Component.page_id == page_id) .first() ) From fe08b92db956470aa72ba9ab74bb5c9ed9ae3480 Mon Sep 17 00:00:00 2001 From: NarenderRajuB Date: Fri, 18 Oct 2024 02:42:56 +0100 Subject: [PATCH 2/6] precommit fixes --- .devcontainer/devcontainer.json | 2 +- .github/workflows/copilot_deploy.yml | 2 +- .../fund_builder/forms/templates.py | 13 +++++-- app/blueprints/fund_builder/routes.py | 5 ++- app/blueprints/templates/routes.py | 37 ++++++++++++------- .../templates/edit_form_template.html | 2 +- .../~2024_10_16_1347-da30746cec39_.py | 34 +++++++++-------- app/db/models/application_config.py | 4 +- app/export_config/generate_form.py | 2 +- .../generate_fund_round_form_jsons.py | 4 +- app/export_config/helpers.py | 13 +++---- app/import_config/load_form_json.py | 10 ++--- config/envs/default.py | 2 +- config/envs/development.py | 2 +- config/envs/unit_test.py | 2 +- tests/conftest.py | 6 ++- tests/test_config_export.py | 3 +- tests/test_config_import.py | 5 +-- tests/test_data/multi-input-exported.json | 2 +- tests/test_data/multi_input.json | 2 +- tests/test_generate_form.py | 30 ++++++++++++++- tests/test_integration.py | 15 +++++--- 22 files changed, 121 insertions(+), 76 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4b21160..7151be8 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,4 +15,4 @@ ] } }, -} \ No newline at end of file +} diff --git a/.github/workflows/copilot_deploy.yml b/.github/workflows/copilot_deploy.yml index ad053fb..f83853b 100644 --- a/.github/workflows/copilot_deploy.yml +++ b/.github/workflows/copilot_deploy.yml @@ -12,7 +12,7 @@ on: - dev - test - uat - + push: # Ignore README markdown and the docs folder # Only automatically deploy when something in the app or tests folder has changed diff --git a/app/blueprints/fund_builder/forms/templates.py b/app/blueprints/fund_builder/forms/templates.py index cf48606..da213e4 100644 --- a/app/blueprints/fund_builder/forms/templates.py +++ b/app/blueprints/fund_builder/forms/templates.py @@ -2,17 +2,19 @@ from wtforms import FileField from wtforms import HiddenField from wtforms import StringField -from wtforms.validators import DataRequired, Regexp +from wtforms.validators import DataRequired +from wtforms.validators import Regexp class TemplateUploadForm(FlaskForm): template_name = StringField("Template Name", validators=[DataRequired()]) file = FileField("Upload File", validators=[DataRequired()]) + class TemplateFormForm(FlaskForm): - form_id=HiddenField() - + form_id = HiddenField() + template_name = StringField( "Template Name", description="Name of this template, only used in FAB", @@ -26,5 +28,8 @@ class TemplateFormForm(FlaskForm): url_path = StringField( "URL Path", description="The portion of the URL that equates to the form name. Use only letters, numbers and hyphens (-)", - validators=[DataRequired(), Regexp("^[a-z0-9-]*$", message="URL Path must only contain lowercase letters, numbers and hyphens (-)")], + validators=[ + DataRequired(), + Regexp("^[a-z0-9-]*$", message="URL Path must only contain lowercase letters, numbers and hyphens (-)"), + ], ) diff --git a/app/blueprints/fund_builder/routes.py b/app/blueprints/fund_builder/routes.py index 4ad8753..90bcebb 100644 --- a/app/blueprints/fund_builder/routes.py +++ b/app/blueprints/fund_builder/routes.py @@ -522,6 +522,7 @@ def view_form_questions(round_id, form_id): "view_questions.html", round=round, fund=fund, question_html=html, title=form.name_in_apply_json["en"] ) + def create_export_zip(directory_to_zip, zip_file_name) -> str: # Output zip file path (temporary) output_zip_path = Config.TEMP_FILE_PATH / zip_file_name @@ -538,7 +539,9 @@ def create_export_files(round_id): generate_config_for_round(round_id) round_short_name = get_round_by_id(round_id).short_name - output_zip_path = create_export_zip(directory_to_zip=Config.TEMP_FILE_PATH / round_short_name, zip_file_name=round_short_name) + output_zip_path = create_export_zip( + directory_to_zip=Config.TEMP_FILE_PATH / round_short_name, zip_file_name=round_short_name + ) # Ensure the file is removed after sending it @after_this_request diff --git a/app/blueprints/templates/routes.py b/app/blueprints/templates/routes.py index 36a9772..d7daf7b 100644 --- a/app/blueprints/templates/routes.py +++ b/app/blueprints/templates/routes.py @@ -1,16 +1,21 @@ import json -from flask import Blueprint, request +from flask import Blueprint from flask import redirect from flask import render_template +from flask import request from flask import url_for from werkzeug.utils import secure_filename -from app.blueprints.fund_builder.forms.templates import TemplateFormForm, TemplateUploadForm +from app.blueprints.fund_builder.forms.templates import TemplateFormForm +from app.blueprints.fund_builder.forms.templates import TemplateUploadForm from app.db.models.application_config import Form -from app.db.queries.application import delete_form, get_all_template_forms, get_form_by_id, update_form +from app.db.queries.application import delete_form +from app.db.queries.application import get_all_template_forms from app.db.queries.application import get_all_template_sections +from app.db.queries.application import get_form_by_id from app.db.queries.application import get_form_by_template_name +from app.db.queries.application import update_form # Blueprint for routes used by FAB PoC to manage templates template_bp = Blueprint( @@ -90,19 +95,23 @@ def view_templates(): def edit_form_template(form_id): template_form = TemplateFormForm() params = { - "breadcrumb_items": [ - {"text": "Home", "href": url_for("build_fund_bp.index")}, - {"text": "Manage Templates", "href": url_for("template_bp.view_templates")}, - {"text": "Rename Template", "href": "#"} - ],} + "breadcrumb_items": [ + {"text": "Home", "href": url_for("build_fund_bp.index")}, + {"text": "Manage Templates", "href": url_for("template_bp.view_templates")}, + {"text": "Rename Template", "href": "#"}, + ], + } if request.method == "POST": if template_form.validate_on_submit(): - update_form(form_id=form_id, new_form_config={ - "runner_publish_name":template_form.url_path.data, - "name_in_apply_json":{"en": template_form.tasklist_name.data}, - "template_name":template_form.template_name.data - }) + update_form( + form_id=form_id, + new_form_config={ + "runner_publish_name": template_form.url_path.data, + "name_in_apply_json": {"en": template_form.tasklist_name.data}, + "template_name": template_form.template_name.data, + }, + ) return redirect(url_for("template_bp.view_templates")) params["template_form"] = template_form return render_template("edit_form_template.html", **params) @@ -118,5 +127,5 @@ def edit_form_template(form_id): template_form.url_path.data = existing_form.runner_publish_name params["template_form"] = template_form return render_template("edit_form_template.html", **params) - + return redirect(url_for("template_bp.view_templates")) diff --git a/app/blueprints/templates/templates/edit_form_template.html b/app/blueprints/templates/templates/edit_form_template.html index 781a8d3..8b8b63c 100644 --- a/app/blueprints/templates/templates/edit_form_template.html +++ b/app/blueprints/templates/templates/edit_form_template.html @@ -23,4 +23,4 @@

{{pageHeading}}

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/db/migrations/versions/~2024_10_16_1347-da30746cec39_.py b/app/db/migrations/versions/~2024_10_16_1347-da30746cec39_.py index 187a0db..2978ff8 100644 --- a/app/db/migrations/versions/~2024_10_16_1347-da30746cec39_.py +++ b/app/db/migrations/versions/~2024_10_16_1347-da30746cec39_.py @@ -5,36 +5,40 @@ Create Date: 2024-10-16 13:47:16.968837 """ -from alembic import op -import sqlalchemy as sa +import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = 'da30746cec39' -down_revision = 'ca61d3b746f6' +revision = "da30746cec39" +down_revision = "ca61d3b746f6" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - + op.execute("ALTER TYPE componenttype ADD VALUE 'MULTI_INPUT_FIELD';") - with op.batch_alter_table('component', schema=None) as batch_op: - batch_op.add_column(sa.Column('children', postgresql.JSON(none_as_null=True, astext_type=sa.Text()), nullable=True)) - batch_op.add_column(sa.Column('schema', postgresql.JSON(astext_type=sa.Text()), nullable=True)) - with op.batch_alter_table('page', schema=None) as batch_op: - batch_op.add_column(sa.Column('options', postgresql.JSON(none_as_null=True, astext_type=sa.Text()), nullable=True)) + with op.batch_alter_table("component", schema=None) as batch_op: + batch_op.add_column( + sa.Column("children", postgresql.JSON(none_as_null=True, astext_type=sa.Text()), nullable=True) + ) + batch_op.add_column(sa.Column("schema", postgresql.JSON(astext_type=sa.Text()), nullable=True)) + with op.batch_alter_table("page", schema=None) as batch_op: + batch_op.add_column( + sa.Column("options", postgresql.JSON(none_as_null=True, astext_type=sa.Text()), nullable=True) + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('component', schema=None) as batch_op: - batch_op.drop_column('children') - batch_op.drop_column('schema') - with op.batch_alter_table('page', schema=None) as batch_op: - batch_op.drop_column('options') + with op.batch_alter_table("component", schema=None) as batch_op: + batch_op.drop_column("children") + batch_op.drop_column("schema") + with op.batch_alter_table("page", schema=None) as batch_op: + batch_op.drop_column("options") # ### end Alembic commands ### diff --git a/app/db/models/application_config.py b/app/db/models/application_config.py index bfea5ac..0ba34c3 100644 --- a/app/db/models/application_config.py +++ b/app/db/models/application_config.py @@ -53,7 +53,7 @@ class ComponentType(Enum): FILE_UPLOAD_FIELD = "FileUploadField" MONTH_YEAR_FIELD = "MonthYearField" TIME_FIELD = "TimeField" - MULTI_INPUT_FIELD="MultiInputField" + MULTI_INPUT_FIELD = "MultiInputField" READ_ONLY_COMPONENTS = [ @@ -165,7 +165,7 @@ class Page(BaseModel): ) source_template_id = Column(UUID(as_uuid=True), nullable=True) controller = Column(String(), nullable=True) - options=Column(JSON(none_as_null=True)) + options = Column(JSON(none_as_null=True)) def __repr__(self): return f"Page(/{self.display_path} - {self.name_in_apply_json['en']}, Components: {self.components})" diff --git a/app/export_config/generate_form.py b/app/export_config/generate_form.py index 172c5b6..2d59169 100644 --- a/app/export_config/generate_form.py +++ b/app/export_config/generate_form.py @@ -109,7 +109,7 @@ def build_component(component: Component) -> dict: built_component.update({"values": {"type": "listRef"}}) if component.type is ComponentType.MULTI_INPUT_FIELD: - built_component.update({"children":component.children}) + built_component.update({"children": component.children}) return built_component diff --git a/app/export_config/generate_fund_round_form_jsons.py b/app/export_config/generate_fund_round_form_jsons.py index dbac694..f2ff5b6 100644 --- a/app/export_config/generate_fund_round_form_jsons.py +++ b/app/export_config/generate_fund_round_form_jsons.py @@ -21,7 +21,7 @@ "properties": { "path": {"type": "string"}, "title": {"type": "string"}, - "options":{"type":"object"}, + "options": {"type": "object"}, "components": { "type": "array", "items": { @@ -42,7 +42,7 @@ "metadata": { "type": "object", }, - "children": {"type": "array"} + "children": {"type": "array"}, }, }, }, diff --git a/app/export_config/helpers.py b/app/export_config/helpers.py index a5733c1..24e874d 100644 --- a/app/export_config/helpers.py +++ b/app/export_config/helpers.py @@ -12,25 +12,24 @@ def write_config(config, filename, round_short_name, config_type): # Get the directory of the current file - # Construct the path to the output directory relative to this file's location base_output_dir = Config.TEMP_FILE_PATH / round_short_name if config_type == "form_json": - output_dir = base_output_dir/ "form_runner" + output_dir = base_output_dir / "form_runner" content_to_write = config - file_path = output_dir/ f"{human_to_kebab_case(filename)}.json" + file_path = output_dir / f"{human_to_kebab_case(filename)}.json" elif config_type == "python_file": - output_dir = base_output_dir/ "fund_store" + output_dir = base_output_dir / "fund_store" config_dict = convert_to_dict(config) # Convert config to dict for non-JSON types content_to_write = "LOADER_CONFIG=" content_to_write += str(config_dict) - file_path = output_dir/ f"{human_to_snake_case(filename)}.py" + file_path = output_dir / f"{human_to_snake_case(filename)}.py" elif config_type == "html": - output_dir = base_output_dir/"html" + output_dir = base_output_dir / "html" content_to_write = config - file_path = output_dir/f"{filename}_all_questions_en.html" + file_path = output_dir / f"{filename}_all_questions_en.html" # Ensure the output directory exists os.makedirs(output_dir, exist_ok=True) diff --git a/app/import_config/load_form_json.py b/app/import_config/load_form_json.py index f6df4ba..3393466 100644 --- a/app/import_config/load_form_json.py +++ b/app/import_config/load_form_json.py @@ -46,11 +46,7 @@ def _build_condition(condition_data, destination_page_path) -> Condition: def _get_component_by_runner_name(db, runner_component_name, page_id): - return ( - db.session.query(Component) - .filter(Component.runner_component_name == runner_component_name) - .first() - ) + return db.session.query(Component).filter(Component.runner_component_name == runner_component_name).first() def add_conditions_to_components(db, page: dict, conditions: dict, page_id): @@ -129,7 +125,7 @@ def insert_component_as_template(component, page_id, page_index, lizts): runner_component_name=component.get("name", None), list_id=list_id, children=component.get("children", None), - schema=component.get("schema", None) + schema=component.get("schema", None), ) try: db.session.add(new_component) @@ -148,7 +144,7 @@ def insert_page_as_template(page, form_id): controller=page.get("controller", None), is_template=True, template_name=page.get("title", None), - options = page.get("options", None), + options=page.get("options", None), ) try: db.session.add(new_page) diff --git a/config/envs/default.py b/config/envs/default.py index 2b2b033..12dfc86 100644 --- a/config/envs/default.py +++ b/config/envs/default.py @@ -20,4 +20,4 @@ class DefaultConfig(object): FORM_RUNNER_URL_REDIRECT = getenv("FORM_RUNNER_EXTERNAL_HOST", "http://localhost:3009") SQLALCHEMY_DATABASE_URI = environ.get("DATABASE_URL") - TEMP_FILE_PATH=Path("/tmp") \ No newline at end of file + TEMP_FILE_PATH = Path("/tmp") diff --git a/config/envs/development.py b/config/envs/development.py index 607c9a8..a93d7ec 100644 --- a/config/envs/development.py +++ b/config/envs/development.py @@ -17,4 +17,4 @@ class DevelopmentConfig(Config): "DATABASE_URL", "postgresql://postgres:password@fab-db:5432/fab", # pragma: allowlist secret ) - TEMP_FILE_PATH=Path("app") / "export_config" / "output" + TEMP_FILE_PATH = Path("app") / "export_config" / "output" diff --git a/config/envs/unit_test.py b/config/envs/unit_test.py index d4bda3b..a94066f 100644 --- a/config/envs/unit_test.py +++ b/config/envs/unit_test.py @@ -19,4 +19,4 @@ class UnitTestConfig(Config): "DATABASE_URL_UNIT_TEST", "postgresql://postgres:postgres@127.0.0.1:5432/fab_unit_test", # pragma: allowlist secret ) - TEMP_FILE_PATH=Path("app") / "export_config" / "output" \ No newline at end of file + TEMP_FILE_PATH = Path("app") / "export_config" / "output" diff --git a/tests/conftest.py b/tests/conftest.py index 3104a0f..892a1e5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,16 @@ +import shutil + import pytest from flask_migrate import upgrade from sqlalchemy import text from app.create_app import create_app +from config import Config from tasks.test_data import init_unit_test_data from tasks.test_data import insert_test_data from tests.helpers import create_test_fund from tests.helpers import create_test_organisation from tests.helpers import create_test_round -from config import Config -import shutil pytest_plugins = ["fsd_test_utils.fixtures.db_fixtures"] @@ -21,6 +22,7 @@ def temp_output_dir(): if temp_dir.exists(): shutil.rmtree(temp_dir) + @pytest.fixture def test_fund(flask_test_client, _db, clear_test_data): """ diff --git a/tests/test_config_export.py b/tests/test_config_export.py index 8756ab0..6ccae17 100644 --- a/tests/test_config_export.py +++ b/tests/test_config_export.py @@ -1,6 +1,5 @@ import ast import json -import shutil from pathlib import Path import pytest @@ -142,7 +141,7 @@ def test_generate_config_for_round_invalid_input(seed_dynamic_data): generate_config_for_round(round_id) -def test_generate_form_jsons_for_round_valid_input(seed_dynamic_data,temp_output_dir): +def test_generate_form_jsons_for_round_valid_input(seed_dynamic_data, temp_output_dir): # Setup: Prepare valid input parameters round_id = seed_dynamic_data["rounds"][0].round_id round_short_name = seed_dynamic_data["rounds"][0].short_name diff --git a/tests/test_config_import.py b/tests/test_config_import.py index f6a5255..839949d 100644 --- a/tests/test_config_import.py +++ b/tests/test_config_import.py @@ -83,11 +83,10 @@ def test_import_multi_input_field(seed_dynamic_data, _db): assert forms.count() == 1 pages = _db.session.query(Page).filter(Page.form_id == forms.first().form_id) assert pages.count() == 3 - page_with_multi_input = next(p for p in pages if p.display_path=='capital-costs-for-your-project') + page_with_multi_input = next(p for p in pages if p.display_path == "capital-costs-for-your-project") assert page_with_multi_input assert page_with_multi_input.options - multi_input_component = next(c for c in page_with_multi_input.components if c.title=='Capital costs') + multi_input_component = next(c for c in page_with_multi_input.components if c.title == "Capital costs") assert multi_input_component assert multi_input_component.type == ComponentType.MULTI_INPUT_FIELD assert len(multi_input_component.children) == 4 - diff --git a/tests/test_data/multi-input-exported.json b/tests/test_data/multi-input-exported.json index a6c377a..4933514 100644 --- a/tests/test_data/multi-input-exported.json +++ b/tests/test_data/multi-input-exported.json @@ -120,4 +120,4 @@ "outputs": [], "skipSummary": false, "name": "Apply for funding to save an asset in your community" -} \ No newline at end of file +} diff --git a/tests/test_data/multi_input.json b/tests/test_data/multi_input.json index bc87c02..bc90d57 100644 --- a/tests/test_data/multi_input.json +++ b/tests/test_data/multi_input.json @@ -125,7 +125,7 @@ ], "section": "bgUGuD" } - + ], "lists": [], "sections": [ diff --git a/tests/test_generate_form.py b/tests/test_generate_form.py index 0e6c9a8..08a4373 100644 --- a/tests/test_generate_form.py +++ b/tests/test_generate_form.py @@ -190,7 +190,7 @@ def test_build_page_controller_not_specified(): name_in_apply_json={"en": "Page with Options Name"}, form_index=1, components=[], - options = {"first": "option"} + options={"first": "option"}, ) ), ], @@ -373,7 +373,33 @@ def test_build_conditions( "hint": "This must be a hint", "schema": {}, "metadata": {}, - "children":[{'name': 'GLQlOh', 'options': {}, 'type': 'TextField', 'title': 'Describe the cost'}, {'name': 'JtwkMy', 'options': {'prefix': '£', 'classes': 'govuk-!-width-one-half'}, 'type': 'NumberField', 'title': 'Amount', 'hint': '', 'schema': {}}, {'name': 'LeTLDo', 'options': {'prefix': '£', 'classes': 'govuk-!-width-one-half'}, 'type': 'NumberField', 'title': 'How much money from the COF25 grant will you use to pay for this cost?', 'hint': '', 'schema': {}}, {'name': 'pHZDWT', 'options': {'prefix': '£', 'classes': 'govuk-!-width-one-half'}, 'type': 'NumberField', 'title': 'How much of the match funding will you use to pay for this cost?', 'hint': '', 'schema': {}}] + "children": [ + {"name": "GLQlOh", "options": {}, "type": "TextField", "title": "Describe the cost"}, + { + "name": "JtwkMy", + "options": {"prefix": "£", "classes": "govuk-!-width-one-half"}, + "type": "NumberField", + "title": "Amount", + "hint": "", + "schema": {}, + }, + { + "name": "LeTLDo", + "options": {"prefix": "£", "classes": "govuk-!-width-one-half"}, + "type": "NumberField", + "title": "How much money from the COF25 grant will you use to pay for this cost?", + "hint": "", + "schema": {}, + }, + { + "name": "pHZDWT", + "options": {"prefix": "£", "classes": "govuk-!-width-one-half"}, + "type": "NumberField", + "title": "How much of the match funding will you use to pay for this cost?", + "hint": "", + "schema": {}, + }, + ], }, ), ], diff --git a/tests/test_integration.py b/tests/test_integration.py index d736468..e93ea7e 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,8 +1,6 @@ import json import os -import shutil from dataclasses import asdict -from pathlib import Path from uuid import uuid4 import pytest @@ -28,8 +26,6 @@ from tasks.test_data import BASIC_FUND_INFO from tasks.test_data import BASIC_ROUND_INFO -from config import Config - def test_build_form_json_no_conditions(seed_dynamic_data): @@ -284,6 +280,7 @@ def test_list_relationship(seed_dynamic_data): assert result.lizt assert result.lizt.name == "classifications_list" + @pytest.mark.parametrize( "input_filename, output_filename,,expected_page_count_for_form,expected_component_count_for_form", [ @@ -292,7 +289,12 @@ def test_list_relationship(seed_dynamic_data): ("required-all-components.json", "required.json", 8, 27), ("favourite-colours-sarah.json", "colours.json", 4, 1), ("funding-required-cof-25.json", "funding-required.json", 12, 21), - ("Organisation-and-local-authority-information-template.json", "local-authority-and-other-organisation-information.json",16, 24), # noqa: E501 + ( + "Organisation-and-local-authority-information-template.json", + "local-authority-and-other-organisation-information.json", + 16, + 24, + ), # noqa: E501 ], ) def test_generate_config_for_round_valid_input( @@ -302,7 +304,8 @@ def test_generate_config_for_round_valid_input( input_filename, output_filename, expected_page_count_for_form, - expected_component_count_for_form,temp_output_dir + expected_component_count_for_form, + temp_output_dir, ): form_configs = [] script_dir = os.path.dirname(__file__) From ca179b9378f3898fbedb1441e415c5e41a9d5e95 Mon Sep 17 00:00:00 2001 From: NarenderRajuB Date: Fri, 18 Oct 2024 10:27:09 +0100 Subject: [PATCH 3/6] update tests --- .../~2024_10_18_0344-ca6ccf18956b_.py | 124 ++++++++++++++++++ app/db/models/application_config.py | 1 + app/export_config/generate_form.py | 5 +- .../generate_fund_round_form_jsons.py | 1 + app/import_config/load_form_json.py | 5 +- app/shared/data_classes.py | 10 ++ docker-compose-dev.yml | 7 + tests/test_data/test-section.json | 62 +++++++++ tests/test_generate_form.py | 2 + tests/test_integration.py | 1 + 10 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 app/db/migrations/versions/~2024_10_18_0344-ca6ccf18956b_.py create mode 100644 tests/test_data/test-section.json diff --git a/app/db/migrations/versions/~2024_10_18_0344-ca6ccf18956b_.py b/app/db/migrations/versions/~2024_10_18_0344-ca6ccf18956b_.py new file mode 100644 index 0000000..7eb056b --- /dev/null +++ b/app/db/migrations/versions/~2024_10_18_0344-ca6ccf18956b_.py @@ -0,0 +1,124 @@ +"""empty message + +Revision ID: ca6ccf18956b +Revises: da30746cec39 +Create Date: 2024-10-18 03:44:16.320217 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'ca6ccf18956b' +down_revision = 'da30746cec39' +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('section', postgresql.JSON(none_as_null=True, astext_type=sa.Text()), nullable=True)) + + with op.batch_alter_table('round', schema=None) as batch_op: + batch_op.alter_column('application_reminder_sent', + existing_type=sa.BOOLEAN(), + server_default=None, + existing_nullable=False) + batch_op.alter_column('reference_contact_page_over_email', + existing_type=sa.BOOLEAN(), + server_default=None, + existing_nullable=False) + batch_op.alter_column('support_times', + existing_type=sa.VARCHAR(), + server_default=None, + existing_nullable=False) + batch_op.alter_column('support_days', + existing_type=sa.VARCHAR(), + server_default=None, + existing_nullable=False) + batch_op.alter_column('project_name_field_id', + existing_type=sa.VARCHAR(), + server_default=None, + existing_nullable=False) + batch_op.alter_column('all_uploaded_documents_section_available', + existing_type=sa.BOOLEAN(), + server_default=None, + existing_nullable=False) + batch_op.alter_column('application_fields_download_available', + existing_type=sa.BOOLEAN(), + server_default=None, + existing_nullable=False) + batch_op.alter_column('display_logo_on_pdf_exports', + existing_type=sa.BOOLEAN(), + server_default=None, + existing_nullable=False) + batch_op.alter_column('mark_as_complete_enabled', + existing_type=sa.BOOLEAN(), + server_default=None, + existing_nullable=False) + batch_op.alter_column('is_expression_of_interest', + existing_type=sa.BOOLEAN(), + server_default=None, + existing_nullable=False) + batch_op.alter_column('section_base_path', + existing_type=sa.INTEGER(), + server_default=sa.text("nextval('section_base_path_seq')"), + existing_nullable=True) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('round', schema=None) as batch_op: + batch_op.alter_column('section_base_path', + existing_type=sa.INTEGER(), + server_default=sa.text("nextval('section_base_path_seq'::regclass)"), + existing_nullable=True) + batch_op.alter_column('is_expression_of_interest', + existing_type=sa.BOOLEAN(), + server_default=sa.text('false'), + existing_nullable=False) + batch_op.alter_column('mark_as_complete_enabled', + existing_type=sa.BOOLEAN(), + server_default=sa.text('false'), + existing_nullable=False) + batch_op.alter_column('display_logo_on_pdf_exports', + existing_type=sa.BOOLEAN(), + server_default=sa.text('false'), + existing_nullable=False) + batch_op.alter_column('application_fields_download_available', + existing_type=sa.BOOLEAN(), + server_default=sa.text('false'), + existing_nullable=False) + batch_op.alter_column('all_uploaded_documents_section_available', + existing_type=sa.BOOLEAN(), + server_default=sa.text('false'), + existing_nullable=False) + batch_op.alter_column('project_name_field_id', + existing_type=sa.VARCHAR(), + server_default=sa.text("''::character varying"), + existing_nullable=False) + batch_op.alter_column('support_days', + existing_type=sa.VARCHAR(), + server_default=sa.text("''::character varying"), + existing_nullable=False) + batch_op.alter_column('support_times', + existing_type=sa.VARCHAR(), + server_default=sa.text("''::character varying"), + existing_nullable=False) + batch_op.alter_column('reference_contact_page_over_email', + existing_type=sa.BOOLEAN(), + server_default=sa.text('false'), + existing_nullable=False) + batch_op.alter_column('application_reminder_sent', + existing_type=sa.BOOLEAN(), + server_default=sa.text('false'), + existing_nullable=False) + + with op.batch_alter_table('page', schema=None) as batch_op: + batch_op.drop_column('section') + + # ### end Alembic commands ### diff --git a/app/db/models/application_config.py b/app/db/models/application_config.py index 0ba34c3..3f124cb 100644 --- a/app/db/models/application_config.py +++ b/app/db/models/application_config.py @@ -166,6 +166,7 @@ class Page(BaseModel): source_template_id = Column(UUID(as_uuid=True), nullable=True) controller = Column(String(), nullable=True) options = Column(JSON(none_as_null=True)) + section = Column(JSON(none_as_null=True)) def __repr__(self): return f"Page(/{self.display_path} - {self.name_in_apply_json['en']}, Components: {self.components})" diff --git a/app/export_config/generate_form.py b/app/export_config/generate_form.py index 2d59169..ac63322 100644 --- a/app/export_config/generate_form.py +++ b/app/export_config/generate_form.py @@ -25,6 +25,7 @@ "title": None, "components": [], "next": [], + "section": None, } @@ -81,7 +82,7 @@ def build_component(component: Component) -> dict: "type": component.type.value if component.type else None, "content": component.content, "options": component.options or {}, - "schema": {}, + "schema": component.schema or {}, "title": component.title, "name": component.runner_component_name, } @@ -269,6 +270,8 @@ def build_form_json(form: Form) -> dict: # Build the basic page structure for page in form.pages: results["pages"].append(build_page(page=page)) + if page.section: + results["sections"].append(page.section) # start page is the page with the controller ending start.js start_page = _find_page_by_controller(form.pages, "start.js") diff --git a/app/export_config/generate_fund_round_form_jsons.py b/app/export_config/generate_fund_round_form_jsons.py index f2ff5b6..9ee844c 100644 --- a/app/export_config/generate_fund_round_form_jsons.py +++ b/app/export_config/generate_fund_round_form_jsons.py @@ -22,6 +22,7 @@ "path": {"type": "string"}, "title": {"type": "string"}, "options": {"type": "object"}, + "section": {"type": "object"}, "components": { "type": "array", "items": { diff --git a/app/import_config/load_form_json.py b/app/import_config/load_form_json.py index 3393466..4bad178 100644 --- a/app/import_config/load_form_json.py +++ b/app/import_config/load_form_json.py @@ -46,7 +46,8 @@ def _build_condition(condition_data, destination_page_path) -> Condition: def _get_component_by_runner_name(db, runner_component_name, page_id): - return db.session.query(Component).filter(Component.runner_component_name == runner_component_name).first() + return db.session.query(Component).filter(Component.runner_component_name == runner_component_name).filter() + db.session.quer() def add_conditions_to_components(db, page: dict, conditions: dict, page_id): @@ -145,6 +146,8 @@ def insert_page_as_template(page, form_id): is_template=True, template_name=page.get("title", None), options=page.get("options", None), + section=page.get("section", None), + ) try: db.session.add(new_page) diff --git a/app/shared/data_classes.py b/app/shared/data_classes.py index 5ff2b39..812cc11 100644 --- a/app/shared/data_classes.py +++ b/app/shared/data_classes.py @@ -155,3 +155,13 @@ class RoundExport: def as_dict(self): return asdict(self) + + +@dataclass +class FormSection: + name: str + title: str + hideTitle: Optional[bool] = None + + def as_dict(self): + return asdict(self) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 862944d..91b3da0 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -51,3 +51,10 @@ services: - 'NODE_CONFIG={"safelist": ["fab"]}' - PREVIEW_MODE=true - NODE_ENV=development + - SINGLE_REDIS=true + - FORM_RUNNER_ADAPTER_REDIS_INSTANCE_URI=redis://redis-data:6379 + + redis-data: + image: redis + ports: + - 6379:6379 diff --git a/tests/test_data/test-section.json b/tests/test_data/test-section.json new file mode 100644 index 0000000..1176ec8 --- /dev/null +++ b/tests/test_data/test-section.json @@ -0,0 +1,62 @@ + { + "metadata": {}, + "startPage": "/first-page", + "pages": [ + { + "title": "First page", + "path": "/first-page", + "components": [], + "next": [ + { + "path": "/second-page" + } + ], + "section": "aucbfI" + }, + { + "path": "/second-page", + "title": "Second page", + "components": [ + { + "name": "sBMuNR", + "options": {}, + "type": "TextField", + "title": "Your name" + } + ], + "next": [ + { + "path": "/summary" + } + ], + "section": "vJLJeJ" + }, + { + "title": "Summary", + "path": "/summary", + "controller": "./pages/summary.js", + "components": [] + } + ], + "lists": [], + "sections": [ + { + "name": "aucbfI", + "title": "Section 1", + "hideTitle": false + }, + { + "name": "vJLJeJ", + "title": "Section 2", + "hideTitle": false + } + ], + "conditions": [], + "fees": [], + "outputs": [], + "version": 2, + "skipSummary": false, + "feeOptions": {}, + "markAsComplete": false, + "name":"section" +} diff --git a/tests/test_generate_form.py b/tests/test_generate_form.py index 08a4373..22e232f 100644 --- a/tests/test_generate_form.py +++ b/tests/test_generate_form.py @@ -110,6 +110,7 @@ def test_build_lists(mocker, pages, exp_result): name_in_apply_json={"en": "Organisation Name"}, form_index=1, components=[mock_c_1], + section={"name": "abc", "title": "Section", "hideTitle": False}, ), { "path": "/organisation-single-name", @@ -126,6 +127,7 @@ def test_build_lists(mocker, pages, exp_result): } ], "next": [], + "section": "abc", }, ) ], diff --git a/tests/test_integration.py b/tests/test_integration.py index e93ea7e..fe24b68 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -295,6 +295,7 @@ def test_list_relationship(seed_dynamic_data): 16, 24, ), # noqa: E501 + ("test-section.json", "section.json", 3, 1), ], ) def test_generate_config_for_round_valid_input( From 0876ea0d9d3e6bc6b2c20b7a3a692c8adea39dae Mon Sep 17 00:00:00 2001 From: NarenderRajuB Date: Fri, 18 Oct 2024 14:26:32 +0100 Subject: [PATCH 4/6] code cleanup --- app/import_config/load_form_json.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/import_config/load_form_json.py b/app/import_config/load_form_json.py index 4bad178..3cbffc4 100644 --- a/app/import_config/load_form_json.py +++ b/app/import_config/load_form_json.py @@ -47,7 +47,6 @@ def _build_condition(condition_data, destination_page_path) -> Condition: def _get_component_by_runner_name(db, runner_component_name, page_id): return db.session.query(Component).filter(Component.runner_component_name == runner_component_name).filter() - db.session.quer() def add_conditions_to_components(db, page: dict, conditions: dict, page_id): From b5ab645a7f97f05f94b933a937acb40fe155b3c1 Mon Sep 17 00:00:00 2001 From: srh-sloan Date: Fri, 18 Oct 2024 13:41:31 +0000 Subject: [PATCH 5/6] fs-4720 fixing unit tests --- app/export_config/generate_form.py | 3 ++- app/export_config/generate_fund_round_form_jsons.py | 2 +- app/import_config/load_form_json.py | 3 +-- tests/test_data/test-section.json | 3 ++- tests/test_generate_form.py | 4 ++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/export_config/generate_form.py b/app/export_config/generate_form.py index ac63322..52e5916 100644 --- a/app/export_config/generate_form.py +++ b/app/export_config/generate_form.py @@ -25,7 +25,6 @@ "title": None, "components": [], "next": [], - "section": None, } @@ -128,6 +127,8 @@ def build_page(page: Page = None) -> dict: "title": page.name_in_apply_json["en"], } ) + if page.section: + built_page.update({"section": page.section}) if page.options: built_page.update({"options": page.options}) # Having a 'null' controller element breaks the form-json, needs to not be there if blank diff --git a/app/export_config/generate_fund_round_form_jsons.py b/app/export_config/generate_fund_round_form_jsons.py index 9ee844c..47e9620 100644 --- a/app/export_config/generate_fund_round_form_jsons.py +++ b/app/export_config/generate_fund_round_form_jsons.py @@ -22,7 +22,7 @@ "path": {"type": "string"}, "title": {"type": "string"}, "options": {"type": "object"}, - "section": {"type": "object"}, + "section": {"type": "string"}, "components": { "type": "array", "items": { diff --git a/app/import_config/load_form_json.py b/app/import_config/load_form_json.py index 3cbffc4..1c7fe32 100644 --- a/app/import_config/load_form_json.py +++ b/app/import_config/load_form_json.py @@ -46,7 +46,7 @@ def _build_condition(condition_data, destination_page_path) -> Condition: def _get_component_by_runner_name(db, runner_component_name, page_id): - return db.session.query(Component).filter(Component.runner_component_name == runner_component_name).filter() + return db.session.query(Component).filter(Component.runner_component_name == runner_component_name).first() def add_conditions_to_components(db, page: dict, conditions: dict, page_id): @@ -146,7 +146,6 @@ def insert_page_as_template(page, form_id): template_name=page.get("title", None), options=page.get("options", None), section=page.get("section", None), - ) try: db.session.add(new_page) diff --git a/tests/test_data/test-section.json b/tests/test_data/test-section.json index 1176ec8..4b7f0e5 100644 --- a/tests/test_data/test-section.json +++ b/tests/test_data/test-section.json @@ -35,7 +35,8 @@ "title": "Summary", "path": "/summary", "controller": "./pages/summary.js", - "components": [] + "components": [], + "next": [] } ], "lists": [], diff --git a/tests/test_generate_form.py b/tests/test_generate_form.py index 22e232f..54486c7 100644 --- a/tests/test_generate_form.py +++ b/tests/test_generate_form.py @@ -110,7 +110,7 @@ def test_build_lists(mocker, pages, exp_result): name_in_apply_json={"en": "Organisation Name"}, form_index=1, components=[mock_c_1], - section={"name": "abc", "title": "Section", "hideTitle": False}, + section="test_section", ), { "path": "/organisation-single-name", @@ -127,7 +127,7 @@ def test_build_lists(mocker, pages, exp_result): } ], "next": [], - "section": "abc", + "section": "test_section", }, ) ], From dd3617306fef7d6e69ca02e5a3dfc78b248ed762 Mon Sep 17 00:00:00 2001 From: NarenderRajuB Date: Fri, 18 Oct 2024 14:49:23 +0100 Subject: [PATCH 6/6] pre commit fixes --- .../~2024_10_18_0344-ca6ccf18956b_.py | 216 ++++++++++-------- app/import_config/load_form_json.py | 7 +- 2 files changed, 125 insertions(+), 98 deletions(-) diff --git a/app/db/migrations/versions/~2024_10_18_0344-ca6ccf18956b_.py b/app/db/migrations/versions/~2024_10_18_0344-ca6ccf18956b_.py index 7eb056b..402f746 100644 --- a/app/db/migrations/versions/~2024_10_18_0344-ca6ccf18956b_.py +++ b/app/db/migrations/versions/~2024_10_18_0344-ca6ccf18956b_.py @@ -5,120 +5,142 @@ Create Date: 2024-10-18 03:44:16.320217 """ -from alembic import op + import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = 'ca6ccf18956b' -down_revision = 'da30746cec39' +revision = "ca6ccf18956b" +down_revision = "da30746cec39" 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('section', postgresql.JSON(none_as_null=True, astext_type=sa.Text()), nullable=True)) + with op.batch_alter_table("page", schema=None) as batch_op: + batch_op.add_column( + sa.Column("section", postgresql.JSON(none_as_null=True, astext_type=sa.Text()), nullable=True) + ) - with op.batch_alter_table('round', schema=None) as batch_op: - batch_op.alter_column('application_reminder_sent', - existing_type=sa.BOOLEAN(), - server_default=None, - existing_nullable=False) - batch_op.alter_column('reference_contact_page_over_email', - existing_type=sa.BOOLEAN(), - server_default=None, - existing_nullable=False) - batch_op.alter_column('support_times', - existing_type=sa.VARCHAR(), - server_default=None, - existing_nullable=False) - batch_op.alter_column('support_days', - existing_type=sa.VARCHAR(), - server_default=None, - existing_nullable=False) - batch_op.alter_column('project_name_field_id', - existing_type=sa.VARCHAR(), - server_default=None, - existing_nullable=False) - batch_op.alter_column('all_uploaded_documents_section_available', - existing_type=sa.BOOLEAN(), - server_default=None, - existing_nullable=False) - batch_op.alter_column('application_fields_download_available', - existing_type=sa.BOOLEAN(), - server_default=None, - existing_nullable=False) - batch_op.alter_column('display_logo_on_pdf_exports', - existing_type=sa.BOOLEAN(), - server_default=None, - existing_nullable=False) - batch_op.alter_column('mark_as_complete_enabled', - existing_type=sa.BOOLEAN(), - server_default=None, - existing_nullable=False) - batch_op.alter_column('is_expression_of_interest', - existing_type=sa.BOOLEAN(), - server_default=None, - existing_nullable=False) - batch_op.alter_column('section_base_path', - existing_type=sa.INTEGER(), - server_default=sa.text("nextval('section_base_path_seq')"), - existing_nullable=True) + with op.batch_alter_table("round", schema=None) as batch_op: + batch_op.alter_column( + "application_reminder_sent", existing_type=sa.BOOLEAN(), server_default=None, existing_nullable=False + ) + batch_op.alter_column( + "reference_contact_page_over_email", + existing_type=sa.BOOLEAN(), + server_default=None, + existing_nullable=False, + ) + batch_op.alter_column("support_times", existing_type=sa.VARCHAR(), server_default=None, existing_nullable=False) + batch_op.alter_column("support_days", existing_type=sa.VARCHAR(), server_default=None, existing_nullable=False) + batch_op.alter_column( + "project_name_field_id", existing_type=sa.VARCHAR(), server_default=None, existing_nullable=False + ) + batch_op.alter_column( + "all_uploaded_documents_section_available", + existing_type=sa.BOOLEAN(), + server_default=None, + existing_nullable=False, + ) + batch_op.alter_column( + "application_fields_download_available", + existing_type=sa.BOOLEAN(), + server_default=None, + existing_nullable=False, + ) + batch_op.alter_column( + "display_logo_on_pdf_exports", existing_type=sa.BOOLEAN(), server_default=None, existing_nullable=False + ) + batch_op.alter_column( + "mark_as_complete_enabled", existing_type=sa.BOOLEAN(), server_default=None, existing_nullable=False + ) + batch_op.alter_column( + "is_expression_of_interest", existing_type=sa.BOOLEAN(), server_default=None, existing_nullable=False + ) + batch_op.alter_column( + "section_base_path", + existing_type=sa.INTEGER(), + server_default=sa.text("nextval('section_base_path_seq')"), + existing_nullable=True, + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('round', schema=None) as batch_op: - batch_op.alter_column('section_base_path', - existing_type=sa.INTEGER(), - server_default=sa.text("nextval('section_base_path_seq'::regclass)"), - existing_nullable=True) - batch_op.alter_column('is_expression_of_interest', - existing_type=sa.BOOLEAN(), - server_default=sa.text('false'), - existing_nullable=False) - batch_op.alter_column('mark_as_complete_enabled', - existing_type=sa.BOOLEAN(), - server_default=sa.text('false'), - existing_nullable=False) - batch_op.alter_column('display_logo_on_pdf_exports', - existing_type=sa.BOOLEAN(), - server_default=sa.text('false'), - existing_nullable=False) - batch_op.alter_column('application_fields_download_available', - existing_type=sa.BOOLEAN(), - server_default=sa.text('false'), - existing_nullable=False) - batch_op.alter_column('all_uploaded_documents_section_available', - existing_type=sa.BOOLEAN(), - server_default=sa.text('false'), - existing_nullable=False) - batch_op.alter_column('project_name_field_id', - existing_type=sa.VARCHAR(), - server_default=sa.text("''::character varying"), - existing_nullable=False) - batch_op.alter_column('support_days', - existing_type=sa.VARCHAR(), - server_default=sa.text("''::character varying"), - existing_nullable=False) - batch_op.alter_column('support_times', - existing_type=sa.VARCHAR(), - server_default=sa.text("''::character varying"), - existing_nullable=False) - batch_op.alter_column('reference_contact_page_over_email', - existing_type=sa.BOOLEAN(), - server_default=sa.text('false'), - existing_nullable=False) - batch_op.alter_column('application_reminder_sent', - existing_type=sa.BOOLEAN(), - server_default=sa.text('false'), - existing_nullable=False) + with op.batch_alter_table("round", schema=None) as batch_op: + batch_op.alter_column( + "section_base_path", + existing_type=sa.INTEGER(), + server_default=sa.text("nextval('section_base_path_seq'::regclass)"), + existing_nullable=True, + ) + batch_op.alter_column( + "is_expression_of_interest", + existing_type=sa.BOOLEAN(), + server_default=sa.text("false"), + existing_nullable=False, + ) + batch_op.alter_column( + "mark_as_complete_enabled", + existing_type=sa.BOOLEAN(), + server_default=sa.text("false"), + existing_nullable=False, + ) + batch_op.alter_column( + "display_logo_on_pdf_exports", + existing_type=sa.BOOLEAN(), + server_default=sa.text("false"), + existing_nullable=False, + ) + batch_op.alter_column( + "application_fields_download_available", + existing_type=sa.BOOLEAN(), + server_default=sa.text("false"), + existing_nullable=False, + ) + batch_op.alter_column( + "all_uploaded_documents_section_available", + existing_type=sa.BOOLEAN(), + server_default=sa.text("false"), + existing_nullable=False, + ) + batch_op.alter_column( + "project_name_field_id", + existing_type=sa.VARCHAR(), + server_default=sa.text("''::character varying"), + existing_nullable=False, + ) + batch_op.alter_column( + "support_days", + existing_type=sa.VARCHAR(), + server_default=sa.text("''::character varying"), + existing_nullable=False, + ) + batch_op.alter_column( + "support_times", + existing_type=sa.VARCHAR(), + server_default=sa.text("''::character varying"), + existing_nullable=False, + ) + batch_op.alter_column( + "reference_contact_page_over_email", + existing_type=sa.BOOLEAN(), + server_default=sa.text("false"), + existing_nullable=False, + ) + batch_op.alter_column( + "application_reminder_sent", + existing_type=sa.BOOLEAN(), + server_default=sa.text("false"), + existing_nullable=False, + ) - with op.batch_alter_table('page', schema=None) as batch_op: - batch_op.drop_column('section') + with op.batch_alter_table("page", schema=None) as batch_op: + batch_op.drop_column("section") # ### end Alembic commands ### diff --git a/app/import_config/load_form_json.py b/app/import_config/load_form_json.py index 1c7fe32..dfdd88f 100644 --- a/app/import_config/load_form_json.py +++ b/app/import_config/load_form_json.py @@ -46,7 +46,12 @@ def _build_condition(condition_data, destination_page_path) -> Condition: def _get_component_by_runner_name(db, runner_component_name, page_id): - return db.session.query(Component).filter(Component.runner_component_name == runner_component_name).first() + return ( + db.session.query(Component) + .filter(Component.runner_component_name == runner_component_name) + .filter(Component.page_id == page_id) + .first() + ) def add_conditions_to_components(db, page: dict, conditions: dict, page_id):