tests.py 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440
  1. import datetime
  2. from django.contrib import admin
  3. from django.contrib.admin.models import LogEntry
  4. from django.contrib.admin.options import IncorrectLookupParameters
  5. from django.contrib.admin.templatetags.admin_list import pagination
  6. from django.contrib.admin.tests import AdminSeleniumTestCase
  7. from django.contrib.admin.views.main import ALL_VAR, SEARCH_VAR
  8. from django.contrib.auth.models import User
  9. from django.contrib.contenttypes.models import ContentType
  10. from django.contrib.messages.storage.cookie import CookieStorage
  11. from django.db import connection, models
  12. from django.db.models import F, Field, IntegerField
  13. from django.db.models.functions import Upper
  14. from django.db.models.lookups import Contains, Exact
  15. from django.template import Context, Template, TemplateSyntaxError
  16. from django.test import TestCase, override_settings
  17. from django.test.client import RequestFactory
  18. from django.test.utils import (
  19. CaptureQueriesContext, isolate_apps, register_lookup,
  20. )
  21. from django.urls import reverse
  22. from django.utils import formats
  23. from .admin import (
  24. BandAdmin, ChildAdmin, ChordsBandAdmin, ConcertAdmin,
  25. CustomPaginationAdmin, CustomPaginator, DynamicListDisplayChildAdmin,
  26. DynamicListDisplayLinksChildAdmin, DynamicListFilterChildAdmin,
  27. DynamicSearchFieldsChildAdmin, EmptyValueChildAdmin, EventAdmin,
  28. FilteredChildAdmin, GroupAdmin, InvitationAdmin,
  29. NoListDisplayLinksParentAdmin, ParentAdmin, QuartetAdmin, SwallowAdmin,
  30. site as custom_site,
  31. )
  32. from .models import (
  33. Band, CharPK, Child, ChordsBand, ChordsMusician, Concert, CustomIdUser,
  34. Event, Genre, Group, Invitation, Membership, Musician, OrderedObject,
  35. Parent, Quartet, Swallow, SwallowOneToOne, UnorderedObject,
  36. )
  37. def build_tbody_html(pk, href, extra_fields):
  38. return (
  39. '<tbody><tr>'
  40. '<td class="action-checkbox">'
  41. '<input type="checkbox" name="_selected_action" value="{}" '
  42. 'class="action-select"></td>'
  43. '<th class="field-name"><a href="{}">name</a></th>'
  44. '{}</tr></tbody>'
  45. ).format(pk, href, extra_fields)
  46. @override_settings(ROOT_URLCONF="admin_changelist.urls")
  47. class ChangeListTests(TestCase):
  48. factory = RequestFactory()
  49. @classmethod
  50. def setUpTestData(cls):
  51. cls.superuser = User.objects.create_superuser(username='super', email='a@b.com', password='xxx')
  52. def _create_superuser(self, username):
  53. return User.objects.create_superuser(username=username, email='a@b.com', password='xxx')
  54. def _mocked_authenticated_request(self, url, user):
  55. request = self.factory.get(url)
  56. request.user = user
  57. return request
  58. def test_specified_ordering_by_f_expression(self):
  59. class OrderedByFBandAdmin(admin.ModelAdmin):
  60. list_display = ['name', 'genres', 'nr_of_members']
  61. ordering = (
  62. F('nr_of_members').desc(nulls_last=True),
  63. Upper(F('name')).asc(),
  64. F('genres').asc(),
  65. )
  66. m = OrderedByFBandAdmin(Band, custom_site)
  67. request = self.factory.get('/band/')
  68. request.user = self.superuser
  69. cl = m.get_changelist_instance(request)
  70. self.assertEqual(cl.get_ordering_field_columns(), {3: 'desc', 2: 'asc'})
  71. def test_specified_ordering_by_f_expression_without_asc_desc(self):
  72. class OrderedByFBandAdmin(admin.ModelAdmin):
  73. list_display = ['name', 'genres', 'nr_of_members']
  74. ordering = (F('nr_of_members'), Upper('name'), F('genres'))
  75. m = OrderedByFBandAdmin(Band, custom_site)
  76. request = self.factory.get('/band/')
  77. request.user = self.superuser
  78. cl = m.get_changelist_instance(request)
  79. self.assertEqual(cl.get_ordering_field_columns(), {3: 'asc', 2: 'asc'})
  80. def test_select_related_preserved(self):
  81. """
  82. Regression test for #10348: ChangeList.get_queryset() shouldn't
  83. overwrite a custom select_related provided by ModelAdmin.get_queryset().
  84. """
  85. m = ChildAdmin(Child, custom_site)
  86. request = self.factory.get('/child/')
  87. request.user = self.superuser
  88. cl = m.get_changelist_instance(request)
  89. self.assertEqual(cl.queryset.query.select_related, {'parent': {}})
  90. def test_select_related_as_tuple(self):
  91. ia = InvitationAdmin(Invitation, custom_site)
  92. request = self.factory.get('/invitation/')
  93. request.user = self.superuser
  94. cl = ia.get_changelist_instance(request)
  95. self.assertEqual(cl.queryset.query.select_related, {'player': {}})
  96. def test_select_related_as_empty_tuple(self):
  97. ia = InvitationAdmin(Invitation, custom_site)
  98. ia.list_select_related = ()
  99. request = self.factory.get('/invitation/')
  100. request.user = self.superuser
  101. cl = ia.get_changelist_instance(request)
  102. self.assertIs(cl.queryset.query.select_related, False)
  103. def test_get_select_related_custom_method(self):
  104. class GetListSelectRelatedAdmin(admin.ModelAdmin):
  105. list_display = ('band', 'player')
  106. def get_list_select_related(self, request):
  107. return ('band', 'player')
  108. ia = GetListSelectRelatedAdmin(Invitation, custom_site)
  109. request = self.factory.get('/invitation/')
  110. request.user = self.superuser
  111. cl = ia.get_changelist_instance(request)
  112. self.assertEqual(cl.queryset.query.select_related, {'player': {}, 'band': {}})
  113. def test_result_list_empty_changelist_value(self):
  114. """
  115. Regression test for #14982: EMPTY_CHANGELIST_VALUE should be honored
  116. for relationship fields
  117. """
  118. new_child = Child.objects.create(name='name', parent=None)
  119. request = self.factory.get('/child/')
  120. request.user = self.superuser
  121. m = ChildAdmin(Child, custom_site)
  122. cl = m.get_changelist_instance(request)
  123. cl.formset = None
  124. template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
  125. context = Context({'cl': cl, 'opts': Child._meta})
  126. table_output = template.render(context)
  127. link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
  128. row_html = build_tbody_html(new_child.id, link, '<td class="field-parent nowrap">-</td>')
  129. self.assertNotEqual(table_output.find(row_html), -1, 'Failed to find expected row element: %s' % table_output)
  130. def test_result_list_set_empty_value_display_on_admin_site(self):
  131. """
  132. Empty value display can be set on AdminSite.
  133. """
  134. new_child = Child.objects.create(name='name', parent=None)
  135. request = self.factory.get('/child/')
  136. request.user = self.superuser
  137. # Set a new empty display value on AdminSite.
  138. admin.site.empty_value_display = '???'
  139. m = ChildAdmin(Child, admin.site)
  140. cl = m.get_changelist_instance(request)
  141. cl.formset = None
  142. template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
  143. context = Context({'cl': cl, 'opts': Child._meta})
  144. table_output = template.render(context)
  145. link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
  146. row_html = build_tbody_html(new_child.id, link, '<td class="field-parent nowrap">???</td>')
  147. self.assertNotEqual(table_output.find(row_html), -1, 'Failed to find expected row element: %s' % table_output)
  148. def test_result_list_set_empty_value_display_in_model_admin(self):
  149. """
  150. Empty value display can be set in ModelAdmin or individual fields.
  151. """
  152. new_child = Child.objects.create(name='name', parent=None)
  153. request = self.factory.get('/child/')
  154. request.user = self.superuser
  155. m = EmptyValueChildAdmin(Child, admin.site)
  156. cl = m.get_changelist_instance(request)
  157. cl.formset = None
  158. template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
  159. context = Context({'cl': cl, 'opts': Child._meta})
  160. table_output = template.render(context)
  161. link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
  162. row_html = build_tbody_html(
  163. new_child.id,
  164. link,
  165. '<td class="field-age_display">&amp;dagger;</td>'
  166. '<td class="field-age">-empty-</td>'
  167. )
  168. self.assertNotEqual(table_output.find(row_html), -1, 'Failed to find expected row element: %s' % table_output)
  169. def test_result_list_html(self):
  170. """
  171. Inclusion tag result_list generates a table when with default
  172. ModelAdmin settings.
  173. """
  174. new_parent = Parent.objects.create(name='parent')
  175. new_child = Child.objects.create(name='name', parent=new_parent)
  176. request = self.factory.get('/child/')
  177. request.user = self.superuser
  178. m = ChildAdmin(Child, custom_site)
  179. cl = m.get_changelist_instance(request)
  180. cl.formset = None
  181. template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
  182. context = Context({'cl': cl, 'opts': Child._meta})
  183. table_output = template.render(context)
  184. link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
  185. row_html = build_tbody_html(new_child.id, link, '<td class="field-parent nowrap">%s</td>' % new_parent)
  186. self.assertNotEqual(table_output.find(row_html), -1, 'Failed to find expected row element: %s' % table_output)
  187. def test_result_list_editable_html(self):
  188. """
  189. Regression tests for #11791: Inclusion tag result_list generates a
  190. table and this checks that the items are nested within the table
  191. element tags.
  192. Also a regression test for #13599, verifies that hidden fields
  193. when list_editable is enabled are rendered in a div outside the
  194. table.
  195. """
  196. new_parent = Parent.objects.create(name='parent')
  197. new_child = Child.objects.create(name='name', parent=new_parent)
  198. request = self.factory.get('/child/')
  199. request.user = self.superuser
  200. m = ChildAdmin(Child, custom_site)
  201. # Test with list_editable fields
  202. m.list_display = ['id', 'name', 'parent']
  203. m.list_display_links = ['id']
  204. m.list_editable = ['name']
  205. cl = m.get_changelist_instance(request)
  206. FormSet = m.get_changelist_formset(request)
  207. cl.formset = FormSet(queryset=cl.result_list)
  208. template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
  209. context = Context({'cl': cl, 'opts': Child._meta})
  210. table_output = template.render(context)
  211. # make sure that hidden fields are in the correct place
  212. hiddenfields_div = (
  213. '<div class="hiddenfields">'
  214. '<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id">'
  215. '</div>'
  216. ) % new_child.id
  217. self.assertInHTML(hiddenfields_div, table_output, msg_prefix='Failed to find hidden fields')
  218. # make sure that list editable fields are rendered in divs correctly
  219. editable_name_field = (
  220. '<input name="form-0-name" value="name" class="vTextField" '
  221. 'maxlength="30" type="text" id="id_form-0-name">'
  222. )
  223. self.assertInHTML(
  224. '<td class="field-name">%s</td>' % editable_name_field,
  225. table_output,
  226. msg_prefix='Failed to find "name" list_editable field',
  227. )
  228. def test_result_list_editable(self):
  229. """
  230. Regression test for #14312: list_editable with pagination
  231. """
  232. new_parent = Parent.objects.create(name='parent')
  233. for i in range(200):
  234. Child.objects.create(name='name %s' % i, parent=new_parent)
  235. request = self.factory.get('/child/', data={'p': -1}) # Anything outside range
  236. request.user = self.superuser
  237. m = ChildAdmin(Child, custom_site)
  238. # Test with list_editable fields
  239. m.list_display = ['id', 'name', 'parent']
  240. m.list_display_links = ['id']
  241. m.list_editable = ['name']
  242. with self.assertRaises(IncorrectLookupParameters):
  243. m.get_changelist_instance(request)
  244. def test_custom_paginator(self):
  245. new_parent = Parent.objects.create(name='parent')
  246. for i in range(200):
  247. Child.objects.create(name='name %s' % i, parent=new_parent)
  248. request = self.factory.get('/child/')
  249. request.user = self.superuser
  250. m = CustomPaginationAdmin(Child, custom_site)
  251. cl = m.get_changelist_instance(request)
  252. cl.get_results(request)
  253. self.assertIsInstance(cl.paginator, CustomPaginator)
  254. def test_distinct_for_m2m_in_list_filter(self):
  255. """
  256. Regression test for #13902: When using a ManyToMany in list_filter,
  257. results shouldn't appear more than once. Basic ManyToMany.
  258. """
  259. blues = Genre.objects.create(name='Blues')
  260. band = Band.objects.create(name='B.B. King Review', nr_of_members=11)
  261. band.genres.add(blues)
  262. band.genres.add(blues)
  263. m = BandAdmin(Band, custom_site)
  264. request = self.factory.get('/band/', data={'genres': blues.pk})
  265. request.user = self.superuser
  266. cl = m.get_changelist_instance(request)
  267. cl.get_results(request)
  268. # There's only one Group instance
  269. self.assertEqual(cl.result_count, 1)
  270. def test_distinct_for_through_m2m_in_list_filter(self):
  271. """
  272. Regression test for #13902: When using a ManyToMany in list_filter,
  273. results shouldn't appear more than once. With an intermediate model.
  274. """
  275. lead = Musician.objects.create(name='Vox')
  276. band = Group.objects.create(name='The Hype')
  277. Membership.objects.create(group=band, music=lead, role='lead voice')
  278. Membership.objects.create(group=band, music=lead, role='bass player')
  279. m = GroupAdmin(Group, custom_site)
  280. request = self.factory.get('/group/', data={'members': lead.pk})
  281. request.user = self.superuser
  282. cl = m.get_changelist_instance(request)
  283. cl.get_results(request)
  284. # There's only one Group instance
  285. self.assertEqual(cl.result_count, 1)
  286. def test_distinct_for_through_m2m_at_second_level_in_list_filter(self):
  287. """
  288. When using a ManyToMany in list_filter at the second level behind a
  289. ForeignKey, distinct() must be called and results shouldn't appear more
  290. than once.
  291. """
  292. lead = Musician.objects.create(name='Vox')
  293. band = Group.objects.create(name='The Hype')
  294. Concert.objects.create(name='Woodstock', group=band)
  295. Membership.objects.create(group=band, music=lead, role='lead voice')
  296. Membership.objects.create(group=band, music=lead, role='bass player')
  297. m = ConcertAdmin(Concert, custom_site)
  298. request = self.factory.get('/concert/', data={'group__members': lead.pk})
  299. request.user = self.superuser
  300. cl = m.get_changelist_instance(request)
  301. cl.get_results(request)
  302. # There's only one Concert instance
  303. self.assertEqual(cl.result_count, 1)
  304. def test_distinct_for_inherited_m2m_in_list_filter(self):
  305. """
  306. Regression test for #13902: When using a ManyToMany in list_filter,
  307. results shouldn't appear more than once. Model managed in the
  308. admin inherits from the one that defines the relationship.
  309. """
  310. lead = Musician.objects.create(name='John')
  311. four = Quartet.objects.create(name='The Beatles')
  312. Membership.objects.create(group=four, music=lead, role='lead voice')
  313. Membership.objects.create(group=four, music=lead, role='guitar player')
  314. m = QuartetAdmin(Quartet, custom_site)
  315. request = self.factory.get('/quartet/', data={'members': lead.pk})
  316. request.user = self.superuser
  317. cl = m.get_changelist_instance(request)
  318. cl.get_results(request)
  319. # There's only one Quartet instance
  320. self.assertEqual(cl.result_count, 1)
  321. def test_distinct_for_m2m_to_inherited_in_list_filter(self):
  322. """
  323. Regression test for #13902: When using a ManyToMany in list_filter,
  324. results shouldn't appear more than once. Target of the relationship
  325. inherits from another.
  326. """
  327. lead = ChordsMusician.objects.create(name='Player A')
  328. three = ChordsBand.objects.create(name='The Chords Trio')
  329. Invitation.objects.create(band=three, player=lead, instrument='guitar')
  330. Invitation.objects.create(band=three, player=lead, instrument='bass')
  331. m = ChordsBandAdmin(ChordsBand, custom_site)
  332. request = self.factory.get('/chordsband/', data={'members': lead.pk})
  333. request.user = self.superuser
  334. cl = m.get_changelist_instance(request)
  335. cl.get_results(request)
  336. # There's only one ChordsBand instance
  337. self.assertEqual(cl.result_count, 1)
  338. def test_distinct_for_non_unique_related_object_in_list_filter(self):
  339. """
  340. Regressions tests for #15819: If a field listed in list_filters
  341. is a non-unique related object, distinct() must be called.
  342. """
  343. parent = Parent.objects.create(name='Mary')
  344. # Two children with the same name
  345. Child.objects.create(parent=parent, name='Daniel')
  346. Child.objects.create(parent=parent, name='Daniel')
  347. m = ParentAdmin(Parent, custom_site)
  348. request = self.factory.get('/parent/', data={'child__name': 'Daniel'})
  349. request.user = self.superuser
  350. cl = m.get_changelist_instance(request)
  351. # Make sure distinct() was called
  352. self.assertEqual(cl.queryset.count(), 1)
  353. def test_changelist_search_form_validation(self):
  354. m = ConcertAdmin(Concert, custom_site)
  355. tests = [
  356. ({SEARCH_VAR: '\x00'}, 'Null characters are not allowed.'),
  357. ({SEARCH_VAR: 'some\x00thing'}, 'Null characters are not allowed.'),
  358. ]
  359. for case, error in tests:
  360. with self.subTest(case=case):
  361. request = self.factory.get('/concert/', case)
  362. request.user = self.superuser
  363. request._messages = CookieStorage(request)
  364. m.get_changelist_instance(request)
  365. messages = [m.message for m in request._messages]
  366. self.assertEqual(1, len(messages))
  367. self.assertEqual(error, messages[0])
  368. def test_distinct_for_non_unique_related_object_in_search_fields(self):
  369. """
  370. Regressions tests for #15819: If a field listed in search_fields
  371. is a non-unique related object, distinct() must be called.
  372. """
  373. parent = Parent.objects.create(name='Mary')
  374. Child.objects.create(parent=parent, name='Danielle')
  375. Child.objects.create(parent=parent, name='Daniel')
  376. m = ParentAdmin(Parent, custom_site)
  377. request = self.factory.get('/parent/', data={SEARCH_VAR: 'daniel'})
  378. request.user = self.superuser
  379. cl = m.get_changelist_instance(request)
  380. # Make sure distinct() was called
  381. self.assertEqual(cl.queryset.count(), 1)
  382. def test_distinct_for_many_to_many_at_second_level_in_search_fields(self):
  383. """
  384. When using a ManyToMany in search_fields at the second level behind a
  385. ForeignKey, distinct() must be called and results shouldn't appear more
  386. than once.
  387. """
  388. lead = Musician.objects.create(name='Vox')
  389. band = Group.objects.create(name='The Hype')
  390. Concert.objects.create(name='Woodstock', group=band)
  391. Membership.objects.create(group=band, music=lead, role='lead voice')
  392. Membership.objects.create(group=band, music=lead, role='bass player')
  393. m = ConcertAdmin(Concert, custom_site)
  394. request = self.factory.get('/concert/', data={SEARCH_VAR: 'vox'})
  395. request.user = self.superuser
  396. cl = m.get_changelist_instance(request)
  397. # There's only one Concert instance
  398. self.assertEqual(cl.queryset.count(), 1)
  399. def test_pk_in_search_fields(self):
  400. band = Group.objects.create(name='The Hype')
  401. Concert.objects.create(name='Woodstock', group=band)
  402. m = ConcertAdmin(Concert, custom_site)
  403. m.search_fields = ['group__pk']
  404. request = self.factory.get('/concert/', data={SEARCH_VAR: band.pk})
  405. request.user = self.superuser
  406. cl = m.get_changelist_instance(request)
  407. self.assertEqual(cl.queryset.count(), 1)
  408. request = self.factory.get('/concert/', data={SEARCH_VAR: band.pk + 5})
  409. request.user = self.superuser
  410. cl = m.get_changelist_instance(request)
  411. self.assertEqual(cl.queryset.count(), 0)
  412. def test_builtin_lookup_in_search_fields(self):
  413. band = Group.objects.create(name='The Hype')
  414. concert = Concert.objects.create(name='Woodstock', group=band)
  415. m = ConcertAdmin(Concert, custom_site)
  416. m.search_fields = ['name__iexact']
  417. request = self.factory.get('/', data={SEARCH_VAR: 'woodstock'})
  418. request.user = self.superuser
  419. cl = m.get_changelist_instance(request)
  420. self.assertCountEqual(cl.queryset, [concert])
  421. request = self.factory.get('/', data={SEARCH_VAR: 'wood'})
  422. request.user = self.superuser
  423. cl = m.get_changelist_instance(request)
  424. self.assertCountEqual(cl.queryset, [])
  425. def test_custom_lookup_in_search_fields(self):
  426. band = Group.objects.create(name='The Hype')
  427. concert = Concert.objects.create(name='Woodstock', group=band)
  428. m = ConcertAdmin(Concert, custom_site)
  429. m.search_fields = ['group__name__cc']
  430. with register_lookup(Field, Contains, lookup_name='cc'):
  431. request = self.factory.get('/', data={SEARCH_VAR: 'Hype'})
  432. request.user = self.superuser
  433. cl = m.get_changelist_instance(request)
  434. self.assertCountEqual(cl.queryset, [concert])
  435. request = self.factory.get('/', data={SEARCH_VAR: 'Woodstock'})
  436. request.user = self.superuser
  437. cl = m.get_changelist_instance(request)
  438. self.assertCountEqual(cl.queryset, [])
  439. def test_spanning_relations_with_custom_lookup_in_search_fields(self):
  440. hype = Group.objects.create(name='The Hype')
  441. concert = Concert.objects.create(name='Woodstock', group=hype)
  442. vox = Musician.objects.create(name='Vox', age=20)
  443. Membership.objects.create(music=vox, group=hype)
  444. # Register a custom lookup on IntegerField to ensure that field
  445. # traversing logic in ModelAdmin.get_search_results() works.
  446. with register_lookup(IntegerField, Exact, lookup_name='exactly'):
  447. m = ConcertAdmin(Concert, custom_site)
  448. m.search_fields = ['group__members__age__exactly']
  449. request = self.factory.get('/', data={SEARCH_VAR: '20'})
  450. request.user = self.superuser
  451. cl = m.get_changelist_instance(request)
  452. self.assertCountEqual(cl.queryset, [concert])
  453. request = self.factory.get('/', data={SEARCH_VAR: '21'})
  454. request.user = self.superuser
  455. cl = m.get_changelist_instance(request)
  456. self.assertCountEqual(cl.queryset, [])
  457. def test_custom_lookup_with_pk_shortcut(self):
  458. self.assertEqual(CharPK._meta.pk.name, 'char_pk') # Not equal to 'pk'.
  459. m = admin.ModelAdmin(CustomIdUser, custom_site)
  460. abc = CharPK.objects.create(char_pk='abc')
  461. abcd = CharPK.objects.create(char_pk='abcd')
  462. m = admin.ModelAdmin(CharPK, custom_site)
  463. m.search_fields = ['pk__exact']
  464. request = self.factory.get('/', data={SEARCH_VAR: 'abc'})
  465. request.user = self.superuser
  466. cl = m.get_changelist_instance(request)
  467. self.assertCountEqual(cl.queryset, [abc])
  468. request = self.factory.get('/', data={SEARCH_VAR: 'abcd'})
  469. request.user = self.superuser
  470. cl = m.get_changelist_instance(request)
  471. self.assertCountEqual(cl.queryset, [abcd])
  472. def test_no_distinct_for_m2m_in_list_filter_without_params(self):
  473. """
  474. If a ManyToManyField is in list_filter but isn't in any lookup params,
  475. the changelist's query shouldn't have distinct.
  476. """
  477. m = BandAdmin(Band, custom_site)
  478. for lookup_params in ({}, {'name': 'test'}):
  479. request = self.factory.get('/band/', lookup_params)
  480. request.user = self.superuser
  481. cl = m.get_changelist_instance(request)
  482. self.assertFalse(cl.queryset.query.distinct)
  483. # A ManyToManyField in params does have distinct applied.
  484. request = self.factory.get('/band/', {'genres': '0'})
  485. request.user = self.superuser
  486. cl = m.get_changelist_instance(request)
  487. self.assertTrue(cl.queryset.query.distinct)
  488. def test_pagination(self):
  489. """
  490. Regression tests for #12893: Pagination in admins changelist doesn't
  491. use queryset set by modeladmin.
  492. """
  493. parent = Parent.objects.create(name='anything')
  494. for i in range(30):
  495. Child.objects.create(name='name %s' % i, parent=parent)
  496. Child.objects.create(name='filtered %s' % i, parent=parent)
  497. request = self.factory.get('/child/')
  498. request.user = self.superuser
  499. # Test default queryset
  500. m = ChildAdmin(Child, custom_site)
  501. cl = m.get_changelist_instance(request)
  502. self.assertEqual(cl.queryset.count(), 60)
  503. self.assertEqual(cl.paginator.count, 60)
  504. self.assertEqual(list(cl.paginator.page_range), [1, 2, 3, 4, 5, 6])
  505. # Test custom queryset
  506. m = FilteredChildAdmin(Child, custom_site)
  507. cl = m.get_changelist_instance(request)
  508. self.assertEqual(cl.queryset.count(), 30)
  509. self.assertEqual(cl.paginator.count, 30)
  510. self.assertEqual(list(cl.paginator.page_range), [1, 2, 3])
  511. def test_computed_list_display_localization(self):
  512. """
  513. Regression test for #13196: output of functions should be localized
  514. in the changelist.
  515. """
  516. self.client.force_login(self.superuser)
  517. event = Event.objects.create(date=datetime.date.today())
  518. response = self.client.get(reverse('admin:admin_changelist_event_changelist'))
  519. self.assertContains(response, formats.localize(event.date))
  520. self.assertNotContains(response, str(event.date))
  521. def test_dynamic_list_display(self):
  522. """
  523. Regression tests for #14206: dynamic list_display support.
  524. """
  525. parent = Parent.objects.create(name='parent')
  526. for i in range(10):
  527. Child.objects.create(name='child %s' % i, parent=parent)
  528. user_noparents = self._create_superuser('noparents')
  529. user_parents = self._create_superuser('parents')
  530. # Test with user 'noparents'
  531. m = custom_site._registry[Child]
  532. request = self._mocked_authenticated_request('/child/', user_noparents)
  533. response = m.changelist_view(request)
  534. self.assertNotContains(response, 'Parent object')
  535. list_display = m.get_list_display(request)
  536. list_display_links = m.get_list_display_links(request, list_display)
  537. self.assertEqual(list_display, ['name', 'age'])
  538. self.assertEqual(list_display_links, ['name'])
  539. # Test with user 'parents'
  540. m = DynamicListDisplayChildAdmin(Child, custom_site)
  541. request = self._mocked_authenticated_request('/child/', user_parents)
  542. response = m.changelist_view(request)
  543. self.assertContains(response, 'Parent object')
  544. custom_site.unregister(Child)
  545. list_display = m.get_list_display(request)
  546. list_display_links = m.get_list_display_links(request, list_display)
  547. self.assertEqual(list_display, ('parent', 'name', 'age'))
  548. self.assertEqual(list_display_links, ['parent'])
  549. # Test default implementation
  550. custom_site.register(Child, ChildAdmin)
  551. m = custom_site._registry[Child]
  552. request = self._mocked_authenticated_request('/child/', user_noparents)
  553. response = m.changelist_view(request)
  554. self.assertContains(response, 'Parent object')
  555. def test_show_all(self):
  556. parent = Parent.objects.create(name='anything')
  557. for i in range(30):
  558. Child.objects.create(name='name %s' % i, parent=parent)
  559. Child.objects.create(name='filtered %s' % i, parent=parent)
  560. # Add "show all" parameter to request
  561. request = self.factory.get('/child/', data={ALL_VAR: ''})
  562. request.user = self.superuser
  563. # Test valid "show all" request (number of total objects is under max)
  564. m = ChildAdmin(Child, custom_site)
  565. m.list_max_show_all = 200
  566. # 200 is the max we'll pass to ChangeList
  567. cl = m.get_changelist_instance(request)
  568. cl.get_results(request)
  569. self.assertEqual(len(cl.result_list), 60)
  570. # Test invalid "show all" request (number of total objects over max)
  571. # falls back to paginated pages
  572. m = ChildAdmin(Child, custom_site)
  573. m.list_max_show_all = 30
  574. # 30 is the max we'll pass to ChangeList for this test
  575. cl = m.get_changelist_instance(request)
  576. cl.get_results(request)
  577. self.assertEqual(len(cl.result_list), 10)
  578. def test_dynamic_list_display_links(self):
  579. """
  580. Regression tests for #16257: dynamic list_display_links support.
  581. """
  582. parent = Parent.objects.create(name='parent')
  583. for i in range(1, 10):
  584. Child.objects.create(id=i, name='child %s' % i, parent=parent, age=i)
  585. m = DynamicListDisplayLinksChildAdmin(Child, custom_site)
  586. superuser = self._create_superuser('superuser')
  587. request = self._mocked_authenticated_request('/child/', superuser)
  588. response = m.changelist_view(request)
  589. for i in range(1, 10):
  590. link = reverse('admin:admin_changelist_child_change', args=(i,))
  591. self.assertContains(response, '<a href="%s">%s</a>' % (link, i))
  592. list_display = m.get_list_display(request)
  593. list_display_links = m.get_list_display_links(request, list_display)
  594. self.assertEqual(list_display, ('parent', 'name', 'age'))
  595. self.assertEqual(list_display_links, ['age'])
  596. def test_no_list_display_links(self):
  597. """#15185 -- Allow no links from the 'change list' view grid."""
  598. p = Parent.objects.create(name='parent')
  599. m = NoListDisplayLinksParentAdmin(Parent, custom_site)
  600. superuser = self._create_superuser('superuser')
  601. request = self._mocked_authenticated_request('/parent/', superuser)
  602. response = m.changelist_view(request)
  603. link = reverse('admin:admin_changelist_parent_change', args=(p.pk,))
  604. self.assertNotContains(response, '<a href="%s">' % link)
  605. def test_clear_all_filters_link(self):
  606. self.client.force_login(self.superuser)
  607. link = '<a href="?">&#10006; Clear all filters</a>'
  608. response = self.client.get(reverse('admin:auth_user_changelist'))
  609. self.assertNotContains(response, link)
  610. for data in (
  611. {SEARCH_VAR: 'test'},
  612. {'is_staff__exact': '0'},
  613. ):
  614. response = self.client.get(reverse('admin:auth_user_changelist'), data=data)
  615. self.assertContains(response, link)
  616. def test_tuple_list_display(self):
  617. swallow = Swallow.objects.create(origin='Africa', load='12.34', speed='22.2')
  618. swallow2 = Swallow.objects.create(origin='Africa', load='12.34', speed='22.2')
  619. swallow_o2o = SwallowOneToOne.objects.create(swallow=swallow2)
  620. model_admin = SwallowAdmin(Swallow, custom_site)
  621. superuser = self._create_superuser('superuser')
  622. request = self._mocked_authenticated_request('/swallow/', superuser)
  623. response = model_admin.changelist_view(request)
  624. # just want to ensure it doesn't blow up during rendering
  625. self.assertContains(response, str(swallow.origin))
  626. self.assertContains(response, str(swallow.load))
  627. self.assertContains(response, str(swallow.speed))
  628. # Reverse one-to-one relations should work.
  629. self.assertContains(response, '<td class="field-swallowonetoone">-</td>')
  630. self.assertContains(response, '<td class="field-swallowonetoone">%s</td>' % swallow_o2o)
  631. def test_multiuser_edit(self):
  632. """
  633. Simultaneous edits of list_editable fields on the changelist by
  634. different users must not result in one user's edits creating a new
  635. object instead of modifying the correct existing object (#11313).
  636. """
  637. # To replicate this issue, simulate the following steps:
  638. # 1. User1 opens an admin changelist with list_editable fields.
  639. # 2. User2 edits object "Foo" such that it moves to another page in
  640. # the pagination order and saves.
  641. # 3. User1 edits object "Foo" and saves.
  642. # 4. The edit made by User1 does not get applied to object "Foo" but
  643. # instead is used to create a new object (bug).
  644. # For this test, order the changelist by the 'speed' attribute and
  645. # display 3 objects per page (SwallowAdmin.list_per_page = 3).
  646. # Setup the test to reflect the DB state after step 2 where User2 has
  647. # edited the first swallow object's speed from '4' to '1'.
  648. a = Swallow.objects.create(origin='Swallow A', load=4, speed=1)
  649. b = Swallow.objects.create(origin='Swallow B', load=2, speed=2)
  650. c = Swallow.objects.create(origin='Swallow C', load=5, speed=5)
  651. d = Swallow.objects.create(origin='Swallow D', load=9, speed=9)
  652. superuser = self._create_superuser('superuser')
  653. self.client.force_login(superuser)
  654. changelist_url = reverse('admin:admin_changelist_swallow_changelist')
  655. # Send the POST from User1 for step 3. It's still using the changelist
  656. # ordering from before User2's edits in step 2.
  657. data = {
  658. 'form-TOTAL_FORMS': '3',
  659. 'form-INITIAL_FORMS': '3',
  660. 'form-MIN_NUM_FORMS': '0',
  661. 'form-MAX_NUM_FORMS': '1000',
  662. 'form-0-uuid': str(d.pk),
  663. 'form-1-uuid': str(c.pk),
  664. 'form-2-uuid': str(a.pk),
  665. 'form-0-load': '9.0',
  666. 'form-0-speed': '9.0',
  667. 'form-1-load': '5.0',
  668. 'form-1-speed': '5.0',
  669. 'form-2-load': '5.0',
  670. 'form-2-speed': '4.0',
  671. '_save': 'Save',
  672. }
  673. response = self.client.post(changelist_url, data, follow=True, extra={'o': '-2'})
  674. # The object User1 edited in step 3 is displayed on the changelist and
  675. # has the correct edits applied.
  676. self.assertContains(response, '1 swallow was changed successfully.')
  677. self.assertContains(response, a.origin)
  678. a.refresh_from_db()
  679. self.assertEqual(a.load, float(data['form-2-load']))
  680. self.assertEqual(a.speed, float(data['form-2-speed']))
  681. b.refresh_from_db()
  682. self.assertEqual(b.load, 2)
  683. self.assertEqual(b.speed, 2)
  684. c.refresh_from_db()
  685. self.assertEqual(c.load, float(data['form-1-load']))
  686. self.assertEqual(c.speed, float(data['form-1-speed']))
  687. d.refresh_from_db()
  688. self.assertEqual(d.load, float(data['form-0-load']))
  689. self.assertEqual(d.speed, float(data['form-0-speed']))
  690. # No new swallows were created.
  691. self.assertEqual(len(Swallow.objects.all()), 4)
  692. def test_get_edited_object_ids(self):
  693. a = Swallow.objects.create(origin='Swallow A', load=4, speed=1)
  694. b = Swallow.objects.create(origin='Swallow B', load=2, speed=2)
  695. c = Swallow.objects.create(origin='Swallow C', load=5, speed=5)
  696. superuser = self._create_superuser('superuser')
  697. self.client.force_login(superuser)
  698. changelist_url = reverse('admin:admin_changelist_swallow_changelist')
  699. m = SwallowAdmin(Swallow, custom_site)
  700. data = {
  701. 'form-TOTAL_FORMS': '3',
  702. 'form-INITIAL_FORMS': '3',
  703. 'form-MIN_NUM_FORMS': '0',
  704. 'form-MAX_NUM_FORMS': '1000',
  705. 'form-0-uuid': str(a.pk),
  706. 'form-1-uuid': str(b.pk),
  707. 'form-2-uuid': str(c.pk),
  708. 'form-0-load': '9.0',
  709. 'form-0-speed': '9.0',
  710. 'form-1-load': '5.0',
  711. 'form-1-speed': '5.0',
  712. 'form-2-load': '5.0',
  713. 'form-2-speed': '4.0',
  714. '_save': 'Save',
  715. }
  716. request = self.factory.post(changelist_url, data=data)
  717. pks = m._get_edited_object_pks(request, prefix='form')
  718. self.assertEqual(sorted(pks), sorted([str(a.pk), str(b.pk), str(c.pk)]))
  719. def test_get_list_editable_queryset(self):
  720. a = Swallow.objects.create(origin='Swallow A', load=4, speed=1)
  721. Swallow.objects.create(origin='Swallow B', load=2, speed=2)
  722. data = {
  723. 'form-TOTAL_FORMS': '2',
  724. 'form-INITIAL_FORMS': '2',
  725. 'form-MIN_NUM_FORMS': '0',
  726. 'form-MAX_NUM_FORMS': '1000',
  727. 'form-0-uuid': str(a.pk),
  728. 'form-0-load': '10',
  729. '_save': 'Save',
  730. }
  731. superuser = self._create_superuser('superuser')
  732. self.client.force_login(superuser)
  733. changelist_url = reverse('admin:admin_changelist_swallow_changelist')
  734. m = SwallowAdmin(Swallow, custom_site)
  735. request = self.factory.post(changelist_url, data=data)
  736. queryset = m._get_list_editable_queryset(request, prefix='form')
  737. self.assertEqual(queryset.count(), 1)
  738. data['form-0-uuid'] = 'INVALD_PRIMARY_KEY'
  739. # The unfiltered queryset is returned if there's invalid data.
  740. request = self.factory.post(changelist_url, data=data)
  741. queryset = m._get_list_editable_queryset(request, prefix='form')
  742. self.assertEqual(queryset.count(), 2)
  743. def test_get_list_editable_queryset_with_regex_chars_in_prefix(self):
  744. a = Swallow.objects.create(origin='Swallow A', load=4, speed=1)
  745. Swallow.objects.create(origin='Swallow B', load=2, speed=2)
  746. data = {
  747. 'form$-TOTAL_FORMS': '2',
  748. 'form$-INITIAL_FORMS': '2',
  749. 'form$-MIN_NUM_FORMS': '0',
  750. 'form$-MAX_NUM_FORMS': '1000',
  751. 'form$-0-uuid': str(a.pk),
  752. 'form$-0-load': '10',
  753. '_save': 'Save',
  754. }
  755. superuser = self._create_superuser('superuser')
  756. self.client.force_login(superuser)
  757. changelist_url = reverse('admin:admin_changelist_swallow_changelist')
  758. m = SwallowAdmin(Swallow, custom_site)
  759. request = self.factory.post(changelist_url, data=data)
  760. queryset = m._get_list_editable_queryset(request, prefix='form$')
  761. self.assertEqual(queryset.count(), 1)
  762. def test_changelist_view_list_editable_changed_objects_uses_filter(self):
  763. """list_editable edits use a filtered queryset to limit memory usage."""
  764. a = Swallow.objects.create(origin='Swallow A', load=4, speed=1)
  765. Swallow.objects.create(origin='Swallow B', load=2, speed=2)
  766. data = {
  767. 'form-TOTAL_FORMS': '2',
  768. 'form-INITIAL_FORMS': '2',
  769. 'form-MIN_NUM_FORMS': '0',
  770. 'form-MAX_NUM_FORMS': '1000',
  771. 'form-0-uuid': str(a.pk),
  772. 'form-0-load': '10',
  773. '_save': 'Save',
  774. }
  775. superuser = self._create_superuser('superuser')
  776. self.client.force_login(superuser)
  777. changelist_url = reverse('admin:admin_changelist_swallow_changelist')
  778. with CaptureQueriesContext(connection) as context:
  779. response = self.client.post(changelist_url, data=data)
  780. self.assertEqual(response.status_code, 200)
  781. self.assertIn('WHERE', context.captured_queries[4]['sql'])
  782. self.assertIn('IN', context.captured_queries[4]['sql'])
  783. # Check only the first few characters since the UUID may have dashes.
  784. self.assertIn(str(a.pk)[:8], context.captured_queries[4]['sql'])
  785. def test_deterministic_order_for_unordered_model(self):
  786. """
  787. The primary key is used in the ordering of the changelist's results to
  788. guarantee a deterministic order, even when the model doesn't have any
  789. default ordering defined (#17198).
  790. """
  791. superuser = self._create_superuser('superuser')
  792. for counter in range(1, 51):
  793. UnorderedObject.objects.create(id=counter, bool=True)
  794. class UnorderedObjectAdmin(admin.ModelAdmin):
  795. list_per_page = 10
  796. def check_results_order(ascending=False):
  797. custom_site.register(UnorderedObject, UnorderedObjectAdmin)
  798. model_admin = UnorderedObjectAdmin(UnorderedObject, custom_site)
  799. counter = 0 if ascending else 51
  800. for page in range(0, 5):
  801. request = self._mocked_authenticated_request('/unorderedobject/?p=%s' % page, superuser)
  802. response = model_admin.changelist_view(request)
  803. for result in response.context_data['cl'].result_list:
  804. counter += 1 if ascending else -1
  805. self.assertEqual(result.id, counter)
  806. custom_site.unregister(UnorderedObject)
  807. # When no order is defined at all, everything is ordered by '-pk'.
  808. check_results_order()
  809. # When an order field is defined but multiple records have the same
  810. # value for that field, make sure everything gets ordered by -pk as well.
  811. UnorderedObjectAdmin.ordering = ['bool']
  812. check_results_order()
  813. # When order fields are defined, including the pk itself, use them.
  814. UnorderedObjectAdmin.ordering = ['bool', '-pk']
  815. check_results_order()
  816. UnorderedObjectAdmin.ordering = ['bool', 'pk']
  817. check_results_order(ascending=True)
  818. UnorderedObjectAdmin.ordering = ['-id', 'bool']
  819. check_results_order()
  820. UnorderedObjectAdmin.ordering = ['id', 'bool']
  821. check_results_order(ascending=True)
  822. def test_deterministic_order_for_model_ordered_by_its_manager(self):
  823. """
  824. The primary key is used in the ordering of the changelist's results to
  825. guarantee a deterministic order, even when the model has a manager that
  826. defines a default ordering (#17198).
  827. """
  828. superuser = self._create_superuser('superuser')
  829. for counter in range(1, 51):
  830. OrderedObject.objects.create(id=counter, bool=True, number=counter)
  831. class OrderedObjectAdmin(admin.ModelAdmin):
  832. list_per_page = 10
  833. def check_results_order(ascending=False):
  834. custom_site.register(OrderedObject, OrderedObjectAdmin)
  835. model_admin = OrderedObjectAdmin(OrderedObject, custom_site)
  836. counter = 0 if ascending else 51
  837. for page in range(0, 5):
  838. request = self._mocked_authenticated_request('/orderedobject/?p=%s' % page, superuser)
  839. response = model_admin.changelist_view(request)
  840. for result in response.context_data['cl'].result_list:
  841. counter += 1 if ascending else -1
  842. self.assertEqual(result.id, counter)
  843. custom_site.unregister(OrderedObject)
  844. # When no order is defined at all, use the model's default ordering (i.e. 'number')
  845. check_results_order(ascending=True)
  846. # When an order field is defined but multiple records have the same
  847. # value for that field, make sure everything gets ordered by -pk as well.
  848. OrderedObjectAdmin.ordering = ['bool']
  849. check_results_order()
  850. # When order fields are defined, including the pk itself, use them.
  851. OrderedObjectAdmin.ordering = ['bool', '-pk']
  852. check_results_order()
  853. OrderedObjectAdmin.ordering = ['bool', 'pk']
  854. check_results_order(ascending=True)
  855. OrderedObjectAdmin.ordering = ['-id', 'bool']
  856. check_results_order()
  857. OrderedObjectAdmin.ordering = ['id', 'bool']
  858. check_results_order(ascending=True)
  859. @isolate_apps('admin_changelist')
  860. def test_total_ordering_optimization(self):
  861. class Related(models.Model):
  862. unique_field = models.BooleanField(unique=True)
  863. class Meta:
  864. ordering = ('unique_field',)
  865. class Model(models.Model):
  866. unique_field = models.BooleanField(unique=True)
  867. unique_nullable_field = models.BooleanField(unique=True, null=True)
  868. related = models.ForeignKey(Related, models.CASCADE)
  869. other_related = models.ForeignKey(Related, models.CASCADE)
  870. related_unique = models.OneToOneField(Related, models.CASCADE)
  871. field = models.BooleanField()
  872. other_field = models.BooleanField()
  873. null_field = models.BooleanField(null=True)
  874. class Meta:
  875. unique_together = {
  876. ('field', 'other_field'),
  877. ('field', 'null_field'),
  878. ('related', 'other_related_id'),
  879. }
  880. class ModelAdmin(admin.ModelAdmin):
  881. def get_queryset(self, request):
  882. return Model.objects.none()
  883. request = self._mocked_authenticated_request('/', self.superuser)
  884. site = admin.AdminSite(name='admin')
  885. model_admin = ModelAdmin(Model, site)
  886. change_list = model_admin.get_changelist_instance(request)
  887. tests = (
  888. ([], ['-pk']),
  889. # Unique non-nullable field.
  890. (['unique_field'], ['unique_field']),
  891. (['-unique_field'], ['-unique_field']),
  892. # Unique nullable field.
  893. (['unique_nullable_field'], ['unique_nullable_field', '-pk']),
  894. # Field.
  895. (['field'], ['field', '-pk']),
  896. # Related field introspection is not implemented.
  897. (['related__unique_field'], ['related__unique_field', '-pk']),
  898. # Related attname unique.
  899. (['related_unique_id'], ['related_unique_id']),
  900. # Related ordering introspection is not implemented.
  901. (['related_unique'], ['related_unique', '-pk']),
  902. # Composite unique.
  903. (['field', '-other_field'], ['field', '-other_field']),
  904. # Composite unique nullable.
  905. (['-field', 'null_field'], ['-field', 'null_field', '-pk']),
  906. # Composite unique and nullable.
  907. (['-field', 'null_field', 'other_field'], ['-field', 'null_field', 'other_field']),
  908. # Composite unique attnames.
  909. (['related_id', '-other_related_id'], ['related_id', '-other_related_id']),
  910. # Composite unique names.
  911. (['related', '-other_related_id'], ['related', '-other_related_id', '-pk']),
  912. )
  913. # F() objects composite unique.
  914. total_ordering = [F('field'), F('other_field').desc(nulls_last=True)]
  915. # F() objects composite unique nullable.
  916. non_total_ordering = [F('field'), F('null_field').desc(nulls_last=True)]
  917. tests += (
  918. (total_ordering, total_ordering),
  919. (non_total_ordering, non_total_ordering + ['-pk']),
  920. )
  921. for ordering, expected in tests:
  922. with self.subTest(ordering=ordering):
  923. self.assertEqual(change_list._get_deterministic_ordering(ordering), expected)
  924. @isolate_apps('admin_changelist')
  925. def test_total_ordering_optimization_meta_constraints(self):
  926. class Related(models.Model):
  927. unique_field = models.BooleanField(unique=True)
  928. class Meta:
  929. ordering = ('unique_field',)
  930. class Model(models.Model):
  931. field_1 = models.BooleanField()
  932. field_2 = models.BooleanField()
  933. field_3 = models.BooleanField()
  934. field_4 = models.BooleanField()
  935. field_5 = models.BooleanField()
  936. field_6 = models.BooleanField()
  937. nullable_1 = models.BooleanField(null=True)
  938. nullable_2 = models.BooleanField(null=True)
  939. related_1 = models.ForeignKey(Related, models.CASCADE)
  940. related_2 = models.ForeignKey(Related, models.CASCADE)
  941. related_3 = models.ForeignKey(Related, models.CASCADE)
  942. related_4 = models.ForeignKey(Related, models.CASCADE)
  943. class Meta:
  944. constraints = [
  945. *[
  946. models.UniqueConstraint(fields=fields, name=''.join(fields))
  947. for fields in (
  948. ['field_1'],
  949. ['nullable_1'],
  950. ['related_1'],
  951. ['related_2_id'],
  952. ['field_2', 'field_3'],
  953. ['field_2', 'nullable_2'],
  954. ['field_2', 'related_3'],
  955. ['field_3', 'related_4_id'],
  956. )
  957. ],
  958. models.CheckConstraint(check=models.Q(id__gt=0), name='foo'),
  959. models.UniqueConstraint(
  960. fields=['field_5'],
  961. condition=models.Q(id__gt=10),
  962. name='total_ordering_1',
  963. ),
  964. models.UniqueConstraint(
  965. fields=['field_6'],
  966. condition=models.Q(),
  967. name='total_ordering',
  968. ),
  969. ]
  970. class ModelAdmin(admin.ModelAdmin):
  971. def get_queryset(self, request):
  972. return Model.objects.none()
  973. request = self._mocked_authenticated_request('/', self.superuser)
  974. site = admin.AdminSite(name='admin')
  975. model_admin = ModelAdmin(Model, site)
  976. change_list = model_admin.get_changelist_instance(request)
  977. tests = (
  978. # Unique non-nullable field.
  979. (['field_1'], ['field_1']),
  980. # Unique nullable field.
  981. (['nullable_1'], ['nullable_1', '-pk']),
  982. # Related attname unique.
  983. (['related_1_id'], ['related_1_id']),
  984. (['related_2_id'], ['related_2_id']),
  985. # Related ordering introspection is not implemented.
  986. (['related_1'], ['related_1', '-pk']),
  987. # Composite unique.
  988. (['-field_2', 'field_3'], ['-field_2', 'field_3']),
  989. # Composite unique nullable.
  990. (['field_2', '-nullable_2'], ['field_2', '-nullable_2', '-pk']),
  991. # Composite unique and nullable.
  992. (
  993. ['field_2', '-nullable_2', 'field_3'],
  994. ['field_2', '-nullable_2', 'field_3'],
  995. ),
  996. # Composite field and related field name.
  997. (['field_2', '-related_3'], ['field_2', '-related_3', '-pk']),
  998. (['field_3', 'related_4'], ['field_3', 'related_4', '-pk']),
  999. # Composite field and related field attname.
  1000. (['field_2', 'related_3_id'], ['field_2', 'related_3_id']),
  1001. (['field_3', '-related_4_id'], ['field_3', '-related_4_id']),
  1002. # Partial unique constraint is ignored.
  1003. (['field_5'], ['field_5', '-pk']),
  1004. # Unique constraint with an empty condition.
  1005. (['field_6'], ['field_6']),
  1006. )
  1007. for ordering, expected in tests:
  1008. with self.subTest(ordering=ordering):
  1009. self.assertEqual(change_list._get_deterministic_ordering(ordering), expected)
  1010. def test_dynamic_list_filter(self):
  1011. """
  1012. Regression tests for ticket #17646: dynamic list_filter support.
  1013. """
  1014. parent = Parent.objects.create(name='parent')
  1015. for i in range(10):
  1016. Child.objects.create(name='child %s' % i, parent=parent)
  1017. user_noparents = self._create_superuser('noparents')
  1018. user_parents = self._create_superuser('parents')
  1019. # Test with user 'noparents'
  1020. m = DynamicListFilterChildAdmin(Child, custom_site)
  1021. request = self._mocked_authenticated_request('/child/', user_noparents)
  1022. response = m.changelist_view(request)
  1023. self.assertEqual(response.context_data['cl'].list_filter, ['name', 'age'])
  1024. # Test with user 'parents'
  1025. m = DynamicListFilterChildAdmin(Child, custom_site)
  1026. request = self._mocked_authenticated_request('/child/', user_parents)
  1027. response = m.changelist_view(request)
  1028. self.assertEqual(response.context_data['cl'].list_filter, ('parent', 'name', 'age'))
  1029. def test_dynamic_search_fields(self):
  1030. child = self._create_superuser('child')
  1031. m = DynamicSearchFieldsChildAdmin(Child, custom_site)
  1032. request = self._mocked_authenticated_request('/child/', child)
  1033. response = m.changelist_view(request)
  1034. self.assertEqual(response.context_data['cl'].search_fields, ('name', 'age'))
  1035. def test_pagination_page_range(self):
  1036. """
  1037. Regression tests for ticket #15653: ensure the number of pages
  1038. generated for changelist views are correct.
  1039. """
  1040. # instantiating and setting up ChangeList object
  1041. m = GroupAdmin(Group, custom_site)
  1042. request = self.factory.get('/group/')
  1043. request.user = self.superuser
  1044. cl = m.get_changelist_instance(request)
  1045. per_page = cl.list_per_page = 10
  1046. for page_num, objects_count, expected_page_range in [
  1047. (0, per_page, []),
  1048. (0, per_page * 2, list(range(2))),
  1049. (5, per_page * 11, list(range(11))),
  1050. (5, per_page * 12, [0, 1, 2, 3, 4, 5, 6, 7, 8, '.', 10, 11]),
  1051. (6, per_page * 12, [0, 1, '.', 3, 4, 5, 6, 7, 8, 9, 10, 11]),
  1052. (6, per_page * 13, [0, 1, '.', 3, 4, 5, 6, 7, 8, 9, '.', 11, 12]),
  1053. ]:
  1054. # assuming we have exactly `objects_count` objects
  1055. Group.objects.all().delete()
  1056. for i in range(objects_count):
  1057. Group.objects.create(name='test band')
  1058. # setting page number and calculating page range
  1059. cl.page_num = page_num
  1060. cl.get_results(request)
  1061. real_page_range = pagination(cl)['page_range']
  1062. self.assertEqual(expected_page_range, list(real_page_range))
  1063. def test_object_tools_displayed_no_add_permission(self):
  1064. """
  1065. When ModelAdmin.has_add_permission() returns False, the object-tools
  1066. block is still shown.
  1067. """
  1068. superuser = self._create_superuser('superuser')
  1069. m = EventAdmin(Event, custom_site)
  1070. request = self._mocked_authenticated_request('/event/', superuser)
  1071. self.assertFalse(m.has_add_permission(request))
  1072. response = m.changelist_view(request)
  1073. self.assertIn('<ul class="object-tools">', response.rendered_content)
  1074. # The "Add" button inside the object-tools shouldn't appear.
  1075. self.assertNotIn('Add ', response.rendered_content)
  1076. class GetAdminLogTests(TestCase):
  1077. def test_custom_user_pk_not_named_id(self):
  1078. """
  1079. {% get_admin_log %} works if the user model's primary key isn't named
  1080. 'id'.
  1081. """
  1082. context = Context({'user': CustomIdUser()})
  1083. template = Template('{% load log %}{% get_admin_log 10 as admin_log for_user user %}')
  1084. # This template tag just logs.
  1085. self.assertEqual(template.render(context), '')
  1086. def test_no_user(self):
  1087. """{% get_admin_log %} works without specifying a user."""
  1088. user = User(username='jondoe', password='secret', email='super@example.com')
  1089. user.save()
  1090. ct = ContentType.objects.get_for_model(User)
  1091. LogEntry.objects.log_action(user.pk, ct.pk, user.pk, repr(user), 1)
  1092. t = Template(
  1093. '{% load log %}'
  1094. '{% get_admin_log 100 as admin_log %}'
  1095. '{% for entry in admin_log %}'
  1096. '{{ entry|safe }}'
  1097. '{% endfor %}'
  1098. )
  1099. self.assertEqual(t.render(Context({})), 'Added “<User: jondoe>”.')
  1100. def test_missing_args(self):
  1101. msg = "'get_admin_log' statements require two arguments"
  1102. with self.assertRaisesMessage(TemplateSyntaxError, msg):
  1103. Template('{% load log %}{% get_admin_log 10 as %}')
  1104. def test_non_integer_limit(self):
  1105. msg = "First argument to 'get_admin_log' must be an integer"
  1106. with self.assertRaisesMessage(TemplateSyntaxError, msg):
  1107. Template('{% load log %}{% get_admin_log "10" as admin_log for_user user %}')
  1108. def test_without_as(self):
  1109. msg = "Second argument to 'get_admin_log' must be 'as'"
  1110. with self.assertRaisesMessage(TemplateSyntaxError, msg):
  1111. Template('{% load log %}{% get_admin_log 10 ad admin_log for_user user %}')
  1112. def test_without_for_user(self):
  1113. msg = "Fourth argument to 'get_admin_log' must be 'for_user'"
  1114. with self.assertRaisesMessage(TemplateSyntaxError, msg):
  1115. Template('{% load log %}{% get_admin_log 10 as admin_log foruser user %}')
  1116. @override_settings(ROOT_URLCONF='admin_changelist.urls')
  1117. class SeleniumTests(AdminSeleniumTestCase):
  1118. available_apps = ['admin_changelist'] + AdminSeleniumTestCase.available_apps
  1119. def setUp(self):
  1120. User.objects.create_superuser(username='super', password='secret', email=None)
  1121. def test_add_row_selection(self):
  1122. """
  1123. The status line for selected rows gets updated correctly (#22038).
  1124. """
  1125. self.admin_login(username='super', password='secret')
  1126. self.selenium.get(self.live_server_url + reverse('admin:auth_user_changelist'))
  1127. form_id = '#changelist-form'
  1128. # Test amount of rows in the Changelist
  1129. rows = self.selenium.find_elements_by_css_selector(
  1130. '%s #result_list tbody tr' % form_id
  1131. )
  1132. self.assertEqual(len(rows), 1)
  1133. row = rows[0]
  1134. selection_indicator = self.selenium.find_element_by_css_selector(
  1135. '%s .action-counter' % form_id
  1136. )
  1137. all_selector = self.selenium.find_element_by_id('action-toggle')
  1138. row_selector = self.selenium.find_element_by_css_selector(
  1139. '%s #result_list tbody tr:first-child .action-select' % form_id
  1140. )
  1141. # Test current selection
  1142. self.assertEqual(selection_indicator.text, "0 of 1 selected")
  1143. self.assertIs(all_selector.get_property('checked'), False)
  1144. self.assertEqual(row.get_attribute('class'), '')
  1145. # Select a row and check again
  1146. row_selector.click()
  1147. self.assertEqual(selection_indicator.text, "1 of 1 selected")
  1148. self.assertIs(all_selector.get_property('checked'), True)
  1149. self.assertEqual(row.get_attribute('class'), 'selected')
  1150. # Deselect a row and check again
  1151. row_selector.click()
  1152. self.assertEqual(selection_indicator.text, "0 of 1 selected")
  1153. self.assertIs(all_selector.get_property('checked'), False)
  1154. self.assertEqual(row.get_attribute('class'), '')
  1155. def test_select_all_across_pages(self):
  1156. Parent.objects.bulk_create([Parent(name='parent%d' % i) for i in range(101)])
  1157. self.admin_login(username='super', password='secret')
  1158. self.selenium.get(self.live_server_url + reverse('admin:admin_changelist_parent_changelist'))
  1159. selection_indicator = self.selenium.find_element_by_css_selector('.action-counter')
  1160. select_all_indicator = self.selenium.find_element_by_css_selector('.actions .all')
  1161. question = self.selenium.find_element_by_css_selector('.actions > .question')
  1162. clear = self.selenium.find_element_by_css_selector('.actions > .clear')
  1163. select_all = self.selenium.find_element_by_id('action-toggle')
  1164. select_across = self.selenium.find_element_by_name('select_across')
  1165. self.assertIs(question.is_displayed(), False)
  1166. self.assertIs(clear.is_displayed(), False)
  1167. self.assertIs(select_all.get_property('checked'), False)
  1168. self.assertEqual(select_across.get_property('value'), '0')
  1169. self.assertIs(selection_indicator.is_displayed(), True)
  1170. self.assertEqual(selection_indicator.text, '0 of 100 selected')
  1171. self.assertIs(select_all_indicator.is_displayed(), False)
  1172. select_all.click()
  1173. self.assertIs(question.is_displayed(), True)
  1174. self.assertIs(clear.is_displayed(), False)
  1175. self.assertIs(select_all.get_property('checked'), True)
  1176. self.assertEqual(select_across.get_property('value'), '0')
  1177. self.assertIs(selection_indicator.is_displayed(), True)
  1178. self.assertEqual(selection_indicator.text, '100 of 100 selected')
  1179. self.assertIs(select_all_indicator.is_displayed(), False)
  1180. question.click()
  1181. self.assertIs(question.is_displayed(), False)
  1182. self.assertIs(clear.is_displayed(), True)
  1183. self.assertIs(select_all.get_property('checked'), True)
  1184. self.assertEqual(select_across.get_property('value'), '1')
  1185. self.assertIs(selection_indicator.is_displayed(), False)
  1186. self.assertIs(select_all_indicator.is_displayed(), True)
  1187. clear.click()
  1188. self.assertIs(question.is_displayed(), False)
  1189. self.assertIs(clear.is_displayed(), False)
  1190. self.assertIs(select_all.get_property('checked'), False)
  1191. self.assertEqual(select_across.get_property('value'), '0')
  1192. self.assertIs(selection_indicator.is_displayed(), True)
  1193. self.assertEqual(selection_indicator.text, '0 of 100 selected')
  1194. self.assertIs(select_all_indicator.is_displayed(), False)
  1195. def test_actions_warn_on_pending_edits(self):
  1196. Parent.objects.create(name='foo')
  1197. self.admin_login(username='super', password='secret')
  1198. self.selenium.get(self.live_server_url + reverse('admin:admin_changelist_parent_changelist'))
  1199. name_input = self.selenium.find_element_by_id('id_form-0-name')
  1200. name_input.clear()
  1201. name_input.send_keys('bar')
  1202. self.selenium.find_element_by_id('action-toggle').click()
  1203. self.selenium.find_element_by_name('index').click() # Go
  1204. alert = self.selenium.switch_to.alert
  1205. try:
  1206. self.assertEqual(
  1207. alert.text,
  1208. 'You have unsaved changes on individual editable fields. If you '
  1209. 'run an action, your unsaved changes will be lost.'
  1210. )
  1211. finally:
  1212. alert.dismiss()
  1213. def test_save_with_changes_warns_on_pending_action(self):
  1214. from selenium.webdriver.support.ui import Select
  1215. Parent.objects.create(name='parent')
  1216. self.admin_login(username='super', password='secret')
  1217. self.selenium.get(self.live_server_url + reverse('admin:admin_changelist_parent_changelist'))
  1218. name_input = self.selenium.find_element_by_id('id_form-0-name')
  1219. name_input.clear()
  1220. name_input.send_keys('other name')
  1221. Select(
  1222. self.selenium.find_element_by_name('action')
  1223. ).select_by_value('delete_selected')
  1224. self.selenium.find_element_by_name('_save').click()
  1225. alert = self.selenium.switch_to.alert
  1226. try:
  1227. self.assertEqual(
  1228. alert.text,
  1229. 'You have selected an action, but you haven’t saved your '
  1230. 'changes to individual fields yet. Please click OK to save. '
  1231. 'You’ll need to re-run the action.',
  1232. )
  1233. finally:
  1234. alert.dismiss()
  1235. def test_save_without_changes_warns_on_pending_action(self):
  1236. from selenium.webdriver.support.ui import Select
  1237. Parent.objects.create(name='parent')
  1238. self.admin_login(username='super', password='secret')
  1239. self.selenium.get(self.live_server_url + reverse('admin:admin_changelist_parent_changelist'))
  1240. Select(
  1241. self.selenium.find_element_by_name('action')
  1242. ).select_by_value('delete_selected')
  1243. self.selenium.find_element_by_name('_save').click()
  1244. alert = self.selenium.switch_to.alert
  1245. try:
  1246. self.assertEqual(
  1247. alert.text,
  1248. 'You have selected an action, and you haven’t made any '
  1249. 'changes on individual fields. You’re probably looking for '
  1250. 'the Go button rather than the Save button.',
  1251. )
  1252. finally:
  1253. alert.dismiss()