From 69d688c36a35a92d3320c3adfed7fcd480af8ab5 Mon Sep 17 00:00:00 2001 From: Paul Schilling Date: Fri, 10 Nov 2023 17:03:27 +0100 Subject: [PATCH] [#1830] Prevent deletion of last website --- src/open_inwoner/configurations/admin.py | 45 ++++++++- .../configurations/tests/test_admin.py | 94 +++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/src/open_inwoner/configurations/admin.py b/src/open_inwoner/configurations/admin.py index 57122b7feb..89edf33db2 100644 --- a/src/open_inwoner/configurations/admin.py +++ b/src/open_inwoner/configurations/admin.py @@ -1,12 +1,17 @@ from django import forms from django.contrib import admin, messages +from django.contrib.admin.actions import delete_selected as default_delete_selected from django.contrib.flatpages.admin import FlatPageAdmin from django.contrib.flatpages.forms import FlatpageForm from django.contrib.flatpages.models import FlatPage +from django.contrib.sites.admin import SiteAdmin +from django.contrib.sites.models import Site from django.core import exceptions from django.core.validators import URLValidator from django.forms import ValidationError -from django.urls import resolve +from django.http import HttpResponseRedirect +from django.shortcuts import redirect +from django.urls import resolve, reverse from django.urls.exceptions import Resolver404 from django.utils.html import format_html, format_html_join from django.utils.translation import ugettext_lazy as _ @@ -23,6 +28,44 @@ from .models import SiteConfiguration, SiteConfigurationPage +@admin.action(description=_("Delete selected websites")) +def delete_selected(modeladmin, request, queryset): + """ + Override `delete_selected` action to prevent accidental deletion of all websites + """ + if queryset.count() == Site.objects.all().count(): + messages.add_message( + request, + messages.WARNING, + _("You cannot delete all websites; at least one site must remain."), + ) + return HttpResponseRedirect(reverse("admin:sites_site_changelist")) + return default_delete_selected(modeladmin, request, queryset) + + +class CustomSiteAdmin(SiteAdmin): + """ + Overrides `SiteAdmin` to prevent deletion of last website + """ + + model = Site + actions = [delete_selected] + + def delete_model(self, request, obj): + if self.model.objects.count() < 2: + messages.add_message( + request, messages.WARNING, _("You cannot delete the last site.") + ) + return redirect("admin:sites_site_changelist") + else: + super().delete_model(request, obj) + + +# re-register `Site` with our CustomSiteAdmin +admin.site.unregister(Site) +admin.site.register(Site, CustomSiteAdmin) + + class SiteConfigurationPageInline(OrderedTabularInline): model = SiteConfigurationPage fields = ( diff --git a/src/open_inwoner/configurations/tests/test_admin.py b/src/open_inwoner/configurations/tests/test_admin.py index dcc4f410ae..d12b7ddc61 100644 --- a/src/open_inwoner/configurations/tests/test_admin.py +++ b/src/open_inwoner/configurations/tests/test_admin.py @@ -1,13 +1,107 @@ +from django.contrib.sites.models import Site from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from django_webtest import WebTest +from pyquery import PyQuery from open_inwoner.accounts.tests.factories import UserFactory from ..models import SiteConfiguration +class TestAdminSite(WebTest): + csrf_checks = False + + @classmethod + def setUpTestData(cls): + cls.user = UserFactory(is_superuser=True, is_staff=True) + + def test_delete_site_success(self): + site2 = Site.objects.create(id=2, domain="example2.com", name="example2") + + # delete site + response = self.app.get( + reverse("admin:sites_site_delete", kwargs={"object_id": site2.id}), + user=self.user, + ) + response = response.form.submit().follow() + + # check sites + sites = Site.objects.all() + self.assertEqual(len(sites), 1) + + # check message + doc = PyQuery(response.content) + + success_msg = doc.find(".success").text() + self.assertIn("met succes verwijderd.", success_msg) + + def test_delete_last_site_fail(self): + # attempt to delete site + response = self.app.get( + reverse("admin:sites_site_delete", kwargs={"object_id": 1}), user=self.user + ) + response = response.form.submit().follow() + + # check sites + sites = Site.objects.all() + self.assertEqual(len(sites), 1) + + # check message + doc = PyQuery(response.content) + + warning_msg = doc.find(".warning").text() + self.assertEqual(warning_msg, "You cannot delete the last site.") + + def test_bulk_delete_success(self): + site2 = Site.objects.create(id=2, domain="example2.com", name="example2") + site3 = Site.objects.create(id=3, domain="example3.com", name="example3") + + # delete sites + data = {"action": "delete_selected", "_selected_action": [site2.pk, site3.pk]} + response = self.app.post( + reverse("admin:sites_site_changelist"), data, user=self.user + ) + response = response.form.submit().follow() # confirmation form + + # check sites + sites = Site.objects.all() + self.assertEqual(len(sites), 1) + + # check message + doc = PyQuery(response.content) + + success_msg = doc.find(".success").text() + self.assertEqual(success_msg, "2 websites met succes verwijderd.") + + def test_bulk_delete_fail(self): + Site.objects.create(id=2, domain="example2.com", name="example2") + Site.objects.create(id=3, domain="example3.com", name="example3") + + # attempt to delete sites + data = { + "action": "delete_selected", + "_selected_action": [site.pk for site in Site.objects.all()], + } + response = self.app.post( + reverse("admin:sites_site_changelist"), data, user=self.user + ).follow() + + # check sites + sites = Site.objects.all() + self.assertEqual(len(sites), 3) + + # check message + doc = PyQuery(response.content) + + warning_msg = doc.find(".warning").text() + self.assertEqual( + warning_msg, + "You cannot delete all websites; at least one site must remain.", + ) + + class TestAdminForm(WebTest): def setUp(self): self.user = UserFactory(is_superuser=True, is_staff=True)