summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBecky Smith <rebkwok@gmail.com>2017-03-30 10:13:15 +0100
committerTim Graham <timograham@gmail.com>2018-02-24 20:41:43 -0500
commit8116e588dbdd17171a322da589b691bdf01f7041 (patch)
treec46f319b831339ec9ee665e850f89bc3f3a169e0
parent9822d88ca0dd855a668f9c33447aeac45a9768ac (diff)
Fixed #17962 -- Added ModelAdmin.get_deleted_objects().
-rw-r--r--AUTHORS1
-rw-r--r--django/contrib/admin/actions.py6
-rw-r--r--django/contrib/admin/options.py11
-rw-r--r--docs/ref/contrib/admin/index.txt30
-rw-r--r--docs/releases/2.1.txt3
-rw-r--r--tests/admin_views/customadmin.py6
-rw-r--r--tests/admin_views/test_actions.py14
-rw-r--r--tests/admin_views/tests.py7
-rw-r--r--tests/modeladmin/tests.py10
9 files changed, 80 insertions, 8 deletions
diff --git a/AUTHORS b/AUTHORS
index fb32b97887..2a2ef3ab3d 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -677,6 +677,7 @@ answer newbie questions, and generally made Django that much better:
Raphaël Barrois <raphael.barrois@m4x.org>
Raphael Michel <mail@raphaelmichel.de>
Raúl Cumplido <raulcumplido@gmail.com>
+ Rebecca Smith <rebkwok@gmail.com>
Remco Wendt <remco.wendt@gmail.com>
Renaud Parent <renaud.parent@gmail.com>
Renbi Yu <averybigant@gmail.com>
diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py
index d4bc3daf85..f64b89205e 100644
--- a/django/contrib/admin/actions.py
+++ b/django/contrib/admin/actions.py
@@ -4,7 +4,7 @@ Built-in, globally-available admin actions.
from django.contrib import messages
from django.contrib.admin import helpers
-from django.contrib.admin.utils import get_deleted_objects, model_ngettext
+from django.contrib.admin.utils import model_ngettext
from django.core.exceptions import PermissionDenied
from django.template.response import TemplateResponse
from django.utils.translation import gettext as _, gettext_lazy
@@ -29,9 +29,7 @@ def delete_selected(modeladmin, request, queryset):
# Populate deletable_objects, a data structure of all related objects that
# will also be deleted.
- deletable_objects, model_count, perms_needed, protected = get_deleted_objects(
- queryset, request.user, modeladmin.admin_site,
- )
+ deletable_objects, model_count, perms_needed, protected = modeladmin.get_deleted_objects(queryset, request)
# The user has already confirmed the deletion.
# Do the deletion and return None to display the change list view again.
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index f9f5f85ef3..bbc402bd74 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1728,6 +1728,13 @@ class ModelAdmin(BaseModelAdmin):
'admin/change_list.html'
], context)
+ def get_deleted_objects(self, objs, request):
+ """
+ Hook for customizing the delete process for the delete view and the
+ "delete selected" action.
+ """
+ return get_deleted_objects(objs, request.user, self.admin_site)
+
@csrf_protect_m
def delete_view(self, request, object_id, extra_context=None):
with transaction.atomic(using=router.db_for_write(self.model)):
@@ -1752,9 +1759,7 @@ class ModelAdmin(BaseModelAdmin):
# Populate deleted_objects, a data structure of all related objects that
# will also be deleted.
- deleted_objects, model_count, perms_needed, protected = get_deleted_objects(
- [obj], request.user, self.admin_site,
- )
+ deleted_objects, model_count, perms_needed, protected = self.get_deleted_objects([obj], request)
if request.POST and not protected: # The user has confirmed the deletion.
if perms_needed:
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index d586513c9a..d1138076ff 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -1998,6 +1998,36 @@ templates used by the :class:`ModelAdmin` views:
def get_changeform_initial_data(self, request):
return {'name': 'custom_initial_value'}
+.. method:: ModelAdmin.get_deleted_objects(objs, request)
+
+ .. versionadded:: 2.1
+
+ A hook for customizing the deletion process of the :meth:`delete_view` and
+ the "delete selected" :doc:`action <actions>`.
+
+ The ``objs`` argument is a homogeneous iterable of objects (a ``QuerySet``
+ or a list of model instances) to be deleted, and ``request`` is the
+ :class:`~django.http.HttpRequest`.
+
+ This method must return a 4-tuple of
+ ``(deleted_objects, model_count, perms_needed, protected)``.
+
+ ``deleted_objects`` is a list of strings representing all the objects that
+ will be deleted. If there are any related objects to be deleted, the list
+ is nested and includes those related objects. The list is formatted in the
+ template using the :tfilter:`unordered_list` filter.
+
+ ``model_count`` is a dictionary mapping each model's
+ :attr:`~django.db.models.Options.verbose_name_plural` to the number of
+ objects that will be deleted.
+
+ ``perms_needed`` is a set of :attr:`~django.db.models.Options.verbose_name`\s
+ of the models that the user doesn't have permission to delete.
+
+ ``protected`` is a list of strings representing of all the protected
+ related objects that can't be deleted. The list is displayed in the
+ template.
+
Other methods
~~~~~~~~~~~~~
diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt
index 0963862f17..0a7d6da6cb 100644
--- a/docs/releases/2.1.txt
+++ b/docs/releases/2.1.txt
@@ -50,6 +50,9 @@ Minor features
* The ``admin_order_field`` attribute for elements in
:attr:`.ModelAdmin.list_display` may now be a query expression.
+* The new :meth:`.ModelAdmin.get_deleted_objects()` method allows customizing
+ the deletion process of the delete view and the "delete selected" action.
+
:mod:`django.contrib.admindocs`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/admin_views/customadmin.py b/tests/admin_views/customadmin.py
index 5ee8c0c159..9331918b37 100644
--- a/tests/admin_views/customadmin.py
+++ b/tests/admin_views/customadmin.py
@@ -46,9 +46,15 @@ class CustomPwdTemplateUserAdmin(UserAdmin):
change_user_password_template = ['admin/auth/user/change_password.html'] # a list, to test fix for #18697
+class BookAdmin(admin.ModelAdmin):
+ def get_deleted_objects(self, objs, request):
+ return ['a deletable object'], {'books': 1}, set(), []
+
+
site = Admin2(name="admin2")
site.register(models.Article, base_admin.ArticleAdmin)
+site.register(models.Book, BookAdmin)
site.register(models.Section, inlines=[base_admin.ArticleInline], search_fields=['name'])
site.register(models.Thing, base_admin.ThingAdmin)
site.register(models.Fabric, base_admin.FabricAdmin)
diff --git a/tests/admin_views/test_actions.py b/tests/admin_views/test_actions.py
index 4823316e3e..423c2f927f 100644
--- a/tests/admin_views/test_actions.py
+++ b/tests/admin_views/test_actions.py
@@ -12,7 +12,7 @@ from django.urls import reverse
from .admin import SubscriberAdmin
from .forms import MediaActionForm
from .models import (
- Actor, Answer, ExternalSubscriber, Question, Subscriber,
+ Actor, Answer, Book, ExternalSubscriber, Question, Subscriber,
UnchangeableObject,
)
@@ -153,6 +153,18 @@ class AdminActionsTest(TestCase):
self.assertIs(SubscriberAdmin.overridden, True)
self.assertEqual(Subscriber.objects.all().count(), 0)
+ def test_delete_selected_uses_get_deleted_objects(self):
+ """The delete_selected action uses ModelAdmin.get_deleted_objects()."""
+ book = Book.objects.create(name='Test Book')
+ data = {
+ ACTION_CHECKBOX_NAME: [book.pk],
+ 'action': 'delete_selected',
+ 'index': 0,
+ }
+ response = self.client.post(reverse('admin2:admin_views_book_changelist'), data)
+ # BookAdmin.get_deleted_objects() returns custom text.
+ self.assertContains(response, 'a deletable object')
+
def test_custom_function_mail_action(self):
"""A custom action may be defined in a function."""
action_data = {
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 011bb2897a..3985e16b47 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -2357,6 +2357,13 @@ class AdminViewDeletedObjectsTest(TestCase):
response = self.client.get(reverse('admin:admin_views_bookmark_delete', args=(bookmark.pk,)))
self.assertContains(response, should_contain)
+ def test_delete_view_uses_get_deleted_objects(self):
+ """The delete view uses ModelAdmin.get_deleted_objects()."""
+ book = Book.objects.create(name='Test Book')
+ response = self.client.get(reverse('admin2:admin_views_book_delete', args=(book.pk,)))
+ # BookAdmin.get_deleted_objects() returns custom text.
+ self.assertContains(response, 'a deletable object')
+
@override_settings(ROOT_URLCONF='admin_views.urls')
class TestGenericRelations(TestCase):
diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py
index 202929df05..81c28cb0ff 100644
--- a/tests/modeladmin/tests.py
+++ b/tests/modeladmin/tests.py
@@ -666,6 +666,16 @@ class ModelAdminTests(TestCase):
finally:
self.site.unregister(Band)
+ def test_get_deleted_objects(self):
+ mock_request = MockRequest()
+ mock_request.user = User.objects.create_superuser(username='bob', email='bob@test.com', password='test')
+ ma = ModelAdmin(Band, self.site)
+ deletable_objects, model_count, perms_needed, protected = ma.get_deleted_objects([self.band], request)
+ self.assertEqual(deletable_objects, ['Band: The Doors'])
+ self.assertEqual(model_count, {'bands': 1})
+ self.assertEqual(perms_needed, set())
+ self.assertEqual(protected, [])
+
class ModelAdminPermissionTests(SimpleTestCase):