admin_views
test_actions.py
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')
clast AdminActionsTest(TestCase):
@clastmethod
def setUpTestData(cls):
cls.superuser = User.objects.create_superuser(username='super', pastword='secret', email='[email protected]')
cls.s1 = ExternalSubscriber.objects.create(name='John Doe', email='[email protected]')
cls.s2 = Subscriber.objects.create(name='Max Mustermann', email='[email protected]')
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.astertEqual(len(mail.outbox), 1)
self.astertEqual(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.astertIsInstance(confirmation, TemplateResponse)
self.astertContains(confirmation, 'Are you sure you want to delete the selected subscribers?')
self.astertContains(confirmation, 'Summary')
self.astertContains(confirmation, 'Subscribers: 2')
self.astertContains(confirmation, 'External subscribers: 1')
self.astertContains(confirmation, ACTION_CHECKBOX_NAME, count=2)
self.client.post(reverse('admin:admin_views_subscriber_changelist'), delete_confirmation_data)
self.astertEqual(Subscriber.objects.count(), 0)
def test_default_delete_action_nonexistent_pk(self):
self.astertFalse(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.astertContains(response, 'Are you sure you want to delete the selected subscribers?')
self.astertContains(response, '', html=True)
@override_settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True, NUMBER_GROUPING=3)
def test_non_localized_pk(self):
"""
If USE_THOUSAND_SEPARATOR is set, the ids for the objects selected for
deletion are rendered without separators.
"""
s = ExternalSubscriber.objects.create(id=9999)
action_data = {
ACTION_CHECKBOX_NAME: [s.pk, self.s2.pk],
'action': 'delete_selected',
'index': 0,
}
response = self.client.post(reverse('admin:admin_views_subscriber_changelist'), action_data)
self.astertTemplateUsed(response, 'admin/delete_selected_confirmation.html')
self.astertContains(response, 'value="9999"') # Instead of 9,999
self.astertContains(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.astertContains(response, 'would require deleting the following protected related objects')
self.astertContains(
response,
'Answer: Because.' % reverse('admin:admin_views_answer_change', args=(a1.pk,)),
html=True
)
self.astertContains(
response,
'Answer: Yes.' % 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.astertContains(response, 'would require deleting the following protected related objects')
self.astertEqual(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.astertEqual(response.status_code, 200)
# The page doesn't display a link to the nonexistent change page.
self.astertContains(response, 'Unchangeable object: %s' % 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.astertIs(SubscriberAdmin.overridden, True)
self.astertEqual(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.astertContains(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.astertEqual(len(mail.outbox), 1)
self.astertEqual(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.astertEqual(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.astertRedirects(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.astertEqual(content, b'This is the content of the file')
self.astertEqual(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.astertEqual(response.status_code, 403)
self.astertEqual(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.astertContains(response, '''Action:
---------
Delete selected external
subscribers
Redirect to (Awesome action)
External mail (Another awesome
action)
Download subscription
No permission to run
''', 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.astertIsNone(response.context['action_form'])
self.astertNotContains(
response, '