Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fs 4533 clones #10

Merged
merged 4 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/blueprints/fund_builder/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from app.blueprints.fund_builder.forms.round import RoundForm
from app.db.models.fund import Fund
from app.db.models.round import Round
from app.db.queries.application import clone_single_round
from app.db.queries.application import get_form_by_id
from app.db.queries.fund import add_fund
from app.db.queries.fund import get_all_funds
Expand Down Expand Up @@ -67,6 +68,15 @@ def view_app_config(round_id):
return render_template("view_application_config.html", round=round, fund=fund)


@build_fund_bp.route("/fund/<fund_id>/round/<round_id>/clone")
def clone_round(round_id, fund_id):

cloned = clone_single_round(round_id=round_id, new_fund_id=fund_id, new_short_name=f"R-C{randint(0,999)}")
flash(f"Cloned new round: {cloned.short_name}")

return redirect(url_for("build_fund_bp.view_fund", fund_id=fund_id))


@build_fund_bp.route("/fund/round/<round_id>/assessment_config")
def view_assess_config(round_id):
"""
Expand Down
6 changes: 6 additions & 0 deletions app/blueprints/fund_builder/templates/view_fund_config.html
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ <h2 class="govuk-heading-m">{{round.title_json["en"]}}</h2>
"href": url_for("build_fund_bp.view_assess_config", round_id=round.round_id),
"classes": "govuk-button--secondary"
}) }}

{{ govukButton({
"text": "Clone this round",
"href": url_for("build_fund_bp.clone_round", round_id=round.round_id, fund_id=fund.fund_id),
"classes": "govuk-button--secondary"
}) }}
{% endfor %}

{% endset %}
Expand Down
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 ###
38 changes: 38 additions & 0 deletions app/db/migrations/versions/~2024_07_19_1320-da88c6b36588_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""empty message

Revision ID: da88c6b36588
Revises: 3de2807b6917
Create Date: 2024-07-19 13:20:24.997440

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "da88c6b36588"
down_revision = "3de2807b6917"
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("prospectus_link", sa.String(), nullable=False))
batch_op.add_column(sa.Column("privacy_notice_link", sa.String(), nullable=False))
batch_op.drop_column("privacy_notice")
batch_op.drop_column("prospectus")

# ### 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.add_column(sa.Column("prospectus", sa.VARCHAR(), autoincrement=False, nullable=False))
batch_op.add_column(sa.Column("privacy_notice", sa.VARCHAR(), autoincrement=False, nullable=False))
batch_op.drop_column("privacy_notice_link")
batch_op.drop_column("prospectus_link")

# ### 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
34 changes: 21 additions & 13 deletions app/db/models/round.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from sqlalchemy import Column
from sqlalchemy import DateTime
from sqlalchemy import ForeignKey
from sqlalchemy import String
from sqlalchemy import UniqueConstraint
from sqlalchemy import inspect
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped
Expand All @@ -23,29 +25,35 @@
class Round(BaseModel):
__table_args__ = (UniqueConstraint("fund_id", "short_name"),)
round_id = Column(
"round_id",
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
nullable=False,
)
fund_id = Column(
"fund_id",
UUID(as_uuid=True),
ForeignKey("fund.fund_id"),
nullable=False,
)
title_json = Column("title_json", JSON(none_as_null=True), nullable=False, unique=False)
short_name = Column("short_name", db.String(), nullable=False, unique=False)
opens = Column("opens", DateTime())
deadline = Column("deadline", DateTime())
assessment_start = Column("assessment_start", DateTime())
reminder_date = Column("reminder_date", DateTime())
assessment_deadline = Column("assessment_deadline", DateTime())
prospectus_link = Column("prospectus", db.String(), nullable=False, unique=False)
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)
title_json = Column(JSON(none_as_null=True), nullable=False, unique=False)
short_name = Column(db.String(), nullable=False, unique=False)
opens = Column(DateTime())
deadline = Column(DateTime())
assessment_start = Column(DateTime())
reminder_date = Column(DateTime())
assessment_deadline = Column(DateTime())
prospectus_link = Column(db.String(), nullable=False, unique=False)
privacy_notice_link = Column(db.String(), nullable=False, unique=False)
audit_info = Column(JSON(none_as_null=True))
is_template = Column(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

def __repr__(self):
return f"Round({self.short_name - self.title_json['en']}, Sections: {self.sections})"

def as_dict(self):
return {col.name: self.__getattribute__(col.name) for col in inspect(self).mapper.columns}
106 changes: 100 additions & 6 deletions app/db/queries/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from app.db.models import Form
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 @@ -62,18 +64,90 @@ def _initiate_cloned_page(to_clone: Page, new_form_id=None):
return clone


def clone_single_page(page_id: str, new_form_id=None) -> Page:
page_to_clone: Page = db.session.query(Page).where(Page.page_id == page_id).one_or_none()
clone = _initiate_cloned_page(page_to_clone, new_form_id)
def _initiate_cloned_form(to_clone: Form, new_section_id: str) -> Form:
clone = Form(**to_clone.as_dict())
clone.form_id = uuid4()
clone.section_id = new_section_id
clone.is_template = False
clone.source_template_id = to_clone.form_id
clone.template_name = None
clone.pages = []
return clone


def _initiate_cloned_section(to_clone: Section, new_round_id: str) -> Form:
clone = Section(**to_clone.as_dict())
clone.round_id = new_round_id
clone.section_id = uuid4()
clone.is_template = False
clone.source_template_id = to_clone.section_id
clone.template_name = None
clone.pages = []
return clone


def clone_single_section(section_id: str, new_round_id=None) -> Section:
section_to_clone: Section = db.session.query(Section).where(Section.section_id == section_id).one_or_none()
clone = _initiate_cloned_section(section_to_clone, new_round_id)

cloned_forms = []
cloned_pages = []
cloned_components = []
# loop through forms in this section and clone each one
for form_to_clone in section_to_clone.forms:
cloned_form = _initiate_cloned_form(form_to_clone, clone.section_id)
# loop through pages in this section and clone each one
for page_to_clone in form_to_clone.pages:
cloned_page = _initiate_cloned_page(page_to_clone, new_form_id=cloned_form.form_id)
cloned_pages.append(cloned_page)
# clone the components on this page
cloned_components.extend(
_initiate_cloned_components_for_page(page_to_clone.components, cloned_page.page_id)
)

cloned_forms.append(cloned_form)

db.session.add_all([clone, *cloned_forms, *cloned_pages, *cloned_components])
db.session.commit()

return clone


def clone_single_form(form_id: str, new_section_id=None) -> Form:
form_to_clone: Form = db.session.query(Form).where(Form.form_id == form_id).one_or_none()
clone = _initiate_cloned_form(form_to_clone, new_section_id)

cloned_pages = []
cloned_components = []
for component_to_clone in page_to_clone.components:
for page_to_clone in form_to_clone.pages:

cloned_page = _initiate_cloned_page(page_to_clone, new_form_id=clone.form_id)
cloned_pages.append(cloned_page)
cloned_components.extend(_initiate_cloned_components_for_page(page_to_clone.components, cloned_page.page_id))
db.session.add_all([clone, *cloned_pages, *cloned_components])
db.session.commit()

return clone


def _initiate_cloned_components_for_page(
components_to_clone: list[Component], new_page_id: str = None, new_theme_id: str = None
):
cloned_components = []
for component_to_clone in components_to_clone:

cloned_component = _initiate_cloned_component(
component_to_clone, new_page_id=clone.page_id, new_theme_id=None
component_to_clone, new_page_id=new_page_id, new_theme_id=None
) # TODO how should themes work when cloning?
cloned_components.append(cloned_component)
# clone.components = cloned_components
return cloned_components


def clone_single_page(page_id: str, new_form_id=None) -> Page:
page_to_clone: Page = db.session.query(Page).where(Page.page_id == page_id).one_or_none()
clone = _initiate_cloned_page(page_to_clone, new_form_id)

cloned_components = _initiate_cloned_components_for_page(page_to_clone.components, new_page_id=clone.page_id)
db.session.add_all([clone, *cloned_components])
db.session.commit()

Expand Down Expand Up @@ -105,3 +179,23 @@ 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, new_short_name) -> Round:
round_to_clone = db.session.query(Round).where(Round.round_id == round_id).one_or_none()
cloned_round = Round(**round_to_clone.as_dict())
cloned_round.short_name = new_short_name
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 = []

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
Loading