diff --git a/app/blueprints/fund/templates/fund_config.html b/app/blueprints/fund/templates/fund_config.html index 81954370..7de1eb9d 100644 --- a/app/blueprints/fund/templates/fund_config.html +++ b/app/blueprints/fund/templates/fund_config.html @@ -369,11 +369,11 @@

Application Rounds

"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), + "href": url_for("round_bp.clone_round", round_id=round.round_id, fund_id=fund.fund_id), "classes": "govuk-button--secondary" }) + govukButton({ "text": "Edit Round", - "href": url_for("build_fund_bp.round", round_id=round.round_id), + "href": url_for("round_bp.round", round_id=round.round_id), "classes": "govuk-button--secondary" }) diff --git a/app/blueprints/fund_builder/routes.py b/app/blueprints/fund_builder/routes.py index 8d83105f..5a2064e1 100644 --- a/app/blueprints/fund_builder/routes.py +++ b/app/blueprints/fund_builder/routes.py @@ -3,7 +3,6 @@ import secrets import shutil import string -from datetime import datetime from random import randint import requests @@ -11,7 +10,6 @@ Blueprint, Response, after_this_request, - flash, g, redirect, render_template, @@ -22,12 +20,9 @@ from fsd_utils.authentication.decorators import login_requested from app.all_questions.metadata_utils import generate_print_data_for_sections -from app.blueprints.fund_builder.forms.round import RoundForm, get_datetime from app.blueprints.fund_builder.forms.section import SectionForm -from app.db.models.round import Round from app.db.queries.application import ( clone_single_form, - clone_single_round, delete_form_from_section, delete_section_from_round, get_all_template_forms, @@ -40,8 +35,8 @@ move_section_up, update_section, ) -from app.db.queries.fund import get_all_funds, get_fund_by_id -from app.db.queries.round import add_round, get_round_by_id, update_round +from app.db.queries.fund import 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 ( generate_assessment_config_for_round, @@ -52,7 +47,6 @@ generate_form_jsons_for_round, ) from app.export_config.generate_fund_round_html import generate_all_round_html -from app.shared.helpers import error_formatter from config import Config BUILD_FUND_BP_DASHBOARD = "build_fund_bp.dashboard" @@ -189,278 +183,6 @@ def build_application(round_id): return render_template("build_application.html", round=round, fund=fund, breadcrumb_items=breadcrumb_items) -@build_fund_bp.route("/fund//round//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)}", # NOSONAR - ) - flash(f"Cloned new round: {cloned.short_name}") - - return redirect(url_for("fund_bp.view_fund", fund_id=fund_id)) - - -@build_fund_bp.route("/round", methods=["GET", "POST"]) -@build_fund_bp.route("/round/", methods=["GET", "POST"]) -def round(round_id=None): - """ - 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 - """ - 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 round_id: - round = get_round_by_id(round_id) - form = populate_form_with_round_data(round) - - if form.validate_on_submit(): - if round_id: - update_existing_round(round, form) - flash(f"Updated round {round.title_json['en']}") - return redirect(url_for("fund_bp.view_fund", fund_id=round.fund_id)) - create_new_round(form) - flash(f"Created round {form.title_en.data}") - return redirect(url_for(BUILD_FUND_BP_DASHBOARD)) - - params["round_id"] = round_id - params["form"] = form - - error = error_formatter(params["form"]) - return render_template("round.html", **params, error=error) - - -def _convert_json_data_for_form(data) -> str: - if isinstance(data, dict): - return json.dumps(data) - return str(data) - - -def _convert_form_data_to_json(data) -> dict: - if data: - return json.loads(data) - return {} - - -def populate_form_with_round_data(round): - """ - Populate a RoundForm with data from a Round object. - - :param Round round: The round object to populate the form with - :return: A RoundForm populated with the round data - """ - round_data = { - "fund_id": round.fund_id, - "round_id": round.round_id, - "title_en": round.title_json.get("en", ""), - "title_cy": round.title_json.get("cy", ""), - "short_name": round.short_name, - "opens": round.opens, - "deadline": round.deadline, - "assessment_start": round.assessment_start, - "reminder_date": round.reminder_date, - "assessment_deadline": round.assessment_deadline, - "prospectus_link": round.prospectus_link, - "privacy_notice_link": round.privacy_notice_link, - "contact_us_banner_en": round.contact_us_banner_json.get("en", "") if round.contact_us_banner_json else "", - "contact_us_banner_cy": round.contact_us_banner_json.get("cy", "") if round.contact_us_banner_json else "", - "reference_contact_page_over_email": "true" if round.reference_contact_page_over_email else "false", - "contact_email": round.contact_email, - "contact_phone": round.contact_phone, - "contact_textphone": round.contact_textphone, - "support_times": round.support_times, - "support_days": round.support_days, - "instructions_en": round.instructions_json.get("en", "") if round.instructions_json else "", - "instructions_cy": round.instructions_json.get("cy", "") if round.instructions_json else "", - "feedback_link": round.feedback_link, - "project_name_field_id": round.project_name_field_id, - "application_guidance_en": ( - round.application_guidance_json.get("en", "") if round.application_guidance_json else "" - ), - "application_guidance_cy": ( - round.application_guidance_json.get("cy", "") if round.application_guidance_json else "" - ), - "guidance_url": round.guidance_url, - "all_uploaded_documents_section_available": ( - "true" if round.all_uploaded_documents_section_available else "false" - ), - "application_fields_download_available": "true" if round.application_fields_download_available else "false", - "display_logo_on_pdf_exports": "true" if round.display_logo_on_pdf_exports else "false", - "mark_as_complete_enabled": "true" if round.mark_as_complete_enabled else "false", - "is_expression_of_interest": "true" if round.is_expression_of_interest else "false", - "has_feedback_survey": ( - "true" - if round.feedback_survey_config and round.feedback_survey_config.get("has_feedback_survey", "") == "true" - else "false" - ), - "has_section_feedback": ( - "true" - if round.feedback_survey_config and round.feedback_survey_config.get("has_section_feedback", "") == "true" - else "false" - ), - "has_research_survey": ( - "true" - if round.feedback_survey_config and round.feedback_survey_config.get("has_research_survey", "") == "true" - else "false" - ), - "is_feedback_survey_optional": ( - "true" - if round.feedback_survey_config - and round.feedback_survey_config.get("is_feedback_survey_optional", "") == "true" - else "false" - ), - "is_section_feedback_optional": ( - "true" - if round.feedback_survey_config - and round.feedback_survey_config.get("is_section_feedback_optional", "") == "true" - else "false" - ), - "is_research_survey_optional": ( - "true" - if round.feedback_survey_config - and round.feedback_survey_config.get("is_research_survey_optional", "") == "true" - else "false" - ), - "eligibility_config": ( - "true" - if round.eligibility_config and round.eligibility_config.get("has_eligibility", "") == "true" - else "false" - ), - "eoi_decision_schema_en": ( - _convert_json_data_for_form(round.eoi_decision_schema.get("en", "")) if round.eoi_decision_schema else "" - ), - "eoi_decision_schema_cy": ( - _convert_json_data_for_form(round.eoi_decision_schema.get("cy", "")) if round.eoi_decision_schema else "" - ), - } - return RoundForm(data=round_data) - - -def update_existing_round(round, form): - """ - Update a Round object with the data from a RoundForm. - - :param Round round: The round object to update - :param RoundForm form: The form with the new round data - """ - round.title_json = {"en": form.title_en.data or None, "cy": form.title_cy.data or None} - round.short_name = form.short_name.data - round.feedback_survey_config = { - "has_feedback_survey": form.has_feedback_survey.data == "true", - "has_section_feedback": form.has_section_feedback.data == "true", - "has_research_survey": form.has_research_survey.data == "true", - "is_feedback_survey_optional": form.is_feedback_survey_optional.data == "true", - "is_section_feedback_optional": form.is_section_feedback_optional.data == "true", - "is_research_survey_optional": form.is_research_survey_optional.data == "true", - } - round.opens = get_datetime(form.opens) - round.deadline = get_datetime(form.deadline) - round.assessment_start = get_datetime(form.assessment_start) - round.reminder_date = get_datetime(form.reminder_date) - round.assessment_deadline = get_datetime(form.assessment_deadline) - round.prospectus_link = form.prospectus_link.data - round.privacy_notice_link = form.privacy_notice_link.data - round.reference_contact_page_over_email = form.reference_contact_page_over_email.data == "true" - round.contact_email = form.contact_email.data - round.contact_phone = form.contact_phone.data - round.contact_textphone = form.contact_textphone.data - round.support_times = form.support_times.data - round.support_days = form.support_days.data - round.feedback_link = form.feedback_link.data - round.project_name_field_id = form.project_name_field_id.data - round.guidance_url = form.guidance_url.data - round.all_uploaded_documents_section_available = form.all_uploaded_documents_section_available.data == "true" - round.application_fields_download_available = form.application_fields_download_available.data == "true" - round.display_logo_on_pdf_exports = form.display_logo_on_pdf_exports.data == "true" - round.mark_as_complete_enabled = form.mark_as_complete_enabled.data == "true" - round.is_expression_of_interest = form.is_expression_of_interest.data == "true" - round.short_name = form.short_name.data - round.contact_us_banner_json = { - "en": form.contact_us_banner_en.data or None, - "cy": form.contact_us_banner_cy.data or None, - } - round.instructions_json = {"en": form.instructions_en.data or None, "cy": form.instructions_cy.data or None} - round.application_guidance_json = { - "en": form.application_guidance_en.data or None, - "cy": form.application_guidance_cy.data or None, - } - round.guidance_url = form.guidance_url.data - round.all_uploaded_documents_section_available = form.all_uploaded_documents_section_available.data == "true" - round.application_fields_download_available = form.application_fields_download_available.data == "true" - round.display_logo_on_pdf_exports = form.display_logo_on_pdf_exports.data == "true" - round.mark_as_complete_enabled = form.mark_as_complete_enabled.data == "true" - round.is_expression_of_interest = form.is_expression_of_interest.data == "true" - round.eligibility_config = {"has_eligibility": form.eligibility_config.data == "true"} - round.eoi_decision_schema = { - "en": _convert_form_data_to_json(form.eoi_decision_schema_en.data), - "cy": _convert_form_data_to_json(form.eoi_decision_schema_cy.data), - } - round.audit_info = {"user": "dummy_user", "timestamp": datetime.now().isoformat(), "action": "update"} - update_round(round) - - -def create_new_round(form): - """ - Create a new Round object with the data from a RoundForm and save it to the database. - - :param RoundForm form: The form with the new round data - """ - new_round = Round( - fund_id=form.fund_id.data, - audit_info={"user": "dummy_user", "timestamp": datetime.now().isoformat(), "action": "create"}, - title_json={"en": form.title_en.data or None, "cy": form.title_cy.data or None}, - short_name=form.short_name.data, - opens=get_datetime(form.opens), - deadline=get_datetime(form.deadline), - assessment_start=get_datetime(form.assessment_start), - reminder_date=get_datetime(form.reminder_date), - assessment_deadline=get_datetime(form.assessment_deadline), - prospectus_link=form.prospectus_link.data, - privacy_notice_link=form.privacy_notice_link.data, - contact_us_banner_json={ - "en": form.contact_us_banner_en.data or None, - "cy": form.contact_us_banner_cy.data or None, - }, - reference_contact_page_over_email=form.reference_contact_page_over_email.data == "true", - contact_email=form.contact_email.data, - contact_phone=form.contact_phone.data, - contact_textphone=form.contact_textphone.data, - support_times=form.support_times.data, - support_days=form.support_days.data, - instructions_json={"en": form.instructions_en.data or None, "cy": form.instructions_cy.data or None}, - feedback_link=form.feedback_link.data, - project_name_field_id=form.project_name_field_id.data, - application_guidance_json={ - "en": form.application_guidance_en.data or None, - "cy": form.application_guidance_cy.data or None, - }, - guidance_url=form.guidance_url.data, - all_uploaded_documents_section_available=form.all_uploaded_documents_section_available.data == "true", - application_fields_download_available=form.application_fields_download_available.data == "true", - display_logo_on_pdf_exports=form.display_logo_on_pdf_exports.data == "true", - mark_as_complete_enabled=form.mark_as_complete_enabled.data == "true", - is_expression_of_interest=form.is_expression_of_interest.data == "true", - feedback_survey_config={ - "has_feedback_survey": form.has_feedback_survey.data == "true", - "has_section_feedback": form.has_section_feedback.data == "true", - "has_research_survey": form.has_research_survey.data == "true", - "is_feedback_survey_optional": form.is_feedback_survey_optional.data == "true", - "is_section_feedback_optional": form.is_section_feedback_optional.data == "true", - "is_research_survey_optional": form.is_research_survey_optional.data == "true", - }, - eligibility_config={"has_eligibility": form.eligibility_config.data == "true"}, - eoi_decision_schema={ - "en": _convert_form_data_to_json(form.eoi_decision_schema_en.data), - "cy": _convert_form_data_to_json(form.eoi_decision_schema_cy.data), - }, - ) - add_round(new_round) - - @build_fund_bp.route("/preview/", methods=["GET"]) def preview_form(form_id): """ diff --git a/app/blueprints/fund_builder/forms/round.py b/app/blueprints/round/forms.py similarity index 100% rename from app/blueprints/fund_builder/forms/round.py rename to app/blueprints/round/forms.py diff --git a/app/blueprints/round/routes.py b/app/blueprints/round/routes.py new file mode 100644 index 00000000..b19c29a9 --- /dev/null +++ b/app/blueprints/round/routes.py @@ -0,0 +1,302 @@ +import json +from datetime import datetime +from random import randint + +from flask import ( + Blueprint, + flash, + redirect, + render_template, + request, + url_for, +) + +from app.blueprints.fund_builder.routes import all_funds_as_govuk_select_items +from app.blueprints.round.forms import RoundForm, get_datetime +from app.db.models.round import Round +from app.db.queries.application import clone_single_round +from app.db.queries.fund import get_all_funds +from app.db.queries.round import add_round, get_round_by_id, update_round +from app.shared.helpers import error_formatter + +BUILD_FUND_BP_DASHBOARD = "build_fund_bp.dashboard" + +# Blueprint for routes used by v1 of FAB - using the DB +round_bp = Blueprint( + "round_bp", + __name__, + url_prefix="/round", + template_folder="templates", +) + + +@round_bp.route("//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)}", # NOSONAR + ) + flash(f"Cloned new round: {cloned.short_name}") + + return redirect(url_for("fund_bp.view_fund", fund_id=fund_id)) + + +@round_bp.route("", methods=["GET", "POST"]) +@round_bp.route("/", methods=["GET", "POST"]) +def round(round_id=None): + """ + 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 + """ + 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 round_id: + round = get_round_by_id(round_id) + form = populate_form_with_round_data(round) + + if form.validate_on_submit(): + if round_id: + update_existing_round(round, form) + flash(f"Updated round {round.title_json['en']}") + return redirect(url_for("fund_bp.view_fund", fund_id=round.fund_id)) + create_new_round(form) + flash(f"Created round {form.title_en.data}") + return redirect(url_for(BUILD_FUND_BP_DASHBOARD)) + + params["round_id"] = round_id + params["form"] = form + + error = error_formatter(params["form"]) + return render_template("round.html", **params, error=error) + + +def _convert_json_data_for_form(data) -> str: + if isinstance(data, dict): + return json.dumps(data) + return str(data) + + +def _convert_form_data_to_json(data) -> dict: + if data: + return json.loads(data) + return {} + + +def populate_form_with_round_data(round): + """ + Populate a RoundForm with data from a Round object. + + :param Round round: The round object to populate the form with + :return: A RoundForm populated with the round data + """ + round_data = { + "fund_id": round.fund_id, + "round_id": round.round_id, + "title_en": round.title_json.get("en", ""), + "title_cy": round.title_json.get("cy", ""), + "short_name": round.short_name, + "opens": round.opens, + "deadline": round.deadline, + "assessment_start": round.assessment_start, + "reminder_date": round.reminder_date, + "assessment_deadline": round.assessment_deadline, + "prospectus_link": round.prospectus_link, + "privacy_notice_link": round.privacy_notice_link, + "contact_us_banner_en": round.contact_us_banner_json.get("en", "") if round.contact_us_banner_json else "", + "contact_us_banner_cy": round.contact_us_banner_json.get("cy", "") if round.contact_us_banner_json else "", + "reference_contact_page_over_email": "true" if round.reference_contact_page_over_email else "false", + "contact_email": round.contact_email, + "contact_phone": round.contact_phone, + "contact_textphone": round.contact_textphone, + "support_times": round.support_times, + "support_days": round.support_days, + "instructions_en": round.instructions_json.get("en", "") if round.instructions_json else "", + "instructions_cy": round.instructions_json.get("cy", "") if round.instructions_json else "", + "feedback_link": round.feedback_link, + "project_name_field_id": round.project_name_field_id, + "application_guidance_en": ( + round.application_guidance_json.get("en", "") if round.application_guidance_json else "" + ), + "application_guidance_cy": ( + round.application_guidance_json.get("cy", "") if round.application_guidance_json else "" + ), + "guidance_url": round.guidance_url, + "all_uploaded_documents_section_available": ( + "true" if round.all_uploaded_documents_section_available else "false" + ), + "application_fields_download_available": "true" if round.application_fields_download_available else "false", + "display_logo_on_pdf_exports": "true" if round.display_logo_on_pdf_exports else "false", + "mark_as_complete_enabled": "true" if round.mark_as_complete_enabled else "false", + "is_expression_of_interest": "true" if round.is_expression_of_interest else "false", + "has_feedback_survey": ( + "true" + if round.feedback_survey_config and round.feedback_survey_config.get("has_feedback_survey", "") == "true" + else "false" + ), + "has_section_feedback": ( + "true" + if round.feedback_survey_config and round.feedback_survey_config.get("has_section_feedback", "") == "true" + else "false" + ), + "has_research_survey": ( + "true" + if round.feedback_survey_config and round.feedback_survey_config.get("has_research_survey", "") == "true" + else "false" + ), + "is_feedback_survey_optional": ( + "true" + if round.feedback_survey_config + and round.feedback_survey_config.get("is_feedback_survey_optional", "") == "true" + else "false" + ), + "is_section_feedback_optional": ( + "true" + if round.feedback_survey_config + and round.feedback_survey_config.get("is_section_feedback_optional", "") == "true" + else "false" + ), + "is_research_survey_optional": ( + "true" + if round.feedback_survey_config + and round.feedback_survey_config.get("is_research_survey_optional", "") == "true" + else "false" + ), + "eligibility_config": ( + "true" + if round.eligibility_config and round.eligibility_config.get("has_eligibility", "") == "true" + else "false" + ), + "eoi_decision_schema_en": ( + _convert_json_data_for_form(round.eoi_decision_schema.get("en", "")) if round.eoi_decision_schema else "" + ), + "eoi_decision_schema_cy": ( + _convert_json_data_for_form(round.eoi_decision_schema.get("cy", "")) if round.eoi_decision_schema else "" + ), + } + return RoundForm(data=round_data) + + +def update_existing_round(round, form): + """ + Update a Round object with the data from a RoundForm. + + :param Round round: The round object to update + :param RoundForm form: The form with the new round data + """ + round.title_json = {"en": form.title_en.data or None, "cy": form.title_cy.data or None} + round.short_name = form.short_name.data + round.feedback_survey_config = { + "has_feedback_survey": form.has_feedback_survey.data == "true", + "has_section_feedback": form.has_section_feedback.data == "true", + "has_research_survey": form.has_research_survey.data == "true", + "is_feedback_survey_optional": form.is_feedback_survey_optional.data == "true", + "is_section_feedback_optional": form.is_section_feedback_optional.data == "true", + "is_research_survey_optional": form.is_research_survey_optional.data == "true", + } + round.opens = get_datetime(form.opens) + round.deadline = get_datetime(form.deadline) + round.assessment_start = get_datetime(form.assessment_start) + round.reminder_date = get_datetime(form.reminder_date) + round.assessment_deadline = get_datetime(form.assessment_deadline) + round.prospectus_link = form.prospectus_link.data + round.privacy_notice_link = form.privacy_notice_link.data + round.reference_contact_page_over_email = form.reference_contact_page_over_email.data == "true" + round.contact_email = form.contact_email.data + round.contact_phone = form.contact_phone.data + round.contact_textphone = form.contact_textphone.data + round.support_times = form.support_times.data + round.support_days = form.support_days.data + round.feedback_link = form.feedback_link.data + round.project_name_field_id = form.project_name_field_id.data + round.guidance_url = form.guidance_url.data + round.all_uploaded_documents_section_available = form.all_uploaded_documents_section_available.data == "true" + round.application_fields_download_available = form.application_fields_download_available.data == "true" + round.display_logo_on_pdf_exports = form.display_logo_on_pdf_exports.data == "true" + round.mark_as_complete_enabled = form.mark_as_complete_enabled.data == "true" + round.is_expression_of_interest = form.is_expression_of_interest.data == "true" + round.short_name = form.short_name.data + round.contact_us_banner_json = { + "en": form.contact_us_banner_en.data or None, + "cy": form.contact_us_banner_cy.data or None, + } + round.instructions_json = {"en": form.instructions_en.data or None, "cy": form.instructions_cy.data or None} + round.application_guidance_json = { + "en": form.application_guidance_en.data or None, + "cy": form.application_guidance_cy.data or None, + } + round.guidance_url = form.guidance_url.data + round.all_uploaded_documents_section_available = form.all_uploaded_documents_section_available.data == "true" + round.application_fields_download_available = form.application_fields_download_available.data == "true" + round.display_logo_on_pdf_exports = form.display_logo_on_pdf_exports.data == "true" + round.mark_as_complete_enabled = form.mark_as_complete_enabled.data == "true" + round.is_expression_of_interest = form.is_expression_of_interest.data == "true" + round.eligibility_config = {"has_eligibility": form.eligibility_config.data == "true"} + round.eoi_decision_schema = { + "en": _convert_form_data_to_json(form.eoi_decision_schema_en.data), + "cy": _convert_form_data_to_json(form.eoi_decision_schema_cy.data), + } + round.audit_info = {"user": "dummy_user", "timestamp": datetime.now().isoformat(), "action": "update"} + update_round(round) + + +def create_new_round(form): + """ + Create a new Round object with the data from a RoundForm and save it to the database. + + :param RoundForm form: The form with the new round data + """ + new_round = Round( + fund_id=form.fund_id.data, + audit_info={"user": "dummy_user", "timestamp": datetime.now().isoformat(), "action": "create"}, + title_json={"en": form.title_en.data or None, "cy": form.title_cy.data or None}, + short_name=form.short_name.data, + opens=get_datetime(form.opens), + deadline=get_datetime(form.deadline), + assessment_start=get_datetime(form.assessment_start), + reminder_date=get_datetime(form.reminder_date), + assessment_deadline=get_datetime(form.assessment_deadline), + prospectus_link=form.prospectus_link.data, + privacy_notice_link=form.privacy_notice_link.data, + contact_us_banner_json={ + "en": form.contact_us_banner_en.data or None, + "cy": form.contact_us_banner_cy.data or None, + }, + reference_contact_page_over_email=form.reference_contact_page_over_email.data == "true", + contact_email=form.contact_email.data, + contact_phone=form.contact_phone.data, + contact_textphone=form.contact_textphone.data, + support_times=form.support_times.data, + support_days=form.support_days.data, + instructions_json={"en": form.instructions_en.data or None, "cy": form.instructions_cy.data or None}, + feedback_link=form.feedback_link.data, + project_name_field_id=form.project_name_field_id.data, + application_guidance_json={ + "en": form.application_guidance_en.data or None, + "cy": form.application_guidance_cy.data or None, + }, + guidance_url=form.guidance_url.data, + all_uploaded_documents_section_available=form.all_uploaded_documents_section_available.data == "true", + application_fields_download_available=form.application_fields_download_available.data == "true", + display_logo_on_pdf_exports=form.display_logo_on_pdf_exports.data == "true", + mark_as_complete_enabled=form.mark_as_complete_enabled.data == "true", + is_expression_of_interest=form.is_expression_of_interest.data == "true", + feedback_survey_config={ + "has_feedback_survey": form.has_feedback_survey.data == "true", + "has_section_feedback": form.has_section_feedback.data == "true", + "has_research_survey": form.has_research_survey.data == "true", + "is_feedback_survey_optional": form.is_feedback_survey_optional.data == "true", + "is_section_feedback_optional": form.is_section_feedback_optional.data == "true", + "is_research_survey_optional": form.is_research_survey_optional.data == "true", + }, + eligibility_config={"has_eligibility": form.eligibility_config.data == "true"}, + eoi_decision_schema={ + "en": _convert_form_data_to_json(form.eoi_decision_schema_en.data), + "cy": _convert_form_data_to_json(form.eoi_decision_schema_cy.data), + }, + ) + add_round(new_round) diff --git a/app/blueprints/fund_builder/templates/round.html b/app/blueprints/round/templates/round.html similarity index 100% rename from app/blueprints/fund_builder/templates/round.html rename to app/blueprints/round/templates/round.html diff --git a/app/create_app.py b/app/create_app.py index d2873a31..0e97eebf 100644 --- a/app/create_app.py +++ b/app/create_app.py @@ -8,6 +8,7 @@ from app.blueprints.fund.routes import fund_bp from app.blueprints.fund_builder.routes import build_fund_bp +from app.blueprints.round.routes import round_bp from app.blueprints.templates.routes import template_bp PUBLIC_ROUTES = [ @@ -31,6 +32,7 @@ def create_app() -> Flask: init_sentry() flask_app = Flask("__name__", static_url_path="/assets") flask_app.register_blueprint(fund_bp) + flask_app.register_blueprint(round_bp) flask_app.register_blueprint(build_fund_bp) flask_app.register_blueprint(template_bp) diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 260ff05e..5c85bab0 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -24,7 +24,7 @@

