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:
@@ -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/