diff options
| author | Tim Graham <timograham@gmail.com> | 2017-06-19 15:10:26 -0400 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2017-06-19 16:00:06 -0400 |
| commit | 7d52de31af40b031aa055f326bb334024c1ec0cc (patch) | |
| tree | 789def5518fe4f238e3a57b7109290e7ef3a871f /tests/admin_views/test_actions.py | |
| parent | 57f4b3ce373c1c1fef794507a16410d40aebdda6 (diff) | |
Moved admin's action view tests to a separate file.
Diffstat (limited to 'tests/admin_views/test_actions.py')
| -rw-r--r-- | tests/admin_views/test_actions.py | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/tests/admin_views/test_actions.py b/tests/admin_views/test_actions.py new file mode 100644 index 0000000000..f869fc2cc2 --- /dev/null +++ b/tests/admin_views/test_actions.py @@ -0,0 +1,366 @@ +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 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 .forms import MediaActionForm +from .models import ( + Actor, Answer, 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) + + @override_settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True) + 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 + self.assertEqual(response.status_code, 200) + # 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_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 + ) |
