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-4927 - Intermediary select pages #182

Merged
merged 8 commits into from
Jan 7, 2025
42 changes: 41 additions & 1 deletion app/blueprints/application/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
update_section,
)
from app.db.queries.clone import clone_single_form
from app.db.queries.fund import get_fund_by_id
from app.db.queries.fund import get_all_funds, get_fund_by_id
from app.db.queries.round import get_round_by_id
from app.export_config.generate_all_questions import print_html
from app.export_config.generate_assessment_config import (
Expand All @@ -54,6 +54,46 @@
)


@application_bp.route("/sections/select-grant", methods=["GET", "POST"]) # NOSONAR
def select_fund():
"""
Intermediary page to select a Fund before building an Application.
"""
if request.method == "POST":
fund_id = request.form.get("fund_id")
if not fund_id:
raise ValueError("Fund ID is required to manage an application")
return redirect(url_for("application_bp.select_application", fund_id=fund_id))
fund_dropdown_items = [{"value": "", "text": "Select a grant"}]
for fund in get_all_funds():
fund_dropdown_items.append(
{"value": str(fund.fund_id), "text": fund.short_name + " - " + fund.title_json["en"]}
)
return render_template("select_fund.html", fund_dropdown_items=fund_dropdown_items)


@application_bp.route("/sections/select-application", methods=["GET", "POST"]) # NOSONAR
def select_application():
"""
Intermediary page to select an Application before managing its tasklist.
"""
if request.method == "POST":
round_id = request.form.get("round_id")
if not round_id:
wjrm500 marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError("Round ID is required to manage an application")
return redirect(url_for("application_bp.build_application", round_id=round_id))
fund_id = request.args.get("fund_id")
if not fund_id:
raise ValueError("Fund ID is required to manage an application")
round_dropdown_items = [{"value": "", "text": "Select an application"}]
fund = get_fund_by_id(fund_id)
for round_ in fund.rounds:
round_dropdown_items.append(
{"value": str(round_.round_id), "text": round_.short_name + " - " + round_.title_json["en"]}
)
return render_template("select_application.html", fund=fund, round_dropdown_items=round_dropdown_items)


