Skip to content

Commit

Permalink
Clone form and section
Browse files Browse the repository at this point in the history
  • Loading branch information
srh-sloan committed Jul 19, 2024
1 parent 8d51e37 commit 7eaabf1
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 70 deletions.
32 changes: 32 additions & 0 deletions app/db/migrations/versions/~2024_07_19_1136-3fffc621bff4_.py
Original file line number Diff line number Diff line change
@@ -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 ###
34 changes: 34 additions & 0 deletions app/db/migrations/versions/~2024_07_19_1233-3de2807b6917_.py
Original file line number Diff line number Diff line change
@@ -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 ###
1 change: 1 addition & 0 deletions app/db/models/application_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})"
Expand Down
3 changes: 3 additions & 0 deletions app/db/models/round.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
20 changes: 20 additions & 0 deletions app/db/queries/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
132 changes: 62 additions & 70 deletions app/question_reuse/generate_form.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import copy
import json
import os

import click

from app.db.models import Component
from app.db.models import Form
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
Expand All @@ -91,13 +92,24 @@ 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)})
return built_component


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)
Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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}"})

Expand All @@ -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 = '<p class="govuk-body">We will ask you about:</p> <ul>'
for page in pages:
ask_about += f"<li>{page['title']}</li>"
for page in form.pages:
ask_about += f"<li>{page.name_in_apply_json['en']}</li>"
ask_about += "</ul>"

result = {
"name": "start-page-content",
"options": {},
"type": "Html",
"content": f'<p class="govuk-body">{content}</p>{ask_about}',
"schema": {},
}
return result
start_page["components"].append(
{
"name": "start-page-content",
"options": {},
"type": "Html",
"content": f'<p class="govuk-body">{content}</p>{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()

0 comments on commit 7eaabf1

Please sign in to comment.