mirror of
https://github.com/django/django.git
synced 2024-11-18 07:26:04 +00:00
d084176cc1
This was fixed earlier but only when the M2M relation was at the first level on the object. This commit fixes the issue even when the M2M is at deeper levels, such as behind a foreign key.
806 lines
36 KiB
Python
806 lines
36 KiB
Python
from __future__ import unicode_literals
|
|
|
|
import datetime
|
|
|
|
from django.contrib import admin
|
|
from django.contrib.admin.models import LogEntry
|
|
from django.contrib.admin.options import IncorrectLookupParameters
|
|
from django.contrib.admin.templatetags.admin_list import pagination
|
|
from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
|
|
from django.contrib.admin.views.main import ALL_VAR, SEARCH_VAR, ChangeList
|
|
from django.contrib.auth.models import User
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.urlresolvers import reverse
|
|
from django.template import Context, Template
|
|
from django.test import TestCase, override_settings
|
|
from django.test.client import RequestFactory
|
|
from django.utils import formats, six
|
|
|
|
from .admin import (
|
|
BandAdmin, ChildAdmin, ChordsBandAdmin, ConcertAdmin,
|
|
CustomPaginationAdmin, CustomPaginator, DynamicListDisplayChildAdmin,
|
|
DynamicListDisplayLinksChildAdmin, DynamicListFilterChildAdmin,
|
|
DynamicSearchFieldsChildAdmin, FilteredChildAdmin, GroupAdmin,
|
|
InvitationAdmin, NoListDisplayLinksParentAdmin, ParentAdmin, QuartetAdmin,
|
|
SwallowAdmin, site as custom_site,
|
|
)
|
|
from .models import (
|
|
Band, Child, ChordsBand, ChordsMusician, Concert, CustomIdUser, Event,
|
|
Genre, Group, Invitation, Membership, Musician, OrderedObject, Parent,
|
|
Quartet, Swallow, UnorderedObject,
|
|
)
|
|
|
|
|
|
@override_settings(ROOT_URLCONF="admin_changelist.urls")
|
|
class ChangeListTests(TestCase):
|
|
|
|
def setUp(self):
|
|
self.factory = RequestFactory()
|
|
|
|
def _create_superuser(self, username):
|
|
return User.objects.create(username=username, is_superuser=True)
|
|
|
|
def _mocked_authenticated_request(self, url, user):
|
|
request = self.factory.get(url)
|
|
request.user = user
|
|
return request
|
|
|
|
def test_select_related_preserved(self):
|
|
"""
|
|
Regression test for #10348: ChangeList.get_queryset() shouldn't
|
|
overwrite a custom select_related provided by ModelAdmin.get_queryset().
|
|
"""
|
|
m = ChildAdmin(Child, admin.site)
|
|
request = self.factory.get('/child/')
|
|
list_select_related = m.get_list_select_related(request)
|
|
cl = ChangeList(request, Child, m.list_display, m.list_display_links,
|
|
m.list_filter, m.date_hierarchy, m.search_fields,
|
|
list_select_related, m.list_per_page,
|
|
m.list_max_show_all, m.list_editable, m)
|
|
self.assertEqual(cl.queryset.query.select_related, {
|
|
'parent': {'name': {}}
|
|
})
|
|
|
|
def test_select_related_as_tuple(self):
|
|
ia = InvitationAdmin(Invitation, admin.site)
|
|
request = self.factory.get('/invitation/')
|
|
list_select_related = ia.get_list_select_related(request)
|
|
cl = ChangeList(request, Child, ia.list_display, ia.list_display_links,
|
|
ia.list_filter, ia.date_hierarchy, ia.search_fields,
|
|
list_select_related, ia.list_per_page,
|
|
ia.list_max_show_all, ia.list_editable, ia)
|
|
self.assertEqual(cl.queryset.query.select_related, {'player': {}})
|
|
|
|
def test_select_related_as_empty_tuple(self):
|
|
ia = InvitationAdmin(Invitation, admin.site)
|
|
ia.list_select_related = ()
|
|
request = self.factory.get('/invitation/')
|
|
list_select_related = ia.get_list_select_related(request)
|
|
cl = ChangeList(request, Child, ia.list_display, ia.list_display_links,
|
|
ia.list_filter, ia.date_hierarchy, ia.search_fields,
|
|
list_select_related, ia.list_per_page,
|
|
ia.list_max_show_all, ia.list_editable, ia)
|
|
self.assertEqual(cl.queryset.query.select_related, False)
|
|
|
|
def test_get_select_related_custom_method(self):
|
|
class GetListSelectRelatedAdmin(admin.ModelAdmin):
|
|
list_display = ('band', 'player')
|
|
|
|
def get_list_select_related(self, request):
|
|
return ('band', 'player')
|
|
|
|
ia = GetListSelectRelatedAdmin(Invitation, admin.site)
|
|
request = self.factory.get('/invitation/')
|
|
list_select_related = ia.get_list_select_related(request)
|
|
cl = ChangeList(request, Child, ia.list_display, ia.list_display_links,
|
|
ia.list_filter, ia.date_hierarchy, ia.search_fields,
|
|
list_select_related, ia.list_per_page,
|
|
ia.list_max_show_all, ia.list_editable, ia)
|
|
self.assertEqual(cl.queryset.query.select_related, {'player': {}, 'band': {}})
|
|
|
|
def test_result_list_empty_changelist_value(self):
|
|
"""
|
|
Regression test for #14982: EMPTY_CHANGELIST_VALUE should be honored
|
|
for relationship fields
|
|
"""
|
|
new_child = Child.objects.create(name='name', parent=None)
|
|
request = self.factory.get('/child/')
|
|
m = ChildAdmin(Child, admin.site)
|
|
list_display = m.get_list_display(request)
|
|
list_display_links = m.get_list_display_links(request, list_display)
|
|
cl = ChangeList(request, Child, list_display, list_display_links,
|
|
m.list_filter, m.date_hierarchy, m.search_fields,
|
|
m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m)
|
|
cl.formset = None
|
|
template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
|
|
context = Context({'cl': cl})
|
|
table_output = template.render(context)
|
|
link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
|
|
row_html = '<tbody><tr class="row1"><th class="field-name"><a href="%s">name</a></th><td class="field-parent nowrap">-</td></tr></tbody>' % link
|
|
self.assertNotEqual(table_output.find(row_html), -1,
|
|
'Failed to find expected row element: %s' % table_output)
|
|
|
|
def test_result_list_html(self):
|
|
"""
|
|
Verifies that inclusion tag result_list generates a table when with
|
|
default ModelAdmin settings.
|
|
"""
|
|
new_parent = Parent.objects.create(name='parent')
|
|
new_child = Child.objects.create(name='name', parent=new_parent)
|
|
request = self.factory.get('/child/')
|
|
m = ChildAdmin(Child, admin.site)
|
|
list_display = m.get_list_display(request)
|
|
list_display_links = m.get_list_display_links(request, list_display)
|
|
cl = ChangeList(request, Child, list_display, list_display_links,
|
|
m.list_filter, m.date_hierarchy, m.search_fields,
|
|
m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m)
|
|
cl.formset = None
|
|
template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
|
|
context = Context({'cl': cl})
|
|
table_output = template.render(context)
|
|
link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
|
|
row_html = '<tbody><tr class="row1"><th class="field-name"><a href="%s">name</a></th><td class="field-parent nowrap">Parent object</td></tr></tbody>' % link
|
|
self.assertNotEqual(table_output.find(row_html), -1,
|
|
'Failed to find expected row element: %s' % table_output)
|
|
|
|
def test_result_list_editable_html(self):
|
|
"""
|
|
Regression tests for #11791: Inclusion tag result_list generates a
|
|
table and this checks that the items are nested within the table
|
|
element tags.
|
|
Also a regression test for #13599, verifies that hidden fields
|
|
when list_editable is enabled are rendered in a div outside the
|
|
table.
|
|
"""
|
|
new_parent = Parent.objects.create(name='parent')
|
|
new_child = Child.objects.create(name='name', parent=new_parent)
|
|
request = self.factory.get('/child/')
|
|
m = ChildAdmin(Child, admin.site)
|
|
|
|
# Test with list_editable fields
|
|
m.list_display = ['id', 'name', 'parent']
|
|
m.list_display_links = ['id']
|
|
m.list_editable = ['name']
|
|
cl = ChangeList(request, Child, m.list_display, m.list_display_links,
|
|
m.list_filter, m.date_hierarchy, m.search_fields,
|
|
m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m)
|
|
FormSet = m.get_changelist_formset(request)
|
|
cl.formset = FormSet(queryset=cl.result_list)
|
|
template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
|
|
context = Context({'cl': cl})
|
|
table_output = template.render(context)
|
|
# make sure that hidden fields are in the correct place
|
|
hiddenfields_div = '<div class="hiddenfields"><input type="hidden" name="form-0-id" value="%d" id="id_form-0-id" /></div>' % new_child.id
|
|
self.assertInHTML(hiddenfields_div, table_output, msg_prefix='Failed to find hidden fields')
|
|
|
|
# make sure that list editable fields are rendered in divs correctly
|
|
editable_name_field = '<input name="form-0-name" value="name" class="vTextField" maxlength="30" type="text" id="id_form-0-name" />'
|
|
self.assertInHTML('<td class="field-name">%s</td>' % editable_name_field, table_output, msg_prefix='Failed to find "name" list_editable field')
|
|
|
|
def test_result_list_editable(self):
|
|
"""
|
|
Regression test for #14312: list_editable with pagination
|
|
"""
|
|
|
|
new_parent = Parent.objects.create(name='parent')
|
|
for i in range(200):
|
|
Child.objects.create(name='name %s' % i, parent=new_parent)
|
|
request = self.factory.get('/child/', data={'p': -1}) # Anything outside range
|
|
m = ChildAdmin(Child, admin.site)
|
|
|
|
# Test with list_editable fields
|
|
m.list_display = ['id', 'name', 'parent']
|
|
m.list_display_links = ['id']
|
|
m.list_editable = ['name']
|
|
self.assertRaises(IncorrectLookupParameters, lambda:
|
|
ChangeList(request, Child, m.list_display, m.list_display_links,
|
|
m.list_filter, m.date_hierarchy, m.search_fields,
|
|
m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m))
|
|
|
|
def test_custom_paginator(self):
|
|
new_parent = Parent.objects.create(name='parent')
|
|
for i in range(200):
|
|
Child.objects.create(name='name %s' % i, parent=new_parent)
|
|
|
|
request = self.factory.get('/child/')
|
|
m = CustomPaginationAdmin(Child, admin.site)
|
|
|
|
cl = ChangeList(request, Child, m.list_display, m.list_display_links,
|
|
m.list_filter, m.date_hierarchy, m.search_fields,
|
|
m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m)
|
|
|
|
cl.get_results(request)
|
|
self.assertIsInstance(cl.paginator, CustomPaginator)
|
|
|
|
def test_distinct_for_m2m_in_list_filter(self):
|
|
"""
|
|
Regression test for #13902: When using a ManyToMany in list_filter,
|
|
results shouldn't appear more than once. Basic ManyToMany.
|
|
"""
|
|
blues = Genre.objects.create(name='Blues')
|
|
band = Band.objects.create(name='B.B. King Review', nr_of_members=11)
|
|
|
|
band.genres.add(blues)
|
|
band.genres.add(blues)
|
|
|
|
m = BandAdmin(Band, admin.site)
|
|
request = self.factory.get('/band/', data={'genres': blues.pk})
|
|
|
|
cl = ChangeList(request, Band, m.list_display,
|
|
m.list_display_links, m.list_filter, m.date_hierarchy,
|
|
m.search_fields, m.list_select_related, m.list_per_page,
|
|
m.list_max_show_all, m.list_editable, m)
|
|
|
|
cl.get_results(request)
|
|
|
|
# There's only one Group instance
|
|
self.assertEqual(cl.result_count, 1)
|
|
|
|
def test_distinct_for_through_m2m_in_list_filter(self):
|
|
"""
|
|
Regression test for #13902: When using a ManyToMany in list_filter,
|
|
results shouldn't appear more than once. With an intermediate model.
|
|
"""
|
|
lead = Musician.objects.create(name='Vox')
|
|
band = Group.objects.create(name='The Hype')
|
|
Membership.objects.create(group=band, music=lead, role='lead voice')
|
|
Membership.objects.create(group=band, music=lead, role='bass player')
|
|
|
|
m = GroupAdmin(Group, admin.site)
|
|
request = self.factory.get('/group/', data={'members': lead.pk})
|
|
|
|
cl = ChangeList(request, Group, m.list_display,
|
|
m.list_display_links, m.list_filter, m.date_hierarchy,
|
|
m.search_fields, m.list_select_related, m.list_per_page,
|
|
m.list_max_show_all, m.list_editable, m)
|
|
|
|
cl.get_results(request)
|
|
|
|
# There's only one Group instance
|
|
self.assertEqual(cl.result_count, 1)
|
|
|
|
def test_distinct_for_through_m2m_at_second_level_in_list_filter(self):
|
|
"""
|
|
When using a ManyToMany in list_filter at the second level behind a
|
|
ForeignKey, distinct() must be called and results shouldn't appear more
|
|
than once.
|
|
"""
|
|
lead = Musician.objects.create(name='Vox')
|
|
band = Group.objects.create(name='The Hype')
|
|
Concert.objects.create(name='Woodstock', group=band)
|
|
Membership.objects.create(group=band, music=lead, role='lead voice')
|
|
Membership.objects.create(group=band, music=lead, role='bass player')
|
|
|
|
m = ConcertAdmin(Concert, admin.site)
|
|
request = self.factory.get('/concert/', data={'group__members': lead.pk})
|
|
|
|
cl = ChangeList(request, Concert, m.list_display,
|
|
m.list_display_links, m.list_filter, m.date_hierarchy,
|
|
m.search_fields, m.list_select_related, m.list_per_page,
|
|
m.list_max_show_all, m.list_editable, m)
|
|
|
|
cl.get_results(request)
|
|
|
|
# There's only one Concert instance
|
|
self.assertEqual(cl.result_count, 1)
|
|
|
|
def test_distinct_for_inherited_m2m_in_list_filter(self):
|
|
"""
|
|
Regression test for #13902: When using a ManyToMany in list_filter,
|
|
results shouldn't appear more than once. Model managed in the
|
|
admin inherits from the one that defins the relationship.
|
|
"""
|
|
lead = Musician.objects.create(name='John')
|
|
four = Quartet.objects.create(name='The Beatles')
|
|
Membership.objects.create(group=four, music=lead, role='lead voice')
|
|
Membership.objects.create(group=four, music=lead, role='guitar player')
|
|
|
|
m = QuartetAdmin(Quartet, admin.site)
|
|
request = self.factory.get('/quartet/', data={'members': lead.pk})
|
|
|
|
cl = ChangeList(request, Quartet, m.list_display,
|
|
m.list_display_links, m.list_filter, m.date_hierarchy,
|
|
m.search_fields, m.list_select_related, m.list_per_page,
|
|
m.list_max_show_all, m.list_editable, m)
|
|
|
|
cl.get_results(request)
|
|
|
|
# There's only one Quartet instance
|
|
self.assertEqual(cl.result_count, 1)
|
|
|
|
def test_distinct_for_m2m_to_inherited_in_list_filter(self):
|
|
"""
|
|
Regression test for #13902: When using a ManyToMany in list_filter,
|
|
results shouldn't appear more than once. Target of the relationship
|
|
inherits from another.
|
|
"""
|
|
lead = ChordsMusician.objects.create(name='Player A')
|
|
three = ChordsBand.objects.create(name='The Chords Trio')
|
|
Invitation.objects.create(band=three, player=lead, instrument='guitar')
|
|
Invitation.objects.create(band=three, player=lead, instrument='bass')
|
|
|
|
m = ChordsBandAdmin(ChordsBand, admin.site)
|
|
request = self.factory.get('/chordsband/', data={'members': lead.pk})
|
|
|
|
cl = ChangeList(request, ChordsBand, m.list_display,
|
|
m.list_display_links, m.list_filter, m.date_hierarchy,
|
|
m.search_fields, m.list_select_related, m.list_per_page,
|
|
m.list_max_show_all, m.list_editable, m)
|
|
|
|
cl.get_results(request)
|
|
|
|
# There's only one ChordsBand instance
|
|
self.assertEqual(cl.result_count, 1)
|
|
|
|
def test_distinct_for_non_unique_related_object_in_list_filter(self):
|
|
"""
|
|
Regressions tests for #15819: If a field listed in list_filters
|
|
is a non-unique related object, distinct() must be called.
|
|
"""
|
|
parent = Parent.objects.create(name='Mary')
|
|
# Two children with the same name
|
|
Child.objects.create(parent=parent, name='Daniel')
|
|
Child.objects.create(parent=parent, name='Daniel')
|
|
|
|
m = ParentAdmin(Parent, admin.site)
|
|
request = self.factory.get('/parent/', data={'child__name': 'Daniel'})
|
|
|
|
cl = ChangeList(request, Parent, m.list_display, m.list_display_links,
|
|
m.list_filter, m.date_hierarchy, m.search_fields,
|
|
m.list_select_related, m.list_per_page,
|
|
m.list_max_show_all, m.list_editable, m)
|
|
|
|
# Make sure distinct() was called
|
|
self.assertEqual(cl.queryset.count(), 1)
|
|
|
|
def test_distinct_for_non_unique_related_object_in_search_fields(self):
|
|
"""
|
|
Regressions tests for #15819: If a field listed in search_fields
|
|
is a non-unique related object, distinct() must be called.
|
|
"""
|
|
parent = Parent.objects.create(name='Mary')
|
|
Child.objects.create(parent=parent, name='Danielle')
|
|
Child.objects.create(parent=parent, name='Daniel')
|
|
|
|
m = ParentAdmin(Parent, admin.site)
|
|
request = self.factory.get('/parent/', data={SEARCH_VAR: 'daniel'})
|
|
|
|
cl = ChangeList(request, Parent, m.list_display, m.list_display_links,
|
|
m.list_filter, m.date_hierarchy, m.search_fields,
|
|
m.list_select_related, m.list_per_page,
|
|
m.list_max_show_all, m.list_editable, m)
|
|
|
|
# Make sure distinct() was called
|
|
self.assertEqual(cl.queryset.count(), 1)
|
|
|
|
def test_distinct_for_many_to_many_at_second_level_in_search_fields(self):
|
|
"""
|
|
When using a ManyToMany in search_fields at the second level behind a
|
|
ForeignKey, distinct() must be called and results shouldn't appear more
|
|
than once.
|
|
"""
|
|
lead = Musician.objects.create(name='Vox')
|
|
band = Group.objects.create(name='The Hype')
|
|
Concert.objects.create(name='Woodstock', group=band)
|
|
Membership.objects.create(group=band, music=lead, role='lead voice')
|
|
Membership.objects.create(group=band, music=lead, role='bass player')
|
|
|
|
m = ConcertAdmin(Concert, admin.site)
|
|
request = self.factory.get('/concert/', data={SEARCH_VAR: 'vox'})
|
|
|
|
cl = ChangeList(request, Concert, m.list_display,
|
|
m.list_display_links, m.list_filter, m.date_hierarchy,
|
|
m.search_fields, m.list_select_related, m.list_per_page,
|
|
m.list_max_show_all, m.list_editable, m)
|
|
|
|
# There's only one Concert instance
|
|
self.assertEqual(cl.queryset.count(), 1)
|
|
|
|
def test_pagination(self):
|
|
"""
|
|
Regression tests for #12893: Pagination in admins changelist doesn't
|
|
use queryset set by modeladmin.
|
|
"""
|
|
parent = Parent.objects.create(name='anything')
|
|
for i in range(30):
|
|
Child.objects.create(name='name %s' % i, parent=parent)
|
|
Child.objects.create(name='filtered %s' % i, parent=parent)
|
|
|
|
request = self.factory.get('/child/')
|
|
|
|
# Test default queryset
|
|
m = ChildAdmin(Child, admin.site)
|
|
cl = ChangeList(request, Child, m.list_display, m.list_display_links,
|
|
m.list_filter, m.date_hierarchy, m.search_fields,
|
|
m.list_select_related, m.list_per_page, m.list_max_show_all,
|
|
m.list_editable, m)
|
|
self.assertEqual(cl.queryset.count(), 60)
|
|
self.assertEqual(cl.paginator.count, 60)
|
|
self.assertEqual(list(cl.paginator.page_range), [1, 2, 3, 4, 5, 6])
|
|
|
|
# Test custom queryset
|
|
m = FilteredChildAdmin(Child, admin.site)
|
|
cl = ChangeList(request, Child, m.list_display, m.list_display_links,
|
|
m.list_filter, m.date_hierarchy, m.search_fields,
|
|
m.list_select_related, m.list_per_page, m.list_max_show_all,
|
|
m.list_editable, m)
|
|
self.assertEqual(cl.queryset.count(), 30)
|
|
self.assertEqual(cl.paginator.count, 30)
|
|
self.assertEqual(list(cl.paginator.page_range), [1, 2, 3])
|
|
|
|
def test_computed_list_display_localization(self):
|
|
"""
|
|
Regression test for #13196: output of functions should be localized
|
|
in the changelist.
|
|
"""
|
|
User.objects.create_superuser(
|
|
username='super', email='super@localhost', password='secret')
|
|
self.client.login(username='super', password='secret')
|
|
event = Event.objects.create(date=datetime.date.today())
|
|
response = self.client.get(reverse('admin:admin_changelist_event_changelist'))
|
|
self.assertContains(response, formats.localize(event.date))
|
|
self.assertNotContains(response, six.text_type(event.date))
|
|
|
|
def test_dynamic_list_display(self):
|
|
"""
|
|
Regression tests for #14206: dynamic list_display support.
|
|
"""
|
|
parent = Parent.objects.create(name='parent')
|
|
for i in range(10):
|
|
Child.objects.create(name='child %s' % i, parent=parent)
|
|
|
|
user_noparents = self._create_superuser('noparents')
|
|
user_parents = self._create_superuser('parents')
|
|
|
|
# Test with user 'noparents'
|
|
m = custom_site._registry[Child]
|
|
request = self._mocked_authenticated_request('/child/', user_noparents)
|
|
response = m.changelist_view(request)
|
|
self.assertNotContains(response, 'Parent object')
|
|
|
|
list_display = m.get_list_display(request)
|
|
list_display_links = m.get_list_display_links(request, list_display)
|
|
self.assertEqual(list_display, ['name', 'age'])
|
|
self.assertEqual(list_display_links, ['name'])
|
|
|
|
# Test with user 'parents'
|
|
m = DynamicListDisplayChildAdmin(Child, admin.site)
|
|
request = self._mocked_authenticated_request('/child/', user_parents)
|
|
response = m.changelist_view(request)
|
|
self.assertContains(response, 'Parent object')
|
|
|
|
custom_site.unregister(Child)
|
|
|
|
list_display = m.get_list_display(request)
|
|
list_display_links = m.get_list_display_links(request, list_display)
|
|
self.assertEqual(list_display, ('parent', 'name', 'age'))
|
|
self.assertEqual(list_display_links, ['parent'])
|
|
|
|
# Test default implementation
|
|
custom_site.register(Child, ChildAdmin)
|
|
m = custom_site._registry[Child]
|
|
request = self._mocked_authenticated_request('/child/', user_noparents)
|
|
response = m.changelist_view(request)
|
|
self.assertContains(response, 'Parent object')
|
|
|
|
def test_show_all(self):
|
|
parent = Parent.objects.create(name='anything')
|
|
for i in range(30):
|
|
Child.objects.create(name='name %s' % i, parent=parent)
|
|
Child.objects.create(name='filtered %s' % i, parent=parent)
|
|
|
|
# Add "show all" parameter to request
|
|
request = self.factory.get('/child/', data={ALL_VAR: ''})
|
|
|
|
# Test valid "show all" request (number of total objects is under max)
|
|
m = ChildAdmin(Child, admin.site)
|
|
# 200 is the max we'll pass to ChangeList
|
|
cl = ChangeList(request, Child, m.list_display, m.list_display_links,
|
|
m.list_filter, m.date_hierarchy, m.search_fields,
|
|
m.list_select_related, m.list_per_page, 200, m.list_editable, m)
|
|
cl.get_results(request)
|
|
self.assertEqual(len(cl.result_list), 60)
|
|
|
|
# Test invalid "show all" request (number of total objects over max)
|
|
# falls back to paginated pages
|
|
m = ChildAdmin(Child, admin.site)
|
|
# 30 is the max we'll pass to ChangeList for this test
|
|
cl = ChangeList(request, Child, m.list_display, m.list_display_links,
|
|
m.list_filter, m.date_hierarchy, m.search_fields,
|
|
m.list_select_related, m.list_per_page, 30, m.list_editable, m)
|
|
cl.get_results(request)
|
|
self.assertEqual(len(cl.result_list), 10)
|
|
|
|
def test_dynamic_list_display_links(self):
|
|
"""
|
|
Regression tests for #16257: dynamic list_display_links support.
|
|
"""
|
|
parent = Parent.objects.create(name='parent')
|
|
for i in range(1, 10):
|
|
Child.objects.create(id=i, name='child %s' % i, parent=parent, age=i)
|
|
|
|
m = DynamicListDisplayLinksChildAdmin(Child, admin.site)
|
|
superuser = self._create_superuser('superuser')
|
|
request = self._mocked_authenticated_request('/child/', superuser)
|
|
response = m.changelist_view(request)
|
|
for i in range(1, 10):
|
|
link = reverse('admin:admin_changelist_child_change', args=(i,))
|
|
self.assertContains(response, '<a href="%s">%s</a>' % (link, i))
|
|
|
|
list_display = m.get_list_display(request)
|
|
list_display_links = m.get_list_display_links(request, list_display)
|
|
self.assertEqual(list_display, ('parent', 'name', 'age'))
|
|
self.assertEqual(list_display_links, ['age'])
|
|
|
|
def test_no_list_display_links(self):
|
|
"""#15185 -- Allow no links from the 'change list' view grid."""
|
|
p = Parent.objects.create(name='parent')
|
|
m = NoListDisplayLinksParentAdmin(Parent, admin.site)
|
|
superuser = self._create_superuser('superuser')
|
|
request = self._mocked_authenticated_request('/parent/', superuser)
|
|
response = m.changelist_view(request)
|
|
link = reverse('admin:admin_changelist_parent_change', args=(p.pk,))
|
|
self.assertNotContains(response, '<a href="%s">' % link)
|
|
|
|
def test_tuple_list_display(self):
|
|
"""
|
|
Regression test for #17128
|
|
(ChangeList failing under Python 2.5 after r16319)
|
|
"""
|
|
swallow = Swallow.objects.create(
|
|
origin='Africa', load='12.34', speed='22.2')
|
|
model_admin = SwallowAdmin(Swallow, admin.site)
|
|
superuser = self._create_superuser('superuser')
|
|
request = self._mocked_authenticated_request('/swallow/', superuser)
|
|
response = model_admin.changelist_view(request)
|
|
# just want to ensure it doesn't blow up during rendering
|
|
self.assertContains(response, six.text_type(swallow.origin))
|
|
self.assertContains(response, six.text_type(swallow.load))
|
|
self.assertContains(response, six.text_type(swallow.speed))
|
|
|
|
def test_deterministic_order_for_unordered_model(self):
|
|
"""
|
|
Ensure that the primary key is systematically used in the ordering of
|
|
the changelist's results to guarantee a deterministic order, even
|
|
when the Model doesn't have any default ordering defined.
|
|
Refs #17198.
|
|
"""
|
|
superuser = self._create_superuser('superuser')
|
|
|
|
for counter in range(1, 51):
|
|
UnorderedObject.objects.create(id=counter, bool=True)
|
|
|
|
class UnorderedObjectAdmin(admin.ModelAdmin):
|
|
list_per_page = 10
|
|
|
|
def check_results_order(ascending=False):
|
|
admin.site.register(UnorderedObject, UnorderedObjectAdmin)
|
|
model_admin = UnorderedObjectAdmin(UnorderedObject, admin.site)
|
|
counter = 0 if ascending else 51
|
|
for page in range(0, 5):
|
|
request = self._mocked_authenticated_request('/unorderedobject/?p=%s' % page, superuser)
|
|
response = model_admin.changelist_view(request)
|
|
for result in response.context_data['cl'].result_list:
|
|
counter += 1 if ascending else -1
|
|
self.assertEqual(result.id, counter)
|
|
admin.site.unregister(UnorderedObject)
|
|
|
|
# When no order is defined at all, everything is ordered by '-pk'.
|
|
check_results_order()
|
|
|
|
# When an order field is defined but multiple records have the same
|
|
# value for that field, make sure everything gets ordered by -pk as well.
|
|
UnorderedObjectAdmin.ordering = ['bool']
|
|
check_results_order()
|
|
|
|
# When order fields are defined, including the pk itself, use them.
|
|
UnorderedObjectAdmin.ordering = ['bool', '-pk']
|
|
check_results_order()
|
|
UnorderedObjectAdmin.ordering = ['bool', 'pk']
|
|
check_results_order(ascending=True)
|
|
UnorderedObjectAdmin.ordering = ['-id', 'bool']
|
|
check_results_order()
|
|
UnorderedObjectAdmin.ordering = ['id', 'bool']
|
|
check_results_order(ascending=True)
|
|
|
|
def test_deterministic_order_for_model_ordered_by_its_manager(self):
|
|
"""
|
|
Ensure that the primary key is systematically used in the ordering of
|
|
the changelist's results to guarantee a deterministic order, even
|
|
when the Model has a manager that defines a default ordering.
|
|
Refs #17198.
|
|
"""
|
|
superuser = self._create_superuser('superuser')
|
|
|
|
for counter in range(1, 51):
|
|
OrderedObject.objects.create(id=counter, bool=True, number=counter)
|
|
|
|
class OrderedObjectAdmin(admin.ModelAdmin):
|
|
list_per_page = 10
|
|
|
|
def check_results_order(ascending=False):
|
|
admin.site.register(OrderedObject, OrderedObjectAdmin)
|
|
model_admin = OrderedObjectAdmin(OrderedObject, admin.site)
|
|
counter = 0 if ascending else 51
|
|
for page in range(0, 5):
|
|
request = self._mocked_authenticated_request('/orderedobject/?p=%s' % page, superuser)
|
|
response = model_admin.changelist_view(request)
|
|
for result in response.context_data['cl'].result_list:
|
|
counter += 1 if ascending else -1
|
|
self.assertEqual(result.id, counter)
|
|
admin.site.unregister(OrderedObject)
|
|
|
|
# When no order is defined at all, use the model's default ordering (i.e. 'number')
|
|
check_results_order(ascending=True)
|
|
|
|
# When an order field is defined but multiple records have the same
|
|
# value for that field, make sure everything gets ordered by -pk as well.
|
|
OrderedObjectAdmin.ordering = ['bool']
|
|
check_results_order()
|
|
|
|
# When order fields are defined, including the pk itself, use them.
|
|
OrderedObjectAdmin.ordering = ['bool', '-pk']
|
|
check_results_order()
|
|
OrderedObjectAdmin.ordering = ['bool', 'pk']
|
|
check_results_order(ascending=True)
|
|
OrderedObjectAdmin.ordering = ['-id', 'bool']
|
|
check_results_order()
|
|
OrderedObjectAdmin.ordering = ['id', 'bool']
|
|
check_results_order(ascending=True)
|
|
|
|
def test_dynamic_list_filter(self):
|
|
"""
|
|
Regression tests for ticket #17646: dynamic list_filter support.
|
|
"""
|
|
parent = Parent.objects.create(name='parent')
|
|
for i in range(10):
|
|
Child.objects.create(name='child %s' % i, parent=parent)
|
|
|
|
user_noparents = self._create_superuser('noparents')
|
|
user_parents = self._create_superuser('parents')
|
|
|
|
# Test with user 'noparents'
|
|
m = DynamicListFilterChildAdmin(Child, admin.site)
|
|
request = self._mocked_authenticated_request('/child/', user_noparents)
|
|
response = m.changelist_view(request)
|
|
self.assertEqual(response.context_data['cl'].list_filter, ['name', 'age'])
|
|
|
|
# Test with user 'parents'
|
|
m = DynamicListFilterChildAdmin(Child, admin.site)
|
|
request = self._mocked_authenticated_request('/child/', user_parents)
|
|
response = m.changelist_view(request)
|
|
self.assertEqual(response.context_data['cl'].list_filter, ('parent', 'name', 'age'))
|
|
|
|
def test_dynamic_search_fields(self):
|
|
child = self._create_superuser('child')
|
|
m = DynamicSearchFieldsChildAdmin(Child, admin.site)
|
|
request = self._mocked_authenticated_request('/child/', child)
|
|
response = m.changelist_view(request)
|
|
self.assertEqual(response.context_data['cl'].search_fields, ('name', 'age'))
|
|
|
|
def test_pagination_page_range(self):
|
|
"""
|
|
Regression tests for ticket #15653: ensure the number of pages
|
|
generated for changelist views are correct.
|
|
"""
|
|
# instantiating and setting up ChangeList object
|
|
m = GroupAdmin(Group, admin.site)
|
|
request = self.factory.get('/group/')
|
|
cl = ChangeList(request, Group, m.list_display,
|
|
m.list_display_links, m.list_filter, m.date_hierarchy,
|
|
m.search_fields, m.list_select_related, m.list_per_page,
|
|
m.list_max_show_all, m.list_editable, m)
|
|
per_page = cl.list_per_page = 10
|
|
|
|
for page_num, objects_count, expected_page_range in [
|
|
(0, per_page, []),
|
|
(0, per_page * 2, list(range(2))),
|
|
(5, per_page * 11, list(range(11))),
|
|
(5, per_page * 12, [0, 1, 2, 3, 4, 5, 6, 7, 8, '.', 10, 11]),
|
|
(6, per_page * 12, [0, 1, '.', 3, 4, 5, 6, 7, 8, 9, 10, 11]),
|
|
(6, per_page * 13, [0, 1, '.', 3, 4, 5, 6, 7, 8, 9, '.', 11, 12]),
|
|
]:
|
|
# assuming we have exactly `objects_count` objects
|
|
Group.objects.all().delete()
|
|
for i in range(objects_count):
|
|
Group.objects.create(name='test band')
|
|
|
|
# setting page number and calculating page range
|
|
cl.page_num = page_num
|
|
cl.get_results(request)
|
|
real_page_range = pagination(cl)['page_range']
|
|
|
|
self.assertListEqual(
|
|
expected_page_range,
|
|
list(real_page_range),
|
|
)
|
|
|
|
|
|
class AdminLogNodeTestCase(TestCase):
|
|
|
|
def test_get_admin_log_templatetag_custom_user(self):
|
|
"""
|
|
Regression test for ticket #20088: admin log depends on User model
|
|
having id field as primary key.
|
|
|
|
The old implementation raised an AttributeError when trying to use
|
|
the id field.
|
|
"""
|
|
context = Context({'user': CustomIdUser()})
|
|
template_string = '{% load log %}{% get_admin_log 10 as admin_log for_user user %}'
|
|
|
|
template = Template(template_string)
|
|
|
|
# Rendering should be u'' since this templatetag just logs,
|
|
# it doesn't render any string.
|
|
self.assertEqual(template.render(context), '')
|
|
|
|
def test_get_admin_log_templatetag_no_user(self):
|
|
"""
|
|
The {% get_admin_log %} tag should work without specifying a user.
|
|
"""
|
|
user = User(username='jondoe', password='secret', email='super@example.com')
|
|
user.save()
|
|
ct = ContentType.objects.get_for_model(User)
|
|
LogEntry.objects.log_action(user.pk, ct.pk, user.pk, repr(user), 1)
|
|
|
|
t = Template(
|
|
'{% load log %}'
|
|
'{% get_admin_log 100 as admin_log %}'
|
|
'{% for entry in admin_log %}'
|
|
'{{ entry|safe }}'
|
|
'{% endfor %}'
|
|
)
|
|
self.assertEqual(t.render(Context({})), 'Added "<User: jondoe>".')
|
|
|
|
|
|
@override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'],
|
|
ROOT_URLCONF="admin_changelist.urls")
|
|
class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
|
|
|
|
available_apps = ['admin_changelist'] + AdminSeleniumWebDriverTestCase.available_apps
|
|
webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
|
|
|
|
def setUp(self):
|
|
# password = "secret"
|
|
User.objects.create(
|
|
pk=100, username='super', first_name='Super', last_name='User', email='super@example.com',
|
|
password='sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158', is_active=True, is_superuser=True,
|
|
is_staff=True, last_login=datetime.datetime(2007, 5, 30, 13, 20, 10),
|
|
date_joined=datetime.datetime(2007, 5, 30, 13, 20, 10)
|
|
)
|
|
|
|
def test_add_row_selection(self):
|
|
"""
|
|
Ensure that the status line for selected rows gets updated correcly (#22038)
|
|
"""
|
|
self.admin_login(username='super', password='secret')
|
|
self.selenium.get('%s%s' % (self.live_server_url,
|
|
reverse('admin:auth_user_changelist')))
|
|
|
|
form_id = '#changelist-form'
|
|
|
|
# Test amount of rows in the Changelist
|
|
rows = self.selenium.find_elements_by_css_selector(
|
|
'%s #result_list tbody tr' % form_id)
|
|
self.assertEqual(len(rows), 1)
|
|
|
|
# Test current selection
|
|
selection_indicator = self.selenium.find_element_by_css_selector(
|
|
'%s .action-counter' % form_id)
|
|
self.assertEqual(selection_indicator.text, "0 of 1 selected")
|
|
|
|
# Select a row and check again
|
|
row_selector = self.selenium.find_element_by_css_selector(
|
|
'%s #result_list tbody tr:first-child .action-select' % form_id)
|
|
row_selector.click()
|
|
self.assertEqual(selection_indicator.text, "1 of 1 selected")
|
|
|
|
|
|
class SeleniumChromeTests(SeleniumFirefoxTests):
|
|
webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver'
|
|
|
|
|
|
class SeleniumIETests(SeleniumFirefoxTests):
|
|
webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver'
|