Skip to content

Commit

Permalink
fs-4642 adding cascade deletes (#47)
Browse files Browse the repository at this point in the history
* fs-4642 adding cascade deletes

* fs-4642 making frontend use cascade deletes
  • Loading branch information
srh-sloan authored Sep 18, 2024
1 parent a763ff8 commit 0e7f934
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 26 deletions.
8 changes: 4 additions & 4 deletions app/blueprints/fund_builder/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@
from app.db.models.round import Round
from app.db.queries.application import clone_single_form
from app.db.queries.application import clone_single_round
from app.db.queries.application import delete_form
from app.db.queries.application import delete_section
from app.db.queries.application import get_all_template_forms
from app.db.queries.application import get_form_by_id
from app.db.queries.application import get_section_by_id
from app.db.queries.application import insert_new_section
from app.db.queries.application import update_form
from app.db.queries.application import update_section
from app.db.queries.fund import add_fund
from app.db.queries.fund import get_all_funds
Expand Down Expand Up @@ -71,7 +72,7 @@ def section(round_id):
}
existing_section = None
if request.args.get("action") == "remove":
update_section(request.args.get("section_id"), {"round_id": None}) # TODO remove properly
delete_section(section_id=request.args.get("section_id"), cascade=True)
return redirect(url_for("build_fund_bp.build_application", round_id=round_id))
if form.validate_on_submit():
count_existing_sections = len(round_obj.sections)
Expand Down Expand Up @@ -119,8 +120,7 @@ def configure_forms_in_section(round_id, section_id):
if request.method == "POST":
if request.args.get("action") == "remove":
form_id = request.args.get("form_id")
# TODO figure out if we want to do a soft or hard delete here
update_form(form_id, {"section_id": None})
delete_form(form_id=form_id, cascade=True)
else:
template_id = request.form.get("template_id")
section = get_section_by_id(section_id=section_id)
Expand Down
35 changes: 32 additions & 3 deletions app/db/queries/application.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from uuid import uuid4

from sqlalchemy import delete

from app.db import db
from app.db.models import Component
from app.db.models import Form
Expand Down Expand Up @@ -283,8 +285,12 @@ def update_section(section_id, new_section_config):
return section


def delete_section(section_id):
def delete_section(section_id, cascade: bool = False):
section = db.session.query(Section).where(Section.section_id == section_id).one_or_none()
if cascade:
_delete_all_components_in_pages(page_ids=[page.page_id for form in section.forms for page in form.pages])
_delete_all_pages_in_forms(form_ids=[f.form_id for f in section.forms])
_delete_all_forms_in_sections(section_ids=[section_id])
db.session.delete(section)
db.session.commit()
return section
Expand Down Expand Up @@ -351,8 +357,17 @@ def update_form(form_id, new_form_config):
return form


def delete_form(form_id):
def _delete_all_forms_in_sections(section_ids: list):
stmt = delete(Form).filter(Form.section_id.in_(section_ids))
db.session.execute(stmt)
db.session.commit()


def delete_form(form_id, cascade: bool = False):
form = db.session.query(Form).where(Form.form_id == form_id).one_or_none()
if cascade:
_delete_all_components_in_pages(page_ids=[p.page_id for p in form.pages])
_delete_all_pages_in_forms(form_ids=[form_id])
db.session.delete(form)
db.session.commit()
return form
Expand Down Expand Up @@ -419,8 +434,16 @@ def update_page(page_id, new_page_config):
return page


def delete_page(page_id):
def _delete_all_pages_in_forms(form_ids: list):
stmt = delete(Page).filter(Page.form_id.in_(form_ids))
db.session.execute(stmt)
db.session.commit()


def delete_page(page_id, cascade: bool = False):
page = db.session.query(Page).where(Page.page_id == page_id).one_or_none()
if cascade:
_delete_all_components_in_pages(page_ids=[page_id])
db.session.delete(page)
db.session.commit()
return page
Expand Down Expand Up @@ -516,3 +539,9 @@ def delete_component(component_id):
db.session.delete(component)
db.session.commit()
return component


def _delete_all_components_in_pages(page_ids):
stmt = delete(Component).filter(Component.page_id.in_(page_ids))
db.session.execute(stmt)
db.session.commit()
3 changes: 2 additions & 1 deletion docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ services:
- DATABASE_URL=postgresql://postgres:password@fab-db:5432/fund_builder # pragma: allowlist secret
- FLASK_DEBUG=0
- FLASK_ENV=development
- SECRET_KEY=local
depends_on: [fab-db, form-runner]

