summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarlton Gibson <carlton.gibson@noumenal.es>2021-07-13 16:06:12 +0200
committerCarlton Gibson <carlton.gibson@noumenal.es>2021-07-14 09:47:03 +0200
commit3a45fea0832c5910acee6e0d29f230f347a50462 (patch)
tree7d273f954660f9489b03cd0b8ea6c6f202cc9122
parent37e8367c359cd115f109d82f99ff32be219f4928 (diff)
Fixed #21936 -- Allowed DeleteView to work with custom Forms and SuccessMessageMixin.
Thanks to Mariusz Felisiak for review. Co-authored-by: Demetris Stavrou <demestav@gmail.com> Co-authored-by: Caroline Simpson <github@hoojiboo.com>
-rw-r--r--django/views/generic/edit.py22
-rw-r--r--docs/ref/class-based-views/generic-editing.txt21
-rw-r--r--docs/releases/4.0.txt6
-rw-r--r--tests/generic_views/forms.py9
-rw-r--r--tests/generic_views/test_edit.py32
-rw-r--r--tests/generic_views/urls.py1
-rw-r--r--tests/generic_views/views.py10
-rw-r--r--tests/messages_tests/models.py5
-rw-r--r--tests/messages_tests/test_mixins.py13
-rw-r--r--tests/messages_tests/urls.py11
10 files changed, 122 insertions, 8 deletions
diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py
index ccfef9cbcd..d0de788c46 100644
--- a/django/views/generic/edit.py
+++ b/django/views/generic/edit.py
@@ -1,5 +1,5 @@
from django.core.exceptions import ImproperlyConfigured
-from django.forms import models as model_forms
+from django.forms import Form, models as model_forms
from django.http import HttpResponseRedirect
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
from django.views.generic.detail import (
@@ -225,12 +225,30 @@ class DeletionMixin:
"No URL to redirect to. Provide a success_url.")
-class BaseDeleteView(DeletionMixin, BaseDetailView):
+class BaseDeleteView(DeletionMixin, FormMixin, BaseDetailView):
"""
Base view for deleting an object.
Using this base class requires subclassing to provide a response mixin.
"""
+ form_class = Form
+
+ def post(self, request, *args, **kwargs):
+ # Set self.object before the usual form processing flow.
+ # Inlined because having DeletionMixin as the first base, for
+ # get_success_url(), makes leveraging super() with ProcessFormView
+ # overly complex.
+ self.object = self.get_object()
+ form = self.get_form()
+ if form.is_valid():
+ return self.form_valid(form)
+ else:
+ return self.form_invalid(form)
+
+ def form_valid(self, form):
+ success_url = self.get_success_url()
+ self.object.delete()
+ return HttpResponseRedirect(success_url)
class DeleteView(SingleObjectTemplateResponseMixin, BaseDeleteView):
diff --git a/docs/ref/class-based-views/generic-editing.txt b/docs/ref/class-based-views/generic-editing.txt
index cf0b7bc13c..cb03b531cc 100644
--- a/docs/ref/class-based-views/generic-editing.txt
+++ b/docs/ref/class-based-views/generic-editing.txt
@@ -275,12 +275,26 @@ editing content:
* :class:`django.views.generic.base.TemplateResponseMixin`
* :class:`django.views.generic.edit.BaseDeleteView`
* :class:`django.views.generic.edit.DeletionMixin`
+ * :class:`django.views.generic.edit.FormMixin`
+ * :class:`django.views.generic.base.ContextMixin`
* :class:`django.views.generic.detail.BaseDetailView`
* :class:`django.views.generic.detail.SingleObjectMixin`
* :class:`django.views.generic.base.View`
**Attributes**
+ .. attribute:: form_class
+
+ .. versionadded:: 4.0
+
+ Inherited from :class:`~django.views.generic.edit.BaseDeleteView`. The
+ form class that will be used to confirm the request. By default
+ :class:`django.forms.Form`, resulting in an empty form that is always
+ valid.
+
+ By providing your own ``Form`` subclass, you can add additional
+ requirements, such as a confirmation checkbox, for example.
+
.. attribute:: template_name_suffix
The ``DeleteView`` page displayed to a ``GET`` request uses a
@@ -305,6 +319,7 @@ editing content:
<form method="post">{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
+ {{ form }}
<input type="submit" value="Confirm">
</form>
@@ -319,4 +334,10 @@ editing content:
This view inherits methods and attributes from the following views:
* :class:`django.views.generic.edit.DeletionMixin`
+ * :class:`django.views.generic.edit.FormMixin`
* :class:`django.views.generic.detail.BaseDetailView`
+
+ .. versionchanged:: 4.0
+
+ In older versions, ``BaseDeleteView`` does not inherit from
+ ``FormMixin``.
diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt
index be59681fda..f0742db7bd 100644
--- a/docs/releases/4.0.txt
+++ b/docs/releases/4.0.txt
@@ -221,7 +221,11 @@ Forms
Generic Views
~~~~~~~~~~~~~
-* ...
+* :class:`~django.views.generic.edit.DeleteView` now uses
+ :class:`~django.views.generic.edit.FormMixin`, allowing you to provide a
+ :class:`~django.forms.Form` subclass, with a checkbox for example, to confirm
+ deletion. In addition, this allows ``DeleteView`` to function with
+ :class:`django.contrib.messages.views.SuccessMessageMixin`.
Internationalization
~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/generic_views/forms.py b/tests/generic_views/forms.py
index fd8106d1a8..7d07dbc576 100644
--- a/tests/generic_views/forms.py
+++ b/tests/generic_views/forms.py
@@ -15,3 +15,12 @@ class AuthorForm(forms.ModelForm):
class ContactForm(forms.Form):
name = forms.CharField()
message = forms.CharField(widget=forms.Textarea)
+
+
+class ConfirmDeleteForm(forms.Form):
+ confirm = forms.BooleanField()
+
+ def clean(self):
+ cleaned_data = super().clean()
+ if 'confirm' not in cleaned_data:
+ raise forms.ValidationError('You must confirm the delete.')
diff --git a/tests/generic_views/test_edit.py b/tests/generic_views/test_edit.py
index ca7c457124..d69ae6463e 100644
--- a/tests/generic_views/test_edit.py
+++ b/tests/generic_views/test_edit.py
@@ -394,3 +394,35 @@ class DeleteViewTests(TestCase):
msg = 'No URL to redirect to. Provide a success_url.'
with self.assertRaisesMessage(ImproperlyConfigured, msg):
self.client.post('/edit/author/%d/delete/naive/' % self.author.pk)
+
+ def test_delete_with_form_as_post(self):
+ res = self.client.get('/edit/author/%d/delete/form/' % self.author.pk)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], self.author)
+ self.assertEqual(res.context['author'], self.author)
+ self.assertTemplateUsed(res, 'generic_views/author_confirm_delete.html')
+ res = self.client.post(
+ '/edit/author/%d/delete/form/' % self.author.pk, data={'confirm': True}
+ )
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, '/list/authors/')
+ self.assertSequenceEqual(Author.objects.all(), [])
+
+ def test_delete_with_form_as_post_with_validation_error(self):
+ res = self.client.get('/edit/author/%d/delete/form/' % self.author.pk)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], self.author)
+ self.assertEqual(res.context['author'], self.author)
+ self.assertTemplateUsed(res, 'generic_views/author_confirm_delete.html')
+
+ res = self.client.post('/edit/author/%d/delete/form/' % self.author.pk)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(len(res.context_data['form'].errors), 2)
+ self.assertEqual(
+ res.context_data['form'].errors['__all__'],
+ ['You must confirm the delete.'],
+ )
+ self.assertEqual(
+ res.context_data['form'].errors['confirm'],
+ ['This field is required.'],
+ )
diff --git a/tests/generic_views/urls.py b/tests/generic_views/urls.py
index e5cb8380d2..940f82d5d0 100644
--- a/tests/generic_views/urls.py
+++ b/tests/generic_views/urls.py
@@ -101,6 +101,7 @@ urlpatterns = [
),
path('edit/author/<int:pk>/delete/', views.AuthorDelete.as_view()),
path('edit/author/<int:pk>/delete/special/', views.SpecializedAuthorDelete.as_view()),
+ path('edit/author/<int:pk>/delete/form/', views.AuthorDeleteFormView.as_view()),
# ArchiveIndexView
path('dates/books/', views.BookArchive.as_view()),
diff --git a/tests/generic_views/views.py b/tests/generic_views/views.py
index 02717333a6..8f3de97388 100644
--- a/tests/generic_views/views.py
+++ b/tests/generic_views/views.py
@@ -4,7 +4,7 @@ from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator
from django.views import generic
-from .forms import AuthorForm, ContactForm
+from .forms import AuthorForm, ConfirmDeleteForm, ContactForm
from .models import Artist, Author, Book, BookSigning, Page
@@ -179,6 +179,14 @@ class AuthorDelete(generic.DeleteView):
success_url = '/list/authors/'
+class AuthorDeleteFormView(generic.DeleteView):
+ model = Author
+ form_class = ConfirmDeleteForm
+
+ def get_success_url(self):
+ return reverse('authors_list')
+
+
class SpecializedAuthorDelete(generic.DeleteView):
queryset = Author.objects.all()
template_name = 'generic_views/confirm_delete.html'
diff --git a/tests/messages_tests/models.py b/tests/messages_tests/models.py
new file mode 100644
index 0000000000..8ee894894b
--- /dev/null
+++ b/tests/messages_tests/models.py
@@ -0,0 +1,5 @@
+from django.db import models
+
+
+class SomeObject(models.Model):
+ name = models.CharField(max_length=255)
diff --git a/tests/messages_tests/test_mixins.py b/tests/messages_tests/test_mixins.py
index 051ed82b6b..f4d0b26d28 100644
--- a/tests/messages_tests/test_mixins.py
+++ b/tests/messages_tests/test_mixins.py
@@ -1,12 +1,13 @@
from django.core.signing import b64_decode
-from django.test import SimpleTestCase, override_settings
+from django.test import TestCase, override_settings
from django.urls import reverse
-from .urls import ContactFormViewWithMsg
+from .models import SomeObject
+from .urls import ContactFormViewWithMsg, DeleteFormViewWithMsg
@override_settings(ROOT_URLCONF='messages_tests.urls')
-class SuccessMessageMixinTests(SimpleTestCase):
+class SuccessMessageMixinTests(TestCase):
def test_set_messages_success(self):
author = {'name': 'John Doe', 'slug': 'success-msg'}
@@ -17,3 +18,9 @@ class SuccessMessageMixinTests(SimpleTestCase):
req.cookies['messages'].value.split(":")[0].encode(),
).decode()
self.assertIn(ContactFormViewWithMsg.success_message % author, value)
+
+ def test_set_messages_success_on_delete(self):
+ object_to_delete = SomeObject.objects.create(name='MyObject')
+ delete_url = reverse('success_msg_on_delete', args=[object_to_delete.pk])
+ response = self.client.post(delete_url, follow=True)
+ self.assertContains(response, DeleteFormViewWithMsg.success_message)
diff --git a/tests/messages_tests/urls.py b/tests/messages_tests/urls.py
index 433a249bb8..983cb33d72 100644
--- a/tests/messages_tests/urls.py
+++ b/tests/messages_tests/urls.py
@@ -6,7 +6,9 @@ from django.template import engines
from django.template.response import TemplateResponse
from django.urls import path, re_path, reverse
from django.views.decorators.cache import never_cache
-from django.views.generic.edit import FormView
+from django.views.generic.edit import DeleteView, FormView
+
+from .models import SomeObject
TEMPLATE = """{% if messages %}
<ul class="messages">
@@ -63,9 +65,16 @@ class ContactFormViewWithMsg(SuccessMessageMixin, FormView):
success_message = "%(name)s was created successfully"
+class DeleteFormViewWithMsg(SuccessMessageMixin, DeleteView):
+ model = SomeObject
+ success_url = '/show/'
+ success_message = 'Object was deleted successfully'
+
+
urlpatterns = [
re_path('^add/(debug|info|success|warning|error)/$', add, name='add_message'),
path('add/msg/', ContactFormViewWithMsg.as_view(), name='add_success_msg'),
+ path('delete/msg/<int:pk>', DeleteFormViewWithMsg.as_view(), name='success_msg_on_delete'),
path('show/', show, name='show_message'),
re_path(
'^template_response/add/(debug|info|success|warning|error)/$',