Fixed #21936 -- Refactored DeleteView to be compatible with SuccessMessa... #2585

Closed
wants to merge 1 commit into
from
Jump to file or symbol
Failed to load files and symbols.
+184 −16
Diff settings

Always

Just for now

@@ -0,0 +1,2 @@
+# This file is required to pretend messages has models.
+# Otherwise test models cannot be registered.
@@ -0,0 +1,13 @@
+from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
+
+
+@python_2_unicode_compatible
+class SomeObject(models.Model):
+ name = models.CharField(max_length=255)
+
+ class Meta:
+ app_label = "messages"
+
+ def __str__(self):
+ return self.name
@@ -1,4 +1,7 @@
-from django.contrib.messages.tests.urls import ContactFormViewWithMsg
+from django.contrib.messages.tests.models import SomeObject
+from django.contrib.messages.tests.urls import (
+ ContactFormViewWithMsg, DeleteFormViewWithMsg,
+)
from django.core.urlresolvers import reverse
from django.test import TestCase, override_settings
@@ -13,3 +16,11 @@ def test_set_messages_success(self):
req = self.client.post(add_url, author)
self.assertIn(ContactFormViewWithMsg.success_message % author,
req.cookies['messages'].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])
+ req = self.client.delete(delete_url)
+ self.assertIn(DeleteFormViewWithMsg.success_message,
+ req.cookies['messages'].value)
@@ -1,13 +1,14 @@
from django import forms
from django.conf.urls import url
from django.contrib import messages
+from django.contrib.messages.tests.models import SomeObject
from django.contrib.messages.views import SuccessMessageMixin
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect
from django.template import engines
from django.template.response import TemplateResponse
from django.views.decorators.cache import never_cache
-from django.views.generic.edit import FormView
+from django.views.generic.edit import DeleteView, FormView
TEMPLATE = """{% if messages %}
@@ -66,13 +67,20 @@ class ContactForm(forms.Form):
class ContactFormViewWithMsg(SuccessMessageMixin, FormView):
form_class = ContactForm
- success_url = show
+ success_url = "/show/"
success_message = "%(name)s was created successfully"
+class DeleteFormViewWithMsg(SuccessMessageMixin, DeleteView):
+ model = SomeObject
+ success_url = "/show/"
+ success_message = "Object was deleted successfully"
+
+
urlpatterns = [
url('^add/(debug|info|success|warning|error)/$', add, name='add_message'),
url('^add/msg/$', ContactFormViewWithMsg.as_view(), name='add_success_msg'),
+ url('^delete/msg/(?P<pk>\d+)$', DeleteFormViewWithMsg.as_view(), name='success_msg_on_delete'),
url('^show/$', show, name='show_message'),
url('^template_response/add/(debug|info|success|warning|error)/$',
add_template_response, name='add_template_response'),
@@ -3,14 +3,14 @@
import warnings
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.utils import six
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_text
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
from django.views.generic.detail import (
- BaseDetailView, SingleObjectMixin, SingleObjectTemplateResponseMixin,
+ SingleObjectMixin, SingleObjectTemplateResponseMixin,
)
PERCENT_PLACEHOLDER_REGEX = re.compile(r'%\([^\)]+\)') # RemovedInDjango20Warning
@@ -82,7 +82,7 @@ def get_form_kwargs(self):
'prefix': self.get_prefix(),
}
- if self.request.method in ('POST', 'PUT'):
+ if self.request.method in ('POST', 'PUT', 'DELETE'):
kwargs.update({
'data': self.request.POST,
'files': self.request.FILES,
@@ -286,21 +286,27 @@ class DeletionMixin(object):
"""
success_url = None
+ def delete_object(self):
+ success_url = self.get_success_url()
+ self.object.delete()
+ return HttpResponseRedirect(success_url)
+
def delete(self, request, *args, **kwargs):
"""
Calls the delete() method on the fetched object and then
redirects to the success URL.
"""
self.object = self.get_object()
- success_url = self.get_success_url()
- self.object.delete()
- return HttpResponseRedirect(success_url)
+ return self.delete_object()
# Add support for browsers which only accept GET and POST for now.
def post(self, request, *args, **kwargs):
return self.delete(request, *args, **kwargs)
def get_success_url(self):
+ """
+ Returns the supplied success URL.
+ """
if self.success_url:
# force_text can be removed with deprecation warning
self.success_url = force_text(self.success_url)
@@ -318,12 +324,25 @@ def get_success_url(self):
"No URL to redirect to. Provide a success_url.")
-class BaseDeleteView(DeletionMixin, BaseDetailView):
+class BaseDeleteView(DeletionMixin, FormMixin, SingleObjectMixin, ProcessFormView):
"""
Base view for deleting an object.
Using this base class requires subclassing to provide a response mixin.
"""
+ form_class = Form
+
+ def get(self, request, *args, **kwargs):
+ self.object = self.get_object()
+ return super(BaseDeleteView, self).get(request, *args, **kwargs)
+
+ def delete(self, request, *args, **kwargs):
+ self.object = self.get_object()
+ # Process form instead of directly deleting the object.
+ return super(DeletionMixin, self).post(request, *args, **kwargs)
+
+ def form_valid(self, form):
+ return self.delete_object()
class DeleteView(SingleObjectTemplateResponseMixin, BaseDeleteView):
@@ -39,6 +39,7 @@ FormView
* :class:`django.views.generic.base.TemplateResponseMixin`
* ``django.views.generic.edit.BaseFormView``
* :class:`django.views.generic.edit.FormMixin`
+ * :class:`django.views.generic.base.ContextMixin`
* :class:`django.views.generic.edit.ProcessFormView`
* :class:`django.views.generic.base.View`
@@ -97,6 +98,7 @@ CreateView
* ``django.views.generic.edit.BaseCreateView``
* :class:`django.views.generic.edit.ModelFormMixin`
* :class:`django.views.generic.edit.FormMixin`
+ * :class:`django.views.generic.base.ContextMixin`
* :class:`django.views.generic.detail.SingleObjectMixin`
* :class:`django.views.generic.edit.ProcessFormView`
* :class:`django.views.generic.base.View`
@@ -154,6 +156,7 @@ UpdateView
* ``django.views.generic.edit.BaseUpdateView``
* :class:`django.views.generic.edit.ModelFormMixin`
* :class:`django.views.generic.edit.FormMixin`
+ * :class:`django.views.generic.base.ContextMixin`
* :class:`django.views.generic.detail.SingleObjectMixin`
* :class:`django.views.generic.edit.ProcessFormView`
* :class:`django.views.generic.base.View`
@@ -198,9 +201,9 @@ DeleteView
.. class:: django.views.generic.edit.DeleteView
A view that displays a confirmation page and deletes an existing object.
- The given object will only be deleted if the request method is ``POST``. If