fab-db:
Expand Down Expand Up @@ -50,4 +51,4 @@ services:
- LOG_LEVEL=debug
- 'NODE_CONFIG={"safelist": ["fab"]}'
- PREVIEW_MODE=true
- NODE_ENV=development
- NODE_ENV=development
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ services:
environment:
- DATABASE_URL=postgresql://postgres:password@fab-db:5432/fab
- DATABASE_URL_UNIT_TEST=postgresql://postgres:password@fab-db:5432/fab_unit_test
- SECRET_KEY=local


fab-db:
Expand Down
4 changes: 4 additions & 0 deletions tasks/db_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
import sys
from os import getenv

from flask_migrate import upgrade
from invoke import task # noqa:E402

from app import app
from app.import_config.load_form_json import load_form_jsons

sys.path.insert(1, ".")
os.environ.update({"FLASK_ENV": "tasks"})
Expand Down Expand Up @@ -49,6 +51,7 @@ def recreate_local_dbs(c):
print(
f"{db_uri} db created...",
)
upgrade()


@task
Expand All @@ -66,6 +69,7 @@ def create_test_data(c):
)
db.session.commit()
insert_test_data(db=db, test_data=init_salmon_fishing_fund())
load_form_jsons()


@task
Expand Down
123 changes: 105 additions & 18 deletions tests/test_db_template_CRUD.py → tests/test_db_application_CRUD.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import uuid
from copy import deepcopy

import pytest
from sqlalchemy.exc import IntegrityError

