tests.py 44 KB

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