Fund Configuration (PoC)

Create a Fund
  • - Create a Round + Create a Round
  • Manage Application Configuration diff --git a/tests/blueprints/fund/__init__.py b/tests/blueprints/fund/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/blueprints/fund_builder/forms/test_round.py b/tests/blueprints/fund_builder/forms/test_round.py index df568ce2..f977402e 100644 --- a/tests/blueprints/fund_builder/forms/test_round.py +++ b/tests/blueprints/fund_builder/forms/test_round.py @@ -1,7 +1,7 @@ import pytest from wtforms.validators import ValidationError -from app.blueprints.fund_builder.forms.round import validate_flexible_url +from app.blueprints.round.forms import validate_flexible_url class MockField: diff --git a/tests/blueprints/round/__init__.py b/tests/blueprints/round/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/blueprints/round/test_routes.py b/tests/blueprints/round/test_routes.py new file mode 100644 index 00000000..74c43ad7 --- /dev/null +++ b/tests/blueprints/round/test_routes.py @@ -0,0 +1,199 @@ +import pytest + +from app.db.models import Round +from app.db.queries.round import get_round_by_id +from tests.helpers import submit_form + + +@pytest.mark.usefixtures("set_auth_cookie", "patch_validate_token_rs256_internal_user") +def test_create_round_with_existing_short_name(flask_test_client, seed_dynamic_data): + """ + Tests that a round can be successfully created using the /round route + Verifies that the created round has the correct attributes + """ + test_fund = seed_dynamic_data["funds"][0] + new_round_data = { + "fund_id": test_fund.fund_id, + "title_en": "New Round", + "short_name": "NR123", + "opens-day": "01", + "opens-month": "10", + "opens-year": "2024", + "opens-hour": "09", + "opens-minute": "00", + "deadline-day": "01", + "deadline-month": "12", + "deadline-year": "2024", + "deadline-hour": "17", + "deadline-minute": "00", + "assessment_start-day": "02", + "assessment_start-month": "12", + "assessment_start-year": "2024", + "assessment_start-hour": "09", + "assessment_start-minute": "00", + "reminder_date-day": "15", + "reminder_date-month": "11", + "reminder_date-year": "2024", + "reminder_date-hour": "09", + "reminder_date-minute": "00", + "assessment_deadline-day": "15", + "assessment_deadline-month": "12", + "assessment_deadline-year": "2024", + "assessment_deadline-hour": "17", + "assessment_deadline-minute": "00", + "prospectus_link": "http://example.com/prospectus", + "privacy_notice_link": "http://example.com/privacy", + "contact_email": "contact@example.com", + "submit": "Submit", + "contact_phone": "1234567890", + "contact_textphone": "0987654321", + "support_times": "9am - 5pm", + "support_days": "Monday to Friday", + "feedback_link": "http://example.com/feedback", + "project_name_field_id": 1, + "guidance_url": "http://example.com/guidance", + } + + error_html = ( + 'Short name: Given short name already exists in the fund funding to improve testing.' + ) + + # Test works fine with first round + response = submit_form(flask_test_client, "/round", new_round_data) + assert response.status_code == 200 + assert error_html not in response.data.decode("utf-8"), "Error HTML found in response" + + # Test works fine with second round but with different short name + new_round_data = {**new_round_data, "short_name": "NR1234"} + response = submit_form(flask_test_client, "/round", new_round_data) + assert response.status_code == 200 + assert error_html not in response.data.decode("utf-8"), "Error HTML found in response" + + # Test doesn't work with third round with same short name as firsrt + new_round_data = {**new_round_data, "short_name": "NR123"} + response = submit_form(flask_test_client, "/round", new_round_data) + assert response.status_code == 200 + assert error_html in response.data.decode("utf-8"), "Error HTML not found in response" + + +@pytest.mark.usefixtures("set_auth_cookie", "patch_validate_token_rs256_internal_user") +def test_create_new_round(flask_test_client, seed_dynamic_data): + """ + Tests that a round can be successfully created using the /round route + Verifies that the created round has the correct attributes + """ + test_fund = seed_dynamic_data["funds"][0] + new_round_data = { + "fund_id": test_fund.fund_id, + "title_en": "New Round", + "short_name": "NR123", + "opens-day": "01", + "opens-month": "10", + "opens-year": "2024", + "opens-hour": "09", + "opens-minute": "00", + "deadline-day": "01", + "deadline-month": "12", + "deadline-year": "2024", + "deadline-hour": "17", + "deadline-minute": "00", + "assessment_start-day": "02", + "assessment_start-month": "12", + "assessment_start-year": "2024", + "assessment_start-hour": "09", + "assessment_start-minute": "00", + "reminder_date-day": "15", + "reminder_date-month": "11", + "reminder_date-year": "2024", + "reminder_date-hour": "09", + "reminder_date-minute": "00", + "assessment_deadline-day": "15", + "assessment_deadline-month": "12", + "assessment_deadline-year": "2024", + "assessment_deadline-hour": "17", + "assessment_deadline-minute": "00", + "prospectus_link": "http://example.com/prospectus", + "privacy_notice_link": "http://example.com/privacy", + "contact_email": "contact@example.com", + "submit": "Submit", + "contact_phone": "1234567890", + "contact_textphone": "0987654321", + "support_times": "9am - 5pm", + "support_days": "Monday to Friday", + "feedback_link": "http://example.com/feedback", + "project_name_field_id": 1, + "guidance_url": "http://example.com/guidance", + } + + response = submit_form(flask_test_client, "/round", new_round_data) + assert response.status_code == 200 + + new_round = Round.query.filter_by(short_name="NR123").first() + assert new_round is not None + assert new_round.title_json["en"] == "New Round" + assert new_round.short_name == "NR123" + + +@pytest.mark.usefixtures("set_auth_cookie", "patch_validate_token_rs256_internal_user") +def test_update_existing_round(flask_test_client, seed_dynamic_data): + """ + Tests that a round can be successfully updated using the /round/ route + Verifies that the updated round has the correct attributes + """ + update_round_data = { + "title_en": "Updated Round", + "short_name": "UR123", + "opens-day": "01", + "opens-month": "10", + "opens-year": "2024", + "opens-hour": "09", + "opens-minute": "00", + "deadline-day": "01", + "deadline-month": "12", + "deadline-year": "2024", + "deadline-hour": "17", + "deadline-minute": "00", + "assessment_start-day": "02", + "assessment_start-month": "12", + "assessment_start-year": "2024", + "assessment_start-hour": "09", + "assessment_start-minute": "00", + "reminder_date-day": "15", + "reminder_date-month": "11", + "reminder_date-year": "2024", + "reminder_date-hour": "09", + "reminder_date-minute": "00", + "assessment_deadline-day": "15", + "assessment_deadline-month": "12", + "assessment_deadline-year": "2024", + "assessment_deadline-hour": "17", + "assessment_deadline-minute": "00", + "prospectus_link": "http://example.com/updated_prospectus", + "privacy_notice_link": "http://example.com/updated_privacy", + "contact_email": "updated_contact@example.com", + "submit": "Submit", + "contact_phone": "1234567890", + "contact_textphone": "0987654321", + "support_times": "9am - 5pm", + "support_days": "Monday to Friday", + "feedback_link": "http://example.com/feedback", + "project_name_field_id": 1, + "guidance_url": "http://example.com/guidance", + "has_feedback_survey": "true", + } + + test_round = seed_dynamic_data["rounds"][0] + response = submit_form(flask_test_client, f"/round/{test_round.round_id}", update_round_data) + assert response.status_code == 200 + + updated_round = get_round_by_id(test_round.round_id) + assert updated_round.title_json["en"] == "Updated Round" + assert updated_round.short_name == "UR123" + assert updated_round.feedback_survey_config == { + "has_feedback_survey": True, + "has_section_feedback": False, + "has_research_survey": False, + "is_feedback_survey_optional": False, + "is_section_feedback_optional": False, + "is_research_survey_optional": False, + } diff --git a/tests/test_routes.py b/tests/test_routes.py index 79f7180f..1bcabda0 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -3,204 +3,7 @@ import pytest from wtforms.validators import ValidationError -from app.blueprints.fund_builder.forms.round import validate_json_field -from app.db.models import Round -from app.db.queries.round import get_round_by_id -from tests.helpers import submit_form - - -@pytest.mark.usefixtures("set_auth_cookie", "patch_validate_token_rs256_internal_user") -def test_create_round_with_existing_short_name(flask_test_client, seed_dynamic_data): - """ - Tests that a round can be successfully created using the /round route - Verifies that the created round has the correct attributes - """ - test_fund = seed_dynamic_data["funds"][0] - new_round_data = { - "fund_id": test_fund.fund_id, - "title_en": "New Round", - "short_name": "NR123", - "opens-day": "01", - "opens-month": "10", - "opens-year": "2024", - "opens-hour": "09", - "opens-minute": "00", - "deadline-day": "01", - "deadline-month": "12", - "deadline-year": "2024", - "deadline-hour": "17", - "deadline-minute": "00", - "assessment_start-day": "02", - "assessment_start-month": "12", - "assessment_start-year": "2024", - "assessment_start-hour": "09", - "assessment_start-minute": "00", - "reminder_date-day": "15", - "reminder_date-month": "11", - "reminder_date-year": "2024", - "reminder_date-hour": "09", - "reminder_date-minute": "00", - "assessment_deadline-day": "15", - "assessment_deadline-month": "12", - "assessment_deadline-year": "2024", - "assessment_deadline-hour": "17", - "assessment_deadline-minute": "00", - "prospectus_link": "http://example.com/prospectus", - "privacy_notice_link": "http://example.com/privacy", - "contact_email": "contact@example.com", - "submit": "Submit", - "contact_phone": "1234567890", - "contact_textphone": "0987654321", - "support_times": "9am - 5pm", - "support_days": "Monday to Friday", - "feedback_link": "http://example.com/feedback", - "project_name_field_id": 1, - "guidance_url": "http://example.com/guidance", - } - - error_html = ( - 'Short name: Given short name already exists in the fund funding to improve testing.' - ) - - # Test works fine with first round - response = submit_form(flask_test_client, "/round", new_round_data) - assert response.status_code == 200 - assert error_html not in response.data.decode("utf-8"), "Error HTML found in response" - - # Test works fine with second round but with different short name - new_round_data = {**new_round_data, "short_name": "NR1234"} - response = submit_form(flask_test_client, "/round", new_round_data) - assert response.status_code == 200 - assert error_html not in response.data.decode("utf-8"), "Error HTML found in response" - - # Test doesn't work with third round with same short name as firsrt - new_round_data = {**new_round_data, "short_name": "NR123"} - response = submit_form(flask_test_client, "/round", new_round_data) - assert response.status_code == 200 - assert error_html in response.data.decode("utf-8"), "Error HTML not found in response" - - -@pytest.mark.usefixtures("set_auth_cookie", "patch_validate_token_rs256_internal_user") -def test_create_new_round(flask_test_client, seed_dynamic_data): - """ - Tests that a round can be successfully created using the /round route - Verifies that the created round has the correct attributes - """ - test_fund = seed_dynamic_data["funds"][0] - new_round_data = { - "fund_id": test_fund.fund_id, - "title_en": "New Round", - "short_name": "NR123", - "opens-day": "01", - "opens-month": "10", - "opens-year": "2024", - "opens-hour": "09", - "opens-minute": "00", - "deadline-day": "01", - "deadline-month": "12", - "deadline-year": "2024", - "deadline-hour": "17", - "deadline-minute": "00", - "assessment_start-day": "02", - "assessment_start-month": "12", - "assessment_start-year": "2024", - "assessment_start-hour": "09", - "assessment_start-minute": "00", - "reminder_date-day": "15", - "reminder_date-month": "11", - "reminder_date-year": "2024", - "reminder_date-hour": "09", - "reminder_date-minute": "00", - "assessment_deadline-day": "15", - "assessment_deadline-month": "12", - "assessment_deadline-year": "2024", - "assessment_deadline-hour": "17", - "assessment_deadline-minute": "00", - "prospectus_link": "http://example.com/prospectus", - "privacy_notice_link": "http://example.com/privacy", - "contact_email": "contact@example.com", - "submit": "Submit", - "contact_phone": "1234567890", - "contact_textphone": "0987654321", - "support_times": "9am - 5pm", - "support_days": "Monday to Friday", - "feedback_link": "http://example.com/feedback", - "project_name_field_id": 1, - "guidance_url": "http://example.com/guidance", - } - - response = submit_form(flask_test_client, "/round", new_round_data) - assert response.status_code == 200 - - new_round = Round.query.filter_by(short_name="NR123").first() - assert new_round is not None - assert new_round.title_json["en"] == "New Round" - assert new_round.short_name == "NR123" - - -@pytest.mark.usefixtures("set_auth_cookie", "patch_validate_token_rs256_internal_user") -def test_update_existing_round(flask_test_client, seed_dynamic_data): - """ - Tests that a round can be successfully updated using the /round/ route - Verifies that the updated round has the correct attributes - """ - update_round_data = { - "title_en": "Updated Round", - "short_name": "UR123", - "opens-day": "01", - "opens-month": "10", - "opens-year": "2024", - "opens-hour": "09", - "opens-minute": "00", - "deadline-day": "01", - "deadline-month": "12", - "deadline-year": "2024", - "deadline-hour": "17", - "deadline-minute": "00", - "assessment_start-day": "02", - "assessment_start-month": "12", - "assessment_start-year": "2024", - "assessment_start-hour": "09", - "assessment_start-minute": "00", - "reminder_date-day": "15", - "reminder_date-month": "11", - "reminder_date-year": "2024", - "reminder_date-hour": "09", - "reminder_date-minute": "00", - "assessment_deadline-day": "15", - "assessment_deadline-month": "12", - "assessment_deadline-year": "2024", - "assessment_deadline-hour": "17", - "assessment_deadline-minute": "00", - "prospectus_link": "http://example.com/updated_prospectus", - "privacy_notice_link": "http://example.com/updated_privacy", - "contact_email": "updated_contact@example.com", - "submit": "Submit", - "contact_phone": "1234567890", - "contact_textphone": "0987654321", - "support_times": "9am - 5pm", - "support_days": "Monday to Friday", - "feedback_link": "http://example.com/feedback", - "project_name_field_id": 1, - "guidance_url": "http://example.com/guidance", - "has_feedback_survey": "true", - } - - test_round = seed_dynamic_data["rounds"][0] - response = submit_form(flask_test_client, f"/round/{test_round.round_id}", update_round_data) - assert response.status_code == 200 - - updated_round = get_round_by_id(test_round.round_id) - assert updated_round.title_json["en"] == "Updated Round" - assert updated_round.short_name == "UR123" - assert updated_round.feedback_survey_config == { - "has_feedback_survey": True, - "has_section_feedback": False, - "has_research_survey": False, - "is_feedback_survey_optional": False, - "is_section_feedback_optional": False, - "is_research_survey_optional": False, - } +from app.blueprints.round.forms import validate_json_field @pytest.mark.parametrize("input_json_string", [(None), (""), ("{}"), (""), ("{}"), ('{"1":"2"}')])