diff --git a/app/db/queries/fund.py b/app/db/queries/fund.py
index 6fe0121d..686a5647 100644
--- a/app/db/queries/fund.py
+++ b/app/db/queries/fund.py
@@ -4,7 +4,7 @@
from sqlalchemy.orm import joinedload
from app.db import db
-from app.db.models import Round, Component, Page, Form
+from app.db.models import Round, Component, Page, Form, Lizt
from app.db.models.fund import Fund, Organisation
from app.db.queries.util import delete_all_related_objects
@@ -48,11 +48,14 @@ def _delete_sections_for_fund_round(fund: Fund):
for round_detail in fund.rounds:
for section in round_detail.sections:
if section:
+ lizt_ids = [component.list_id for form in section.forms for page in form.pages for component in
+ page.components]
page_ids = [page.page_id for form in section.forms for page in form.pages]
form_ids = [form.form_id for form in section.forms]
section_ids = [section.section_id]
delete_all_related_objects(db=db, model=Component, column=Component.page_id, ids=page_ids)
+ delete_all_related_objects(db=db, model=Lizt, column=Lizt.list_id, ids=lizt_ids)
delete_all_related_objects(db=db, model=Page, column=Page.form_id, ids=form_ids)
delete_all_related_objects(db=db, model=Form, column=Form.section_id, ids=section_ids)
@@ -61,7 +64,7 @@ def _delete_sections_for_fund_round(fund: Fund):
def delete_selected_fund(fund_id):
- fund: Fund = db.session.query(Fund).options(joinedload(Fund.rounds).joinedload(Round.sections)).get(fund_id)
+ fund: Fund = db.session.get(Fund, fund_id, options=[joinedload(Fund.rounds).joinedload(Round.sections)])
if not fund:
raise ValueError(f"Fund with id {fund_id} not found")
try:
diff --git a/app/db/queries/round.py b/app/db/queries/round.py
index 431309ba..0a45e38f 100644
--- a/app/db/queries/round.py
+++ b/app/db/queries/round.py
@@ -3,7 +3,7 @@
from sqlalchemy.orm import joinedload
from app.db import db
-from app.db.models import Fund, Component, Page, Form
+from app.db.models import Fund, Component, Page, Form, Lizt
from app.db.models.round import Round
from app.db.queries.util import delete_all_related_objects
@@ -39,11 +39,14 @@ def get_all_rounds() -> list[Round]:
def _delete_sections_for_round(round_detail: Round):
for section_detail in round_detail.sections:
+ lizt_ids = [component.list_id for form in section_detail.forms for page in form.pages for component in
+ page.components]
page_ids = [page.page_id for form in section_detail.forms for page in form.pages]
form_ids = [form.form_id for form in section_detail.forms]
section_ids = [section_detail.section_id]
delete_all_related_objects(db=db, model=Component, column=Component.page_id, ids=page_ids)
+ delete_all_related_objects(db=db, model=Lizt, column=Lizt.list_id, ids=lizt_ids)
delete_all_related_objects(db=db, model=Page, column=Page.form_id, ids=form_ids)
delete_all_related_objects(db=db, model=Form, column=Form.section_id, ids=section_ids)
@@ -52,7 +55,7 @@ def _delete_sections_for_round(round_detail: Round):
def delete_selected_round(round_id):
- round_detail: Round = db.session.query(Round).options(joinedload(Round.sections)).get(round_id)
+ round_detail: Round = db.session.get(Round, round_id, options=[joinedload(Round.sections)])
if not round_detail:
raise ValueError(f"Round with id {round_id} not found")
try:
diff --git a/tests/blueprints/fund/test_routes.py b/tests/blueprints/fund/test_routes.py
index b68af8f8..be137526 100644
--- a/tests/blueprints/fund/test_routes.py
+++ b/tests/blueprints/fund/test_routes.py
@@ -2,10 +2,12 @@
from bs4 import BeautifulSoup
from flask import g
-from app.db.models import Fund
+from app.db.models import Fund, Round, Form, Section, Component, Lizt
from app.db.models.fund import FundingType
from app.db.queries.fund import get_fund_by_id
from tests.helpers import submit_form
+from config import Config
+from sqlalchemy.orm import joinedload
@pytest.mark.usefixtures("set_auth_cookie", "patch_validate_token_rs256_internal_user")
@@ -329,8 +331,8 @@ def test_view_fund_details(flask_test_client, seed_dynamic_data):
html = response.data.decode("utf-8")
assert f'
{test_fund.name_json["en"]}
' in html
assert (
- f'Change'
- f' Grant name' in html # noqa: E501
+ f'Change'
+ f' Grant name' in html # noqa: E501
)
assert 'Back' in html
@@ -356,3 +358,36 @@ def test_create_fund_welsh_error_messages(flask_test_client, seed_dynamic_data):
assert b"Enter the Welsh grant name" in response.data # Validation error message
assert b"Enter the Welsh application name" in response.data # Validation error message
assert b"Enter the Welsh grant description" in response.data # Validation error message
+
+
+@pytest.mark.usefixtures("set_auth_cookie", "patch_validate_token_rs256_internal_user")
+def test_delete_fund_feature_disabled(flask_test_client, monkeypatch, seed_fund_without_assessment):
+ """Test that the delete endpoint returns 403 when a feature flag is disabled."""
+ test_fund: Fund = seed_fund_without_assessment["funds"][0]
+ monkeypatch.setattr(Config, "FEATURE_FLAGS", {"feature_delete": False})
+ response = flask_test_client.get(f"/grants/{test_fund.fund_id}/delete", follow_redirects=True)
+ assert response.status_code == 403
+ assert b"Delete Feature Disabled" in response.data
+
+
+@pytest.mark.usefixtures("set_auth_cookie", "patch_validate_token_rs256_internal_user")
+def test_delete_fund_feature_enabled(_db, flask_test_client, monkeypatch, seed_fund_without_assessment):
+ """Test that the delete endpoint redirects when a feature flag is enabled."""
+ test_fund: Fund = seed_fund_without_assessment["funds"][0]
+ monkeypatch.setattr(Config, "FEATURE_FLAGS", {"feature_delete": True})
+ output: Fund = _db.session.get(Fund, test_fund.fund_id,
+ options=[joinedload(Fund.rounds).joinedload(Round.sections)])
+ assert output is not None, "No values present in the db"
+ response = flask_test_client.get(f"/grants/{test_fund.fund_id}/delete", follow_redirects=True)
+ assert response.status_code == 200 # Assuming redirection to a valid page
+ _db.session.commit()
+ output_f = _db.session.get(Fund, test_fund.fund_id, options=[joinedload(Fund.rounds).joinedload(Round.sections)])
+ assert output_f is None, "Grant delete did not happened"
+ output_r = _db.session.query(Round).all()
+ assert not output_r, "Round delete did not happened"
+ output_s = _db.session.query(Section).all()
+ assert not output_s, "Section delete did not happened"
+ output_c = _db.session.query(Component).all()
+ assert not output_c, "Component delete did not happened"
+ output_l = _db.session.query(Lizt).all()
+ assert not output_l, "Lizt delete did not happened"
diff --git a/tests/blueprints/round/test_routes.py b/tests/blueprints/round/test_routes.py
index 6d8413d2..0602545d 100644
--- a/tests/blueprints/round/test_routes.py
+++ b/tests/blueprints/round/test_routes.py
@@ -2,9 +2,11 @@
from bs4 import BeautifulSoup
from flask import g, url_for
-from app.db.models import Round
+from app.db.models import Round, Fund, Section, Component, Lizt
from app.db.queries.round import get_round_by_id
from tests.helpers import submit_form
+from config import Config
+from sqlalchemy.orm import joinedload
round_data_info = {
"opens": ["01", "10", "2024", "09", "00"],
@@ -330,3 +332,36 @@ def test_clone_round(flask_test_client, seed_dynamic_data):
soup = BeautifulSoup(response.data, "html.parser")
notification = soup.find("div", {"class": "govuk-notification-banner__content"})
assert notification.text.strip() == "Error copying application"
+
+
+@pytest.mark.usefixtures("set_auth_cookie", "patch_validate_token_rs256_internal_user")
+def test_delete_fund_feature_disabled(flask_test_client, monkeypatch, seed_fund_without_assessment):
+ """Test that the delete endpoint returns 403 when a feature flag is disabled."""
+ test_round: Round = seed_fund_without_assessment["rounds"][0]
+ monkeypatch.setattr(Config, "FEATURE_FLAGS", {"feature_delete": False})
+ response = flask_test_client.get(f"/rounds/{test_round.round_id}/delete", follow_redirects=True)
+ assert response.status_code == 403
+ assert b"Delete Feature Disabled" in response.data
+
+
+@pytest.mark.usefixtures("set_auth_cookie", "patch_validate_token_rs256_internal_user")
+def test_delete_fund_feature_enabled(_db, flask_test_client, monkeypatch, seed_fund_without_assessment):
+ """Test that the delete endpoint redirects when a feature flag is enabled."""
+ test_round: Round = seed_fund_without_assessment["rounds"][0]
+ monkeypatch.setattr(Config, "FEATURE_FLAGS", {"feature_delete": True})
+ output: Fund = _db.session.get(Fund, test_round.fund_id,
+ options=[joinedload(Fund.rounds).joinedload(Round.sections)])
+ assert output is not None, "No values present in the db"
+ response = flask_test_client.get(f"/rounds/{test_round.round_id}/delete", follow_redirects=True)
+ assert response.status_code == 200 # Assuming redirection to a valid page
+ _db.session.commit()
+ output_f = _db.session.get(Fund, test_round.fund_id, options=[joinedload(Fund.rounds).joinedload(Round.sections)])
+ assert output_f is not None, "Grant deleted"
+ output_r = _db.session.query(Round).all()
+ assert not output_r, "Round delete did not happened"
+ output_s = _db.session.query(Section).all()
+ assert not output_s, "Section delete did not happened"
+ output_c = _db.session.query(Component).all()
+ assert not output_c, "Component delete did not happened"
+ output_l = _db.session.query(Lizt).all()
+ assert not output_l, "Lizt delete did not happened"
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
index 52cbebae..a3ce8cee 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -11,7 +11,7 @@
from app.create_app import create_app
from app.import_config.load_form_json import load_form_jsons
from config import Config
-from tests.seed_test_data import init_unit_test_data, insert_test_data
+from tests.seed_test_data import init_unit_test_data, insert_test_data, fund_without_assessment
pytest_plugins = ["fsd_test_utils.fixtures.db_fixtures"]
@@ -47,6 +47,29 @@ def seed_dynamic_data(request, app, clear_test_data, _db, enable_preserve_test_d
_db.session.commit()
+@pytest.fixture(scope="function")
+def seed_fund_without_assessment(request, app, clear_test_data, _db, enable_preserve_test_data):
+ marker = request.node.get_closest_marker("seed_config")
+
+ if marker is None:
+ fab_seed_data = fund_without_assessment()
+ else:
+ fab_seed_data = marker.args[0]
+ insert_test_data(db=_db, test_data=fab_seed_data)
+ yield fab_seed_data
+ # cleanup data after test
+ # rollback incase of any errors during test session
+ _db.session.rollback()
+ # disable foreign key checks
+ _db.session.execute(text("SET session_replication_role = replica"))
+ # delete all data from tables
+ for table in reversed(_db.metadata.sorted_tables):
+ _db.session.execute(table.delete())
+ # reset foreign key checks
+ _db.session.execute(text("SET session_replication_role = DEFAULT"))
+ _db.session.commit()
+
+
@pytest.fixture(scope="function")
def db_with_templates(app, _db):
"""Ensures a clean database but with templates already loaded"""
diff --git a/tests/seed_test_data.py b/tests/seed_test_data.py
index f747c085..dcd5d8b7 100644
--- a/tests/seed_test_data.py
+++ b/tests/seed_test_data.py
@@ -39,7 +39,7 @@
"project_name_field_id": 1,
"prospectus_link": "https://www.gov.uk/government/organisations/ministry-of-housing-communities-local-government",
"privacy_notice_link": "https://www.gov.uk/government/organisations/"
- "ministry-of-housing-communities-local-government",
+ "ministry-of-housing-communities-local-government",
"contact_email": "help@fab.gov.uk",
"instructions_json": {},
"feedback_link": "https://www.gov.uk/government/organisations/ministry-of-housing-communities-local-government",
@@ -71,7 +71,7 @@
page_five_id = uuid4()
alt_page_id = uuid4()
-
+#NOSONAR Ignore since this data is related to unit tests
def init_salmon_fishing_fund():
organisation_uuid = uuid4()
o: Organisation = Organisation(
@@ -355,7 +355,7 @@ def init_salmon_fishing_fund():
"organisations": [o],
}
-
+#NOSONAR Ignore since this data is related to unit tests
def init_unit_test_data() -> dict:
organisation_uuid = uuid4()
o: Organisation = Organisation(
@@ -481,6 +481,142 @@ def init_unit_test_data() -> dict:
"themes": [t1],
}
+#NOSONAR Ignore since this data is related to unit tests
+def fund_without_assessment() -> dict:
+ organisation_uuid = uuid4()
+ o: Organisation = Organisation(
+ organisation_id=organisation_uuid,
+ name=f"Ministry of Testing - {str(organisation_uuid)[:5]}",
+ short_name=f"MoT-{str(organisation_uuid)[:5]}",
+ logo_uri="https://www.google.com",
+ audit_info={"user": "dummy_user", "timestamp": datetime.now().isoformat(), "action": "create"},
+ )
+
+ f2: Fund = Fund(
+ fund_id=uuid4(),
+ name_json={"en": "Unit Test Fund 2"},
+ title_json={"en": "funding to improve testing"},
+ description_json={"en": "A £10m fund to improve testing across the devolved nations."},
+ welsh_available=False,
+ short_name=f"UTF{randint(0, 999)}",
+ owner_organisation_id=o.organisation_id,
+ funding_type=FundingType.COMPETITIVE,
+ ggis_scheme_reference_number="G3-SCH-0000092414",
+ )
+
+ f2_r1: Round = Round(
+ round_id=uuid4(),
+ fund_id=f2.fund_id,
+ audit_info={"user": "dummy_user", "timestamp": datetime.now().isoformat(), "action": "create"},
+ title_json={"en": "round the first"},
+ short_name=f"UTR{randint(0, 999)}",
+ opens=datetime.now(),
+ deadline=datetime.now(),
+ assessment_start=datetime.now(),
+ reminder_date=datetime.now(),
+ assessment_deadline=datetime.now(),
+ prospectus_link="https://www.google.com",
+ privacy_notice_link="https://www.google.com",
+ contact_email="test@test.com",
+ feedback_link="https://www.google.com",
+ project_name_field_id="12312312312",
+ guidance_url="https://www.google.com",
+ feedback_survey_config={
+ "has_feedback_survey": False,
+ "has_section_feedback": False,
+ "has_research_survey": False,
+ "is_feedback_survey_optional": False,
+ "is_section_feedback_optional": False,
+ "is_research_survey_optional": False,
+ },
+ eligibility_config={"has_eligibility": False},
+ eoi_decision_schema={"en": {"valid": True}, "cy": {"valid": False}},
+ )
+
+ f2_r1_s1: Section = Section(
+ section_id=uuid4(), index=1, round_id=f2_r1.round_id, name_in_apply_json={"en": "Organisation Information 2"}
+ )
+
+ f2_r1_s2: Section = Section(
+ section_id=uuid4(), index=1, round_id=f2_r1.round_id, name_in_apply_json={"en": "Organisation Information 3"}
+ )
+
+ f2_r1_s1_f1: Form = Form(
+ form_id=uuid4(),
+ section_id=f2_r1_s1.section_id,
+ name_in_apply_json={"en": "About your organisation"},
+ section_index=1,
+ runner_publish_name="about-your-org",
+ template_name="About your organization template",
+ )
+
+ f2_r1_s1_f2: Form = Form(
+ form_id=uuid4(),
+ section_id=f2_r1_s2.section_id,
+ name_in_apply_json={"en": "About your organisation 2"},
+ section_index=1,
+ runner_publish_name="about-your-org",
+ template_name="About your organization template",
+ )
+
+ f2_r1_s1_f1_p1: Page = Page(
+ page_id=uuid4(),
+ form_id=f2_r1_s1_f1.form_id,
+ display_path="organisation-name",
+ name_in_apply_json={"en": "Organisation Name"},
+ form_index=1,
+ default_next_page_id=None,
+ )
+
+ f2_r1_s1_f1_p2: Page = Page(
+ page_id=uuid4(),
+ form_id=f2_r1_s1_f2.form_id,
+ display_path="organisation-name",
+ name_in_apply_json={"en": "Organisation Name"},
+ form_index=1,
+ default_next_page_id=None,
+ )
+
+ f2_r1_s1_f1_p1_c1: Component = Component(
+ component_id=uuid4(),
+ page_id=f2_r1_s1_f1_p1.page_id,
+ title="What is your organisation's name?",
+ hint_text="This must match the registered legal organisation name",
+ type=ComponentType.TEXT_FIELD,
+ page_index=1,
+ options={"hideTitle": False, "classes": ""},
+ runner_component_name="organisation_name",
+ )
+
+ l1: Lizt = Lizt(
+ list_id=uuid4(),
+ name="classifications_list",
+ type="string",
+ items=[{"text": "Charity", "value": "charity"}, {"text": "Public Limited Company", "value": "plc"}],
+ is_template=True,
+ )
+
+ f2_r1_s1_f1_p1_c2_with_list: Component = Component(
+ component_id=uuid4(),
+ page_id=f2_r1_s1_f1_p2.page_id,
+ title="How is your organisation classified?",
+ type=ComponentType.RADIOS_FIELD,
+ page_index=2,
+ options={"hideTitle": False, "classes": ""},
+ runner_component_name="organisation_classification",
+ list_id=l1.list_id,
+ )
+ return {
+ "lists": [l1],
+ "funds": [f2],
+ "organisations": [o],
+ "rounds": [f2_r1],
+ "sections": [f2_r1_s1, f2_r1_s2],
+ "forms": [f2_r1_s1_f1, f2_r1_s1_f2],
+ "pages": [f2_r1_s1_f1_p1, f2_r1_s1_f1_p2],
+ "components": [f2_r1_s1_f1_p1_c1, f2_r1_s1_f1_p1_c2_with_list]
+ }
+
def add_default_page_paths(db, default_next_page_config):
# set up the default paths