diff --git a/AIPscan/Aggregator/templates/confirm.html b/AIPscan/Aggregator/templates/confirm.html new file mode 100644 index 00000000..68d5612b --- /dev/null +++ b/AIPscan/Aggregator/templates/confirm.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

{{ prompt }}

+ +
+ + + Cancel +
+
+{% endblock %} diff --git a/AIPscan/Aggregator/tests/test_views.py b/AIPscan/Aggregator/tests/test_views.py index b806586c..6385769f 100644 --- a/AIPscan/Aggregator/tests/test_views.py +++ b/AIPscan/Aggregator/tests/test_views.py @@ -51,21 +51,32 @@ def test_edit_storage_service(app_with_populated_files): def test_delete_storage_service(app_with_populated_files, mocker): with current_app.test_client() as test_client: + # The delete confirmation page should not show up for a non-existent storage service response = test_client.get("/aggregator/delete_storage_service/0") assert response.status_code == 404 + # Deletion should not be attempted for a non-existent storage service + response = test_client.get("/aggregator/delete_storage_service/0?confirm=1") + assert response.status_code == 404 + + # Don't actually instantiate Celery job mocker.patch("AIPscan.Aggregator.tasks.delete_storage_service.delay") + # The delete confirmation page should show up for an existing storage service response = test_client.get("/aggregator/delete_storage_service/1") + assert response.status_code == 200 + + # Deletion should be attempted if the storage service exists + response = test_client.get("/aggregator/delete_storage_service/1?confirm=1") assert response.status_code == 302 def test_delete_fetch_job(app_with_populated_files, mocker): with current_app.test_client() as test_client: - response = test_client.get("/aggregator/delete_fetch_job/0") + response = test_client.get("/aggregator/delete_fetch_job/0?confirm=1") assert response.status_code == 404 mocker.patch("AIPscan.Aggregator.tasks.delete_fetch_job.delay") - response = test_client.get("/aggregator/delete_fetch_job/1") + response = test_client.get("/aggregator/delete_fetch_job/1?confirm=1") assert response.status_code == 302 diff --git a/AIPscan/Aggregator/views.py b/AIPscan/Aggregator/views.py index 619b2875..793826fc 100644 --- a/AIPscan/Aggregator/views.py +++ b/AIPscan/Aggregator/views.py @@ -15,7 +15,7 @@ url_for, ) -from AIPscan import db, typesense_helpers +from AIPscan import db, decorators, typesense_helpers from AIPscan.Aggregator import database_helpers, tasks from AIPscan.Aggregator.forms import StorageServiceForm from AIPscan.Aggregator.task_helpers import ( @@ -171,6 +171,13 @@ def new_storage_service(): @aggregator.route("/delete_storage_service/", methods=["GET"]) +@decorators.confirm_required( + StorageService, + "storage_service_id", + "Are you sure you'd like to delete this storage service?", + "Delete", + "aggregator.ss_default", +) def delete_storage_service(storage_service_id): storage_service = StorageService.query.get(storage_service_id) @@ -241,6 +248,13 @@ def new_fetch_job(fetch_job_id): @aggregator.route("/delete_fetch_job/", methods=["GET"]) +@decorators.confirm_required( + FetchJob, + "fetch_job_id", + "Are you sure you'd like to delete this fetch job?", + "Delete", + "aggregator.ss_default", +) def delete_fetch_job(fetch_job_id): fetch_job = FetchJob.query.get(fetch_job_id) diff --git a/AIPscan/decorators.py b/AIPscan/decorators.py new file mode 100644 index 00000000..563ccad8 --- /dev/null +++ b/AIPscan/decorators.py @@ -0,0 +1,27 @@ +from functools import wraps + +from flask import abort, render_template, request + + +def confirm_required(model, id_argument, prompt, action, cancel_route): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + instance = model.query.get(kwargs[id_argument]) + + if not instance: + abort(404) + + if request.args.get("confirm"): + return f(*args, **kwargs) + + return render_template( + "confirm.html", + prompt=prompt, + action=action, + cancel_route=cancel_route, + ) + + return decorated_function + + return decorator