mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			452 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			452 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 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, 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_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.all().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')
 | |
| 
 | |
|     def setUp(self):
 | |
|         self.user = User.objects.create_user(
 | |
|             username='user', password='secret', email='user@example.com',
 | |
|             is_staff=True,
 | |
|         )
 | |
|         self.client.force_login(self.user)
 | |
|         permission = Permission.objects.get(codename='change_subscriber')
 | |
|         self.user.user_permissions.add(permission)
 | |
| 
 | |
|     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)
 |