from app.db.models import ComponentType
from app.db.models.application_config import Component
from app.db.models.application_config import Form
from app.db.models.application_config import Lizt
from app.db.models.application_config import Page
from app.db.models.application_config import Section
from app.db.queries.application import delete_component
Expand Down Expand Up @@ -106,6 +108,25 @@ def test_delete_section(flask_test_client, _db, clear_test_data, seed_dynamic_da
assert _db.session.query(Section).filter(Section.section_id == new_section.section_id).one_or_none() is None


def test_failed_delete_section_cascade(flask_test_client, _db, clear_test_data, seed_dynamic_data):
new_section_config["round_id"] = None
section = insert_new_section(new_section_config)
# CREATE FK link to Form
new_form_config["section_id"] = section.section_id
form = insert_new_form(new_form_config)
# check inserted form has same section_id
assert form.section_id == section.section_id
assert isinstance(section, Section)
assert section.audit_info == new_section_config["audit_info"]
assert isinstance(form, Form)
new_form_id = form.form_id

delete_section(form.section_id, cascade=True)

assert _db.session.query(Section).filter(Section.section_id == section.section_id).one_or_none() is None
assert _db.session.query(Form).where(Form.form_id == new_form_id).one_or_none() is None


def test_failed_delete_section_with_fk_to_forms(flask_test_client, _db, clear_test_data, seed_dynamic_data):
new_section_config["round_id"] = None
section = insert_new_section(new_section_config)
Expand All @@ -117,12 +138,9 @@ def test_failed_delete_section_with_fk_to_forms(flask_test_client, _db, clear_te
assert isinstance(section, Section)
assert section.audit_info == new_section_config["audit_info"]

try:
delete_section(form.section_id)
assert False, "Expected IntegrityError was not raised"
except IntegrityError:
_db.session.rollback() # Rollback the failed transaction to maintain DB integrity
assert True # Explicitly pass the test to indicate the expected error was caught
with pytest.raises(IntegrityError):
delete_section(form.section_id, cascade=False)
_db.session.rollback() # Rollback the failed transaction to maintain DB integrity

existing_section = _db.session.query(Section).filter(Section.section_id == section.section_id).one_or_none()
assert existing_section is not None, "Section was unexpectedly deleted"
Expand Down Expand Up @@ -226,19 +244,33 @@ def test_delete_form(flask_test_client, _db, clear_test_data, seed_dynamic_data)
assert _db.session.query(Form).filter(Form.form_id == new_form.form_id).one_or_none() is None


def test_delete_form_cascade(flask_test_client, _db, clear_test_data, seed_dynamic_data):
new_form_config["section_id"] = None
new_form = insert_new_form(new_form_config)
# CREATE FK link to Form
new_page_config["form_id"] = new_form.form_id
new_page = insert_new_page(new_page_config)

assert isinstance(new_form, Form)
assert new_form.audit_info == new_form_config["audit_info"]
assert isinstance(new_page, Page)
new_page_id = new_page.page_id

delete_form(new_form.form_id, cascade=True)
assert _db.session.query(Form).filter(Form.form_id == new_form.form_id).one_or_none() is None
assert _db.session.query(Page).where(Page.page_id == new_page_id).one_or_none() is None


def test_failed_delete_form_with_fk_to_page(flask_test_client, _db, clear_test_data, seed_dynamic_data):
new_form_config["section_id"] = None
form = insert_new_form(new_form_config)
# CREATE FK link to Form
new_page_config["form_id"] = form.form_id
page = insert_new_page(new_page_config)

try:
delete_form(page.form_id)
assert False, "Expected IntegrityError was not raised"
except IntegrityError:
_db.session.rollback() # Rollback the failed transaction to maintain DB integrity
assert True # Explicitly pass the test to indicate the expected error was caught
with pytest.raises(IntegrityError):
delete_form(page.form_id, cascade=False)
_db.session.rollback() # Rollback the failed transaction to maintain DB integrity

existing_form = _db.session.query(Form).filter(Form.form_id == form.form_id).one_or_none()
assert existing_form is not None, "Form was unexpectedly deleted"
Expand Down Expand Up @@ -340,6 +372,27 @@ def test_delete_page(flask_test_client, _db, clear_test_data, seed_dynamic_data)
assert _db.session.query(Page).filter(Page.page_id == new_page.page_id).one_or_none() is None


def test_delete_page_cascade(flask_test_client, _db, clear_test_data, seed_dynamic_data):
new_page_config["form_id"] = None
new_page = insert_new_page(new_page_config)

assert isinstance(new_page, Page)
page_id_to_delete = new_page.page_id
assert new_page.audit_info == new_page_config["audit_info"]

# create component on that page
new_component_config["page_id"] = new_page.page_id
new_component_config["list_id"] = None
new_component_config["theme_id"] = None
new_component = insert_new_component(new_component_config=new_component_config)
assert isinstance(new_component, Component)
component_id_to_delete = new_component.component_id

delete_page(new_page.page_id, cascade=True)
assert _db.session.query(Page).where(Page.page_id == page_id_to_delete).one_or_none() is None
assert _db.session.query(Component).where(Component.component_id == component_id_to_delete).one_or_none() is None


def test_failed_delete_page_with_fk_to_component(flask_test_client, _db, clear_test_data, seed_dynamic_data):
new_page_config["form_id"] = None
new_page = insert_new_page(new_page_config)
Expand All @@ -353,12 +406,9 @@ def test_failed_delete_page_with_fk_to_component(flask_test_client, _db, clear_t
assert isinstance(new_page, Page)
assert new_page.audit_info == new_page_config["audit_info"]

try:
delete_page(component.page_id)
assert False, "Expected IntegrityError was not raised"
except IntegrityError:
_db.session.rollback() # Rollback the failed transaction to maintain DB integrity
assert True # Explicitly pass the test to indicate the expected error was caught
with pytest.raises(IntegrityError):
delete_page(new_page.page_id, cascade=False)
_db.session.rollback() # Rollback the failed transaction to maintain DB integrity

existing_page = _db.session.query(Page).filter(Page.page_id == new_page.page_id).one_or_none()
assert existing_page is not None, "Page was unexpectedly deleted"
Expand Down Expand Up @@ -518,3 +568,40 @@ def test_delete_component(flask_test_client, _db, clear_test_data, seed_dynamic_

delete_component(component.component_id)
assert _db.session.query(Component).filter(Component.component_id == component.component_id).one_or_none() is None
assert _db.session.query(Lizt).where(Lizt.list_id == list_id).one_or_none() is not None


def test_delete_section_with_full_cascade(flask_test_client, _db, clear_test_data, seed_dynamic_data):
new_section_config["round_id"] = None
new_section = insert_new_section(new_section_config)
assert isinstance(new_section, Section)
new_section_id = new_section.section_id

# CREATE FK link to Form
new_form_config["section_id"] = new_section_id
new_form = insert_new_form(new_form_config)
assert isinstance(new_form, Form)
assert new_form.section_id == new_section_id
new_form_id = new_form.form_id

# create FK link to page
new_page_config["form_id"] = new_form_id
new_page = insert_new_page(new_page_config)
assert isinstance(new_page, Page)
assert new_page.form_id == new_form_id
new_page_id = new_page.page_id

# create component on that page
new_component_config["page_id"] = new_page_id
new_component_config["list_id"] = None
new_component_config["theme_id"] = None
new_component = insert_new_component(new_component_config=new_component_config)
assert isinstance(new_component, Component)
new_component_id = new_component.component_id

# Should successfully delete everything with cascade == true
delete_section(new_section_id, cascade=True)
assert _db.session.query(Section).where(Section.section_id == new_section_id).one_or_none() is None
assert _db.session.query(Form).where(Form.form_id == new_form_id).one_or_none() is None
assert _db.session.query(Page).where(Page.page_id == new_page_id).one_or_none() is None
assert _db.session.query(Component).where(Component.component_id == new_component_id).one_or_none() is None

0 comments on commit 0e7f934

Please sign in to comment.