This comment has been minimized.

@timgraham

timgraham Apr 18, 2014

Member

need a versionchanged annotation here with a brief description of the change

@timgraham

timgraham Apr 18, 2014

Member

need a versionchanged annotation here with a brief description of the change

This comment has been minimized.

@cpsimpson

cpsimpson Apr 18, 2014

Contributor

I haven't updated this documentation yet, while I find out whether we should be using the DELETE method, or if this should revert to the original.

@cpsimpson

cpsimpson Apr 18, 2014

Contributor

I haven't updated this documentation yet, while I find out whether we should be using the DELETE method, or if this should revert to the original.

- this view is fetched via ``GET``, it will display a confirmation page that
- should contain a form that POSTs to the same URL.
+ The given object will only be deleted if the request method is ``POST`` or
+ ``DELETE``. If this view is fetched via ``GET``, it will display a
+ confirmation page that should contain a form that POSTs to the same URL.
**Ancestors (MRO)**
@@ -210,8 +213,11 @@ DeleteView
* :class:`django.views.generic.base.TemplateResponseMixin`
* ``django.views.generic.edit.BaseDeleteView``
* :class:`django.views.generic.edit.DeletionMixin`
- * ``django.views.generic.detail.BaseDetailView``
+ * :class:`django.views.generic.edit.ModelFormMixin`
+ * :class:`django.views.generic.edit.FormMixin`
+ * :class:`django.views.generic.base.ContextMixin`
* :class:`django.views.generic.detail.SingleObjectMixin`
+ * :class:`django.views.generic.edit.ProcessFormView`
* :class:`django.views.generic.base.View`
**Attributes**
@@ -352,7 +352,9 @@ Adding messages in Class Based Views
.. class:: views.SuccessMessageMixin
Adds a success message attribute to
- :class:`~django.views.generic.edit.FormView` based classes
+ :class:`~django.views.generic.edit.FormView` based classes (for example
+ :class:`~django.views.generic.edit.CreateView`, :class:`~django.views.generic.edit.UpdateView`,
+ :class:`~django.views.generic.edit.DeleteView`)
.. method:: get_success_message(cleaned_data)
View
@@ -114,6 +114,11 @@ Generic Views
* Class based views generated using ``as_view()`` now have ``view_class``
and ``view_initkwargs`` attributes.
+* :class:`~django.views.generic.edit.DeleteView` now inherits from
+ :class:`~django.views.generic.edit.ProcessFormView`. This allows compatibility
+ with features like the :class:`~django.contrib.messages.views.SuccessMessageMixin`
+ from :mod:`~django.contrib.messages`.
+
Internationalization
^^^^^^^^^^^^^^^^^^^^
@@ -426,3 +426,83 @@ def test_delete_without_redirect(self):
# get_absolute_url provided
with self.assertRaises(ImproperlyConfigured):
self.client.post('/edit/author/%d/delete/naive/' % a.pk)
+
+ def test_delete_with_form_as_post(self):
+ a = Author.objects.create(
+ name='Randall Munroe',
+ slug='randall-munroe',
+ )
+
+ res = self.client.get('/edit/author/%d/delete/form/' % a.pk)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Author.objects.get(pk=a.pk))
+ self.assertEqual(res.context['author'], Author.objects.get(pk=a.pk))
+ self.assertTemplateUsed(res, 'generic_views/author_confirm_delete.html')
+
+ res = self.client.post('/edit/author/%d/delete/form/' % a.pk,
+ data={"confirm": True})
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/list/authors/')
+ self.assertQuerysetEqual(Author.objects.all(), [])
+
+ def test_delete_with_form_as_post_with_validation_error(self):
+ a = Author.objects.create(
+ name='Randall Munroe',
+ slug='randall-munroe',
+ )
+
+ res = self.client.get('/edit/author/%d/delete/form/' % a.pk)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Author.objects.get(pk=a.pk))
+ self.assertEqual(res.context['author'], Author.objects.get(pk=a.pk))
+ self.assertTemplateUsed(res, 'generic_views/author_confirm_delete.html')
+
+ res = self.client.post('/edit/author/%d/delete/form/' % a.pk)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(len(res.context_data["form"].errors), 2)
+ self.assertEqual(res.context_data["form"].errors["__all__"],
+ [u'You must confirm the delete.'])
+ self.assertEqual(res.context_data["form"].errors["confirm"],
+ [u'This field is required.'])
+
+ def test_delete_with_form_as_delete(self):
+ a = Author.objects.create(
+ name='Randall Munroe',
+ slug='randall-munroe',
+ )
+
+ res = self.client.get('/edit/author/%d/delete/form/' % a.pk)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Author.objects.get(pk=a.pk))
+ self.assertEqual(res.context['author'], Author.objects.get(pk=a.pk))
+ self.assertTemplateUsed(res, 'generic_views/author_confirm_delete.html')
+
+ res = self.client.delete('/edit/author/%d/delete/form/' % a.pk,
+ data={"confirm": True})
+ # data cannot be sent with a delete method.
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(len(res.context_data["form"].errors), 2)
+ self.assertEqual(res.context_data["form"].errors["__all__"],
+ [u'You must confirm the delete.'])
+ self.assertEqual(res.context_data["form"].errors["confirm"],
+ [u'This field is required.'])
+
+ def test_delete_with_form_as_delete_with_validation_error(self):
+ a = Author.objects.create(
+ name='Randall Munroe',
+ slug='randall-munroe',
+ )
+
+ res = self.client.get('/edit/author/%d/delete/form/' % a.pk)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Author.objects.get(pk=a.pk))
+ self.assertEqual(res.context['author'], Author.objects.get(pk=a.pk))
+ self.assertTemplateUsed(res, 'generic_views/author_confirm_delete.html')
+
+ res = self.client.delete('/edit/author/%d/delete/form/' % a.pk)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(len(res.context_data["form"].errors), 2)
+ self.assertEqual(res.context_data["form"].errors["__all__"],
+ [u'You must confirm the delete.'])
+ self.assertEqual(res.context_data["form"].errors["confirm"],
+ [u'This field is required.'])
@@ -17,3 +17,15 @@ class Meta:
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(ConfirmDeleteForm, self).clean()
+ delete_confirmed = cleaned_data.get("confirm")
+
+ # import pdb; pdb.set_trace()
+ if not delete_confirmed:
+ raise forms.ValidationError("You must confirm the delete.")
@@ -113,6 +113,8 @@
views.AuthorDelete.as_view()),
url(r'^edit/author/(?P<pk>[0-9]+)/delete/special/$',
views.SpecializedAuthorDelete.as_view()),
+ url(r'^edit/author/(?P<pk>\d+)/delete/form/$',
+ views.AuthorDeleteFormView.as_view()),
# ArchiveIndexView
url(r'^dates/books/$',
@@ -7,7 +7,7 @@
from django.views import generic
from .models import Artist, Author, Book, BookSigning, Page
-from .test_forms import AuthorForm, ContactForm
+from .test_forms import AuthorForm, ConfirmDeleteForm, ContactForm
class CustomTemplateView(generic.TemplateView):
@@ -175,6 +175,14 @@ class SpecializedAuthorDelete(generic.DeleteView):
success_url = reverse_lazy('authors_list')
+class AuthorDeleteFormView(generic.DeleteView):
+ model = Author
+ form_class = ConfirmDeleteForm
+
+ def get_success_url(self):
+ return reverse('authors_list')
+
+
class BookConfig(object):
queryset = Book.objects.all()
date_field = 'pubdate'