import json from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.admin.views.main import IS_POPUP_VAR from django.contrib.auth.models import Permission, User from django.core import mail from django.db import connection from django.template.loader import render_to_string from django.template.response import TemplateResponse from django.test import TestCase, override_settings from django.test.utils import CaptureQueriesContext from django.urls import reverse from .admin import SubscriberAdmin from .forms import MediaActionForm from .models import ( Actor, Answer, Book, ExternalSubscriber, Question, Subscriber, UnchangeableObject, ) @override_settings(ROOT_URLCONF="admin_views.urls") class AdminActionsTest(TestCase): @classmethod def setUpTestData(cls): cls.superuser = User.objects.create_superuser( username="super", password="secret", email="super@example.com" ) cls.s1 = ExternalSubscriber.objects.create( name="John Doe", email="john@example.org" ) cls.s2 = Subscriber.objects.create( name="Max Mustermann", email="max@example.org" ) def setUp(self): self.client.force_login(self.superuser) def test_model_admin_custom_action(self): """A custom action defined in a ModelAdmin method.""" action_data = { ACTION_CHECKBOX_NAME: [self.s1.pk], "action": "mail_admin", "index": 0, } self.client.post( reverse("admin:admin_views_subscriber_changelist"), action_data ) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, "Greetings from a ModelAdmin action") def test_model_admin_default_delete_action(self): action_data = { ACTION_CHECKBOX_NAME: [self.s1.pk, self.s2.pk], "action": "delete_selected", "index": 0, } delete_confirmation_data = { ACTION_CHECKBOX_NAME: [self.s1.pk, self.s2.pk], "action": "delete_selected", "post": "yes", } confirmation = self.client.post( reverse("admin:admin_views_subscriber_changelist"), action_data ) self.assertIsInstance(confirmation, TemplateResponse) self.assertContains( confirmation, "Are you sure you want to delete the selected subscribers?" ) self.assertContains(confirmation, "

Delete multiple objects

") self.assertContains(confirmation, "

Summary

") self.assertContains(confirmation, "
  • Subscribers: 2
  • ") self.assertContains(confirmation, "
  • External subscribers: 1
  • ") self.assertContains(confirmation, ACTION_CHECKBOX_NAME, count=2) with CaptureQueriesContext(connection) as ctx: self.client.post( reverse("admin:admin_views_subscriber_changelist"), delete_confirmation_data, ) # Log entries are inserted in bulk. self.assertEqual( len( [ q["sql"] for q in ctx.captured_queries if q["sql"].startswith("INSERT") ] ), 1, ) self.assertEqual(Subscriber.objects.count(), 0) def test_default_delete_action_nonexistent_pk(self): self.assertFalse(Subscriber.objects.filter(id=9998).exists()) action_data = { ACTION_CHECKBOX_NAME: ["9998"], "action": "delete_selected", "index": 0, } response = self.client.post( reverse("admin:admin_views_subscriber_changelist"), action_data ) self.assertContains( response, "Are you sure you want to delete the selected subscribers?" ) self.assertContains(response, "", html=True) @override_settings(USE_THOUSAND_SEPARATOR=True, NUMBER_GROUPING=3) def test_non_localized_pk(self): """ If USE_THOUSAND_SEPARATOR is set, the ids for the objects selected for deletion are rendered without separators. """ s = ExternalSubscriber.objects.create(id=9999) action_data = { ACTION_CHECKBOX_NAME: [s.pk, self.s2.pk], "action": "delete_selected", "index": 0, } response = self.client.post( reverse("admin:admin_views_subscriber_changelist"), action_data ) self.assertTemplateUsed(response, "admin/delete_selected_confirmation.html") self.assertContains(response, 'value="9999"') # Instead of 9,999 self.assertContains(response, 'value="%s"' % self.s2.pk) def test_model_admin_default_delete_action_protected(self): """ The default delete action where some related objects are protected from deletion. """ q1 = Question.objects.create(question="Why?") a1 = Answer.objects.create(question=q1, answer="Because.") a2 = Answer.objects.create(question=q1, answer="Yes.") q2 = Question.objects.create(question="Wherefore?") action_data = { ACTION_CHECKBOX_NAME: [q1.pk, q2.pk], "action": "delete_selected", "index": 0, } delete_confirmation_data = action_data.copy() delete_confirmation_data["post"] = "yes" response = self.client.post( reverse("admin:admin_views_question_changelist"), action_data ) self.assertContains( response, "would require deleting the following protected related objects" ) self.assertContains( response, '
  • Answer: Because.
  • ' % reverse("admin:admin_views_answer_change", args=(a1.pk,)), html=True, ) self.assertContains( response, '
  • Answer: Yes.
  • ' % reverse("admin:admin_views_answer_change", args=(a2.pk,)), html=True, ) # A POST request to delete protected objects displays the page which # says the deletion is prohibited. response = self.client.post( reverse("admin:admin_views_question_changelist"), delete_confirmation_data ) self.assertContains( response, "would require deleting the following protected related objects" ) self.assertEqual(Question.objects.count(), 2) def test_model_admin_default_delete_action_no_change_url(self): """ The default delete action doesn't break if a ModelAdmin removes the change_view URL (#20640). """ obj = UnchangeableObject.objects.create() action_data = { ACTION_CHECKBOX_NAME: obj.pk, "action": "delete_selected", "index": "0", } response = self.client.post( reverse("admin:admin_views_unchangeableobject_changelist"), action_data ) # No 500 caused by NoReverseMatch. The page doesn't display a link to # the nonexistent change page. self.assertContains( response, "
  • Unchangeable object: %s
  • " % obj, 1, html=True ) def test_delete_queryset_hook(self): delete_confirmation_data = { ACTION_CHECKBOX_NAME: [self.s1.pk, self.s2.pk], "action": "delete_selected", "post": "yes", "index": 0, } SubscriberAdmin.overridden = False self.client.post( reverse("admin:admin_views_subscriber_changelist"), delete_confirmation_data ) # SubscriberAdmin.delete_queryset() sets overridden to True. self.assertIs(SubscriberAdmin.overridden, True) self.assertEqual(Subscriber.objects.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 = { ACTION_CHECKBOX_NAME: [self.s1.pk], "action": "external_mail", "index": 0, } self.client.post( reverse("admin:admin_views_externalsubscriber_changelist"), action_data ) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, "Greetings from a function action") def test_custom_function_action_with_redirect(self): """Another custom action defined in a function.""" action_data = { ACTION_CHECKBOX_NAME: [self.s1.pk], "action": "redirect_to", "index": 0, } response = self.client.post( reverse("admin:admin_views_externalsubscriber_changelist"), action_data ) self.assertEqual(response.status_code, 302) def test_default_redirect(self): """ Actions which don't return an HttpResponse are redirected to the same page, retaining the querystring (which may contain changelist info). """ action_data = { ACTION_CHECKBOX_NAME: [self.s1.pk], "action": "external_mail", "index": 0, } url = reverse("admin:admin_views_externalsubscriber_changelist") + "?o=1" response = self.client.post(url, action_data) self.assertRedirects(response, url) def test_custom_function_action_streaming_response(self): """A custom action may return a StreamingHttpResponse.""" action_data = { ACTION_CHECKBOX_NAME: [self.s1.pk], "action": "download", "index": 0, } response = self.client.post( reverse("admin:admin_views_externalsubscriber_changelist"), action_data ) content = b"".join(list(response)) self.assertEqual(content, b"This is the content of the file") self.assertEqual(response.status_code, 200) def test_custom_function_action_no_perm_response(self): """A custom action may returns an HttpResponse with a 403 code.""" action_data = { ACTION_CHECKBOX_NAME: [self.s1.pk], "action": "no_perm", "index": 0, } response = self.client.post( reverse("admin:admin_views_externalsubscriber_changelist"), action_data ) self.assertEqual(response.status_code, 403) self.assertEqual(response.content, b"No permission to perform this action") def test_actions_ordering(self): """Actions are ordered as expected.""" response = self.client.get( reverse("admin:admin_views_externalsubscriber_changelist") ) self.assertContains( response, """