mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Moved admin's action view tests to a separate file.
This commit is contained in:
		
							
								
								
									
										366
									
								
								tests/admin_views/test_actions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										366
									
								
								tests/admin_views/test_actions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
|         ) | ||||
| @@ -1,5 +1,4 @@ | ||||
| import datetime | ||||
| import json | ||||
| import os | ||||
| import re | ||||
| import unittest | ||||
| @@ -24,7 +23,6 @@ from django.core import mail | ||||
| from django.core.checks import Error | ||||
| from django.core.files import temp as tempfile | ||||
| from django.forms.utils import ErrorList | ||||
| from django.template.loader import render_to_string | ||||
| from django.template.response import TemplateResponse | ||||
| from django.test import ( | ||||
|     SimpleTestCase, TestCase, ignore_warnings, modify_settings, | ||||
| @@ -41,26 +39,23 @@ from django.utils.http import urlencode | ||||
|  | ||||
| from . import customadmin | ||||
| from .admin import CityAdmin, site, site2 | ||||
| from .forms import MediaActionForm | ||||
| from .models import ( | ||||
|     Actor, AdminOrderedAdminMethod, AdminOrderedCallable, AdminOrderedField, | ||||
|     AdminOrderedModelMethod, Answer, Answer2, Article, BarAccount, Book, | ||||
|     Bookmark, Category, Chapter, ChapterXtra1, ChapterXtra2, Character, Child, | ||||
|     Choice, City, Collector, Color, ComplexSortedPerson, CoverLetter, | ||||
|     CustomArticle, CyclicOne, CyclicTwo, DooHickey, Employee, EmptyModel, | ||||
|     ExternalSubscriber, Fabric, FancyDoodad, FieldOverridePost, | ||||
|     FilteredManager, FooAccount, FoodDelivery, FunkyTag, Gallery, Grommet, | ||||
|     Inquisition, Language, Link, MainPrepopulated, Media, | ||||
|     ModelWithStringPrimaryKey, OtherStory, Paper, Parent, | ||||
|     ParentWithDependentChildren, ParentWithUUIDPK, Person, Persona, Picture, | ||||
|     Pizza, Plot, PlotDetails, PluggableSearchPerson, Podcast, Post, | ||||
|     Fabric, FancyDoodad, FieldOverridePost, FilteredManager, FooAccount, | ||||
|     FoodDelivery, FunkyTag, Gallery, Grommet, Inquisition, Language, Link, | ||||
|     MainPrepopulated, Media, ModelWithStringPrimaryKey, OtherStory, Paper, | ||||
|     Parent, ParentWithDependentChildren, ParentWithUUIDPK, Person, Persona, | ||||
|     Picture, Pizza, Plot, PlotDetails, PluggableSearchPerson, Podcast, Post, | ||||
|     PrePopulatedPost, Promo, Question, ReadablePizza, Recommendation, | ||||
|     Recommender, RelatedPrepopulated, RelatedWithUUIDPKModel, Report, | ||||
|     Restaurant, RowLevelChangePermissionModel, SecretHideout, Section, | ||||
|     ShortMessage, Simple, State, Story, Subscriber, SuperSecretHideout, | ||||
|     SuperVillain, Telegram, TitleTranslation, Topping, UnchangeableObject, | ||||
|     UndeletableObject, UnorderedObject, Villain, Vodcast, Whatsit, Widget, | ||||
|     Worker, WorkHour, | ||||
|     ShortMessage, Simple, State, Story, SuperSecretHideout, SuperVillain, | ||||
|     Telegram, TitleTranslation, Topping, UnchangeableObject, UndeletableObject, | ||||
|     UnorderedObject, Villain, Vodcast, Whatsit, Widget, Worker, WorkHour, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -3202,366 +3197,6 @@ class AdminInheritedInlinesTest(TestCase): | ||||
|         self.assertEqual(Persona.objects.all()[0].accounts.count(), 2) | ||||
|  | ||||
|  | ||||
| @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): | ||||
|         "Tests 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): | ||||
|         "Tests the default delete action defined as a ModelAdmin method" | ||||
|         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, make sure that the ids for | ||||
|         the objects selected for deletion are rendered without separators. | ||||
|         Refs #14895. | ||||
|         """ | ||||
|         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): | ||||
|         """ | ||||
|         Tests the default delete action defined as a ModelAdmin method in the | ||||
|         case 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 should display 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): | ||||
|         """ | ||||
|         Default delete action shouldn't break if a user's ModelAdmin removes the url for change_view. | ||||
|  | ||||
|         Regression test for #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 shouldn'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): | ||||
|         "Tests a custom action 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): | ||||
|         "Tests a 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 | ||||
|         information). | ||||
|         """ | ||||
|         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): | ||||
|         """Tests a custom action that returns 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): | ||||
|         """Tests a custom action that returns an HttpResponse with 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): | ||||
|         "Tests a ModelAdmin without any action" | ||||
|         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 gets jQuery included in 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 we clicked "go" on the top form. | ||||
|             'index': 0 | ||||
|         } | ||||
|         self.client.post(reverse('admin:admin_views_externalsubscriber_changelist'), action_data) | ||||
|  | ||||
|         # Send mail, don't delete. | ||||
|         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 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 should see 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 should see 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) | ||||
|         msg = """No action selected.""" | ||||
|         self.assertContains(response, msg) | ||||
|         self.assertEqual(Subscriber.objects.count(), 2) | ||||
|  | ||||
|     def test_selection_counter(self): | ||||
|         """ | ||||
|         Check if 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 should not be shown in popups. """ | ||||
|         response = self.client.get(reverse('admin:admin_views_subscriber_changelist')) | ||||
|         self.assertIsNotNone(response.context["action_form"]) | ||||
|         response = self.client.get( | ||||
|             reverse('admin:admin_views_subscriber_changelist') + '?%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 TestCustomChangeList(TestCase): | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user