From 560c2a31baea4ecc64de9b02f9717ed19ae8d519 Mon Sep 17 00:00:00 2001
From: a <455919189@qq.com>
Date: Tue, 17 May 2022 21:06:37 +0800
Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?=
=?UTF-8?q?=20'test/Django-2.1.15/tests/admin=5Fchangelist'?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../tests/admin_changelist/__init__.py | Bin 0 -> 1024 bytes
.../tests/admin_changelist/admin.py | 149 +++
.../tests/admin_changelist/models.py | 121 ++
.../admin_changelist/test_date_hierarchy.py | 61 +
.../tests/admin_changelist/tests.py | 1100 +++++++++++++++++
5 files changed, 1431 insertions(+)
create mode 100644 test/Django-2.1.15/tests/admin_changelist/__init__.py
create mode 100644 test/Django-2.1.15/tests/admin_changelist/admin.py
create mode 100644 test/Django-2.1.15/tests/admin_changelist/models.py
create mode 100644 test/Django-2.1.15/tests/admin_changelist/test_date_hierarchy.py
create mode 100644 test/Django-2.1.15/tests/admin_changelist/tests.py
diff --git a/test/Django-2.1.15/tests/admin_changelist/__init__.py b/test/Django-2.1.15/tests/admin_changelist/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..06d7405020018ddf3cacee90fd4af10487da3d20
GIT binary patch
literal 1024
ScmZQz7zLvtFd70QH3R?z00031
literal 0
HcmV?d00001
diff --git a/test/Django-2.1.15/tests/admin_changelist/admin.py b/test/Django-2.1.15/tests/admin_changelist/admin.py
new file mode 100644
index 0000000..1e686bd
--- /dev/null
+++ b/test/Django-2.1.15/tests/admin_changelist/admin.py
@@ -0,0 +1,149 @@
+from django.contrib import admin
+from django.contrib.auth.admin import UserAdmin
+from django.contrib.auth.models import User
+from django.core.paginator import Paginator
+
+from .models import Child, Event, Parent, Swallow
+
+site = admin.AdminSite(name="admin")
+
+site.register(User, UserAdmin)
+
+
+class CustomPaginator(Paginator):
+ def __init__(self, queryset, page_size, orphans=0, allow_empty_first_page=True):
+ super().__init__(queryset, 5, orphans=2, allow_empty_first_page=allow_empty_first_page)
+
+
+class EventAdmin(admin.ModelAdmin):
+ date_hierarchy = 'date'
+ list_display = ['event_date_func']
+
+ def event_date_func(self, event):
+ return event.date
+
+ def has_add_permission(self, request):
+ return False
+
+
+site.register(Event, EventAdmin)
+
+
+class ParentAdmin(admin.ModelAdmin):
+ list_filter = ['child__name']
+ search_fields = ['child__name']
+
+
+class ChildAdmin(admin.ModelAdmin):
+ list_display = ['name', 'parent']
+ list_per_page = 10
+ list_filter = ['parent', 'age']
+
+ def get_queryset(self, request):
+ return super().get_queryset(request).select_related("parent")
+
+
+class CustomPaginationAdmin(ChildAdmin):
+ paginator = CustomPaginator
+
+
+class FilteredChildAdmin(admin.ModelAdmin):
+ list_display = ['name', 'parent']
+ list_per_page = 10
+
+ def get_queryset(self, request):
+ return super().get_queryset(request).filter(name__contains='filtered')
+
+
+class BandAdmin(admin.ModelAdmin):
+ list_filter = ['genres']
+
+
+class GroupAdmin(admin.ModelAdmin):
+ list_filter = ['members']
+
+
+class ConcertAdmin(admin.ModelAdmin):
+ list_filter = ['group__members']
+ search_fields = ['group__members__name']
+
+
+class QuartetAdmin(admin.ModelAdmin):
+ list_filter = ['members']
+
+
+class ChordsBandAdmin(admin.ModelAdmin):
+ list_filter = ['members']
+
+
+class InvitationAdmin(admin.ModelAdmin):
+ list_display = ('band', 'player')
+ list_select_related = ('player',)
+
+
+class DynamicListDisplayChildAdmin(admin.ModelAdmin):
+ list_display = ('parent', 'name', 'age')
+
+ def get_list_display(self, request):
+ my_list_display = super().get_list_display(request)
+ if request.user.username == 'noparents':
+ my_list_display = list(my_list_display)
+ my_list_display.remove('parent')
+ return my_list_display
+
+
+class DynamicListDisplayLinksChildAdmin(admin.ModelAdmin):
+ list_display = ('parent', 'name', 'age')
+ list_display_links = ['parent', 'name']
+
+ def get_list_display_links(self, request, list_display):
+ return ['age']
+
+
+site.register(Child, DynamicListDisplayChildAdmin)
+
+
+class NoListDisplayLinksParentAdmin(admin.ModelAdmin):
+ list_display_links = None
+
+
+site.register(Parent, NoListDisplayLinksParentAdmin)
+
+
+class SwallowAdmin(admin.ModelAdmin):
+ actions = None # prevent ['action_checkbox'] + list(list_display)
+ list_display = ('origin', 'load', 'speed', 'swallowonetoone')
+ list_editable = ['load', 'speed']
+ list_per_page = 3
+
+
+site.register(Swallow, SwallowAdmin)
+
+
+class DynamicListFilterChildAdmin(admin.ModelAdmin):
+ list_filter = ('parent', 'name', 'age')
+
+ def get_list_filter(self, request):
+ my_list_filter = super().get_list_filter(request)
+ if request.user.username == 'noparents':
+ my_list_filter = list(my_list_filter)
+ my_list_filter.remove('parent')
+ return my_list_filter
+
+
+class DynamicSearchFieldsChildAdmin(admin.ModelAdmin):
+ search_fields = ('name',)
+
+ def get_search_fields(self, request):
+ search_fields = super().get_search_fields(request)
+ search_fields += ('age',)
+ return search_fields
+
+
+class EmptyValueChildAdmin(admin.ModelAdmin):
+ empty_value_display = '-empty-'
+ list_display = ('name', 'age_display', 'age')
+
+ def age_display(self, obj):
+ return obj.age
+ age_display.empty_value_display = '†'
diff --git a/test/Django-2.1.15/tests/admin_changelist/models.py b/test/Django-2.1.15/tests/admin_changelist/models.py
new file mode 100644
index 0000000..81d7fdf
--- /dev/null
+++ b/test/Django-2.1.15/tests/admin_changelist/models.py
@@ -0,0 +1,121 @@
+import uuid
+
+from django.db import models
+
+
+class Event(models.Model):
+ # Oracle can have problems with a column named "date"
+ date = models.DateField(db_column="event_date")
+
+
+class Parent(models.Model):
+ name = models.CharField(max_length=128)
+
+
+class Child(models.Model):
+ parent = models.ForeignKey(Parent, models.SET_NULL, editable=False, null=True)
+ name = models.CharField(max_length=30, blank=True)
+ age = models.IntegerField(null=True, blank=True)
+
+
+class Genre(models.Model):
+ name = models.CharField(max_length=20)
+
+
+class Band(models.Model):
+ name = models.CharField(max_length=20)
+ nr_of_members = models.PositiveIntegerField()
+ genres = models.ManyToManyField(Genre)
+
+
+class Musician(models.Model):
+ name = models.CharField(max_length=30)
+ age = models.IntegerField(null=True, blank=True)
+
+ def __str__(self):
+ return self.name
+
+
+class Group(models.Model):
+ name = models.CharField(max_length=30)
+ members = models.ManyToManyField(Musician, through='Membership')
+
+ def __str__(self):
+ return self.name
+
+
+class Concert(models.Model):
+ name = models.CharField(max_length=30)
+ group = models.ForeignKey(Group, models.CASCADE)
+
+
+class Membership(models.Model):
+ music = models.ForeignKey(Musician, models.CASCADE)
+ group = models.ForeignKey(Group, models.CASCADE)
+ role = models.CharField(max_length=15)
+
+
+class Quartet(Group):
+ pass
+
+
+class ChordsMusician(Musician):
+ pass
+
+
+class ChordsBand(models.Model):
+ name = models.CharField(max_length=30)
+ members = models.ManyToManyField(ChordsMusician, through='Invitation')
+
+
+class Invitation(models.Model):
+ player = models.ForeignKey(ChordsMusician, models.CASCADE)
+ band = models.ForeignKey(ChordsBand, models.CASCADE)
+ instrument = models.CharField(max_length=15)
+
+
+class Swallow(models.Model):
+ uuid = models.UUIDField(primary_key=True, default=uuid.uuid4)
+ origin = models.CharField(max_length=255)
+ load = models.FloatField()
+ speed = models.FloatField()
+
+ class Meta:
+ ordering = ('speed', 'load')
+
+
+class SwallowOneToOne(models.Model):
+ swallow = models.OneToOneField(Swallow, models.CASCADE)
+
+
+class UnorderedObject(models.Model):
+ """
+ Model without any defined `Meta.ordering`.
+ Refs #17198.
+ """
+ bool = models.BooleanField(default=True)
+
+
+class OrderedObjectManager(models.Manager):
+ def get_queryset(self):
+ return super().get_queryset().order_by('number')
+
+
+class OrderedObject(models.Model):
+ """
+ Model with Manager that defines a default order.
+ Refs #17198.
+ """
+ name = models.CharField(max_length=255)
+ bool = models.BooleanField(default=True)
+ number = models.IntegerField(default=0, db_column='number_val')
+
+ objects = OrderedObjectManager()
+
+
+class CustomIdUser(models.Model):
+ uuid = models.AutoField(primary_key=True)
+
+
+class CharPK(models.Model):
+ char_pk = models.CharField(max_length=100, primary_key=True)
diff --git a/test/Django-2.1.15/tests/admin_changelist/test_date_hierarchy.py b/test/Django-2.1.15/tests/admin_changelist/test_date_hierarchy.py
new file mode 100644
index 0000000..f19e38f
--- /dev/null
+++ b/test/Django-2.1.15/tests/admin_changelist/test_date_hierarchy.py
@@ -0,0 +1,61 @@
+from datetime import datetime
+
+from django.contrib.admin.options import IncorrectLookupParameters
+from django.contrib.auth.models import User
+from django.test import RequestFactory, TestCase
+from django.utils.timezone import make_aware
+
+from .admin import EventAdmin, site as custom_site
+from .models import Event
+
+
+class DateHierarchyTests(TestCase):
+ factory = RequestFactory()
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.superuser = User.objects.create_superuser(username='super', email='a@b.com', password='xxx')
+
+ def assertDateParams(self, query, expected_from_date, expected_to_date):
+ query = {'date__%s' % field: val for field, val in query.items()}
+ request = self.factory.get('/', query)
+ request.user = self.superuser
+ changelist = EventAdmin(Event, custom_site).get_changelist_instance(request)
+ _, _, lookup_params, _ = changelist.get_filters(request)
+ self.assertEqual(lookup_params['date__gte'], expected_from_date)
+ self.assertEqual(lookup_params['date__lt'], expected_to_date)
+
+ def test_bounded_params(self):
+ tests = (
+ ({'year': 2017}, datetime(2017, 1, 1), datetime(2018, 1, 1)),
+ ({'year': 2017, 'month': 2}, datetime(2017, 2, 1), datetime(2017, 3, 1)),
+ ({'year': 2017, 'month': 12}, datetime(2017, 12, 1), datetime(2018, 1, 1)),
+ ({'year': 2017, 'month': 12, 'day': 15}, datetime(2017, 12, 15), datetime(2017, 12, 16)),
+ ({'year': 2017, 'month': 12, 'day': 31}, datetime(2017, 12, 31), datetime(2018, 1, 1)),
+ ({'year': 2017, 'month': 2, 'day': 28}, datetime(2017, 2, 28), datetime(2017, 3, 1)),
+ )
+ for query, expected_from_date, expected_to_date in tests:
+ with self.subTest(query=query):
+ self.assertDateParams(query, expected_from_date, expected_to_date)
+
+ def test_bounded_params_with_time_zone(self):
+ with self.settings(USE_TZ=True, TIME_ZONE='Asia/Jerusalem'):
+ self.assertDateParams(
+ {'year': 2017, 'month': 2, 'day': 28},
+ make_aware(datetime(2017, 2, 28)),
+ make_aware(datetime(2017, 3, 1)),
+ )
+
+ def test_invalid_params(self):
+ tests = (
+ {'year': 'x'},
+ {'year': 2017, 'month': 'x'},
+ {'year': 2017, 'month': 12, 'day': 'x'},
+ {'year': 2017, 'month': 13},
+ {'year': 2017, 'month': 12, 'day': 32},
+ {'year': 2017, 'month': 0},
+ {'year': 2017, 'month': 12, 'day': 0},
+ )
+ for invalid_query in tests:
+ with self.subTest(query=invalid_query), self.assertRaises(IncorrectLookupParameters):
+ self.assertDateParams(invalid_query, None, None)
diff --git a/test/Django-2.1.15/tests/admin_changelist/tests.py b/test/Django-2.1.15/tests/admin_changelist/tests.py
new file mode 100644
index 0000000..c12bfc7
--- /dev/null
+++ b/test/Django-2.1.15/tests/admin_changelist/tests.py
@@ -0,0 +1,1100 @@
+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 AdminSeleniumTestCase
+from django.contrib.admin.views.main import ALL_VAR, SEARCH_VAR
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.db import connection
+from django.db.models import F
+from django.db.models.fields import Field, IntegerField
+from django.db.models.functions import Upper
+from django.db.models.lookups import Contains, Exact
+from django.template import Context, Template, TemplateSyntaxError
+from django.test import TestCase, override_settings
+from django.test.client import RequestFactory
+from django.test.utils import CaptureQueriesContext
+from django.urls import reverse
+from django.utils import formats
+
+from .admin import (
+ BandAdmin, ChildAdmin, ChordsBandAdmin, ConcertAdmin,
+ CustomPaginationAdmin, CustomPaginator, DynamicListDisplayChildAdmin,
+ DynamicListDisplayLinksChildAdmin, DynamicListFilterChildAdmin,
+ DynamicSearchFieldsChildAdmin, EmptyValueChildAdmin, EventAdmin,
+ FilteredChildAdmin, GroupAdmin, InvitationAdmin,
+ NoListDisplayLinksParentAdmin, ParentAdmin, QuartetAdmin, SwallowAdmin,
+ site as custom_site,
+)
+from .models import (
+ Band, CharPK, Child, ChordsBand, ChordsMusician, Concert, CustomIdUser,
+ Event, Genre, Group, Invitation, Membership, Musician, OrderedObject,
+ Parent, Quartet, Swallow, SwallowOneToOne, UnorderedObject,
+)
+
+
+def build_tbody_html(pk, href, extra_fields):
+ return (
+ '
'
+ ''
+ ' | '
+ 'name | '
+ '{}
'
+ ).format(pk, href, extra_fields)
+
+
+@override_settings(ROOT_URLCONF="admin_changelist.urls")
+class ChangeListTests(TestCase):
+
+ def setUp(self):
+ self.factory = RequestFactory()
+ self.superuser = User.objects.create_superuser(username='super', email='a@b.com', password='xxx')
+
+ def _create_superuser(self, username):
+ return User.objects.create_superuser(username=username, email='a@b.com', password='xxx')
+
+ def _mocked_authenticated_request(self, url, user):
+ request = self.factory.get(url)
+ request.user = user
+ return request
+
+ def test_specified_ordering_by_f_expression(self):
+ class OrderedByFBandAdmin(admin.ModelAdmin):
+ list_display = ['name', 'genres', 'nr_of_members']
+ ordering = (
+ F('nr_of_members').desc(nulls_last=True),
+ Upper(F('name')).asc(),
+ F('genres').asc(),
+ )
+
+ m = OrderedByFBandAdmin(Band, custom_site)
+ request = self.factory.get('/band/')
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertEqual(cl.get_ordering_field_columns(), {3: 'desc', 2: 'asc'})
+
+ def test_specified_ordering_by_f_expression_without_asc_desc(self):
+ class OrderedByFBandAdmin(admin.ModelAdmin):
+ list_display = ['name', 'genres', 'nr_of_members']
+ ordering = (F('nr_of_members'), Upper('name'), F('genres'))
+
+ m = OrderedByFBandAdmin(Band, custom_site)
+ request = self.factory.get('/band/')
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertEqual(cl.get_ordering_field_columns(), {3: 'asc', 2: 'asc'})
+
+ 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, custom_site)
+ request = self.factory.get('/child/')
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertEqual(cl.queryset.query.select_related, {'parent': {}})
+
+ def test_select_related_as_tuple(self):
+ ia = InvitationAdmin(Invitation, custom_site)
+ request = self.factory.get('/invitation/')
+ request.user = self.superuser
+ cl = ia.get_changelist_instance(request)
+ self.assertEqual(cl.queryset.query.select_related, {'player': {}})
+
+ def test_select_related_as_empty_tuple(self):
+ ia = InvitationAdmin(Invitation, custom_site)
+ ia.list_select_related = ()
+ request = self.factory.get('/invitation/')
+ request.user = self.superuser
+ cl = ia.get_changelist_instance(request)
+ self.assertIs(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, custom_site)
+ request = self.factory.get('/invitation/')
+ request.user = self.superuser
+ cl = ia.get_changelist_instance(request)
+ 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/')
+ request.user = self.superuser
+ m = ChildAdmin(Child, custom_site)
+ cl = m.get_changelist_instance(request)
+ cl.formset = None
+ template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
+ context = Context({'cl': cl, 'opts': Child._meta})
+ table_output = template.render(context)
+ link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
+ row_html = build_tbody_html(new_child.id, link, '- | ')
+ self.assertNotEqual(table_output.find(row_html), -1, 'Failed to find expected row element: %s' % table_output)
+
+ def test_result_list_set_empty_value_display_on_admin_site(self):
+ """
+ Empty value display can be set on AdminSite.
+ """
+ new_child = Child.objects.create(name='name', parent=None)
+ request = self.factory.get('/child/')
+ request.user = self.superuser
+ # Set a new empty display value on AdminSite.
+ admin.site.empty_value_display = '???'
+ m = ChildAdmin(Child, admin.site)
+ cl = m.get_changelist_instance(request)
+ cl.formset = None
+ template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
+ context = Context({'cl': cl, 'opts': Child._meta})
+ table_output = template.render(context)
+ link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
+ row_html = build_tbody_html(new_child.id, link, '??? | ')
+ self.assertNotEqual(table_output.find(row_html), -1, 'Failed to find expected row element: %s' % table_output)
+
+ def test_result_list_set_empty_value_display_in_model_admin(self):
+ """
+ Empty value display can be set in ModelAdmin or individual fields.
+ """
+ new_child = Child.objects.create(name='name', parent=None)
+ request = self.factory.get('/child/')
+ request.user = self.superuser
+ m = EmptyValueChildAdmin(Child, admin.site)
+ cl = m.get_changelist_instance(request)
+ cl.formset = None
+ template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
+ context = Context({'cl': cl, 'opts': Child._meta})
+ table_output = template.render(context)
+ link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
+ row_html = build_tbody_html(
+ new_child.id,
+ link,
+ '† | '
+ '-empty- | '
+ )
+ self.assertNotEqual(table_output.find(row_html), -1, 'Failed to find expected row element: %s' % table_output)
+
+ def test_result_list_html(self):
+ """
+ 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/')
+ request.user = self.superuser
+ m = ChildAdmin(Child, custom_site)
+ cl = m.get_changelist_instance(request)
+ cl.formset = None
+ template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
+ context = Context({'cl': cl, 'opts': Child._meta})
+ table_output = template.render(context)
+ link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
+ row_html = build_tbody_html(new_child.id, link, '%s | ' % new_parent)
+ 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/')
+ request.user = self.superuser
+ m = ChildAdmin(Child, custom_site)
+
+ # Test with list_editable fields
+ m.list_display = ['id', 'name', 'parent']
+ m.list_display_links = ['id']
+ m.list_editable = ['name']
+ cl = m.get_changelist_instance(request)
+ 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, 'opts': Child._meta})
+ table_output = template.render(context)
+ # make sure that hidden fields are in the correct place
+ hiddenfields_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 = (
+ ''
+ )
+ self.assertInHTML(
+ '%s | ' % 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
+ request.user = self.superuser
+ m = ChildAdmin(Child, custom_site)
+
+ # Test with list_editable fields
+ m.list_display = ['id', 'name', 'parent']
+ m.list_display_links = ['id']
+ m.list_editable = ['name']
+ with self.assertRaises(IncorrectLookupParameters):
+ m.get_changelist_instance(request)
+
+ 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/')
+ request.user = self.superuser
+ m = CustomPaginationAdmin(Child, custom_site)
+
+ cl = m.get_changelist_instance(request)
+ 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, custom_site)
+ request = self.factory.get('/band/', data={'genres': blues.pk})
+ request.user = self.superuser
+
+ cl = m.get_changelist_instance(request)
+ 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, custom_site)
+ request = self.factory.get('/group/', data={'members': lead.pk})
+ request.user = self.superuser
+
+ cl = m.get_changelist_instance(request)
+ 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, custom_site)
+ request = self.factory.get('/concert/', data={'group__members': lead.pk})
+ request.user = self.superuser
+
+ cl = m.get_changelist_instance(request)
+ 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 defines 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, custom_site)
+ request = self.factory.get('/quartet/', data={'members': lead.pk})
+ request.user = self.superuser
+
+ cl = m.get_changelist_instance(request)
+ 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, custom_site)
+ request = self.factory.get('/chordsband/', data={'members': lead.pk})
+ request.user = self.superuser
+
+ cl = m.get_changelist_instance(request)
+ 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, custom_site)
+ request = self.factory.get('/parent/', data={'child__name': 'Daniel'})
+ request.user = self.superuser
+
+ cl = m.get_changelist_instance(request)
+ # 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, custom_site)
+ request = self.factory.get('/parent/', data={SEARCH_VAR: 'daniel'})
+ request.user = self.superuser
+
+ cl = m.get_changelist_instance(request)
+ # 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, custom_site)
+ request = self.factory.get('/concert/', data={SEARCH_VAR: 'vox'})
+ request.user = self.superuser
+
+ cl = m.get_changelist_instance(request)
+ # There's only one Concert instance
+ self.assertEqual(cl.queryset.count(), 1)
+
+ def test_pk_in_search_fields(self):
+ band = Group.objects.create(name='The Hype')
+ Concert.objects.create(name='Woodstock', group=band)
+
+ m = ConcertAdmin(Concert, custom_site)
+ m.search_fields = ['group__pk']
+
+ request = self.factory.get('/concert/', data={SEARCH_VAR: band.pk})
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertEqual(cl.queryset.count(), 1)
+
+ request = self.factory.get('/concert/', data={SEARCH_VAR: band.pk + 5})
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertEqual(cl.queryset.count(), 0)
+
+ def test_builtin_lookup_in_search_fields(self):
+ band = Group.objects.create(name='The Hype')
+ concert = Concert.objects.create(name='Woodstock', group=band)
+
+ m = ConcertAdmin(Concert, custom_site)
+ m.search_fields = ['name__iexact']
+
+ request = self.factory.get('/', data={SEARCH_VAR: 'woodstock'})
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertCountEqual(cl.queryset, [concert])
+
+ request = self.factory.get('/', data={SEARCH_VAR: 'wood'})
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertCountEqual(cl.queryset, [])
+
+ def test_custom_lookup_in_search_fields(self):
+ band = Group.objects.create(name='The Hype')
+ concert = Concert.objects.create(name='Woodstock', group=band)
+
+ m = ConcertAdmin(Concert, custom_site)
+ m.search_fields = ['group__name__cc']
+ Field.register_lookup(Contains, 'cc')
+ try:
+ request = self.factory.get('/', data={SEARCH_VAR: 'Hype'})
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertCountEqual(cl.queryset, [concert])
+
+ request = self.factory.get('/', data={SEARCH_VAR: 'Woodstock'})
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertCountEqual(cl.queryset, [])
+ finally:
+ Field._unregister_lookup(Contains, 'cc')
+
+ def test_spanning_relations_with_custom_lookup_in_search_fields(self):
+ hype = Group.objects.create(name='The Hype')
+ concert = Concert.objects.create(name='Woodstock', group=hype)
+ vox = Musician.objects.create(name='Vox', age=20)
+ Membership.objects.create(music=vox, group=hype)
+ # Register a custom lookup on IntegerField to ensure that field
+ # traversing logic in ModelAdmin.get_search_results() works.
+ IntegerField.register_lookup(Exact, 'exactly')
+ try:
+ m = ConcertAdmin(Concert, custom_site)
+ m.search_fields = ['group__members__age__exactly']
+
+ request = self.factory.get('/', data={SEARCH_VAR: '20'})
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertCountEqual(cl.queryset, [concert])
+
+ request = self.factory.get('/', data={SEARCH_VAR: '21'})
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertCountEqual(cl.queryset, [])
+ finally:
+ IntegerField._unregister_lookup(Exact, 'exactly')
+
+ def test_custom_lookup_with_pk_shortcut(self):
+ self.assertEqual(CharPK._meta.pk.name, 'char_pk') # Not equal to 'pk'.
+ m = admin.ModelAdmin(CustomIdUser, custom_site)
+
+ abc = CharPK.objects.create(char_pk='abc')
+ abcd = CharPK.objects.create(char_pk='abcd')
+ m = admin.ModelAdmin(CharPK, custom_site)
+ m.search_fields = ['pk__exact']
+
+ request = self.factory.get('/', data={SEARCH_VAR: 'abc'})
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertCountEqual(cl.queryset, [abc])
+
+ request = self.factory.get('/', data={SEARCH_VAR: 'abcd'})
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertCountEqual(cl.queryset, [abcd])
+
+ def test_no_distinct_for_m2m_in_list_filter_without_params(self):
+ """
+ If a ManyToManyField is in list_filter but isn't in any lookup params,
+ the changelist's query shouldn't have distinct.
+ """
+ m = BandAdmin(Band, custom_site)
+ for lookup_params in ({}, {'name': 'test'}):
+ request = self.factory.get('/band/', lookup_params)
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertFalse(cl.queryset.query.distinct)
+
+ # A ManyToManyField in params does have distinct applied.
+ request = self.factory.get('/band/', {'genres': '0'})
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertTrue(cl.queryset.query.distinct)
+
+ 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/')
+ request.user = self.superuser
+
+ # Test default queryset
+ m = ChildAdmin(Child, custom_site)
+ cl = m.get_changelist_instance(request)
+ 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, custom_site)
+ cl = m.get_changelist_instance(request)
+ 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.
+ """
+ self.client.force_login(self.superuser)
+ 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, str(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, custom_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: ''})
+ request.user = self.superuser
+
+ # Test valid "show all" request (number of total objects is under max)
+ m = ChildAdmin(Child, custom_site)
+ m.list_max_show_all = 200
+ # 200 is the max we'll pass to ChangeList
+ cl = m.get_changelist_instance(request)
+ 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, custom_site)
+ m.list_max_show_all = 30
+ # 30 is the max we'll pass to ChangeList for this test
+ cl = m.get_changelist_instance(request)
+ 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, custom_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, '%s' % (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, custom_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, '' % link)
+
+ def test_tuple_list_display(self):
+ swallow = Swallow.objects.create(origin='Africa', load='12.34', speed='22.2')
+ swallow2 = Swallow.objects.create(origin='Africa', load='12.34', speed='22.2')
+ swallow_o2o = SwallowOneToOne.objects.create(swallow=swallow2)
+
+ model_admin = SwallowAdmin(Swallow, custom_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, str(swallow.origin))
+ self.assertContains(response, str(swallow.load))
+ self.assertContains(response, str(swallow.speed))
+ # Reverse one-to-one relations should work.
+ self.assertContains(response, '- | ')
+ self.assertContains(response, '%s | ' % swallow_o2o)
+
+ def test_multiuser_edit(self):
+ """
+ Simultaneous edits of list_editable fields on the changelist by
+ different users must not result in one user's edits creating a new
+ object instead of modifying the correct existing object (#11313).
+ """
+ # To replicate this issue, simulate the following steps:
+ # 1. User1 opens an admin changelist with list_editable fields.
+ # 2. User2 edits object "Foo" such that it moves to another page in
+ # the pagination order and saves.
+ # 3. User1 edits object "Foo" and saves.
+ # 4. The edit made by User1 does not get applied to object "Foo" but
+ # instead is used to create a new object (bug).
+
+ # For this test, order the changelist by the 'speed' attribute and
+ # display 3 objects per page (SwallowAdmin.list_per_page = 3).
+
+ # Setup the test to reflect the DB state after step 2 where User2 has
+ # edited the first swallow object's speed from '4' to '1'.
+ a = Swallow.objects.create(origin='Swallow A', load=4, speed=1)
+ b = Swallow.objects.create(origin='Swallow B', load=2, speed=2)
+ c = Swallow.objects.create(origin='Swallow C', load=5, speed=5)
+ d = Swallow.objects.create(origin='Swallow D', load=9, speed=9)
+
+ superuser = self._create_superuser('superuser')
+ self.client.force_login(superuser)
+ changelist_url = reverse('admin:admin_changelist_swallow_changelist')
+
+ # Send the POST from User1 for step 3. It's still using the changelist
+ # ordering from before User2's edits in step 2.
+ data = {
+ 'form-TOTAL_FORMS': '3',
+ 'form-INITIAL_FORMS': '3',
+ 'form-MIN_NUM_FORMS': '0',
+ 'form-MAX_NUM_FORMS': '1000',
+ 'form-0-uuid': str(d.pk),
+ 'form-1-uuid': str(c.pk),
+ 'form-2-uuid': str(a.pk),
+ 'form-0-load': '9.0',
+ 'form-0-speed': '9.0',
+ 'form-1-load': '5.0',
+ 'form-1-speed': '5.0',
+ 'form-2-load': '5.0',
+ 'form-2-speed': '4.0',
+ '_save': 'Save',
+ }
+ response = self.client.post(changelist_url, data, follow=True, extra={'o': '-2'})
+
+ # The object User1 edited in step 3 is displayed on the changelist and
+ # has the correct edits applied.
+ self.assertContains(response, '1 swallow was changed successfully.')
+ self.assertContains(response, a.origin)
+ a.refresh_from_db()
+ self.assertEqual(a.load, float(data['form-2-load']))
+ self.assertEqual(a.speed, float(data['form-2-speed']))
+ b.refresh_from_db()
+ self.assertEqual(b.load, 2)
+ self.assertEqual(b.speed, 2)
+ c.refresh_from_db()
+ self.assertEqual(c.load, float(data['form-1-load']))
+ self.assertEqual(c.speed, float(data['form-1-speed']))
+ d.refresh_from_db()
+ self.assertEqual(d.load, float(data['form-0-load']))
+ self.assertEqual(d.speed, float(data['form-0-speed']))
+ # No new swallows were created.
+ self.assertEqual(len(Swallow.objects.all()), 4)
+
+ def test_get_edited_object_ids(self):
+ a = Swallow.objects.create(origin='Swallow A', load=4, speed=1)
+ b = Swallow.objects.create(origin='Swallow B', load=2, speed=2)
+ c = Swallow.objects.create(origin='Swallow C', load=5, speed=5)
+ superuser = self._create_superuser('superuser')
+ self.client.force_login(superuser)
+ changelist_url = reverse('admin:admin_changelist_swallow_changelist')
+ m = SwallowAdmin(Swallow, custom_site)
+ data = {
+ 'form-TOTAL_FORMS': '3',
+ 'form-INITIAL_FORMS': '3',
+ 'form-MIN_NUM_FORMS': '0',
+ 'form-MAX_NUM_FORMS': '1000',
+ 'form-0-uuid': str(a.pk),
+ 'form-1-uuid': str(b.pk),
+ 'form-2-uuid': str(c.pk),
+ 'form-0-load': '9.0',
+ 'form-0-speed': '9.0',
+ 'form-1-load': '5.0',
+ 'form-1-speed': '5.0',
+ 'form-2-load': '5.0',
+ 'form-2-speed': '4.0',
+ '_save': 'Save',
+ }
+ request = self.factory.post(changelist_url, data=data)
+ pks = m._get_edited_object_pks(request, prefix='form')
+ self.assertEqual(sorted(pks), sorted([str(a.pk), str(b.pk), str(c.pk)]))
+
+ def test_get_list_editable_queryset(self):
+ a = Swallow.objects.create(origin='Swallow A', load=4, speed=1)
+ Swallow.objects.create(origin='Swallow B', load=2, speed=2)
+ data = {
+ 'form-TOTAL_FORMS': '2',
+ 'form-INITIAL_FORMS': '2',
+ 'form-MIN_NUM_FORMS': '0',
+ 'form-MAX_NUM_FORMS': '1000',
+ 'form-0-uuid': str(a.pk),
+ 'form-0-load': '10',
+ '_save': 'Save',
+ }
+ superuser = self._create_superuser('superuser')
+ self.client.force_login(superuser)
+ changelist_url = reverse('admin:admin_changelist_swallow_changelist')
+ m = SwallowAdmin(Swallow, custom_site)
+ request = self.factory.post(changelist_url, data=data)
+ queryset = m._get_list_editable_queryset(request, prefix='form')
+ self.assertEqual(queryset.count(), 1)
+ data['form-0-uuid'] = 'INVALD_PRIMARY_KEY'
+ # The unfiltered queryset is returned if there's invalid data.
+ request = self.factory.post(changelist_url, data=data)
+ queryset = m._get_list_editable_queryset(request, prefix='form')
+ self.assertEqual(queryset.count(), 2)
+
+ def test_changelist_view_list_editable_changed_objects_uses_filter(self):
+ """list_editable edits use a filtered queryset to limit memory usage."""
+ a = Swallow.objects.create(origin='Swallow A', load=4, speed=1)
+ Swallow.objects.create(origin='Swallow B', load=2, speed=2)
+ data = {
+ 'form-TOTAL_FORMS': '2',
+ 'form-INITIAL_FORMS': '2',
+ 'form-MIN_NUM_FORMS': '0',
+ 'form-MAX_NUM_FORMS': '1000',
+ 'form-0-uuid': str(a.pk),
+ 'form-0-load': '10',
+ '_save': 'Save',
+ }
+ superuser = self._create_superuser('superuser')
+ self.client.force_login(superuser)
+ changelist_url = reverse('admin:admin_changelist_swallow_changelist')
+ with CaptureQueriesContext(connection) as context:
+ response = self.client.post(changelist_url, data=data)
+ self.assertEqual(response.status_code, 200)
+ self.assertIn('WHERE', context.captured_queries[4]['sql'])
+ self.assertIn('IN', context.captured_queries[4]['sql'])
+ # Check only the first few characters since the UUID may have dashes.
+ self.assertIn(str(a.pk)[:8], context.captured_queries[4]['sql'])
+
+ def test_deterministic_order_for_unordered_model(self):
+ """
+ The primary key is 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 (#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):
+ custom_site.register(UnorderedObject, UnorderedObjectAdmin)
+ model_admin = UnorderedObjectAdmin(UnorderedObject, custom_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)
+ custom_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):
+ """
+ The primary key is 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 (#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):
+ custom_site.register(OrderedObject, OrderedObjectAdmin)
+ model_admin = OrderedObjectAdmin(OrderedObject, custom_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)
+ custom_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, custom_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, custom_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, custom_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, custom_site)
+ request = self.factory.get('/group/')
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ 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.assertEqual(expected_page_range, list(real_page_range))
+
+ def test_object_tools_displayed_no_add_permission(self):
+ """
+ When ModelAdmin.has_add_permission() returns False, the object-tools
+ block is still shown.
+ """
+ superuser = self._create_superuser('superuser')
+ m = EventAdmin(Event, custom_site)
+ request = self._mocked_authenticated_request('/event/', superuser)
+ self.assertFalse(m.has_add_permission(request))
+ response = m.changelist_view(request)
+ self.assertIn('