tests.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  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, ChangeList
  8. from django.contrib.auth.models import User
  9. from django.contrib.contenttypes.models import ContentType
  10. from django.template import Context, Template
  11. from django.test import TestCase, override_settings
  12. from django.test.client import RequestFactory
  13. from django.urls import reverse
  14. from django.utils import formats
  15. from .admin import (
  16. BandAdmin, ChildAdmin, ChordsBandAdmin, ConcertAdmin,
  17. CustomPaginationAdmin, CustomPaginator, DynamicListDisplayChildAdmin,
  18. DynamicListDisplayLinksChildAdmin, DynamicListFilterChildAdmin,
  19. DynamicSearchFieldsChildAdmin, EmptyValueChildAdmin, EventAdmin,
  20. FilteredChildAdmin, GroupAdmin, InvitationAdmin,
  21. NoListDisplayLinksParentAdmin, ParentAdmin, QuartetAdmin, SwallowAdmin,
  22. site as custom_site,
  23. )
  24. from .models import (
  25. Band, Child, ChordsBand, ChordsMusician, Concert, CustomIdUser, Event,
  26. Genre, Group, Invitation, Membership, Musician, OrderedObject, Parent,
  27. Quartet, Swallow, SwallowOneToOne, UnorderedObject,
  28. )
  29. def get_changelist_args(modeladmin, **kwargs):
  30. m = modeladmin
  31. args = (
  32. kwargs.pop('list_display', m.list_display),
  33. kwargs.pop('list_display_links', m.list_display_links),
  34. kwargs.pop('list_filter', m.list_filter),
  35. kwargs.pop('date_hierarchy', m.date_hierarchy),
  36. kwargs.pop('search_fields', m.search_fields),
  37. kwargs.pop('list_select_related', m.list_select_related),
  38. kwargs.pop('list_per_page', m.list_per_page),
  39. kwargs.pop('list_max_show_all', m.list_max_show_all),
  40. kwargs.pop('list_editable', m.list_editable),
  41. m,
  42. )
  43. assert not kwargs, "Unexpected kwarg %s" % kwargs
  44. return args
  45. @override_settings(ROOT_URLCONF="admin_changelist.urls")
  46. class ChangeListTests(TestCase):
  47. def setUp(self):
  48. self.factory = RequestFactory()
  49. def _create_superuser(self, username):
  50. return User.objects.create_superuser(username=username, email='a@b.com', password='xxx')
  51. def _mocked_authenticated_request(self, url, user):
  52. request = self.factory.get(url)
  53. request.user = user
  54. return request
  55. def test_select_related_preserved(self):
  56. """
  57. Regression test for #10348: ChangeList.get_queryset() shouldn't
  58. overwrite a custom select_related provided by ModelAdmin.get_queryset().
  59. """
  60. m = ChildAdmin(Child, custom_site)
  61. request = self.factory.get('/child/')
  62. cl = ChangeList(
  63. request, Child,
  64. *get_changelist_args(m, list_select_related=m.get_list_select_related(request))
  65. )
  66. self.assertEqual(cl.queryset.query.select_related, {'parent': {}})
  67. def test_select_related_as_tuple(self):
  68. ia = InvitationAdmin(Invitation, custom_site)
  69. request = self.factory.get('/invitation/')
  70. cl = ChangeList(
  71. request, Child,
  72. *get_changelist_args(ia, list_select_related=ia.get_list_select_related(request))
  73. )
  74. self.assertEqual(cl.queryset.query.select_related, {'player': {}})
  75. def test_select_related_as_empty_tuple(self):
  76. ia = InvitationAdmin(Invitation, custom_site)
  77. ia.list_select_related = ()
  78. request = self.factory.get('/invitation/')
  79. cl = ChangeList(
  80. request, Child,
  81. *get_changelist_args(ia, list_select_related=ia.get_list_select_related(request))
  82. )
  83. self.assertIs(cl.queryset.query.select_related, False)
  84. def test_get_select_related_custom_method(self):
  85. class GetListSelectRelatedAdmin(admin.ModelAdmin):
  86. list_display = ('band', 'player')
  87. def get_list_select_related(self, request):
  88. return ('band', 'player')
  89. ia = GetListSelectRelatedAdmin(Invitation, custom_site)
  90. request = self.factory.get('/invitation/')
  91. cl = ChangeList(
  92. request, Child,
  93. *get_changelist_args(ia, list_select_related=ia.get_list_select_related(request))
  94. )
  95. self.assertEqual(cl.queryset.query.select_related, {'player': {}, 'band': {}})
  96. def test_result_list_empty_changelist_value(self):
  97. """
  98. Regression test for #14982: EMPTY_CHANGELIST_VALUE should be honored
  99. for relationship fields
  100. """
  101. new_child = Child.objects.create(name='name', parent=None)
  102. request = self.factory.get('/child/')
  103. m = ChildAdmin(Child, custom_site)
  104. cl = ChangeList(request, Child, *get_changelist_args(m))
  105. cl.formset = None
  106. template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
  107. context = Context({'cl': cl})
  108. table_output = template.render(context)
  109. link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
  110. row_html = (
  111. '<tbody><tr class="row1"><th class="field-name"><a href="%s">name</a></th>'
  112. '<td class="field-parent nowrap">-</td></tr></tbody>' % link
  113. )
  114. self.assertNotEqual(table_output.find(row_html), -1, 'Failed to find expected row element: %s' % table_output)
  115. def test_result_list_set_empty_value_display_on_admin_site(self):
  116. """
  117. Empty value display can be set on AdminSite.
  118. """
  119. new_child = Child.objects.create(name='name', parent=None)
  120. request = self.factory.get('/child/')
  121. # Set a new empty display value on AdminSite.
  122. admin.site.empty_value_display = '???'
  123. m = ChildAdmin(Child, admin.site)
  124. cl = ChangeList(request, Child, *get_changelist_args(m))
  125. cl.formset = None
  126. template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
  127. context = Context({'cl': cl})
  128. table_output = template.render(context)
  129. link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
  130. row_html = (
  131. '<tbody><tr class="row1"><th class="field-name"><a href="%s">name</a></th>'
  132. '<td class="field-parent nowrap">???</td></tr></tbody>' % link
  133. )
  134. self.assertNotEqual(table_output.find(row_html), -1, 'Failed to find expected row element: %s' % table_output)
  135. def test_result_list_set_empty_value_display_in_model_admin(self):
  136. """
  137. Empty value display can be set in ModelAdmin or individual fields.
  138. """
  139. new_child = Child.objects.create(name='name', parent=None)
  140. request = self.factory.get('/child/')
  141. m = EmptyValueChildAdmin(Child, admin.site)
  142. cl = ChangeList(request, Child, *get_changelist_args(m))
  143. cl.formset = None
  144. template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
  145. context = Context({'cl': cl})
  146. table_output = template.render(context)
  147. link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
  148. row_html = (
  149. '<tbody><tr class="row1"><th class="field-name"><a href="%s">name</a></th>'
  150. '<td class="field-age_display">&amp;dagger;</td><td class="field-age">-empty-</td></tr></tbody>' % link
  151. )
  152. self.assertNotEqual(table_output.find(row_html), -1, 'Failed to find expected row element: %s' % table_output)
  153. def test_result_list_html(self):
  154. """
  155. Inclusion tag result_list generates a table when with default
  156. ModelAdmin settings.
  157. """
  158. new_parent = Parent.objects.create(name='parent')
  159. new_child = Child.objects.create(name='name', parent=new_parent)
  160. request = self.factory.get('/child/')
  161. m = ChildAdmin(Child, custom_site)
  162. cl = ChangeList(request, Child, *get_changelist_args(m))
  163. cl.formset = None
  164. template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
  165. context = Context({'cl': cl})
  166. table_output = template.render(context)
  167. link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
  168. row_html = (
  169. '<tbody><tr class="row1"><th class="field-name"><a href="%s">name</a></th>'
  170. '<td class="field-parent nowrap">Parent object</td></tr></tbody>' % link
  171. )
  172. self.assertNotEqual(table_output.find(row_html), -1, 'Failed to find expected row element: %s' % table_output)
  173. def test_result_list_editable_html(self):
  174. """
  175. Regression tests for #11791: Inclusion tag result_list generates a
  176. table and this checks that the items are nested within the table
  177. element tags.
  178. Also a regression test for #13599, verifies that hidden fields
  179. when list_editable is enabled are rendered in a div outside the
  180. table.
  181. """
  182. new_parent = Parent.objects.create(name='parent')
  183. new_child = Child.objects.create(name='name', parent=new_parent)
  184. request = self.factory.get('/child/')
  185. m = ChildAdmin(Child, custom_site)
  186. # Test with list_editable fields
  187. m.list_display = ['id', 'name', 'parent']
  188. m.list_display_links = ['id']
  189. m.list_editable = ['name']
  190. cl = ChangeList(request, Child, *get_changelist_args(m))
  191. FormSet = m.get_changelist_formset(request)
  192. cl.formset = FormSet(queryset=cl.result_list)
  193. template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
  194. context = Context({'cl': cl})
  195. table_output = template.render(context)
  196. # make sure that hidden fields are in the correct place
  197. hiddenfields_div = (
  198. '<div class="hiddenfields">'
  199. '<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id" />'
  200. '</div>'
  201. ) % new_child.id
  202. self.assertInHTML(hiddenfields_div, table_output, msg_prefix='Failed to find hidden fields')
  203. # make sure that list editable fields are rendered in divs correctly
  204. editable_name_field = (
  205. '<input name="form-0-name" value="name" class="vTextField" '
  206. 'maxlength="30" type="text" id="id_form-0-name" />'
  207. )
  208. self.assertInHTML(
  209. '<td class="field-name">%s</td>' % editable_name_field,
  210. table_output,
  211. msg_prefix='Failed to find "name" list_editable field',
  212. )
  213. def test_result_list_editable(self):
  214. """
  215. Regression test for #14312: list_editable with pagination
  216. """
  217. new_parent = Parent.objects.create(name='parent')
  218. for i in range(200):
  219. Child.objects.create(name='name %s' % i, parent=new_parent)
  220. request = self.factory.get('/child/', data={'p': -1}) # Anything outside range
  221. m = ChildAdmin(Child, custom_site)
  222. # Test with list_editable fields
  223. m.list_display = ['id', 'name', 'parent']
  224. m.list_display_links = ['id']
  225. m.list_editable = ['name']
  226. with self.assertRaises(IncorrectLookupParameters):
  227. ChangeList(request, Child, *get_changelist_args(m))
  228. def test_custom_paginator(self):
  229. new_parent = Parent.objects.create(name='parent')
  230. for i in range(200):
  231. Child.objects.create(name='name %s' % i, parent=new_parent)
  232. request = self.factory.get('/child/')
  233. m = CustomPaginationAdmin(Child, custom_site)
  234. cl = ChangeList(request, Child, *get_changelist_args(m))
  235. cl.get_results(request)
  236. self.assertIsInstance(cl.paginator, CustomPaginator)
  237. def test_distinct_for_m2m_in_list_filter(self):
  238. """
  239. Regression test for #13902: When using a ManyToMany in list_filter,
  240. results shouldn't appear more than once. Basic ManyToMany.
  241. """
  242. blues = Genre.objects.create(name='Blues')
  243. band = Band.objects.create(name='B.B. King Review', nr_of_members=11)
  244. band.genres.add(blues)
  245. band.genres.add(blues)
  246. m = BandAdmin(Band, custom_site)
  247. request = self.factory.get('/band/', data={'genres': blues.pk})
  248. cl = ChangeList(request, Band, *get_changelist_args(m))
  249. cl.get_results(request)
  250. # There's only one Group instance
  251. self.assertEqual(cl.result_count, 1)
  252. def test_distinct_for_through_m2m_in_list_filter(self):
  253. """
  254. Regression test for #13902: When using a ManyToMany in list_filter,
  255. results shouldn't appear more than once. With an intermediate model.
  256. """
  257. lead = Musician.objects.create(name='Vox')
  258. band = Group.objects.create(name='The Hype')
  259. Membership.objects.create(group=band, music=lead, role='lead voice')
  260. Membership.objects.create(group=band, music=lead, role='bass player')
  261. m = GroupAdmin(Group, custom_site)
  262. request = self.factory.get('/group/', data={'members': lead.pk})
  263. cl = ChangeList(request, Group, *get_changelist_args(m))
  264. cl.get_results(request)
  265. # There's only one Group instance
  266. self.assertEqual(cl.result_count, 1)
  267. def test_distinct_for_through_m2m_at_second_level_in_list_filter(self):
  268. """
  269. When using a ManyToMany in list_filter at the second level behind a
  270. ForeignKey, distinct() must be called and results shouldn't appear more
  271. than once.
  272. """
  273. lead = Musician.objects.create(name='Vox')
  274. band = Group.objects.create(name='The Hype')
  275. Concert.objects.create(name='Woodstock', group=band)
  276. Membership.objects.create(group=band, music=lead, role='lead voice')
  277. Membership.objects.create(group=band, music=lead, role='bass player')
  278. m = ConcertAdmin(Concert, custom_site)
  279. request = self.factory.get('/concert/', data={'group__members': lead.pk})
  280. cl = ChangeList(request, Concert, *get_changelist_args(m))
  281. cl.get_results(request)
  282. # There's only one Concert instance
  283. self.assertEqual(cl.result_count, 1)
  284. def test_distinct_for_inherited_m2m_in_list_filter(self):
  285. """
  286. Regression test for #13902: When using a ManyToMany in list_filter,
  287. results shouldn't appear more than once. Model managed in the
  288. admin inherits from the one that defins the relationship.
  289. """
  290. lead = Musician.objects.create(name='John')
  291. four = Quartet.objects.create(name='The Beatles')
  292. Membership.objects.create(group=four, music=lead, role='lead voice')
  293. Membership.objects.create(group=four, music=lead, role='guitar player')
  294. m = QuartetAdmin(Quartet, custom_site)
  295. request = self.factory.get('/quartet/', data={'members': lead.pk})
  296. cl = ChangeList(request, Quartet, *get_changelist_args(m))
  297. cl.get_results(request)
  298. # There's only one Quartet instance
  299. self.assertEqual(cl.result_count, 1)
  300. def test_distinct_for_m2m_to_inherited_in_list_filter(self):
  301. """
  302. Regression test for #13902: When using a ManyToMany in list_filter,
  303. results shouldn't appear more than once. Target of the relationship
  304. inherits from another.
  305. """
  306. lead = ChordsMusician.objects.create(name='Player A')
  307. three = ChordsBand.objects.create(name='The Chords Trio')
  308. Invitation.objects.create(band=three, player=lead, instrument='guitar')
  309. Invitation.objects.create(band=three, player=lead, instrument='bass')
  310. m = ChordsBandAdmin(ChordsBand, custom_site)
  311. request = self.factory.get('/chordsband/', data={'members': lead.pk})
  312. cl = ChangeList(request, ChordsBand, *get_changelist_args(m))
  313. cl.get_results(request)
  314. # There's only one ChordsBand instance
  315. self.assertEqual(cl.result_count, 1)
  316. def test_distinct_for_non_unique_related_object_in_list_filter(self):
  317. """
  318. Regressions tests for #15819: If a field listed in list_filters
  319. is a non-unique related object, distinct() must be called.
  320. """
  321. parent = Parent.objects.create(name='Mary')
  322. # Two children with the same name
  323. Child.objects.create(parent=parent, name='Daniel')
  324. Child.objects.create(parent=parent, name='Daniel')
  325. m = ParentAdmin(Parent, custom_site)
  326. request = self.factory.get('/parent/', data={'child__name': 'Daniel'})
  327. cl = ChangeList(request, Parent, *get_changelist_args(m))
  328. # Make sure distinct() was called
  329. self.assertEqual(cl.queryset.count(), 1)
  330. def test_distinct_for_non_unique_related_object_in_search_fields(self):
  331. """
  332. Regressions tests for #15819: If a field listed in search_fields
  333. is a non-unique related object, distinct() must be called.
  334. """
  335. parent = Parent.objects.create(name='Mary')
  336. Child.objects.create(parent=parent, name='Danielle')
  337. Child.objects.create(parent=parent, name='Daniel')
  338. m = ParentAdmin(Parent, custom_site)
  339. request = self.factory.get('/parent/', data={SEARCH_VAR: 'daniel'})
  340. cl = ChangeList(request, Parent, *get_changelist_args(m))
  341. # Make sure distinct() was called
  342. self.assertEqual(cl.queryset.count(), 1)
  343. def test_distinct_for_many_to_many_at_second_level_in_search_fields(self):
  344. """
  345. When using a ManyToMany in search_fields at the second level behind a
  346. ForeignKey, distinct() must be called and results shouldn't appear more
  347. than once.
  348. """
  349. lead = Musician.objects.create(name='Vox')
  350. band = Group.objects.create(name='The Hype')
  351. Concert.objects.create(name='Woodstock', group=band)
  352. Membership.objects.create(group=band, music=lead, role='lead voice')
  353. Membership.objects.create(group=band, music=lead, role='bass player')
  354. m = ConcertAdmin(Concert, custom_site)
  355. request = self.factory.get('/concert/', data={SEARCH_VAR: 'vox'})
  356. cl = ChangeList(request, Concert, *get_changelist_args(m))
  357. # There's only one Concert instance
  358. self.assertEqual(cl.queryset.count(), 1)
  359. def test_no_distinct_for_m2m_in_list_filter_without_params(self):
  360. """
  361. If a ManyToManyField is in list_filter but isn't in any lookup params,
  362. the changelist's query shouldn't have distinct.
  363. """
  364. m = BandAdmin(Band, custom_site)
  365. for lookup_params in ({}, {'name': 'test'}):
  366. request = self.factory.get('/band/', lookup_params)
  367. cl = ChangeList(request, Band, *get_changelist_args(m))
  368. self.assertFalse(cl.queryset.query.distinct)
  369. # A ManyToManyField in params does have distinct applied.
  370. request = self.factory.get('/band/', {'genres': '0'})
  371. cl = ChangeList(request, Band, *get_changelist_args(m))
  372. self.assertTrue(cl.queryset.query.distinct)
  373. def test_pagination(self):
  374. """
  375. Regression tests for #12893: Pagination in admins changelist doesn't
  376. use queryset set by modeladmin.
  377. """
  378. parent = Parent.objects.create(name='anything')
  379. for i in range(30):
  380. Child.objects.create(name='name %s' % i, parent=parent)
  381. Child.objects.create(name='filtered %s' % i, parent=parent)
  382. request = self.factory.get('/child/')
  383. # Test default queryset
  384. m = ChildAdmin(Child, custom_site)
  385. cl = ChangeList(request, Child, *get_changelist_args(m))
  386. self.assertEqual(cl.queryset.count(), 60)
  387. self.assertEqual(cl.paginator.count, 60)
  388. self.assertEqual(list(cl.paginator.page_range), [1, 2, 3, 4, 5, 6])
  389. # Test custom queryset
  390. m = FilteredChildAdmin(Child, custom_site)
  391. cl = ChangeList(request, Child, *get_changelist_args(m))
  392. self.assertEqual(cl.queryset.count(), 30)
  393. self.assertEqual(cl.paginator.count, 30)
  394. self.assertEqual(list(cl.paginator.page_range), [1, 2, 3])
  395. def test_computed_list_display_localization(self):
  396. """
  397. Regression test for #13196: output of functions should be localized
  398. in the changelist.
  399. """
  400. superuser = User.objects.create_superuser(username='super', email='super@localhost', password='secret')
  401. self.client.force_login(superuser)
  402. event = Event.objects.create(date=datetime.date.today())
  403. response = self.client.get(reverse('admin:admin_changelist_event_changelist'))
  404. self.assertContains(response, formats.localize(event.date))
  405. self.assertNotContains(response, str(event.date))
  406. def test_dynamic_list_display(self):
  407. """
  408. Regression tests for #14206: dynamic list_display support.
  409. """
  410. parent = Parent.objects.create(name='parent')
  411. for i in range(10):
  412. Child.objects.create(name='child %s' % i, parent=parent)
  413. user_noparents = self._create_superuser('noparents')
  414. user_parents = self._create_superuser('parents')
  415. # Test with user 'noparents'
  416. m = custom_site._registry[Child]
  417. request = self._mocked_authenticated_request('/child/', user_noparents)
  418. response = m.changelist_view(request)
  419. self.assertNotContains(response, 'Parent object')
  420. list_display = m.get_list_display(request)
  421. list_display_links = m.get_list_display_links(request, list_display)
  422. self.assertEqual(list_display, ['name', 'age'])
  423. self.assertEqual(list_display_links, ['name'])
  424. # Test with user 'parents'
  425. m = DynamicListDisplayChildAdmin(Child, custom_site)
  426. request = self._mocked_authenticated_request('/child/', user_parents)
  427. response = m.changelist_view(request)
  428. self.assertContains(response, 'Parent object')
  429. custom_site.unregister(Child)
  430. list_display = m.get_list_display(request)
  431. list_display_links = m.get_list_display_links(request, list_display)
  432. self.assertEqual(list_display, ('parent', 'name', 'age'))
  433. self.assertEqual(list_display_links, ['parent'])
  434. # Test default implementation
  435. custom_site.register(Child, ChildAdmin)
  436. m = custom_site._registry[Child]
  437. request = self._mocked_authenticated_request('/child/', user_noparents)
  438. response = m.changelist_view(request)
  439. self.assertContains(response, 'Parent object')
  440. def test_show_all(self):
  441. parent = Parent.objects.create(name='anything')
  442. for i in range(30):
  443. Child.objects.create(name='name %s' % i, parent=parent)
  444. Child.objects.create(name='filtered %s' % i, parent=parent)
  445. # Add "show all" parameter to request
  446. request = self.factory.get('/child/', data={ALL_VAR: ''})
  447. # Test valid "show all" request (number of total objects is under max)
  448. m = ChildAdmin(Child, custom_site)
  449. m.list_max_show_all = 200
  450. # 200 is the max we'll pass to ChangeList
  451. cl = ChangeList(request, Child, *get_changelist_args(m))
  452. cl.get_results(request)
  453. self.assertEqual(len(cl.result_list), 60)
  454. # Test invalid "show all" request (number of total objects over max)
  455. # falls back to paginated pages
  456. m = ChildAdmin(Child, custom_site)
  457. m.list_max_show_all = 30
  458. # 30 is the max we'll pass to ChangeList for this test
  459. cl = ChangeList(request, Child, *get_changelist_args(m))
  460. cl.get_results(request)
  461. self.assertEqual(len(cl.result_list), 10)
  462. def test_dynamic_list_display_links(self):
  463. """
  464. Regression tests for #16257: dynamic list_display_links support.
  465. """
  466. parent = Parent.objects.create(name='parent')
  467. for i in range(1, 10):
  468. Child.objects.create(id=i, name='child %s' % i, parent=parent, age=i)
  469. m = DynamicListDisplayLinksChildAdmin(Child, custom_site)
  470. superuser = self._create_superuser('superuser')
  471. request = self._mocked_authenticated_request('/child/', superuser)
  472. response = m.changelist_view(request)
  473. for i in range(1, 10):
  474. link = reverse('admin:admin_changelist_child_change', args=(i,))
  475. self.assertContains(response, '<a href="%s">%s</a>' % (link, i))
  476. list_display = m.get_list_display(request)
  477. list_display_links = m.get_list_display_links(request, list_display)
  478. self.assertEqual(list_display, ('parent', 'name', 'age'))
  479. self.assertEqual(list_display_links, ['age'])
  480. def test_no_list_display_links(self):
  481. """#15185 -- Allow no links from the 'change list' view grid."""
  482. p = Parent.objects.create(name='parent')
  483. m = NoListDisplayLinksParentAdmin(Parent, custom_site)
  484. superuser = self._create_superuser('superuser')
  485. request = self._mocked_authenticated_request('/parent/', superuser)
  486. response = m.changelist_view(request)
  487. link = reverse('admin:admin_changelist_parent_change', args=(p.pk,))
  488. self.assertNotContains(response, '<a href="%s">' % link)
  489. def test_tuple_list_display(self):
  490. swallow = Swallow.objects.create(origin='Africa', load='12.34', speed='22.2')
  491. swallow2 = Swallow.objects.create(origin='Africa', load='12.34', speed='22.2')
  492. swallow_o2o = SwallowOneToOne.objects.create(swallow=swallow2)
  493. model_admin = SwallowAdmin(Swallow, custom_site)
  494. superuser = self._create_superuser('superuser')
  495. request = self._mocked_authenticated_request('/swallow/', superuser)
  496. response = model_admin.changelist_view(request)
  497. # just want to ensure it doesn't blow up during rendering
  498. self.assertContains(response, str(swallow.origin))
  499. self.assertContains(response, str(swallow.load))
  500. self.assertContains(response, str(swallow.speed))
  501. # Reverse one-to-one relations should work.
  502. self.assertContains(response, '<td class="field-swallowonetoone">-</td>')
  503. self.assertContains(response, '<td class="field-swallowonetoone">%s</td>' % swallow_o2o)
  504. def test_multiuser_edit(self):
  505. """
  506. Simultaneous edits of list_editable fields on the changelist by
  507. different users must not result in one user's edits creating a new
  508. object instead of modifying the correct existing object (#11313).
  509. """
  510. # To replicate this issue, simulate the following steps:
  511. # 1. User1 opens an admin changelist with list_editable fields.
  512. # 2. User2 edits object "Foo" such that it moves to another page in
  513. # the pagination order and saves.
  514. # 3. User1 edits object "Foo" and saves.
  515. # 4. The edit made by User1 does not get applied to object "Foo" but
  516. # instead is used to create a new object (bug).
  517. # For this test, order the changelist by the 'speed' attribute and
  518. # display 3 objects per page (SwallowAdmin.list_per_page = 3).
  519. # Setup the test to reflect the DB state after step 2 where User2 has
  520. # edited the first swallow object's speed from '4' to '1'.
  521. a = Swallow.objects.create(origin='Swallow A', load=4, speed=1)
  522. b = Swallow.objects.create(origin='Swallow B', load=2, speed=2)
  523. c = Swallow.objects.create(origin='Swallow C', load=5, speed=5)
  524. d = Swallow.objects.create(origin='Swallow D', load=9, speed=9)
  525. superuser = self._create_superuser('superuser')
  526. self.client.force_login(superuser)
  527. changelist_url = reverse('admin:admin_changelist_swallow_changelist')
  528. # Send the POST from User1 for step 3. It's still using the changelist
  529. # ordering from before User2's edits in step 2.
  530. data = {
  531. 'form-TOTAL_FORMS': '3',
  532. 'form-INITIAL_FORMS': '3',
  533. 'form-MIN_NUM_FORMS': '0',
  534. 'form-MAX_NUM_FORMS': '1000',
  535. 'form-0-id': str(d.pk),
  536. 'form-1-id': str(c.pk),
  537. 'form-2-id': str(a.pk),
  538. 'form-0-load': '9.0',
  539. 'form-0-speed': '9.0',
  540. 'form-1-load': '5.0',
  541. 'form-1-speed': '5.0',
  542. 'form-2-load': '5.0',
  543. 'form-2-speed': '4.0',
  544. '_save': 'Save',
  545. }
  546. response = self.client.post(changelist_url, data, follow=True, extra={'o': '-2'})
  547. # The object User1 edited in step 3 is displayed on the changelist and
  548. # has the correct edits applied.
  549. self.assertContains(response, '1 swallow was changed successfully.')
  550. self.assertContains(response, a.origin)
  551. a.refresh_from_db()
  552. self.assertEqual(a.load, float(data['form-2-load']))
  553. self.assertEqual(a.speed, float(data['form-2-speed']))
  554. b.refresh_from_db()
  555. self.assertEqual(b.load, 2)
  556. self.assertEqual(b.speed, 2)
  557. c.refresh_from_db()
  558. self.assertEqual(c.load, float(data['form-1-load']))
  559. self.assertEqual(c.speed, float(data['form-1-speed']))
  560. d.refresh_from_db()
  561. self.assertEqual(d.load, float(data['form-0-load']))
  562. self.assertEqual(d.speed, float(data['form-0-speed']))
  563. # No new swallows were created.
  564. self.assertEqual(len(Swallow.objects.all()), 4)
  565. def test_deterministic_order_for_unordered_model(self):
  566. """
  567. The primary key is used in the ordering of the changelist's results to
  568. guarantee a deterministic order, even when the model doesn't have any
  569. default ordering defined (#17198).
  570. """
  571. superuser = self._create_superuser('superuser')
  572. for counter in range(1, 51):
  573. UnorderedObject.objects.create(id=counter, bool=True)
  574. class UnorderedObjectAdmin(admin.ModelAdmin):
  575. list_per_page = 10
  576. def check_results_order(ascending=False):
  577. custom_site.register(UnorderedObject, UnorderedObjectAdmin)
  578. model_admin = UnorderedObjectAdmin(UnorderedObject, custom_site)
  579. counter = 0 if ascending else 51
  580. for page in range(0, 5):
  581. request = self._mocked_authenticated_request('/unorderedobject/?p=%s' % page, superuser)
  582. response = model_admin.changelist_view(request)
  583. for result in response.context_data['cl'].result_list:
  584. counter += 1 if ascending else -1
  585. self.assertEqual(result.id, counter)
  586. custom_site.unregister(UnorderedObject)
  587. # When no order is defined at all, everything is ordered by '-pk'.
  588. check_results_order()
  589. # When an order field is defined but multiple records have the same
  590. # value for that field, make sure everything gets ordered by -pk as well.
  591. UnorderedObjectAdmin.ordering = ['bool']
  592. check_results_order()
  593. # When order fields are defined, including the pk itself, use them.
  594. UnorderedObjectAdmin.ordering = ['bool', '-pk']
  595. check_results_order()
  596. UnorderedObjectAdmin.ordering = ['bool', 'pk']
  597. check_results_order(ascending=True)
  598. UnorderedObjectAdmin.ordering = ['-id', 'bool']
  599. check_results_order()
  600. UnorderedObjectAdmin.ordering = ['id', 'bool']
  601. check_results_order(ascending=True)
  602. def test_deterministic_order_for_model_ordered_by_its_manager(self):
  603. """
  604. The primary key is used in the ordering of the changelist's results to
  605. guarantee a deterministic order, even when the model has a manager that
  606. defines a default ordering (#17198).
  607. """
  608. superuser = self._create_superuser('superuser')
  609. for counter in range(1, 51):
  610. OrderedObject.objects.create(id=counter, bool=True, number=counter)
  611. class OrderedObjectAdmin(admin.ModelAdmin):
  612. list_per_page = 10
  613. def check_results_order(ascending=False):
  614. custom_site.register(OrderedObject, OrderedObjectAdmin)
  615. model_admin = OrderedObjectAdmin(OrderedObject, custom_site)
  616. counter = 0 if ascending else 51
  617. for page in range(0, 5):
  618. request = self._mocked_authenticated_request('/orderedobject/?p=%s' % page, superuser)
  619. response = model_admin.changelist_view(request)
  620. for result in response.context_data['cl'].result_list:
  621. counter += 1 if ascending else -1
  622. self.assertEqual(result.id, counter)
  623. custom_site.unregister(OrderedObject)
  624. # When no order is defined at all, use the model's default ordering (i.e. 'number')
  625. check_results_order(ascending=True)
  626. # When an order field is defined but multiple records have the same
  627. # value for that field, make sure everything gets ordered by -pk as well.
  628. OrderedObjectAdmin.ordering = ['bool']
  629. check_results_order()
  630. # When order fields are defined, including the pk itself, use them.
  631. OrderedObjectAdmin.ordering = ['bool', '-pk']
  632. check_results_order()
  633. OrderedObjectAdmin.ordering = ['bool', 'pk']
  634. check_results_order(ascending=True)
  635. OrderedObjectAdmin.ordering = ['-id', 'bool']
  636. check_results_order()
  637. OrderedObjectAdmin.ordering = ['id', 'bool']
  638. check_results_order(ascending=True)
  639. def test_dynamic_list_filter(self):
  640. """
  641. Regression tests for ticket #17646: dynamic list_filter support.
  642. """
  643. parent = Parent.objects.create(name='parent')
  644. for i in range(10):
  645. Child.objects.create(name='child %s' % i, parent=parent)
  646. user_noparents = self._create_superuser('noparents')
  647. user_parents = self._create_superuser('parents')
  648. # Test with user 'noparents'
  649. m = DynamicListFilterChildAdmin(Child, custom_site)
  650. request = self._mocked_authenticated_request('/child/', user_noparents)
  651. response = m.changelist_view(request)
  652. self.assertEqual(response.context_data['cl'].list_filter, ['name', 'age'])
  653. # Test with user 'parents'
  654. m = DynamicListFilterChildAdmin(Child, custom_site)
  655. request = self._mocked_authenticated_request('/child/', user_parents)
  656. response = m.changelist_view(request)
  657. self.assertEqual(response.context_data['cl'].list_filter, ('parent', 'name', 'age'))
  658. def test_dynamic_search_fields(self):
  659. child = self._create_superuser('child')
  660. m = DynamicSearchFieldsChildAdmin(Child, custom_site)
  661. request = self._mocked_authenticated_request('/child/', child)
  662. response = m.changelist_view(request)
  663. self.assertEqual(response.context_data['cl'].search_fields, ('name', 'age'))
  664. def test_pagination_page_range(self):
  665. """
  666. Regression tests for ticket #15653: ensure the number of pages
  667. generated for changelist views are correct.
  668. """
  669. # instantiating and setting up ChangeList object
  670. m = GroupAdmin(Group, custom_site)
  671. request = self.factory.get('/group/')
  672. cl = ChangeList(request, Group, *get_changelist_args(m))
  673. per_page = cl.list_per_page = 10
  674. for page_num, objects_count, expected_page_range in [
  675. (0, per_page, []),
  676. (0, per_page * 2, list(range(2))),
  677. (5, per_page * 11, list(range(11))),
  678. (5, per_page * 12, [0, 1, 2, 3, 4, 5, 6, 7, 8, '.', 10, 11]),
  679. (6, per_page * 12, [0, 1, '.', 3, 4, 5, 6, 7, 8, 9, 10, 11]),
  680. (6, per_page * 13, [0, 1, '.', 3, 4, 5, 6, 7, 8, 9, '.', 11, 12]),
  681. ]:
  682. # assuming we have exactly `objects_count` objects
  683. Group.objects.all().delete()
  684. for i in range(objects_count):
  685. Group.objects.create(name='test band')
  686. # setting page number and calculating page range
  687. cl.page_num = page_num
  688. cl.get_results(request)
  689. real_page_range = pagination(cl)['page_range']
  690. self.assertListEqual(
  691. expected_page_range,
  692. list(real_page_range),
  693. )
  694. def test_object_tools_displayed_no_add_permission(self):
  695. """
  696. When ModelAdmin.has_add_permission() returns False, the object-tools
  697. block is still shown.
  698. """
  699. superuser = self._create_superuser('superuser')
  700. m = EventAdmin(Event, custom_site)
  701. request = self._mocked_authenticated_request('/event/', superuser)
  702. self.assertFalse(m.has_add_permission(request))
  703. response = m.changelist_view(request)
  704. self.assertIn('<ul class="object-tools">', response.rendered_content)
  705. # The "Add" button inside the object-tools shouldn't appear.
  706. self.assertNotIn('Add ', response.rendered_content)
  707. class AdminLogNodeTestCase(TestCase):
  708. def test_get_admin_log_templatetag_custom_user(self):
  709. """
  710. Regression test for ticket #20088: admin log depends on User model
  711. having id field as primary key.
  712. The old implementation raised an AttributeError when trying to use
  713. the id field.
  714. """
  715. context = Context({'user': CustomIdUser()})
  716. template_string = '{% load log %}{% get_admin_log 10 as admin_log for_user user %}'
  717. template = Template(template_string)
  718. # Rendering should be u'' since this templatetag just logs,
  719. # it doesn't render any string.
  720. self.assertEqual(template.render(context), '')
  721. def test_get_admin_log_templatetag_no_user(self):
  722. """
  723. The {% get_admin_log %} tag should work without specifying a user.
  724. """
  725. user = User(username='jondoe', password='secret', email='super@example.com')
  726. user.save()
  727. ct = ContentType.objects.get_for_model(User)
  728. LogEntry.objects.log_action(user.pk, ct.pk, user.pk, repr(user), 1)
  729. t = Template(
  730. '{% load log %}'
  731. '{% get_admin_log 100 as admin_log %}'
  732. '{% for entry in admin_log %}'
  733. '{{ entry|safe }}'
  734. '{% endfor %}'
  735. )
  736. self.assertEqual(t.render(Context({})), 'Added "<User: jondoe>".')
  737. @override_settings(ROOT_URLCONF='admin_changelist.urls')
  738. class SeleniumTests(AdminSeleniumTestCase):
  739. available_apps = ['admin_changelist'] + AdminSeleniumTestCase.available_apps
  740. def setUp(self):
  741. User.objects.create_superuser(username='super', password='secret', email=None)
  742. def test_add_row_selection(self):
  743. """
  744. The status line for selected rows gets updated correctly (#22038).
  745. """
  746. self.admin_login(username='super', password='secret')
  747. self.selenium.get(self.live_server_url + reverse('admin:auth_user_changelist'))
  748. form_id = '#changelist-form'
  749. # Test amount of rows in the Changelist
  750. rows = self.selenium.find_elements_by_css_selector(
  751. '%s #result_list tbody tr' % form_id)
  752. self.assertEqual(len(rows), 1)
  753. # Test current selection
  754. selection_indicator = self.selenium.find_element_by_css_selector(
  755. '%s .action-counter' % form_id)
  756. self.assertEqual(selection_indicator.text, "0 of 1 selected")
  757. # Select a row and check again
  758. row_selector = self.selenium.find_element_by_css_selector(
  759. '%s #result_list tbody tr:first-child .action-select' % form_id)
  760. row_selector.click()
  761. self.assertEqual(selection_indicator.text, "1 of 1 selected")