Criteria Name
@@ -38,7 +44,7 @@
Subcriteria of this criteria
{% for subcriteria in criteria.subcriteria %}
- {{ subcriteria.criteria_index }}. {{ subcriteria.name }}
+ {{ subcriteria.criteria.index }}. {{ subcriteria.criteria_index }}. {{ subcriteria.name }}
Edit
@@ -55,10 +61,40 @@ {{ subcriteria.criteria_index }}. {{ subcriteria.nam
- {{ subcriteria.criteria_index }}. {{ theme.subcriteria_index }}. {{ theme.name }}
+ {{ subcriteria.criteria.index }}. {{ subcriteria.criteria_index }}. {{ theme.subcriteria_index }}. {{ theme.name }}
+
+ Edit
+ Remove
+ {% if theme.subcriteria_index > 1 %}
+ Move up
+ {% endif %}
+ {% if theme.subcriteria_index < subcriteria.themes | length %}
+ Move down
+ {% endif %}
+
+ {% for component in theme.components %}
+
+
+
+ {{ subcriteria.criteria.index }}. {{ subcriteria.criteria_index }}. {{ theme.subcriteria_index }}. {{ component.theme_index }}.
+ {{ component.runner_component_name }} -
+ {{ (component.title[:50] + '...') if component.title and component.title|length > 50 else component.title }}
+
+
+
+ Remove
+ {% if component.theme_index > 1 %}
+ Move up
+ {% endif %}
+ {% if component.theme_index < theme.components | length %}
+ Move down
+ {% endif %}
+
+
+ {% endfor %}
{% endfor %}
diff --git a/app/blueprints/fund_builder/templates/subcriteria.html b/app/blueprints/fund_builder/templates/subcriteria.html
index b9d9793..d29645c 100644
--- a/app/blueprints/fund_builder/templates/subcriteria.html
+++ b/app/blueprints/fund_builder/templates/subcriteria.html
@@ -25,15 +25,54 @@
Subcriteria Name
{% if form.subcriteria_id.data %}
-
Themes
+
Themes of this subcriteria
{{
govukButton({
"text": "Add Themes",
- "href": "" ,
+ "href": url_for("build_fund_bp.theme", subcriteria_id=subcriteria.subcriteria_id),
"classes": "govuk-button--secondary"
})
}}
+ {% for theme in subcriteria.themes %}
+ -
+
+
{{ subcriteria.criteria.index }}. {{ subcriteria.criteria_index }}. {{ theme.subcriteria_index }}. {{ theme.name }}
+
+
+ Edit
+ Remove
+ {% if theme.subcriteria_index > 1 %}
+ Move up
+ {% endif %}
+ {% if theme.subcriteria_index < subcriteria.themes | length %}
+ Move down
+ {% endif %}
+
+
+ {% for component in theme.components %}
+ -
+
+
+ {{ subcriteria.criteria.index }}. {{ subcriteria.criteria_index }}. {{ theme.subcriteria_index }}. {{ component.theme_index }}.
+ {{ component.runner_component_name }} -
+ {{ (component.title[:50] + '...') if component.title and component.title|length > 50 else component.title }}
+
+
+
+ Remove
+ {% if component.theme_index > 1 %}
+ Move up
+ {% endif %}
+ {% if component.theme_index < theme.components | length %}
+ Move down
+ {% endif %}
+
+
+ {% endfor %}
+
+
+ {% endfor %}
diff --git a/app/blueprints/fund_builder/templates/theme.html b/app/blueprints/fund_builder/templates/theme.html
new file mode 100644
index 0000000..f9d7464
--- /dev/null
+++ b/app/blueprints/fund_builder/templates/theme.html
@@ -0,0 +1,70 @@
+{% extends "base.html" %}
+{% set pageHeading %}
+ {% if form.subcriteria_id.data %}Update{% else %}Create{% endif %} Theme
+{% endset %}
+
+{% from "macros/wtfToGovUk.html" import input %}
+{% from "macros/wtfToGovUk.html" import yes_no %}
+{%- from "govuk_frontend_jinja/components/button/macro.html" import govukButton -%}
+{%- from "govuk_frontend_jinja/components/select/macro.html" import govukSelect -%}
+
+{% block content %}
+
+ {% if form.theme_id.data %}
+
+
+
+ Components of this theme's forms
+
+
+ {% for component in theme.components %}
+ -
+
+
+ {{ theme.subcriteria.criteria.index }}. {{ theme.subcriteria_index }}. {{ component.theme_index }}.
+ {{ component.runner_component_name }} -
+ {{ (component.title[:50] + '...') if component.title and component.title|length > 50 else component.title }}
+
+
+
+ Remove
+ {% if component.theme_index > 1 %}
+ Move up
+ {% endif %}
+ {% if component.theme_index < theme.components | length %}
+ Move down
+ {% endif %}
+
+
+ {% endfor %}
+
+
Add Form From Template
+
+
+
+ {% endif %}
+{% endblock content %}
diff --git a/app/db/models/application_config.py b/app/db/models/application_config.py
index b349431..1dd44f7 100644
--- a/app/db/models/application_config.py
+++ b/app/db/models/application_config.py
@@ -66,7 +66,6 @@ class ComponentType(Enum):
@dataclass
class Section(BaseModel):
-
round_id = Column(
UUID(as_uuid=True),
ForeignKey("round.round_id"),
@@ -102,7 +101,6 @@ def as_dict(self, include_relationships=False):
@dataclass
class Form(BaseModel):
-
section_id = Column(
UUID(as_uuid=True),
ForeignKey("section.section_id"),
@@ -156,7 +154,6 @@ def as_dict(self):
@dataclass
class Page(BaseModel):
-
form_id = Column(
UUID(as_uuid=True),
ForeignKey("form.form_id"),
@@ -188,6 +185,7 @@ class Page(BaseModel):
ForeignKey("formsection.form_section_id"),
nullable=True,
)
+ form = relationship("Form", back_populates="pages")
form_section_id: Mapped[int | None] = mapped_column(ForeignKey(FormSection.form_section_id))
formsection: Mapped[FormSection | None] = relationship()
@@ -225,7 +223,6 @@ def as_dict(self):
@dataclass
class Component(BaseModel):
-
component_id = Column(
UUID(as_uuid=True),
primary_key=True,
@@ -236,6 +233,7 @@ class Component(BaseModel):
ForeignKey("page.page_id"),
nullable=True, # will be null where this is a template and not linked to a page
)
+ page = relationship("Page", back_populates="components")
theme_id = Column(
UUID(as_uuid=True),
ForeignKey("theme.theme_id"),
@@ -257,7 +255,8 @@ class Component(BaseModel):
conditions = Column(JSON(none_as_null=True))
source_template_id = Column(UUID(as_uuid=True), nullable=True)
runner_component_name = Column(
- String(), nullable=True # None for display only fields
+ String(),
+ nullable=True, # None for display only fields
) # TODO add validation to make sure it's only letters, numbers and _
list_id = Column(
UUID(as_uuid=True),
diff --git a/app/db/models/assessment_config.py b/app/db/models/assessment_config.py
index dfd9f3d..59d0c72 100644
--- a/app/db/models/assessment_config.py
+++ b/app/db/models/assessment_config.py
@@ -88,10 +88,16 @@ class Theme(BaseModel):
ForeignKey("subcriteria.subcriteria_id"),
nullable=True,
)
+ subcriteria = relationship("Subcriteria", back_populates="themes")
name = Column(String())
template_name = Column("Template Name", String(), nullable=True)
is_template = Column("is_template", Boolean, default=False, nullable=False)
audit_info = Column("audit_info", JSON(none_as_null=True))
- components: Mapped[List["Component"]] = relationship("Component")
+ components: Mapped[List["Component"]] = relationship(
+ "Component",
+ order_by="Component.theme_index",
+ collection_class=ordering_list("theme_index", count_from=1),
+ passive_deletes="all",
+ )
subcriteria_index = Column(Integer())
source_template_id = Column(UUID(as_uuid=True), nullable=True)
diff --git a/app/db/queries/application.py b/app/db/queries/application.py
index e24877c..1c70a60 100644
--- a/app/db/queries/application.py
+++ b/app/db/queries/application.py
@@ -1,3 +1,4 @@
+from itertools import chain
from uuid import uuid4
from sqlalchemy import delete
@@ -11,6 +12,7 @@
from app.db.models import Section
from app.db.models.assessment_config import Criteria
from app.db.models.assessment_config import Subcriteria
+from app.db.models.assessment_config import Theme
from app.db.models.round import Round
from app.db.queries.round import get_round_by_id
@@ -32,6 +34,10 @@ def get_subcriteria_by_id(subcriteria_id) -> Subcriteria:
return db.session.query(Subcriteria).where(Subcriteria.subcriteria_id == subcriteria_id).one_or_none()
+def get_theme_by_id(theme_id) -> Theme:
+ return db.session.query(Theme).where(Theme.theme_id == theme_id).one_or_none()
+
+
def get_all_template_forms() -> list[Form]:
return db.session.query(Form).where(Form.is_template == True).all() # noqa:E712
@@ -175,6 +181,21 @@ def clone_single_form(form_id: str, new_section_id=None, section_index=0) -> For
return clone
+def assign_components_to_theme(form_id: str, theme: Theme):
+ form: Form = db.session.query(Form).where(Form.form_id == form_id).one_or_none()
+ new_theme_index = max(len(theme.components) + 1, 1)
+ components = list(chain.from_iterable([page.components for page in form.pages]))
+ for component in components:
+ if component.theme_id is not None:
+ continue
+
+ component.theme_id = theme.theme_id
+ component.theme_index = new_theme_index
+ new_theme_index += 1
+
+ db.session.commit()
+
+
def _initiate_cloned_components_for_page(
components_to_clone: list[Component], new_page_id: str = None, new_theme_id: str = None
):
@@ -318,6 +339,24 @@ def insert_new_subcriteria(new_subcriteria_config):
return subcriteria
+def insert_new_theme(new_theme_config):
+ theme = Theme(
+ theme_id=uuid4(),
+ subcriteria_id=new_theme_config.get("subcriteria_id"),
+ name=new_theme_config.get("name"),
+ template_name=new_theme_config.get("template_name", None),
+ is_template=new_theme_config.get("is_template", False),
+ source_template_id=new_theme_config.get("source_template_id", None),
+ audit_info=new_theme_config.get("audit_info", {}),
+ subcriteria_index=new_theme_config.get("subcriteria_index"),
+ )
+
+ db.session.add(theme)
+ db.session.commit()
+
+ return theme
+
+
def update_section(section_id, new_section_config):
section = db.session.query(Section).where(Section.section_id == section_id).one_or_none()
if section:
@@ -361,6 +400,20 @@ def update_subcriteria(subcriteria_id, new_subcriteria_config):
return subcriteria
+def update_theme(theme_id, new_theme_config):
+ theme = db.session.query(Theme).where(Theme.theme_id == theme_id).one_or_none()
+ if theme:
+ allowed_keys = ["name", "is_template", "audit_info", "subcriteria_index"]
+
+ for key, value in new_theme_config.items():
+ if key in allowed_keys:
+ setattr(theme, key, value)
+
+ db.session.commit()
+
+ return theme
+
+
def delete_section_from_round(round_id, section_id, cascade: bool = False):
"""Removes a section from the application config for a round. Uses `reorder()` on the
round to update numbering so if there are 3 sections in a round numbered as follows:
@@ -498,6 +551,55 @@ def delete_form_from_section(section_id, form_id, cascade: bool = False):
db.session.commit()
+def delete_criteria_from_round(criteria_id, round_id):
+ round = db.session.query(Round).where(Round.round_id == round_id).one_or_none()
+ criteria = db.session.query(Criteria).where(Criteria.criteria_id == criteria_id).one_or_none()
+
+ # cascade delete subscriteria attached to scriteria
+ for subcriteria in criteria.subcriteria:
+ delete_subcriteria_from_criteria(subcriteria_id=subcriteria.subcriteria_id, criteria_id=criteria.criteria_id)
+
+ db.session.delete(criteria)
+ round.criteria.reorder()
+ db.session.commit()
+
+
+def delete_subcriteria_from_criteria(subcriteria_id, criteria_id):
+ criteria = db.session.query(Criteria).where(Criteria.criteria_id == criteria_id).one_or_none()
+ subcriteria = db.session.query(Subcriteria).where(Subcriteria.subcriteria_id == subcriteria_id).one_or_none()
+
+ # cascade delete themes attached to subscriteria
+ for theme in subcriteria.themes:
+ delete_theme_from_subcriteria(theme_id=theme.theme_id, subcriteria_id=subcriteria_id)
+
+ db.session.delete(subcriteria)
+ criteria.subcriteria.reorder()
+ db.session.commit()
+
+
+def delete_theme_from_subcriteria(theme_id, subcriteria_id):
+ subcriteria = db.session.query(Subcriteria).where(Subcriteria.subcriteria_id == subcriteria_id).one_or_none()
+ theme = db.session.query(Theme).where(Theme.theme_id == theme_id).one_or_none()
+
+ # detach components from theme
+ for component in theme.components:
+ delete_component_from_theme(component_id=component.component_id, theme_id=theme.theme_id)
+
+ db.session.delete(theme)
+ subcriteria.themes.reorder()
+ db.session.commit()
+
+
+def delete_component_from_theme(component_id, theme_id):
+ theme = get_theme_by_id(theme_id=theme_id)
+ component = get_component_by_id(component_id=component_id)
+ component.theme_id = None
+ component.theme_index = None
+ theme.components.reorder()
+
+ db.session.commit()
+
+
def delete_form(form_id, cascade: bool = False):
"""Deletes a form. If cascade==True, cascades this delete down through the hierarchy to
pages within this form and then components on those pages. It DOES NOT update the section_index
@@ -822,6 +924,72 @@ def move_form_up(section_id, form_index_to_move_up: int):
db.session.commit()
+def move_component_up(theme_id, index_to_move_up: int):
+ theme: Theme = get_theme_by_id(theme_id)
+ list_index_to_move_up = index_to_move_up - 1 # Need the 0-based index inside the list
+
+ theme.components = swap_elements_in_list(theme.components, list_index_to_move_up, list_index_to_move_up - 1)
+ db.session.commit()
+
+
+def move_component_down(theme_id, index_to_move_down: int):
+ theme: Theme = get_theme_by_id(theme_id)
+ list_index_to_move_down = index_to_move_down - 1 # Need the 0-based index inside the list
+
+ theme.components = swap_elements_in_list(theme.components, list_index_to_move_down, list_index_to_move_down + 1)
+ db.session.commit()
+
+
+def move_theme_up(subcriteria_id, index_to_move_up: int):
+ subcriteria: Subcriteria = get_subcriteria_by_id(subcriteria_id)
+ list_index_to_move_up = index_to_move_up - 1 # Need the 0-based index inside the list
+
+ subcriteria.themes = swap_elements_in_list(subcriteria.themes, list_index_to_move_up, list_index_to_move_up - 1)
+ db.session.commit()
+
+
+def move_theme_down(subcriteria_id, index_to_move_down: int):
+ subcriteria: Subcriteria = get_subcriteria_by_id(subcriteria_id)
+ list_index_to_move_down = index_to_move_down - 1 # Need the 0-based index inside the list
+
+ subcriteria.themes = swap_elements_in_list(subcriteria.themes, list_index_to_move_down, list_index_to_move_down + 1)
+ db.session.commit()
+
+
+def move_subcriteria_up(criteria_id, index_to_move_up: int):
+ criteria: Criteria = get_criteria_by_id(criteria_id)
+ list_index_to_move_up = index_to_move_up - 1 # Need the 0-based index inside the list
+
+ criteria.subcriteria = swap_elements_in_list(criteria.subcriteria, list_index_to_move_up, list_index_to_move_up - 1)
+ db.session.commit()
+
+
+def move_subcriteria_down(criteria_id, index_to_move_down: int):
+ criteria: Criteria = get_criteria_by_id(criteria_id)
+ list_index_to_move_down = index_to_move_down - 1 # Need the 0-based index inside the list
+
+ criteria.subcriteria = swap_elements_in_list(
+ criteria.subcriteria, list_index_to_move_down, list_index_to_move_down + 1
+ )
+ db.session.commit()
+
+
+def move_criteria_up(round_id, index_to_move_up: int):
+ round: Round = get_round_by_id(round_id)
+ list_index_to_move_up = index_to_move_up - 1 # Need the 0-based index inside the list
+
+ round.criteria = swap_elements_in_list(round.criteria, list_index_to_move_up, list_index_to_move_up - 1)
+ db.session.commit()
+
+
+def move_criteria_down(round_id, index_to_move_down: int):
+ round: Round = get_round_by_id(round_id)
+ list_index_to_move_down = index_to_move_down - 1 # Need the 0-based index inside the list
+
+ round.criteria = swap_elements_in_list(round.criteria, list_index_to_move_down, list_index_to_move_down + 1)
+ db.session.commit()
+
+
def insert_list(list_config: dict, do_commit: bool = True) -> Lizt:
new_list = Lizt(
is_template=True,
diff --git a/app/export_config/generate_assessment_config.py b/app/export_config/generate_assessment_config.py
index b0baa91..540668e 100644
--- a/app/export_config/generate_assessment_config.py
+++ b/app/export_config/generate_assessment_config.py
@@ -1,4 +1,3 @@
-import copy
import json
import os
@@ -8,10 +7,9 @@
from app.db import db
from app.db.models import Component
from app.db.models import Criteria
-from app.db.models import Section
from app.db.models import Subcriteria
from app.db.models import Theme
-from app.db.models.application_config import READ_ONLY_COMPONENTS
+from app.db.models.round import Round
from app.db.queries.application import get_form_for_component
from app.export_config import helpers
from app.export_config.generate_form import human_to_kebab_case
@@ -212,67 +210,62 @@ def generate_assessment_config(
json.dump(assessment_config, f)
-def generate_assessment_config_for_round(fund_config, round_config, base_output_dir):
- # The following config is not tested for production use
- # It is generated to make local testing easier - you can add an application to fab and export it with a basic
- # auto-generated assessment config.
- # Each form is a sub-critiera, each page a theme. Half scored, half unscored.
- # The output in the assessment_store folder needs to be added to the
- # assessment_mapping_fund_round file in assessment-store
- fund_id = fund_config["id"]
+def generate_assessment_config_for_round(round_config, base_output_dir):
round_id = round_config["id"]
- fund_short_name = fund_config["short_name"]
- round_short_name = round_config["short_name"]
- fund_round = f"{str.upper(fund_short_name)}{str.upper(round_short_name)}"
- fund_round_ids = f"{fund_id}:{round_id}"
+ round = db.session.query(Round).filter(Round.round_id == round_id).one_or_none()
+ unscored_criteria = ["unscored", "declarations"]
unscored = []
- sections = db.session.query(Section).filter(Section.round_id == round_id).order_by(Section.index).all()
- for i, section in enumerate(sections, start=1):
- criteria = {
- "id": human_to_kebab_case(section.name_in_apply_json["en"]),
- "name": section.name_in_apply_json["en"],
- "sub_criteria": [],
- }
- unscored.append(criteria)
+ scored = []
+ for criteria in round.criteria:
+ criteria_slug_name = human_to_kebab_case(criteria.name)
+ is_unscored = criteria_slug_name in unscored_criteria
+
+ export_subcrieria = []
+ for subcriteria in criteria.subcriteria:
+ export_theme = []
+ for theme in subcriteria.themes:
+ answers = []
+ for component in theme.components:
+ answers.append(
+ {
+ "field_id": component.runner_component_name,
+ "form_name": component.page.form.runner_publish_name,
+ "field_type": component.type.value[0].lower() + component.type.value[1:],
+ "presentation_type": form_json_to_assessment_display_types.get(component.type.name, "text"),
+ "question": component.title,
+ }
+ )
+
+ export_theme.append(
+ {
+ "id": human_to_kebab_case(theme.name),
+ "name": theme.name,
+ "answers": answers,
+ }
+ )
- for form in section.forms:
- sc = {
- "id": form.runner_publish_name,
- "name": form.name_in_apply_json["en"],
- "themes": [],
- }
- for page in form.pages:
- if page.display_path == "summary":
- continue
- theme = {
- "id": human_to_kebab_case(page.name_in_apply_json["en"]),
- "name": page.name_in_apply_json["en"],
- "answers": [],
+ export_subcrieria.append(
+ {
+ "id": human_to_kebab_case(subcriteria.name),
+ "name": subcriteria.name,
+ "themes": export_theme,
}
- for component in page.components:
- if component.type in READ_ONLY_COMPONENTS:
- continue
- answer = {
- "field_id": component.runner_component_name,
- "form_name": form.runner_publish_name,
- "field_type": component.type.value[0].lower() + component.type.value[1:],
- "presentation_type": form_json_to_assessment_display_types.get(component.type.name, "text"),
- "question": component.title,
- }
- theme["answers"].append(answer)
- sc["themes"].append(theme)
- criteria["sub_criteria"].append(sc)
- assess_output = copy.deepcopy(helpers.assess_output)
- assess_output = assess_output.substitute(
- fund_round=fund_round,
- fund_id=fund_id,
- round_id=round_id,
- fund_round_ids=fund_round_ids,
- fund_short_name=fund_short_name,
- unscored=json.dumps(unscored),
- )
- helpers.write_config(assess_output, "assessment_config", round_short_name, "assessment", base_output_dir)
+ )
+
+ export_crieria = {
+ "id": criteria_slug_name,
+ "name": criteria.name,
+ "sub_criteria": export_subcrieria,
+ }
+
+ if is_unscored:
+ unscored.append(export_crieria)
+ else:
+ scored.append(export_crieria)
+
+ helpers.write_config(unscored, "unscored", round.short_name, "assessment", base_output_dir)
+ helpers.write_config(scored, "scored", round.short_name, "assessment", base_output_dir)
if __name__ == "__main__":