@application_bp.route("/<round_id>/sections")
def build_application(round_id):
"""
Expand Down
2 changes: 1 addition & 1 deletion app/blueprints/fund/templates/fund_config.html
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ <h2 class="govuk-heading-m">Application Rounds</h2>
"classes": "govuk-button--secondary"
}) + govukButton({
"text": "Edit Round",
"href": url_for("round_bp.round", round_id=round.round_id),
"href": url_for("round_bp.edit_round", round_id=round.round_id),
"classes": "govuk-button--secondary"
})

Expand Down
4 changes: 2 additions & 2 deletions app/blueprints/index/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ <h2 class="govuk-heading-s">Fund Configuration (PoC)</h2>
<a class="govuk-link" href="{{ url_for("fund_bp.fund") }}">Create a Fund</a>
</li>
<li class="govuk-body">
<a class="govuk-link" href="{{ url_for("round_bp.round") }}">Create a Round</a>
<a class="govuk-link" href="{{ url_for("round_bp.select_fund") }}">Create a Round</a>
</li>
<li class="govuk-body">
<a class="govuk-link" href="{{ url_for("fund_bp.view_fund") }}">Manage Application Configuration</a>
<a class="govuk-link" href="{{ url_for("application_bp.select_fund") }}">Build Application</a>
</li>
<li class="govuk-body">
<a class="govuk-link" href="{{ url_for("template_bp.view_templates") }}">Manage Templates</a>
Expand Down
82 changes: 58 additions & 24 deletions app/blueprints/round/routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
from random import randint

from flask import (
Expand All @@ -17,9 +16,9 @@
update_existing_round,
)
from app.db.queries.clone import clone_single_round
from app.db.queries.fund import get_all_funds
from app.db.queries.fund import get_all_funds, get_fund_by_id
from app.db.queries.round import get_round_by_id
from app.shared.helpers import all_funds_as_govuk_select_items, error_formatter
from app.shared.helpers import error_formatter

INDEX_BP_DASHBOARD = "index_bp.dashboard"

Expand All @@ -31,42 +30,77 @@
)


@round_bp.route("/create", methods=["GET", "POST"])
@round_bp.route("/<round_id>", methods=["GET", "POST"])
def round(round_id=None):
@round_bp.route("/select-grant", methods=["GET", "POST"]) # NOSONAR
def select_fund():
"""
Renders a template to select a fund and add or update a round to that fund. If saved, validates the round form data
and saves to DB
Intermediary page to select a Fund before creating a Round.
"""
form = RoundForm()
all_funds = get_all_funds()
params = {"all_funds": all_funds_as_govuk_select_items(all_funds)}
params["selected_fund_id"] = request.form.get("fund_id", None)
params["welsh_availability"] = json.dumps({str(fund.fund_id): fund.welsh_available for fund in all_funds})
if request.method == "POST":
fund_id = request.form.get("fund_id")
if not fund_id:
raise ValueError("Fund ID is required to create a round")
return redirect(url_for("round_bp.create_round", fund_id=fund_id))
fund_dropdown_items = [{"value": "", "text": "Select a grant"}]
for fund in get_all_funds():
fund_dropdown_items.append(
{"value": str(fund.fund_id), "text": fund.short_name + " - " + fund.title_json["en"]}
)
return render_template("select_fund.html", fund_dropdown_items=fund_dropdown_items)
wjrm500 marked this conversation as resolved.
Show resolved Hide resolved

if round_id:
existing_round = get_round_by_id(round_id)
form = populate_form_with_round_data(existing_round, RoundForm)

@round_bp.route("/create", methods=["GET", "POST"])
def create_round():
"""
Create a new round for a chosen fund.
Expects a ?fund_id=... in the query string, set by the select_fund route or other means.
"""
form = RoundForm()
fund_id = request.args.get("fund_id", None)
if not fund_id:
raise ValueError("Fund ID is required to create a round")
fund = get_fund_by_id(fund_id)
if form.validate_on_submit():
if round_id:
update_existing_round(existing_round, form)
flash(f"Updated round {existing_round.title_json['en']}")
return redirect(url_for("fund_bp.view_fund", fund_id=existing_round.fund_id))

new_round = create_new_round(form)
flash(f"Created round {new_round.title_json['en']}")
return redirect(url_for(INDEX_BP_DASHBOARD))
params = {
"form": form,
"fund": fund,
"round_id": None, # Since we're creating a new round, there's no round ID yet
}
error = error_formatter(form)
return render_template("round.html", **params, error=error)

params["round_id"] = round_id
params["form"] = form
error = error_formatter(params["form"])

@round_bp.route("/<round_id>", methods=["GET", "POST"]) # NOSONAR
def edit_round(round_id):
"""
Edit an existing round.
"""
existing_round = get_round_by_id(round_id)
if not existing_round:
raise ValueError(f"Round with ID {round_id} not found")
form = RoundForm()
if request.method == "GET":
form = populate_form_with_round_data(existing_round, RoundForm)
if form.validate_on_submit():
update_existing_round(existing_round, form)
flash(f"Updated round {existing_round.title_json['en']}")
return redirect(url_for("fund_bp.view_fund", fund_id=existing_round.fund_id))
params = {
"form": form,
"fund": get_fund_by_id(existing_round.fund_id),
"round_id": round_id,
}
error = error_formatter(form)
return render_template("round.html", **params, error=error)


@round_bp.route("/<round_id>/clone")
def clone_round(round_id, fund_id):
"""
Clone an existing round.
"""
cloned = clone_single_round(
round_id=round_id,
new_fund_id=fund_id,
Expand Down
28 changes: 6 additions & 22 deletions app/blueprints/round/templates/round.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,15 @@
}) }}
{% endif %}
<h1 class="govuk-heading-l">{{ pageHeading }}</h1>
<p class="govuk-body govuk-!-margin-bottom-1">Grant: <a href="{{ url_for('fund_bp.fund', fund_id=fund.fund_id) }}" class="govuk-link">{{ fund.title_json["en"] }}</a></p>
<p class="govuk-body govuk-!-margin-bottom-7">
<a href="{{ url_for('round_bp.select_fund') }}" class="govuk-link">Change grant</a>
</p>
<div class="govuk-form-group">
<fieldset class="govuk-fieldset">
<form method="POST">
{{ form.hidden_tag()}}
{% if not round_id %}
{{ govukSelect({
"id": form.fund_id.id,
"name": form.fund_id.name,
"label": {
"text": form.fund_id.label
},
"items": all_funds,
"value": selected_fund_id,
"errorMessage": {
"text": form.fund_id.errors[0] if form.fund_id.errors else ""
} if form.fund_id.errors else None
})}}
{% endif %}
<input type="hidden" name="fund_id" value="{{ fund.fund_id }}" />
{{input(form.title_en)}}
{{input(form.title_cy, classes="welsh-field")}}
{{input(form.short_name)}}
Expand Down Expand Up @@ -85,13 +76,10 @@ <h1 class="govuk-heading-l">{{ pageHeading }}</h1>

<script>
// Store Welsh availability mapping passed from the route handler
const welshAvailability = {{ welsh_availability | safe }};
const isWelshAvailable = {{ fund.welsh_available | safe }};

function toggleWelshFields() {
const fundSelect = document.querySelector('#fund_id');
const isWelshAvailable = welshAvailability[fundSelect.value];
const welshFields = document.querySelectorAll('.welsh-field');

welshFields.forEach(field => {
const fieldContainer = field.closest('.govuk-form-group');
if (isWelshAvailable) {
Expand All @@ -105,10 +93,6 @@ <h1 class="govuk-heading-l">{{ pageHeading }}</h1>
// Run on page load
document.addEventListener('DOMContentLoaded', function() {
toggleWelshFields();

// Add change event listener to fund select
const fundSelect = document.querySelector('#fund_id');
fundSelect.addEventListener('change', toggleWelshFields);
});
</script>
{% endblock content %}
7 changes: 5 additions & 2 deletions app/db/models/fund.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import uuid
from dataclasses import dataclass
from enum import Enum
from typing import List
from typing import TYPE_CHECKING, List

from flask_sqlalchemy.model import DefaultMeta
from sqlalchemy import Column, ForeignKey
Expand All @@ -12,7 +12,8 @@

from app.db import db

from .round import Round
if TYPE_CHECKING:
from .round import Round

BaseModel: DefaultMeta = db.Model

Expand Down Expand Up @@ -74,3 +75,5 @@ class Fund(BaseModel):
owner_organisation: Mapped["Organisation"] = relationship("Organisation", back_populates="funds")
funding_type = Column(ENUM(FundingType), nullable=False, unique=False)
ggis_scheme_reference_number = Column("ggis_scheme_reference_number", db.String(255), nullable=True, unique=False)

rounds: Mapped[List["Round"]] = relationship("Round", back_populates="fund", passive_deletes="all")
wjrm500 marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 6 additions & 0 deletions app/db/models/round.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import uuid
from dataclasses import dataclass
from typing import TYPE_CHECKING

from flask_sqlalchemy.model import DefaultMeta
from sqlalchemy import Column, DateTime, ForeignKey, Integer, Sequence, String, UniqueConstraint, inspect
Expand All @@ -11,6 +12,9 @@
from app.db import db
from app.db.models import Criteria, Section

if TYPE_CHECKING:
from .fund import Fund

BaseModel: DefaultMeta = db.Model


Expand Down Expand Up @@ -76,6 +80,8 @@ class Round(BaseModel):
server_default=base_path_seq.next_value(),
)

fund: Mapped["Fund"] = relationship("Fund", back_populates="rounds")
wjrm500 marked this conversation as resolved.
Show resolved Hide resolved

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

Expand Down
2 changes: 1 addition & 1 deletion app/db/queries/fund.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def update_fund(fund: Fund) -> Fund:
return fund


def get_all_funds() -> list:
def get_all_funds() -> list[Fund]:
stmt = select(Fund).order_by(Fund.short_name)
return db.session.scalars(stmt).all()

Expand Down
2 changes: 1 addition & 1 deletion app/db/queries/round.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ def get_round_by_short_name_and_fund_id(fund_id: str, short_name: str) -> Round:
return db.session.query(Round).filter_by(fund_id=fund_id, short_name=short_name).first()


def get_all_rounds() -> list:
def get_all_rounds() -> list[Round]:
stmt = select(Round).order_by(Round.short_name)
return db.session.scalars(stmt).all()
52 changes: 52 additions & 0 deletions app/templates/select_application.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{% extends "base.html" %}
{% from "govuk_frontend_jinja/components/button/macro.html" import govukButton %}
{% from "govuk_frontend_jinja/components/select/macro.html" import govukSelect %}
{% from "govuk_frontend_jinja/components/back-link/macro.html" import govukBackLink %}


{% block content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-full">
{{ govukBackLink({
"text": "Back",
"href": url_for("application_bp.select_fund")
}) }}

<span class="govuk-caption-m">{{ fund.short_name + " - " + fund.title_json["en"] }}</span>
<h1 class="govuk-heading-l">Select an application</h1>
<p class="govuk-body">Select the application you want to manage or create a new one</p>

<form method="POST">
{{ govukSelect({
"id": "round_id",
"name": "round_id",
"items": round_dropdown_items,
"label": "",
"attributes": {
"required": "required"
},
"classes": "govuk-!-width-one-half",
"formGroup": {
"classes": "govuk-!-margin-bottom-3"
}
}) }}

<p class="govuk-body govuk-!-margin-top-1 govuk-!-margin-bottom-7">
<a href="{{ url_for('round_bp.create_round', fund_id=fund.fund_id) }}" class="govuk-link">Add a new application</a>
</p>

<div class="govuk-button-group">
{{ govukButton({
"text": "Continue",
"type": "submit"
}) }}
{{ govukButton({
"text": "Cancel",
"href": url_for("index_bp.dashboard"),
"classes": "govuk-button--secondary"
}) }}
</div>
</form>
</div>
</div>
{% endblock content %}
Loading
Loading