diff --git a/app/db/migrations/versions/~2024_07_19_1136-3fffc621bff4_.py b/app/db/migrations/versions/~2024_07_19_1136-3fffc621bff4_.py new file mode 100644 index 0000000..a9636f0 --- /dev/null +++ b/app/db/migrations/versions/~2024_07_19_1136-3fffc621bff4_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 3fffc621bff4 +Revises: 5c63de4e4e49 +Create Date: 2024-07-19 11:36:32.716999 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "3fffc621bff4" +down_revision = "5c63de4e4e49" +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("controller", sa.String(), nullable=True)) + + # ### 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_column("controller") + + # ### end Alembic commands ### diff --git a/app/db/migrations/versions/~2024_07_19_1233-3de2807b6917_.py b/app/db/migrations/versions/~2024_07_19_1233-3de2807b6917_.py new file mode 100644 index 0000000..d184a72 --- /dev/null +++ b/app/db/migrations/versions/~2024_07_19_1233-3de2807b6917_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 3de2807b6917 +Revises: 3fffc621bff4 +Create Date: 2024-07-19 12:33:29.898715 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "3de2807b6917" +down_revision = "3fffc621bff4" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("round", schema=None) as batch_op: + batch_op.add_column(sa.Column("source_template_id", sa.UUID(), nullable=True)) + batch_op.add_column(sa.Column("template_name", sa.String(), 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.drop_column("template_name") + batch_op.drop_column("source_template_id") + + # ### end Alembic commands ### diff --git a/app/db/models/application_config.py b/app/db/models/application_config.py index 49a41a7..ad59dde 100644 --- a/app/db/models/application_config.py +++ b/app/db/models/application_config.py @@ -120,6 +120,7 @@ class Page(BaseModel): "Component", order_by="Component.page_index", collection_class=ordering_list("page_index") ) source_template_id = Column(UUID(as_uuid=True), nullable=True) + controller = Column(String(), nullable=True) def __repr__(self): return f"Page(/{self.display_path} - {self.name_in_apply_json['en']}, Components: {self.components})" diff --git a/app/db/models/round.py b/app/db/models/round.py index c86b372..289c26d 100644 --- a/app/db/models/round.py +++ b/app/db/models/round.py @@ -5,6 +5,7 @@ from sqlalchemy import Column from sqlalchemy import DateTime from sqlalchemy import ForeignKey +from sqlalchemy import String from sqlalchemy import UniqueConstraint from sqlalchemy.dialects.postgresql import JSON from sqlalchemy.dialects.postgresql import UUID @@ -46,6 +47,8 @@ class Round(BaseModel): privacy_notice_link = Column("privacy_notice", db.String(), nullable=False, unique=False) audit_info = Column("audit_info", JSON(none_as_null=True)) is_template = Column("is_template", Boolean, default=False, nullable=False) + source_template_id = Column(UUID(as_uuid=True), nullable=True) + template_name = Column(String(), nullable=True) sections: Mapped[list["Section"]] = relationship("Section") criteria: Mapped[list["Criteria"]] = relationship("Criteria") # several other fields to add diff --git a/app/db/queries/application.py b/app/db/queries/application.py index 4c13171..338adc1 100644 --- a/app/db/queries/application.py +++ b/app/db/queries/application.py @@ -6,6 +6,7 @@ from app.db.models import Lizt from app.db.models import Page from app.db.models.application_config import Section +from app.db.models.round import Round def get_form_for_component(component: Component) -> Form: @@ -178,3 +179,22 @@ def clone_multiple_components(component_ids: list[str], new_page_id=None, new_th db.session.commit() return clones + + +def clone_single_round(round_id, new_fund_id) -> Round: + round_to_clone = db.session.query(Round).where(Round.round_id == round_id).one_or_none() + cloned_round = Round(**round_to_clone) + cloned_round.round_id = uuid4() + cloned_round.fund_id = new_fund_id + cloned_round.is_template = False + cloned_round.source_template_id = round_to_clone.round_id + cloned_round.template_name = None + cloned_round.sections = None + + db.session.add(cloned_round) + db.session.commit() + + for section in round_to_clone.sections: + clone_single_section(section.section_id, cloned_round.round_id) + + return cloned_round diff --git a/app/question_reuse/generate_form.py b/app/question_reuse/generate_form.py index 011e39d..c16f3c4 100644 --- a/app/question_reuse/generate_form.py +++ b/app/question_reuse/generate_form.py @@ -1,8 +1,4 @@ import copy -import json -import os - -import click from app.db.models import Component from app.db.models import Form @@ -50,8 +46,10 @@ } -# Takes in a simple set of conditions and builds them into the form runner format def build_conditions(component: Component) -> list: + """ + Takes in a simple set of conditions and builds them into the form runner format + """ results = [] for condition in component.conditions: result = { @@ -82,6 +80,9 @@ def build_conditions(component: Component) -> list: def build_component(component: Component) -> dict: + """ + Builds the component json in form runner format for the supplied Component object + """ built_component = { "options": component.options or {}, "type": component.type.value, @@ -91,6 +92,7 @@ def build_component(component: Component) -> dict: "name": component.runner_component_name, "metadata": {"fund_builder_id": str(component.component_id)}, } + # add a reference to the relevant list if this component use a list if component.lizt: built_component.update({"list": component.lizt.name}) built_component["metadata"].update({"fund_builder_list_id": str(component.list_id)}) @@ -98,6 +100,16 @@ def build_component(component: Component) -> dict: def build_page(page: Page = None, page_display_path: str = None) -> dict: + """ + Builds the form runner JSON structure for the supplied page. If that page is None, retrieves a template + page with the display_path matching page_display_path. + + This accounts for conditional logic where the destination target will be the display path of a template + page, but that page does not actually live in the main hierarchy as branching logic uses a fixed set of + conditions at this stage. + + Then builds all the components on this page and adds them to the page json structure + """ if not page: page = get_template_page_by_display_path(page_display_path) built_page = copy.deepcopy(BASIC_PAGE_STRUCTURE) @@ -108,8 +120,9 @@ def build_page(page: Page = None, page_display_path: str = None) -> dict: } ) # Having a 'null' controller element breaks the form-json, needs to not be there if blank - # if controller := input_page.get("controller", None): - # page["controller"] = controller + if page.controller: + built_page["controller"] = page.controller + for component in page.components: built_component = build_component(component) @@ -121,6 +134,7 @@ 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 @@ -165,7 +179,7 @@ def build_navigation(partial_form_json: dict, input_pages: list[Page]) -> dict: } ) - # If there were no conditions and we just continue to the next page + # If there were no conditions we just continue to the next page if not has_conditions: this_page_in_results["next"].append({"path": f"/{next_path}"}) @@ -185,93 +199,71 @@ def build_lists(pages: list[dict]) -> list: return lists -def build_start_page_content_component(content: str, pages) -> dict: +def build_start_page(content: str, form: Form) -> dict: + """ + Builds the start page which contains just an html component comprising a bullet + list of the headings of all pages in this form + """ + start_page = copy.deepcopy(BASIC_PAGE_STRUCTURE) + start_page.update( + { + "title": form.name_in_apply_json["en"], + "path": f"/intro-{human_to_kebab_case(form.name_in_apply_json['en'])}", + "controller": "./pages/start.js", + "next": [{"path": f"/{form.pages[0].display_path}"}], + } + ) ask_about = '

