123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531 |
- 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.template.loader import render_to_string
- from django.template.response import TemplateResponse
- from django.test import TestCase, override_settings
- 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, "<h2>Summary</h2>")
- self.assertContains(confirmation, "<li>Subscribers: 2</li>")
- self.assertContains(confirmation, "<li>External subscribers: 1</li>")
- self.assertContains(confirmation, ACTION_CHECKBOX_NAME, count=2)
- self.client.post(
- reverse("admin:admin_views_subscriber_changelist"), delete_confirmation_data
- )
- 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, "<ul></ul>", 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,
- '<li>Answer: <a href="%s">Because.</a></li>'
- % reverse("admin:admin_views_answer_change", args=(a1.pk,)),
- html=True,
- )
- self.assertContains(
- response,
- '<li>Answer: <a href="%s">Yes.</a></li>'
- % 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, "<li>Unchangeable object: %s</li>" % 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(response.streaming_content)
- 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,
- """<label>Action: <select name="action" required>
- <option value="" selected>---------</option>
- <option value="delete_selected">Delete selected external
- subscribers</option>
- <option value="redirect_to">Redirect to (Awesome action)</option>
- <option value="external_mail">External mail (Another awesome
- action)</option>
- <option value="download">Download subscription</option>
- <option value="no_perm">No permission to run</option>
- </select>""",
- html=True,
- )
- def test_model_without_action(self):
- """A ModelAdmin might not have any actions."""
- response = self.client.get(
- reverse("admin:admin_views_oldsubscriber_changelist")
- )
- self.assertIsNone(response.context["action_form"])
- self.assertNotContains(
- response,
- '<input type="checkbox" class="action-select"',
- msg_prefix="Found an unexpected action toggle checkboxbox in response",
- )
- self.assertNotContains(response, '<input type="checkbox" class="action-select"')
- def test_model_without_action_still_has_jquery(self):
- """
- A ModelAdmin without any actions still has jQuery included on the page.
- """
- response = self.client.get(
- reverse("admin:admin_views_oldsubscriber_changelist")
- )
- self.assertIsNone(response.context["action_form"])
- self.assertContains(
- response,
- "jquery.min.js",
- msg_prefix=(
- "jQuery missing from admin pages for model with no admin actions"
- ),
- )
- def test_action_column_class(self):
- """The checkbox column class is present in the response."""
- response = self.client.get(reverse("admin:admin_views_subscriber_changelist"))
- self.assertIsNotNone(response.context["action_form"])
- self.assertContains(response, "action-checkbox-column")
- def test_multiple_actions_form(self):
- """
- Actions come from the form whose submit button was pressed (#10618).
- """
- action_data = {
- ACTION_CHECKBOX_NAME: [self.s1.pk],
- # Two different actions selected on the two forms...
- "action": ["external_mail", "delete_selected"],
- # ...but "go" was clicked on the top form.
- "index": 0,
- }
- self.client.post(
- reverse("admin:admin_views_externalsubscriber_changelist"), action_data
- )
- # The action sends mail rather than deletes.
- self.assertEqual(len(mail.outbox), 1)
- self.assertEqual(mail.outbox[0].subject, "Greetings from a function action")
- def test_media_from_actions_form(self):
- """
- The action form's media is included in the changelist view's media.
- """
- response = self.client.get(reverse("admin:admin_views_subscriber_changelist"))
- media_path = MediaActionForm.Media.js[0]
- self.assertIsInstance(response.context["action_form"], MediaActionForm)
- self.assertIn("media", response.context)
- self.assertIn(media_path, response.context["media"]._js)
- self.assertContains(response, media_path)
- def test_user_message_on_none_selected(self):
- """
- User sees a warning when 'Go' is pressed and no items are selected.
- """
- action_data = {
- ACTION_CHECKBOX_NAME: [],
- "action": "delete_selected",
- "index": 0,
- }
- url = reverse("admin:admin_views_subscriber_changelist")
- response = self.client.post(url, action_data)
- self.assertRedirects(response, url, fetch_redirect_response=False)
- response = self.client.get(response.url)
- msg = (
- "Items must be selected in order to perform actions on them. No items have "
- "been changed."
- )
- self.assertContains(response, msg)
- self.assertEqual(Subscriber.objects.count(), 2)
- def test_user_message_on_no_action(self):
- """
- User sees a warning when 'Go' is pressed and no action is selected.
- """
- action_data = {
- ACTION_CHECKBOX_NAME: [self.s1.pk, self.s2.pk],
- "action": "",
- "index": 0,
- }
- url = reverse("admin:admin_views_subscriber_changelist")
- response = self.client.post(url, action_data)
- self.assertRedirects(response, url, fetch_redirect_response=False)
- response = self.client.get(response.url)
- self.assertContains(response, "No action selected.")
- self.assertEqual(Subscriber.objects.count(), 2)
- def test_selection_counter(self):
- """The selection counter is there."""
- response = self.client.get(reverse("admin:admin_views_subscriber_changelist"))
- self.assertContains(response, "0 of 2 selected")
- def test_popup_actions(self):
- """Actions aren't shown in popups."""
- changelist_url = reverse("admin:admin_views_subscriber_changelist")
- response = self.client.get(changelist_url)
- self.assertIsNotNone(response.context["action_form"])
- response = self.client.get(changelist_url + "?%s" % IS_POPUP_VAR)
- self.assertIsNone(response.context["action_form"])
- def test_popup_template_response_on_add(self):
- """
- Success on popups shall be rendered from template in order to allow
- easy customization.
- """
- response = self.client.post(
- reverse("admin:admin_views_actor_add") + "?%s=1" % IS_POPUP_VAR,
- {"name": "Troy McClure", "age": "55", IS_POPUP_VAR: "1"},
- )
- self.assertEqual(response.status_code, 200)
- self.assertEqual(
- response.template_name,
- [
- "admin/admin_views/actor/popup_response.html",
- "admin/admin_views/popup_response.html",
- "admin/popup_response.html",
- ],
- )
- self.assertTemplateUsed(response, "admin/popup_response.html")
- def test_popup_template_response_on_change(self):
- instance = Actor.objects.create(name="David Tennant", age=45)
- response = self.client.post(
- reverse("admin:admin_views_actor_change", args=(instance.pk,))
- + "?%s=1" % IS_POPUP_VAR,
- {"name": "David Tennant", "age": "46", IS_POPUP_VAR: "1"},
- )
- self.assertEqual(response.status_code, 200)
- self.assertEqual(
- response.template_name,
- [
- "admin/admin_views/actor/popup_response.html",
- "admin/admin_views/popup_response.html",
- "admin/popup_response.html",
- ],
- )
- self.assertTemplateUsed(response, "admin/popup_response.html")
- def test_popup_template_response_on_delete(self):
- instance = Actor.objects.create(name="David Tennant", age=45)
- response = self.client.post(
- reverse("admin:admin_views_actor_delete", args=(instance.pk,))
- + "?%s=1" % IS_POPUP_VAR,
- {IS_POPUP_VAR: "1"},
- )
- self.assertEqual(response.status_code, 200)
- self.assertEqual(
- response.template_name,
- [
- "admin/admin_views/actor/popup_response.html",
- "admin/admin_views/popup_response.html",
- "admin/popup_response.html",
- ],
- )
- self.assertTemplateUsed(response, "admin/popup_response.html")
- def test_popup_template_escaping(self):
- popup_response_data = json.dumps(
- {
- "new_value": "new_value\\",
- "obj": "obj\\",
- "value": "value\\",
- }
- )
- context = {
- "popup_response_data": popup_response_data,
- }
- output = render_to_string("admin/popup_response.html", context)
- self.assertIn(r""value\\"", output)
- self.assertIn(r""new_value\\"", output)
- self.assertIn(r""obj\\"", output)
- @override_settings(ROOT_URLCONF="admin_views.urls")
- class AdminActionsPermissionTests(TestCase):
- @classmethod
- def setUpTestData(cls):
- cls.s1 = ExternalSubscriber.objects.create(
- name="John Doe", email="john@example.org"
- )
- cls.s2 = Subscriber.objects.create(
- name="Max Mustermann", email="max@example.org"
- )
- cls.user = User.objects.create_user(
- username="user",
- password="secret",
- email="user@example.com",
- is_staff=True,
- )
- permission = Permission.objects.get(codename="change_subscriber")
- cls.user.user_permissions.add(permission)
- def setUp(self):
- self.client.force_login(self.user)
- def test_model_admin_no_delete_permission(self):
- """
- Permission is denied if the user doesn't have delete permission for the
- model (Subscriber).
- """
- action_data = {
- ACTION_CHECKBOX_NAME: [self.s1.pk],
- "action": "delete_selected",
- }
- url = reverse("admin:admin_views_subscriber_changelist")
- response = self.client.post(url, action_data)
- self.assertRedirects(response, url, fetch_redirect_response=False)
- response = self.client.get(response.url)
- self.assertContains(response, "No action selected.")
- def test_model_admin_no_delete_permission_externalsubscriber(self):
- """
- Permission is denied if the user doesn't have delete permission for a
- related model (ExternalSubscriber).
- """
- permission = Permission.objects.get(codename="delete_subscriber")
- self.user.user_permissions.add(permission)
- delete_confirmation_data = {
- ACTION_CHECKBOX_NAME: [self.s1.pk, self.s2.pk],
- "action": "delete_selected",
- "post": "yes",
- }
- response = self.client.post(
- reverse("admin:admin_views_subscriber_changelist"), delete_confirmation_data
- )
- self.assertEqual(response.status_code, 403)
|