1. import json
    
  2. 
    
  3. from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
    
  4. from django.contrib.admin.views.main import IS_POPUP_VAR
    
  5. from django.contrib.auth.models import Permission, User
    
  6. from django.core import mail
    
  7. from django.template.loader import render_to_string
    
  8. from django.template.response import TemplateResponse
    
  9. from django.test import TestCase, override_settings
    
  10. from django.urls import reverse
    
  11. 
    
  12. from .admin import SubscriberAdmin
    
  13. from .forms import MediaActionForm
    
  14. from .models import (
    
  15.     Actor,
    
  16.     Answer,
    
  17.     Book,
    
  18.     ExternalSubscriber,
    
  19.     Question,
    
  20.     Subscriber,
    
  21.     UnchangeableObject,
    
  22. )
    
  23. 
    
  24. 
    
  25. @override_settings(ROOT_URLCONF="admin_views.urls")
    
  26. class AdminActionsTest(TestCase):
    
  27.     @classmethod
    
  28.     def setUpTestData(cls):
    
  29.         cls.superuser = User.objects.create_superuser(
    
  30.             username="super", password="secret", email="[email protected]"
    
  31.         )
    
  32.         cls.s1 = ExternalSubscriber.objects.create(
    
  33.             name="John Doe", email="[email protected]"
    
  34.         )
    
  35.         cls.s2 = Subscriber.objects.create(
    
  36.             name="Max Mustermann", email="[email protected]"
    
  37.         )
    
  38. 
    
  39.     def setUp(self):
    
  40.         self.client.force_login(self.superuser)
    
  41. 
    
  42.     def test_model_admin_custom_action(self):
    
  43.         """A custom action defined in a ModelAdmin method."""
    
  44.         action_data = {
    
  45.             ACTION_CHECKBOX_NAME: [self.s1.pk],
    
  46.             "action": "mail_admin",
    
  47.             "index": 0,
    
  48.         }
    
  49.         self.client.post(
    
  50.             reverse("admin:admin_views_subscriber_changelist"), action_data
    
  51.         )
    
  52.         self.assertEqual(len(mail.outbox), 1)
    
  53.         self.assertEqual(mail.outbox[0].subject, "Greetings from a ModelAdmin action")
    
  54. 
    
  55.     def test_model_admin_default_delete_action(self):
    
  56.         action_data = {
    
  57.             ACTION_CHECKBOX_NAME: [self.s1.pk, self.s2.pk],
    
  58.             "action": "delete_selected",
    
  59.             "index": 0,
    
  60.         }
    
  61.         delete_confirmation_data = {
    
  62.             ACTION_CHECKBOX_NAME: [self.s1.pk, self.s2.pk],
    
  63.             "action": "delete_selected",
    
  64.             "post": "yes",
    
  65.         }
    
  66.         confirmation = self.client.post(
    
  67.             reverse("admin:admin_views_subscriber_changelist"), action_data
    
  68.         )
    
  69.         self.assertIsInstance(confirmation, TemplateResponse)
    
  70.         self.assertContains(
    
  71.             confirmation, "Are you sure you want to delete the selected subscribers?"
    
  72.         )
    
  73.         self.assertContains(confirmation, "<h2>Summary</h2>")
    
  74.         self.assertContains(confirmation, "<li>Subscribers: 2</li>")
    
  75.         self.assertContains(confirmation, "<li>External subscribers: 1</li>")
    
  76.         self.assertContains(confirmation, ACTION_CHECKBOX_NAME, count=2)
    
  77.         self.client.post(
    
  78.             reverse("admin:admin_views_subscriber_changelist"), delete_confirmation_data
    
  79.         )
    
  80.         self.assertEqual(Subscriber.objects.count(), 0)
    
  81. 
    
  82.     def test_default_delete_action_nonexistent_pk(self):
    
  83.         self.assertFalse(Subscriber.objects.filter(id=9998).exists())
    
  84.         action_data = {
    
  85.             ACTION_CHECKBOX_NAME: ["9998"],
    
  86.             "action": "delete_selected",
    
  87.             "index": 0,
    
  88.         }
    
  89.         response = self.client.post(
    
  90.             reverse("admin:admin_views_subscriber_changelist"), action_data
    
  91.         )
    
  92.         self.assertContains(
    
  93.             response, "Are you sure you want to delete the selected subscribers?"
    
  94.         )
    
  95.         self.assertContains(response, "<ul></ul>", html=True)
    
  96. 
    
  97.     @override_settings(USE_THOUSAND_SEPARATOR=True, NUMBER_GROUPING=3)
    
  98.     def test_non_localized_pk(self):
    
  99.         """
    
  100.         If USE_THOUSAND_SEPARATOR is set, the ids for the objects selected for
    
  101.         deletion are rendered without separators.
    
  102.         """
    
  103.         s = ExternalSubscriber.objects.create(id=9999)
    
  104.         action_data = {
    
  105.             ACTION_CHECKBOX_NAME: [s.pk, self.s2.pk],
    
  106.             "action": "delete_selected",
    
  107.             "index": 0,
    
  108.         }
    
  109.         response = self.client.post(
    
  110.             reverse("admin:admin_views_subscriber_changelist"), action_data
    
  111.         )
    
  112.         self.assertTemplateUsed(response, "admin/delete_selected_confirmation.html")
    
  113.         self.assertContains(response, 'value="9999"')  # Instead of 9,999
    
  114.         self.assertContains(response, 'value="%s"' % self.s2.pk)
    
  115. 
    
  116.     def test_model_admin_default_delete_action_protected(self):
    
  117.         """
    
  118.         The default delete action where some related objects are protected
    
  119.         from deletion.
    
  120.         """
    
  121.         q1 = Question.objects.create(question="Why?")
    
  122.         a1 = Answer.objects.create(question=q1, answer="Because.")
    
  123.         a2 = Answer.objects.create(question=q1, answer="Yes.")
    
  124.         q2 = Question.objects.create(question="Wherefore?")
    
  125.         action_data = {
    
  126.             ACTION_CHECKBOX_NAME: [q1.pk, q2.pk],
    
  127.             "action": "delete_selected",
    
  128.             "index": 0,
    
  129.         }
    
  130.         delete_confirmation_data = action_data.copy()
    
  131.         delete_confirmation_data["post"] = "yes"
    
  132.         response = self.client.post(
    
  133.             reverse("admin:admin_views_question_changelist"), action_data
    
  134.         )
    
  135.         self.assertContains(
    
  136.             response, "would require deleting the following protected related objects"
    
  137.         )
    
  138.         self.assertContains(
    
  139.             response,
    
  140.             '<li>Answer: <a href="%s">Because.</a></li>'
    
  141.             % reverse("admin:admin_views_answer_change", args=(a1.pk,)),
    
  142.             html=True,
    
  143.         )
    
  144.         self.assertContains(
    
  145.             response,
    
  146.             '<li>Answer: <a href="%s">Yes.</a></li>'
    
  147.             % reverse("admin:admin_views_answer_change", args=(a2.pk,)),
    
  148.             html=True,
    
  149.         )
    
  150.         # A POST request to delete protected objects displays the page which
    
  151.         # says the deletion is prohibited.
    
  152.         response = self.client.post(
    
  153.             reverse("admin:admin_views_question_changelist"), delete_confirmation_data
    
  154.         )
    
  155.         self.assertContains(
    
  156.             response, "would require deleting the following protected related objects"
    
  157.         )
    
  158.         self.assertEqual(Question.objects.count(), 2)
    
  159. 
    
  160.     def test_model_admin_default_delete_action_no_change_url(self):
    
  161.         """
    
  162.         The default delete action doesn't break if a ModelAdmin removes the
    
  163.         change_view URL (#20640).
    
  164.         """
    
  165.         obj = UnchangeableObject.objects.create()
    
  166.         action_data = {
    
  167.             ACTION_CHECKBOX_NAME: obj.pk,
    
  168.             "action": "delete_selected",
    
  169.             "index": "0",
    
  170.         }
    
  171.         response = self.client.post(
    
  172.             reverse("admin:admin_views_unchangeableobject_changelist"), action_data
    
  173.         )
    
  174.         # No 500 caused by NoReverseMatch. The page doesn't display a link to
    
  175.         # the nonexistent change page.
    
  176.         self.assertContains(
    
  177.             response, "<li>Unchangeable object: %s</li>" % obj, 1, html=True
    
  178.         )
    
  179. 
    
  180.     def test_delete_queryset_hook(self):
    
  181.         delete_confirmation_data = {
    
  182.             ACTION_CHECKBOX_NAME: [self.s1.pk, self.s2.pk],
    
  183.             "action": "delete_selected",
    
  184.             "post": "yes",
    
  185.             "index": 0,
    
  186.         }
    
  187.         SubscriberAdmin.overridden = False
    
  188.         self.client.post(
    
  189.             reverse("admin:admin_views_subscriber_changelist"), delete_confirmation_data
    
  190.         )
    
  191.         # SubscriberAdmin.delete_queryset() sets overridden to True.
    
  192.         self.assertIs(SubscriberAdmin.overridden, True)
    
  193.         self.assertEqual(Subscriber.objects.count(), 0)
    
  194. 
    
  195.     def test_delete_selected_uses_get_deleted_objects(self):
    
  196.         """The delete_selected action uses ModelAdmin.get_deleted_objects()."""
    
  197.         book = Book.objects.create(name="Test Book")
    
  198.         data = {
    
  199.             ACTION_CHECKBOX_NAME: [book.pk],
    
  200.             "action": "delete_selected",
    
  201.             "index": 0,
    
  202.         }
    
  203.         response = self.client.post(reverse("admin2:admin_views_book_changelist"), data)
    
  204.         # BookAdmin.get_deleted_objects() returns custom text.
    
  205.         self.assertContains(response, "a deletable object")
    
  206. 
    
  207.     def test_custom_function_mail_action(self):
    
  208.         """A custom action may be defined in a function."""
    
  209.         action_data = {
    
  210.             ACTION_CHECKBOX_NAME: [self.s1.pk],
    
  211.             "action": "external_mail",
    
  212.             "index": 0,
    
  213.         }
    
  214.         self.client.post(
    
  215.             reverse("admin:admin_views_externalsubscriber_changelist"), action_data
    
  216.         )
    
  217.         self.assertEqual(len(mail.outbox), 1)
    
  218.         self.assertEqual(mail.outbox[0].subject, "Greetings from a function action")
    
  219. 
    
  220.     def test_custom_function_action_with_redirect(self):
    
  221.         """Another custom action defined in a function."""
    
  222.         action_data = {
    
  223.             ACTION_CHECKBOX_NAME: [self.s1.pk],
    
  224.             "action": "redirect_to",
    
  225.             "index": 0,
    
  226.         }
    
  227.         response = self.client.post(
    
  228.             reverse("admin:admin_views_externalsubscriber_changelist"), action_data
    
  229.         )
    
  230.         self.assertEqual(response.status_code, 302)
    
  231. 
    
  232.     def test_default_redirect(self):
    
  233.         """
    
  234.         Actions which don't return an HttpResponse are redirected to the same
    
  235.         page, retaining the querystring (which may contain changelist info).
    
  236.         """
    
  237.         action_data = {
    
  238.             ACTION_CHECKBOX_NAME: [self.s1.pk],
    
  239.             "action": "external_mail",
    
  240.             "index": 0,
    
  241.         }
    
  242.         url = reverse("admin:admin_views_externalsubscriber_changelist") + "?o=1"
    
  243.         response = self.client.post(url, action_data)
    
  244.         self.assertRedirects(response, url)
    
  245. 
    
  246.     def test_custom_function_action_streaming_response(self):
    
  247.         """A custom action may return a StreamingHttpResponse."""
    
  248.         action_data = {
    
  249.             ACTION_CHECKBOX_NAME: [self.s1.pk],
    
  250.             "action": "download",
    
  251.             "index": 0,
    
  252.         }
    
  253.         response = self.client.post(
    
  254.             reverse("admin:admin_views_externalsubscriber_changelist"), action_data
    
  255.         )
    
  256.         content = b"".join(response.streaming_content)
    
  257.         self.assertEqual(content, b"This is the content of the file")
    
  258.         self.assertEqual(response.status_code, 200)
    
  259. 
    
  260.     def test_custom_function_action_no_perm_response(self):
    
  261.         """A custom action may returns an HttpResponse with a 403 code."""
    
  262.         action_data = {
    
  263.             ACTION_CHECKBOX_NAME: [self.s1.pk],
    
  264.             "action": "no_perm",
    
  265.             "index": 0,
    
  266.         }
    
  267.         response = self.client.post(
    
  268.             reverse("admin:admin_views_externalsubscriber_changelist"), action_data
    
  269.         )
    
  270.         self.assertEqual(response.status_code, 403)
    
  271.         self.assertEqual(response.content, b"No permission to perform this action")
    
  272. 
    
  273.     def test_actions_ordering(self):
    
  274.         """Actions are ordered as expected."""
    
  275.         response = self.client.get(
    
  276.             reverse("admin:admin_views_externalsubscriber_changelist")
    
  277.         )
    
  278.         self.assertContains(
    
  279.             response,
    
  280.             """<label>Action: <select name="action" required>
    
  281. <option value="" selected>---------</option>
    
  282. <option value="delete_selected">Delete selected external
    
  283. subscribers</option>
    
  284. <option value="redirect_to">Redirect to (Awesome action)</option>
    
  285. <option value="external_mail">External mail (Another awesome
    
  286. action)</option>
    
  287. <option value="download">Download subscription</option>
    
  288. <option value="no_perm">No permission to run</option>
    
  289. </select>""",
    
  290.             html=True,
    
  291.         )
    
  292. 
    
  293.     def test_model_without_action(self):
    
  294.         """A ModelAdmin might not have any actions."""
    
  295.         response = self.client.get(
    
  296.             reverse("admin:admin_views_oldsubscriber_changelist")
    
  297.         )
    
  298.         self.assertIsNone(response.context["action_form"])
    
  299.         self.assertNotContains(
    
  300.             response,
    
  301.             '<input type="checkbox" class="action-select"',
    
  302.             msg_prefix="Found an unexpected action toggle checkboxbox in response",
    
  303.         )
    
  304.         self.assertNotContains(response, '<input type="checkbox" class="action-select"')
    
  305. 
    
  306.     def test_model_without_action_still_has_jquery(self):
    
  307.         """
    
  308.         A ModelAdmin without any actions still has jQuery included on the page.
    
  309.         """
    
  310.         response = self.client.get(
    
  311.             reverse("admin:admin_views_oldsubscriber_changelist")
    
  312.         )
    
  313.         self.assertIsNone(response.context["action_form"])
    
  314.         self.assertContains(
    
  315.             response,
    
  316.             "jquery.min.js",
    
  317.             msg_prefix=(
    
  318.                 "jQuery missing from admin pages for model with no admin actions"
    
  319.             ),
    
  320.         )
    
  321. 
    
  322.     def test_action_column_class(self):
    
  323.         """The checkbox column class is present in the response."""
    
  324.         response = self.client.get(reverse("admin:admin_views_subscriber_changelist"))
    
  325.         self.assertIsNotNone(response.context["action_form"])
    
  326.         self.assertContains(response, "action-checkbox-column")
    
  327. 
    
  328.     def test_multiple_actions_form(self):
    
  329.         """
    
  330.         Actions come from the form whose submit button was pressed (#10618).
    
  331.         """
    
  332.         action_data = {
    
  333.             ACTION_CHECKBOX_NAME: [self.s1.pk],
    
  334.             # Two different actions selected on the two forms...
    
  335.             "action": ["external_mail", "delete_selected"],
    
  336.             # ...but "go" was clicked on the top form.
    
  337.             "index": 0,
    
  338.         }
    
  339.         self.client.post(
    
  340.             reverse("admin:admin_views_externalsubscriber_changelist"), action_data
    
  341.         )
    
  342.         # The action sends mail rather than deletes.
    
  343.         self.assertEqual(len(mail.outbox), 1)
    
  344.         self.assertEqual(mail.outbox[0].subject, "Greetings from a function action")
    
  345. 
    
  346.     def test_media_from_actions_form(self):
    
  347.         """
    
  348.         The action form's media is included in the changelist view's media.
    
  349.         """
    
  350.         response = self.client.get(reverse("admin:admin_views_subscriber_changelist"))
    
  351.         media_path = MediaActionForm.Media.js[0]
    
  352.         self.assertIsInstance(response.context["action_form"], MediaActionForm)
    
  353.         self.assertIn("media", response.context)
    
  354.         self.assertIn(media_path, response.context["media"]._js)
    
  355.         self.assertContains(response, media_path)
    
  356. 
    
  357.     def test_user_message_on_none_selected(self):
    
  358.         """
    
  359.         User sees a warning when 'Go' is pressed and no items are selected.
    
  360.         """
    
  361.         action_data = {
    
  362.             ACTION_CHECKBOX_NAME: [],
    
  363.             "action": "delete_selected",
    
  364.             "index": 0,
    
  365.         }
    
  366.         url = reverse("admin:admin_views_subscriber_changelist")
    
  367.         response = self.client.post(url, action_data)
    
  368.         self.assertRedirects(response, url, fetch_redirect_response=False)
    
  369.         response = self.client.get(response.url)
    
  370.         msg = (
    
  371.             "Items must be selected in order to perform actions on them. No items have "
    
  372.             "been changed."
    
  373.         )
    
  374.         self.assertContains(response, msg)
    
  375.         self.assertEqual(Subscriber.objects.count(), 2)
    
  376. 
    
  377.     def test_user_message_on_no_action(self):
    
  378.         """
    
  379.         User sees a warning when 'Go' is pressed and no action is selected.
    
  380.         """
    
  381.         action_data = {
    
  382.             ACTION_CHECKBOX_NAME: [self.s1.pk, self.s2.pk],
    
  383.             "action": "",
    
  384.             "index": 0,
    
  385.         }
    
  386.         url = reverse("admin:admin_views_subscriber_changelist")
    
  387.         response = self.client.post(url, action_data)
    
  388.         self.assertRedirects(response, url, fetch_redirect_response=False)
    
  389.         response = self.client.get(response.url)
    
  390.         self.assertContains(response, "No action selected.")
    
  391.         self.assertEqual(Subscriber.objects.count(), 2)
    
  392. 
    
  393.     def test_selection_counter(self):
    
  394.         """The selection counter is there."""
    
  395.         response = self.client.get(reverse("admin:admin_views_subscriber_changelist"))
    
  396.         self.assertContains(response, "0 of 2 selected")
    
  397. 
    
  398.     def test_popup_actions(self):
    
  399.         """Actions aren't shown in popups."""
    
  400.         changelist_url = reverse("admin:admin_views_subscriber_changelist")
    
  401.         response = self.client.get(changelist_url)
    
  402.         self.assertIsNotNone(response.context["action_form"])
    
  403.         response = self.client.get(changelist_url + "?%s" % IS_POPUP_VAR)
    
  404.         self.assertIsNone(response.context["action_form"])
    
  405. 
    
  406.     def test_popup_template_response_on_add(self):
    
  407.         """
    
  408.         Success on popups shall be rendered from template in order to allow
    
  409.         easy customization.
    
  410.         """
    
  411.         response = self.client.post(
    
  412.             reverse("admin:admin_views_actor_add") + "?%s=1" % IS_POPUP_VAR,
    
  413.             {"name": "Troy McClure", "age": "55", IS_POPUP_VAR: "1"},
    
  414.         )
    
  415.         self.assertEqual(response.status_code, 200)
    
  416.         self.assertEqual(
    
  417.             response.template_name,
    
  418.             [
    
  419.                 "admin/admin_views/actor/popup_response.html",
    
  420.                 "admin/admin_views/popup_response.html",
    
  421.                 "admin/popup_response.html",
    
  422.             ],
    
  423.         )
    
  424.         self.assertTemplateUsed(response, "admin/popup_response.html")
    
  425. 
    
  426.     def test_popup_template_response_on_change(self):
    
  427.         instance = Actor.objects.create(name="David Tennant", age=45)
    
  428.         response = self.client.post(
    
  429.             reverse("admin:admin_views_actor_change", args=(instance.pk,))
    
  430.             + "?%s=1" % IS_POPUP_VAR,
    
  431.             {"name": "David Tennant", "age": "46", IS_POPUP_VAR: "1"},
    
  432.         )
    
  433.         self.assertEqual(response.status_code, 200)
    
  434.         self.assertEqual(
    
  435.             response.template_name,
    
  436.             [
    
  437.                 "admin/admin_views/actor/popup_response.html",
    
  438.                 "admin/admin_views/popup_response.html",
    
  439.                 "admin/popup_response.html",
    
  440.             ],
    
  441.         )
    
  442.         self.assertTemplateUsed(response, "admin/popup_response.html")
    
  443. 
    
  444.     def test_popup_template_response_on_delete(self):
    
  445.         instance = Actor.objects.create(name="David Tennant", age=45)
    
  446.         response = self.client.post(
    
  447.             reverse("admin:admin_views_actor_delete", args=(instance.pk,))
    
  448.             + "?%s=1" % IS_POPUP_VAR,
    
  449.             {IS_POPUP_VAR: "1"},
    
  450.         )
    
  451.         self.assertEqual(response.status_code, 200)
    
  452.         self.assertEqual(
    
  453.             response.template_name,
    
  454.             [
    
  455.                 "admin/admin_views/actor/popup_response.html",
    
  456.                 "admin/admin_views/popup_response.html",
    
  457.                 "admin/popup_response.html",
    
  458.             ],
    
  459.         )
    
  460.         self.assertTemplateUsed(response, "admin/popup_response.html")
    
  461. 
    
  462.     def test_popup_template_escaping(self):
    
  463.         popup_response_data = json.dumps(
    
  464.             {
    
  465.                 "new_value": "new_value\\",
    
  466.                 "obj": "obj\\",
    
  467.                 "value": "value\\",
    
  468.             }
    
  469.         )
    
  470.         context = {
    
  471.             "popup_response_data": popup_response_data,
    
  472.         }
    
  473.         output = render_to_string("admin/popup_response.html", context)
    
  474.         self.assertIn(r"&quot;value\\&quot;", output)
    
  475.         self.assertIn(r"&quot;new_value\\&quot;", output)
    
  476.         self.assertIn(r"&quot;obj\\&quot;", output)
    
  477. 
    
  478. 
    
  479. @override_settings(ROOT_URLCONF="admin_views.urls")
    
  480. class AdminActionsPermissionTests(TestCase):
    
  481.     @classmethod
    
  482.     def setUpTestData(cls):
    
  483.         cls.s1 = ExternalSubscriber.objects.create(
    
  484.             name="John Doe", email="[email protected]"
    
  485.         )
    
  486.         cls.s2 = Subscriber.objects.create(
    
  487.             name="Max Mustermann", email="[email protected]"
    
  488.         )
    
  489.         cls.user = User.objects.create_user(
    
  490.             username="user",
    
  491.             password="secret",
    
  492.             email="[email protected]",
    
  493.             is_staff=True,
    
  494.         )
    
  495.         permission = Permission.objects.get(codename="change_subscriber")
    
  496.         cls.user.user_permissions.add(permission)
    
  497. 
    
  498.     def setUp(self):
    
  499.         self.client.force_login(self.user)
    
  500. 
    
  501.     def test_model_admin_no_delete_permission(self):
    
  502.         """
    
  503.         Permission is denied if the user doesn't have delete permission for the
    
  504.         model (Subscriber).
    
  505.         """
    
  506.         action_data = {
    
  507.             ACTION_CHECKBOX_NAME: [self.s1.pk],
    
  508.             "action": "delete_selected",
    
  509.         }
    
  510.         url = reverse("admin:admin_views_subscriber_changelist")
    
  511.         response = self.client.post(url, action_data)
    
  512.         self.assertRedirects(response, url, fetch_redirect_response=False)
    
  513.         response = self.client.get(response.url)
    
  514.         self.assertContains(response, "No action selected.")
    
  515. 
    
  516.     def test_model_admin_no_delete_permission_externalsubscriber(self):
    
  517.         """
    
  518.         Permission is denied if the user doesn't have delete permission for a
    
  519.         related model (ExternalSubscriber).
    
  520.         """
    
  521.         permission = Permission.objects.get(codename="delete_subscriber")
    
  522.         self.user.user_permissions.add(permission)
    
  523.         delete_confirmation_data = {
    
  524.             ACTION_CHECKBOX_NAME: [self.s1.pk, self.s2.pk],
    
  525.             "action": "delete_selected",
    
  526.             "post": "yes",
    
  527.         }
    
  528.         response = self.client.post(
    
  529.             reverse("admin:admin_views_subscriber_changelist"), delete_confirmation_data
    
  530.         )
    
  531.         self.assertEqual(response.status_code, 403)