Skip to content

Commit 5f688f7

Browse files
authored
Merge pull request #3648 from Uninett/feat/improve-dashboard-delete-warning
Show confirmation modal when deleting dashboards
2 parents f253c01 + f1f928f commit 5f688f7

File tree

6 files changed

+181
-61
lines changed

6 files changed

+181
-61
lines changed

changelog.d/3648.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add confirmation modal when deleting dashboards

python/nav/web/static/js/src/webfront.js

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -288,24 +288,6 @@ require([
288288
}
289289

290290

291-
/** Functions for deleting a dashboard */
292-
function addDeleteDashboardListener(feedback) {
293-
$('#form-delete-dashboard').submit(function (event) {
294-
event.preventDefault();
295-
var $this = $(this);
296-
var doDelete = confirm('Really delete dashboard and all widgets on it?');
297-
if (doDelete) {
298-
var request = $.post(this.getAttribute('action'), $this.serialize());
299-
request.done(function (response) {
300-
window.location = '/';
301-
});
302-
request.fail(function (response) {
303-
feedback.addFeedback(response.responseText, 'error');
304-
});
305-
}
306-
});
307-
}
308-
309291
/**
310292
* Load runner - runs on page load
311293
*/
@@ -363,7 +345,6 @@ require([
363345
addDefaultDashboardListener(feedback);
364346
addCreateDashboardListener(feedback);
365347
addRenameDashboardListener(feedback);
366-
addDeleteDashboardListener(feedback);
367348
});
368349

369350
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{% if not error_message %}
2+
<h4>Delete Dashboard?</h4>
3+
<p>
4+
Are you sure you want to permanently delete the dashboard <em>{{ dashboard.name }}</em> and all its associated navlets?
5+
</p>
6+
{% if subscribers_count > 0 %}
7+
<p data-test-id="subscriber-warning">
8+
This dashboard has <strong>{{ subscribers_count }}</strong>
9+
{{ subscribers_count|pluralize:"subscriber,subscribers" }}.
10+
They will lose access if you proceed.
11+
</p>
12+
{% endif %}
13+
<form hx-post="{% url 'delete-dashboard' dashboard.pk %}">
14+
{% csrf_token %}
15+
<input type="hidden" name="confirm_delete" value="true">
16+
<button class="small alert" type="submit">Yes, delete this dashboard</button>
17+
<button class="small secondary" type="button" hx-on:click="htmx.remove(this.closest('.nav-modal'))">
18+
Cancel
19+
</button>
20+
</form>
21+
{% else %}
22+
<div class="alert-box error">
23+
{{ error_message }}
24+
</div>
25+
<button class="small secondary" type="button" hx-on:click="htmx.remove(this.closest('.nav-modal'))">
26+
Close modal
27+
</button>
28+
{% endif %}

python/nav/web/templates/webfront/_dashboard_settings_form.html

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,20 @@
4646

4747
<div class="column medium-4">
4848
{% if request.account.account_dashboards.count > 1 and not dashboard.is_default %}
49-
<form id="form-delete-dashboard" method="post" action="{% url 'delete-dashboard' dashboard.pk %}">
50-
{% csrf_token %}
51-
<input type="submit" class="small button alert expand" value="Delete dashboard">
52-
</form>
49+
<form
50+
hx-post="{% url 'delete-dashboard' dashboard.pk %}"
51+
hx-target="body"
52+
hx-swap="beforeend"
53+
>
54+
{% csrf_token %}
55+
<button
56+
class="small alert expand"
57+
type="submit"
58+
close-popover
59+
>
60+
Delete dashboard
61+
</button>
62+
</form>
5363
{% endif %}
5464

5565
</div>

python/nav/web/webfront/views.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from django.db import models
2626
from django.db.models import Q
2727
from django.http import (
28-
HttpResponseBadRequest,
2928
HttpResponseForbidden,
3029
HttpResponseRedirect,
3130
HttpResponse,
@@ -561,20 +560,39 @@ def add_dashboard(request):
561560

562561
@require_POST
563562
def delete_dashboard(request, did):
564-
"""Delete this dashboard and all widgets on it"""
563+
"""Delete this dashboard and all widgets on it, with confirmation modal"""
565564
account = get_account(request)
566-
is_last = AccountDashboard.objects.filter(account=account).count() == 1
567-
if is_last:
568-
return HttpResponseBadRequest('Cannot delete last dashboard')
565+
dashboard = get_object_or_404(AccountDashboard, pk=did, account=account)
569566

570-
dash = get_object_or_404(AccountDashboard, pk=did, account=account)
567+
is_last = AccountDashboard.objects.filter(account=account).count() == 1
568+
if is_last or dashboard.is_default:
569+
error_message = (
570+
"Cannot delete last dashboard"
571+
if is_last
572+
else "Cannot delete default dashboard"
573+
)
574+
return render_modal(
575+
request,
576+
'webfront/_dashboard_settings_delete_confirmation.html',
577+
context={'error_message': error_message},
578+
modal_id='delete-dashboard-confirmation',
579+
size='small',
580+
)
571581

572-
if dash.is_default:
573-
return HttpResponseBadRequest('Cannot delete default dashboard')
582+
confirm_delete = request.POST.get("confirm_delete", None) == "true"
583+
if confirm_delete:
584+
dashboard.delete()
585+
return HttpResponseClientRedirect(reverse('webfront-index'))
574586

575-
dash.delete()
587+
subscribers_count = dashboard.subscribers.count()
576588

577-
return HttpResponse('Dashboard deleted')
589+
return render_modal(
590+
request,
591+
'webfront/_dashboard_settings_delete_confirmation.html',
592+
context={'dashboard': dashboard, 'subscribers_count': subscribers_count},
593+
modal_id='delete-dashboard-confirmation',
594+
size='small',
595+
)
578596

579597

580598
@require_POST

tests/integration/web/webfront_test.py

Lines changed: 110 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -81,39 +81,121 @@ def test_set_default_dashboard_with_multiple_previous_defaults_should_succeed(
8181
)
8282

8383

84-
def test_delete_last_dashboard_should_fail(db, client, admin_account):
85-
"""Tests that the last dashboard cannot be deleted"""
86-
dashboard = AccountDashboard.objects.get(
87-
is_default=True,
88-
account=admin_account,
89-
)
90-
url = reverse("delete-dashboard", args=(dashboard.pk,))
91-
response = client.post(url, follow=True)
84+
class TestDeleteDashboardView:
85+
"""Tests for the delete_dashboard view which allows deleting dashboards"""
9286

93-
assert response.status_code == 400
94-
assert "Cannot delete last dashboard" in smart_str(response.content)
95-
assert AccountDashboard.objects.filter(id=dashboard.id).exists()
87+
def test_given_dashboard_when_delete_unconfirmed_then_do_not_delete_dashboard(
88+
self, db, client, admin_account
89+
):
90+
"""
91+
Tests that the dashboard is not deleted when deletion is not yet confirmed
92+
"""
93+
dashboard = self._create_dashboard(
94+
admin_account,
95+
name="to_be_deleted",
96+
is_default=False,
97+
)
98+
url = reverse("delete-dashboard", args=(dashboard.pk,))
99+
response = client.post(url)
96100

101+
assert response.status_code == 200
102+
assert AccountDashboard.objects.filter(id=dashboard.id).exists()
97103

98-
def test_delete_default_dashboard_should_fail(db, client, admin_account):
99-
"""Tests that the default dashboard cannot be deleted"""
100-
# Creating another dashboard, so that default is not the last one
101-
AccountDashboard.objects.create(
102-
name="non_default",
103-
is_default=False,
104-
account=admin_account,
105-
)
104+
def test_given_dashboard_id_when_delete_unconfirmed_then_render_confirmation(
105+
self, db, client, admin_account
106+
):
107+
"""
108+
Tests that the deletion confirmation is rendered when deletion is unconfirmed
109+
"""
110+
dashboard = self._create_dashboard(
111+
admin_account,
112+
name="to_be_deleted",
113+
is_default=False,
114+
)
115+
url = reverse("delete-dashboard", args=(dashboard.pk,))
116+
response = client.post(url)
106117

107-
default_dashboard = AccountDashboard.objects.get(
108-
is_default=True,
109-
account=admin_account,
110-
)
111-
url = reverse("delete-dashboard", args=(default_dashboard.pk,))
112-
response = client.post(url, follow=True)
118+
assert response.status_code == 200
119+
assert 'id="delete-dashboard-confirmation"' in smart_str(response.content)
120+
121+
def test_given_dashboard_with_subscribers_then_render_subscriber_warning(
122+
self, db, client, admin_account, non_admin_account
123+
):
124+
"""Tests that the deletion confirmation shows subscriber info when applicable"""
125+
dashboard = self._create_dashboard(
126+
admin_account,
127+
is_shared=True,
128+
)
129+
AccountDashboardSubscription.objects.create(
130+
account=non_admin_account,
131+
dashboard=dashboard,
132+
)
133+
url = reverse("delete-dashboard", args=(dashboard.pk,))
134+
response = client.post(url)
135+
response_content = smart_str(response.content)
136+
137+
assert response.status_code == 200
138+
assert 'data-test-id="subscriber-warning"' in response_content
139+
140+
def test_given_dashboard_when_delete_confirmed_then_delete_dashboard(
141+
self, db, client, admin_account
142+
):
143+
"""Tests that an existing dashboard can be deleted"""
144+
dashboard = self._create_dashboard(admin_account)
145+
url = reverse("delete-dashboard", args=(dashboard.pk,))
146+
response = client.post(
147+
url,
148+
data={"confirm_delete": "true"},
149+
follow=True,
150+
)
151+
152+
assert response.status_code == 200
153+
assert not AccountDashboard.objects.filter(id=dashboard.id).exists()
154+
155+
def test_given_last_dashboard_of_account_then_render_error_message(
156+
self, db, client, admin_account
157+
):
158+
"""Tests that the last dashboard cannot be deleted"""
113159

114-
assert response.status_code == 400
115-
assert "Cannot delete default dashboard" in smart_str(response.content)
116-
assert AccountDashboard.objects.filter(id=default_dashboard.id).exists()
160+
# Ensure only one dashboard exists for the account
161+
AccountDashboard.objects.filter(account=admin_account).delete()
162+
dashboard = self._create_dashboard(admin_account, name="last_dashboard")
163+
164+
url = reverse("delete-dashboard", args=(dashboard.pk,))
165+
response = client.post(
166+
url,
167+
)
168+
169+
assert response.status_code == 200
170+
assert "Cannot delete last dashboard" in smart_str(response.content)
171+
assert AccountDashboard.objects.filter(id=dashboard.id).exists()
172+
173+
def test_given_default_dashboard_of_account_then_render_error_message(
174+
self, db, client, admin_account
175+
):
176+
"""Tests that the default dashboard cannot be deleted"""
177+
# Ensure at least one non-default dashboard exists
178+
self._create_dashboard(admin_account, name="non_default_dashboard")
179+
default_dashboard = AccountDashboard.objects.get(
180+
is_default=True, account=admin_account
181+
)
182+
url = reverse("delete-dashboard", args=(default_dashboard.pk,))
183+
response = client.post(url)
184+
185+
assert response.status_code == 200
186+
assert "Cannot delete default dashboard" in smart_str(response.content)
187+
assert AccountDashboard.objects.filter(id=default_dashboard.id).exists()
188+
189+
@staticmethod
190+
def _create_dashboard(
191+
account, name="to_be_deleted", is_default=False, is_shared=False
192+
):
193+
return AccountDashboard.objects.create(
194+
name=name,
195+
is_default=is_default,
196+
account=account,
197+
is_shared=is_shared,
198+
)
117199

118200

119201
def test_when_logging_in_it_should_change_the_session_id(

0 commit comments

Comments
 (0)