We will ask you about:

" - result = { - "name": "start-page-content", - "options": {}, - "type": "Html", - "content": f'

{content}

{ask_about}', - "schema": {}, - } - return result + start_page["components"].append( + { + "name": "start-page-content", + "options": {}, + "type": "Html", + "content": f'

{content}

{ask_about}', + "schema": {}, + } + ) + return start_page def human_to_kebab_case(word: str) -> str | None: + """ + Converts the supplied string into all lower case, and replaces spaces with hyphens + """ if word: return word.replace(" ", "-").strip().lower() def build_form_json(form: Form) -> dict: + """ + Takes in a single Form object and then generates the form runner json for that form. + + Inserts a start page to the beginning of the form, and the summary page at the end. + """ results = copy.deepcopy(BASIC_FORM_STRUCTURE) results["name"] = form.name_in_apply_json["en"] + # Build the basic page structure for page in form.pages: results["pages"].append(build_page(page=page)) - start_page = copy.deepcopy(BASIC_PAGE_STRUCTURE) - start_page.update( - { - "title": form.name_in_apply_json["en"], - "path": f"/intro-{human_to_kebab_case(form.name_in_apply_json['en'])}", - "controller": "./pages/start.js", - "next": [{"path": f"/{form.pages[0].display_path}"}], - } - ) - intro_content = build_start_page_content_component(content=None, pages=results["pages"]) - start_page["components"].append(intro_content) - + # 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) + # Build the list values results["lists"] = build_lists(results["pages"]) + # Add on the summary page results["pages"].append(SUMMARY_PAGE) return results - - -@click.command() -@click.option( - "--input_folder", - default="./question_reuse/test_data/in/", - help="Input configuration", - prompt=True, -) -@click.option( - "--input_file", - default="org-info_basic_name_address.json", - help="Input configuration", - prompt=True, -) -@click.option( - "--output_folder", - default="../digital-form-builder/runner/dist/server/forms/", - help="Output destination", - prompt=True, -) -@click.option( - "--output_file", - default="single_name_address.json", - help="Output destination", - prompt=True, -) -def generate_form_json(input_folder, input_file, output_folder, output_file): - with open(os.path.join(input_folder, input_file), "r") as f: - input_data = json.load(f) - - form_json = build_form_json(input_data) - - with open(os.path.join(output_folder, output_file), "w") as f: - json.dump(form_json, f) - - -if __name__ == "__main__": - generate_form_json()