Moved admin's action view tests to a separate file.
This commit is contained in:
parent
57f4b3ce37
commit
7d52de31af
|
@ -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 datetime
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import unittest
|
import unittest
|
||||||
|
@ -24,7 +23,6 @@ from django.core import mail
|
||||||
from django.core.checks import Error
|
from django.core.checks import Error
|
||||||
from django.core.files import temp as tempfile
|
from django.core.files import temp as tempfile
|
||||||
from django.forms.utils import ErrorList
|
from django.forms.utils import ErrorList
|
||||||
from django.template.loader import render_to_string
|
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.test import (
|
from django.test import (
|
||||||
SimpleTestCase, TestCase, ignore_warnings, modify_settings,
|
SimpleTestCase, TestCase, ignore_warnings, modify_settings,
|
||||||
|
@ -41,26 +39,23 @@ from django.utils.http import urlencode
|
||||||
|
|
||||||
from . import customadmin
|
from . import customadmin
|
||||||
from .admin import CityAdmin, site, site2
|
from .admin import CityAdmin, site, site2
|
||||||
from .forms import MediaActionForm
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Actor, AdminOrderedAdminMethod, AdminOrderedCallable, AdminOrderedField,
|
Actor, AdminOrderedAdminMethod, AdminOrderedCallable, AdminOrderedField,
|
||||||
AdminOrderedModelMethod, Answer, Answer2, Article, BarAccount, Book,
|
AdminOrderedModelMethod, Answer, Answer2, Article, BarAccount, Book,
|
||||||
Bookmark, Category, Chapter, ChapterXtra1, ChapterXtra2, Character, Child,
|
Bookmark, Category, Chapter, ChapterXtra1, ChapterXtra2, Character, Child,
|
||||||
Choice, City, Collector, Color, ComplexSortedPerson, CoverLetter,
|
Choice, City, Collector, Color, ComplexSortedPerson, CoverLetter,
|
||||||
CustomArticle, CyclicOne, CyclicTwo, DooHickey, Employee, EmptyModel,
|
CustomArticle, CyclicOne, CyclicTwo, DooHickey, Employee, EmptyModel,
|
||||||
ExternalSubscriber, Fabric, FancyDoodad, FieldOverridePost,
|
Fabric, FancyDoodad, FieldOverridePost, FilteredManager, FooAccount,
|
||||||
FilteredManager, FooAccount, FoodDelivery, FunkyTag, Gallery, Grommet,
|
FoodDelivery, FunkyTag, Gallery, Grommet, Inquisition, Language, Link,
|
||||||
Inquisition, Language, Link, MainPrepopulated, Media,
|
MainPrepopulated, Media, ModelWithStringPrimaryKey, OtherStory, Paper,
|
||||||
ModelWithStringPrimaryKey, OtherStory, Paper, Parent,
|
Parent, ParentWithDependentChildren, ParentWithUUIDPK, Person, Persona,
|
||||||
ParentWithDependentChildren, ParentWithUUIDPK, Person, Persona, Picture,
|
Picture, Pizza, Plot, PlotDetails, PluggableSearchPerson, Podcast, Post,
|
||||||
Pizza, Plot, PlotDetails, PluggableSearchPerson, Podcast, Post,
|
|
||||||
PrePopulatedPost, Promo, Question, ReadablePizza, Recommendation,
|
PrePopulatedPost, Promo, Question, ReadablePizza, Recommendation,
|
||||||
Recommender, RelatedPrepopulated, RelatedWithUUIDPKModel, Report,
|
Recommender, RelatedPrepopulated, RelatedWithUUIDPKModel, Report,
|
||||||
Restaurant, RowLevelChangePermissionModel, SecretHideout, Section,
|
Restaurant, RowLevelChangePermissionModel, SecretHideout, Section,
|
||||||
ShortMessage, Simple, State, Story, Subscriber, SuperSecretHideout,
|
ShortMessage, Simple, State, Story, SuperSecretHideout, SuperVillain,
|
||||||
SuperVillain, Telegram, TitleTranslation, Topping, UnchangeableObject,
|
Telegram, TitleTranslation, Topping, UnchangeableObject, UndeletableObject,
|
||||||
UndeletableObject, UnorderedObject, Villain, Vodcast, Whatsit, Widget,
|
UnorderedObject, Villain, Vodcast, Whatsit, Widget, Worker, WorkHour,
|
||||||
Worker, WorkHour,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -3202,366 +3197,6 @@ class AdminInheritedInlinesTest(TestCase):
|
||||||
self.assertEqual(Persona.objects.all()[0].accounts.count(), 2)
|
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')
|
@override_settings(ROOT_URLCONF='admin_views.urls')
|
||||||
class TestCustomChangeList(TestCase):
|
class TestCustomChangeList(TestCase):
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue