tests.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  1. from datetime import date
  2. from django import forms
  3. from django.contrib.admin.models import ADDITION, CHANGE, DELETION, LogEntry
  4. from django.contrib.admin.options import (
  5. HORIZONTAL, VERTICAL, ModelAdmin, TabularInline,
  6. get_content_type_for_model,
  7. )
  8. from django.contrib.admin.sites import AdminSite
  9. from django.contrib.admin.widgets import (
  10. AdminDateWidget, AdminRadioSelect, AutocompleteSelect,
  11. AutocompleteSelectMultiple,
  12. )
  13. from django.contrib.auth.models import User
  14. from django.db import models
  15. from django.forms.widgets import Select
  16. from django.test import SimpleTestCase, TestCase
  17. from django.test.utils import isolate_apps
  18. from .models import Band, Concert, Song
  19. class MockRequest:
  20. pass
  21. class MockSuperUser:
  22. def has_perm(self, perm):
  23. return True
  24. request = MockRequest()
  25. request.user = MockSuperUser()
  26. class ModelAdminTests(TestCase):
  27. def setUp(self):
  28. self.band = Band.objects.create(
  29. name='The Doors',
  30. bio='',
  31. sign_date=date(1965, 1, 1),
  32. )
  33. self.site = AdminSite()
  34. def test_modeladmin_str(self):
  35. ma = ModelAdmin(Band, self.site)
  36. self.assertEqual(str(ma), 'modeladmin.ModelAdmin')
  37. # form/fields/fieldsets interaction ##############################
  38. def test_default_fields(self):
  39. ma = ModelAdmin(Band, self.site)
  40. self.assertEqual(list(ma.get_form(request).base_fields), ['name', 'bio', 'sign_date'])
  41. self.assertEqual(list(ma.get_fields(request)), ['name', 'bio', 'sign_date'])
  42. self.assertEqual(list(ma.get_fields(request, self.band)), ['name', 'bio', 'sign_date'])
  43. self.assertIsNone(ma.get_exclude(request, self.band))
  44. def test_default_fieldsets(self):
  45. # fieldsets_add and fieldsets_change should return a special data structure that
  46. # is used in the templates. They should generate the "right thing" whether we
  47. # have specified a custom form, the fields argument, or nothing at all.
  48. #
  49. # Here's the default case. There are no custom form_add/form_change methods,
  50. # no fields argument, and no fieldsets argument.
  51. ma = ModelAdmin(Band, self.site)
  52. self.assertEqual(ma.get_fieldsets(request), [(None, {'fields': ['name', 'bio', 'sign_date']})])
  53. self.assertEqual(ma.get_fieldsets(request, self.band), [(None, {'fields': ['name', 'bio', 'sign_date']})])
  54. def test_get_fieldsets(self):
  55. # get_fieldsets() is called when figuring out form fields (#18681).
  56. class BandAdmin(ModelAdmin):
  57. def get_fieldsets(self, request, obj=None):
  58. return [(None, {'fields': ['name', 'bio']})]
  59. ma = BandAdmin(Band, self.site)
  60. form = ma.get_form(None)
  61. self.assertEqual(form._meta.fields, ['name', 'bio'])
  62. class InlineBandAdmin(TabularInline):
  63. model = Concert
  64. fk_name = 'main_band'
  65. can_delete = False
  66. def get_fieldsets(self, request, obj=None):
  67. return [(None, {'fields': ['day', 'transport']})]
  68. ma = InlineBandAdmin(Band, self.site)
  69. form = ma.get_formset(None).form
  70. self.assertEqual(form._meta.fields, ['day', 'transport'])
  71. def test_lookup_allowed_allows_nonexistent_lookup(self):
  72. """
  73. A lookup_allowed allows a parameter whose field lookup doesn't exist.
  74. (#21129).
  75. """
  76. class BandAdmin(ModelAdmin):
  77. fields = ['name']
  78. ma = BandAdmin(Band, self.site)
  79. self.assertTrue(ma.lookup_allowed('name__nonexistent', 'test_value'))
  80. @isolate_apps('modeladmin')
  81. def test_lookup_allowed_onetoone(self):
  82. class Department(models.Model):
  83. code = models.CharField(max_length=4, unique=True)
  84. class Employee(models.Model):
  85. department = models.ForeignKey(Department, models.CASCADE, to_field="code")
  86. class EmployeeProfile(models.Model):
  87. employee = models.OneToOneField(Employee, models.CASCADE)
  88. class EmployeeInfo(models.Model):
  89. employee = models.OneToOneField(Employee, models.CASCADE)
  90. description = models.CharField(max_length=100)
  91. class EmployeeProfileAdmin(ModelAdmin):
  92. list_filter = [
  93. 'employee__employeeinfo__description',
  94. 'employee__department__code',
  95. ]
  96. ma = EmployeeProfileAdmin(EmployeeProfile, self.site)
  97. # Reverse OneToOneField
  98. self.assertIs(ma.lookup_allowed('employee__employeeinfo__description', 'test_value'), True)
  99. # OneToOneField and ForeignKey
  100. self.assertIs(ma.lookup_allowed('employee__department__code', 'test_value'), True)
  101. def test_field_arguments(self):
  102. # If fields is specified, fieldsets_add and fieldsets_change should
  103. # just stick the fields into a formsets structure and return it.
  104. class BandAdmin(ModelAdmin):
  105. fields = ['name']
  106. ma = BandAdmin(Band, self.site)
  107. self.assertEqual(list(ma.get_fields(request)), ['name'])
  108. self.assertEqual(list(ma.get_fields(request, self.band)), ['name'])
  109. self.assertEqual(ma.get_fieldsets(request), [(None, {'fields': ['name']})])
  110. self.assertEqual(ma.get_fieldsets(request, self.band), [(None, {'fields': ['name']})])
  111. def test_field_arguments_restricted_on_form(self):
  112. # If fields or fieldsets is specified, it should exclude fields on the
  113. # Form class to the fields specified. This may cause errors to be
  114. # raised in the db layer if required model fields aren't in fields/
  115. # fieldsets, but that's preferable to ghost errors where a field in the
  116. # Form class isn't being displayed because it's not in fields/fieldsets.
  117. # Using `fields`.
  118. class BandAdmin(ModelAdmin):
  119. fields = ['name']
  120. ma = BandAdmin(Band, self.site)
  121. self.assertEqual(list(ma.get_form(request).base_fields), ['name'])
  122. self.assertEqual(list(ma.get_form(request, self.band).base_fields), ['name'])
  123. # Using `fieldsets`.
  124. class BandAdmin(ModelAdmin):
  125. fieldsets = [(None, {'fields': ['name']})]
  126. ma = BandAdmin(Band, self.site)
  127. self.assertEqual(list(ma.get_form(request).base_fields), ['name'])
  128. self.assertEqual(list(ma.get_form(request, self.band).base_fields), ['name'])
  129. # Using `exclude`.
  130. class BandAdmin(ModelAdmin):
  131. exclude = ['bio']
  132. ma = BandAdmin(Band, self.site)
  133. self.assertEqual(list(ma.get_form(request).base_fields), ['name', 'sign_date'])
  134. # You can also pass a tuple to `exclude`.
  135. class BandAdmin(ModelAdmin):
  136. exclude = ('bio',)
  137. ma = BandAdmin(Band, self.site)
  138. self.assertEqual(list(ma.get_form(request).base_fields), ['name', 'sign_date'])
  139. # Using `fields` and `exclude`.
  140. class BandAdmin(ModelAdmin):
  141. fields = ['name', 'bio']
  142. exclude = ['bio']
  143. ma = BandAdmin(Band, self.site)
  144. self.assertEqual(list(ma.get_form(request).base_fields), ['name'])
  145. def test_custom_form_meta_exclude_with_readonly(self):
  146. """
  147. The custom ModelForm's `Meta.exclude` is respected when used in
  148. conjunction with `ModelAdmin.readonly_fields` and when no
  149. `ModelAdmin.exclude` is defined (#14496).
  150. """
  151. # With ModelAdmin
  152. class AdminBandForm(forms.ModelForm):
  153. class Meta:
  154. model = Band
  155. exclude = ['bio']
  156. class BandAdmin(ModelAdmin):
  157. readonly_fields = ['name']
  158. form = AdminBandForm
  159. ma = BandAdmin(Band, self.site)
  160. self.assertEqual(list(ma.get_form(request).base_fields), ['sign_date'])
  161. # With InlineModelAdmin
  162. class AdminConcertForm(forms.ModelForm):
  163. class Meta:
  164. model = Concert
  165. exclude = ['day']
  166. class ConcertInline(TabularInline):
  167. readonly_fields = ['transport']
  168. form = AdminConcertForm
  169. fk_name = 'main_band'
  170. model = Concert
  171. class BandAdmin(ModelAdmin):
  172. inlines = [ConcertInline]
  173. ma = BandAdmin(Band, self.site)
  174. self.assertEqual(
  175. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  176. ['main_band', 'opening_band', 'id', 'DELETE'])
  177. def test_custom_formfield_override_readonly(self):
  178. class AdminBandForm(forms.ModelForm):
  179. name = forms.CharField()
  180. class Meta:
  181. exclude = ()
  182. model = Band
  183. class BandAdmin(ModelAdmin):
  184. form = AdminBandForm
  185. readonly_fields = ['name']
  186. ma = BandAdmin(Band, self.site)
  187. # `name` shouldn't appear in base_fields because it's part of
  188. # readonly_fields.
  189. self.assertEqual(
  190. list(ma.get_form(request).base_fields),
  191. ['bio', 'sign_date']
  192. )
  193. # But it should appear in get_fields()/fieldsets() so it can be
  194. # displayed as read-only.
  195. self.assertEqual(
  196. list(ma.get_fields(request)),
  197. ['bio', 'sign_date', 'name']
  198. )
  199. self.assertEqual(
  200. list(ma.get_fieldsets(request)),
  201. [(None, {'fields': ['bio', 'sign_date', 'name']})]
  202. )
  203. def test_custom_form_meta_exclude(self):
  204. """
  205. The custom ModelForm's `Meta.exclude` is overridden if
  206. `ModelAdmin.exclude` or `InlineModelAdmin.exclude` are defined (#14496).
  207. """
  208. # With ModelAdmin
  209. class AdminBandForm(forms.ModelForm):
  210. class Meta:
  211. model = Band
  212. exclude = ['bio']
  213. class BandAdmin(ModelAdmin):
  214. exclude = ['name']
  215. form = AdminBandForm
  216. ma = BandAdmin(Band, self.site)
  217. self.assertEqual(list(ma.get_form(request).base_fields), ['bio', 'sign_date'])
  218. # With InlineModelAdmin
  219. class AdminConcertForm(forms.ModelForm):
  220. class Meta:
  221. model = Concert
  222. exclude = ['day']
  223. class ConcertInline(TabularInline):
  224. exclude = ['transport']
  225. form = AdminConcertForm
  226. fk_name = 'main_band'
  227. model = Concert
  228. class BandAdmin(ModelAdmin):
  229. inlines = [ConcertInline]
  230. ma = BandAdmin(Band, self.site)
  231. self.assertEqual(
  232. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  233. ['main_band', 'opening_band', 'day', 'id', 'DELETE']
  234. )
  235. def test_overriding_get_exclude(self):
  236. class BandAdmin(ModelAdmin):
  237. def get_exclude(self, request, obj=None):
  238. return ['name']
  239. self.assertEqual(
  240. list(BandAdmin(Band, self.site).get_form(request).base_fields),
  241. ['bio', 'sign_date']
  242. )
  243. def test_get_exclude_overrides_exclude(self):
  244. class BandAdmin(ModelAdmin):
  245. exclude = ['bio']
  246. def get_exclude(self, request, obj=None):
  247. return ['name']
  248. self.assertEqual(
  249. list(BandAdmin(Band, self.site).get_form(request).base_fields),
  250. ['bio', 'sign_date']
  251. )
  252. def test_get_exclude_takes_obj(self):
  253. class BandAdmin(ModelAdmin):
  254. def get_exclude(self, request, obj=None):
  255. if obj:
  256. return ['sign_date']
  257. return ['name']
  258. self.assertEqual(
  259. list(BandAdmin(Band, self.site).get_form(request, self.band).base_fields),
  260. ['name', 'bio']
  261. )
  262. def test_custom_form_validation(self):
  263. # If a form is specified, it should use it allowing custom validation
  264. # to work properly. This won't break any of the admin widgets or media.
  265. class AdminBandForm(forms.ModelForm):
  266. delete = forms.BooleanField()
  267. class BandAdmin(ModelAdmin):
  268. form = AdminBandForm
  269. ma = BandAdmin(Band, self.site)
  270. self.assertEqual(list(ma.get_form(request).base_fields), ['name', 'bio', 'sign_date', 'delete'])
  271. self.assertEqual(type(ma.get_form(request).base_fields['sign_date'].widget), AdminDateWidget)
  272. def test_form_exclude_kwarg_override(self):
  273. """
  274. The `exclude` kwarg passed to `ModelAdmin.get_form()` overrides all
  275. other declarations (#8999).
  276. """
  277. class AdminBandForm(forms.ModelForm):
  278. class Meta:
  279. model = Band
  280. exclude = ['name']
  281. class BandAdmin(ModelAdmin):
  282. exclude = ['sign_date']
  283. form = AdminBandForm
  284. def get_form(self, request, obj=None, **kwargs):
  285. kwargs['exclude'] = ['bio']
  286. return super().get_form(request, obj, **kwargs)
  287. ma = BandAdmin(Band, self.site)
  288. self.assertEqual(list(ma.get_form(request).base_fields), ['name', 'sign_date'])
  289. def test_formset_exclude_kwarg_override(self):
  290. """
  291. The `exclude` kwarg passed to `InlineModelAdmin.get_formset()`
  292. overrides all other declarations (#8999).
  293. """
  294. class AdminConcertForm(forms.ModelForm):
  295. class Meta:
  296. model = Concert
  297. exclude = ['day']
  298. class ConcertInline(TabularInline):
  299. exclude = ['transport']
  300. form = AdminConcertForm
  301. fk_name = 'main_band'
  302. model = Concert
  303. def get_formset(self, request, obj=None, **kwargs):
  304. kwargs['exclude'] = ['opening_band']
  305. return super().get_formset(request, obj, **kwargs)
  306. class BandAdmin(ModelAdmin):
  307. inlines = [ConcertInline]
  308. ma = BandAdmin(Band, self.site)
  309. self.assertEqual(
  310. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  311. ['main_band', 'day', 'transport', 'id', 'DELETE']
  312. )
  313. def test_formset_overriding_get_exclude_with_form_fields(self):
  314. class AdminConcertForm(forms.ModelForm):
  315. class Meta:
  316. model = Concert
  317. fields = ['main_band', 'opening_band', 'day', 'transport']
  318. class ConcertInline(TabularInline):
  319. form = AdminConcertForm
  320. fk_name = 'main_band'
  321. model = Concert
  322. def get_exclude(self, request, obj=None):
  323. return ['opening_band']
  324. class BandAdmin(ModelAdmin):
  325. inlines = [ConcertInline]
  326. ma = BandAdmin(Band, self.site)
  327. self.assertEqual(
  328. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  329. ['main_band', 'day', 'transport', 'id', 'DELETE']
  330. )
  331. def test_formset_overriding_get_exclude_with_form_exclude(self):
  332. class AdminConcertForm(forms.ModelForm):
  333. class Meta:
  334. model = Concert
  335. exclude = ['day']
  336. class ConcertInline(TabularInline):
  337. form = AdminConcertForm
  338. fk_name = 'main_band'
  339. model = Concert
  340. def get_exclude(self, request, obj=None):
  341. return ['opening_band']
  342. class BandAdmin(ModelAdmin):
  343. inlines = [ConcertInline]
  344. ma = BandAdmin(Band, self.site)
  345. self.assertEqual(
  346. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  347. ['main_band', 'day', 'transport', 'id', 'DELETE']
  348. )
  349. def test_queryset_override(self):
  350. # If the queryset of a ModelChoiceField in a custom form is overridden,
  351. # RelatedFieldWidgetWrapper doesn't mess that up.
  352. band2 = Band.objects.create(name='The Beatles', bio='', sign_date=date(1962, 1, 1))
  353. ma = ModelAdmin(Concert, self.site)
  354. form = ma.get_form(request)()
  355. self.assertHTMLEqual(
  356. str(form["main_band"]),
  357. '<div class="related-widget-wrapper">'
  358. '<select name="main_band" id="id_main_band" required>'
  359. '<option value="" selected>---------</option>'
  360. '<option value="%d">The Beatles</option>'
  361. '<option value="%d">The Doors</option>'
  362. '</select></div>' % (band2.id, self.band.id)
  363. )
  364. class AdminConcertForm(forms.ModelForm):
  365. def __init__(self, *args, **kwargs):
  366. super().__init__(*args, **kwargs)
  367. self.fields["main_band"].queryset = Band.objects.filter(name='The Doors')
  368. class ConcertAdminWithForm(ModelAdmin):
  369. form = AdminConcertForm
  370. ma = ConcertAdminWithForm(Concert, self.site)
  371. form = ma.get_form(request)()
  372. self.assertHTMLEqual(
  373. str(form["main_band"]),
  374. '<div class="related-widget-wrapper">'
  375. '<select name="main_band" id="id_main_band" required>'
  376. '<option value="" selected>---------</option>'
  377. '<option value="%d">The Doors</option>'
  378. '</select></div>' % self.band.id
  379. )
  380. def test_regression_for_ticket_15820(self):
  381. """
  382. `obj` is passed from `InlineModelAdmin.get_fieldsets()` to
  383. `InlineModelAdmin.get_formset()`.
  384. """
  385. class CustomConcertForm(forms.ModelForm):
  386. class Meta:
  387. model = Concert
  388. fields = ['day']
  389. class ConcertInline(TabularInline):
  390. model = Concert
  391. fk_name = 'main_band'
  392. def get_formset(self, request, obj=None, **kwargs):
  393. if obj:
  394. kwargs['form'] = CustomConcertForm
  395. return super().get_formset(request, obj, **kwargs)
  396. class BandAdmin(ModelAdmin):
  397. inlines = [ConcertInline]
  398. Concert.objects.create(main_band=self.band, opening_band=self.band, day=1)
  399. ma = BandAdmin(Band, self.site)
  400. inline_instances = ma.get_inline_instances(request)
  401. fieldsets = list(inline_instances[0].get_fieldsets(request))
  402. self.assertEqual(fieldsets[0][1]['fields'], ['main_band', 'opening_band', 'day', 'transport'])
  403. fieldsets = list(inline_instances[0].get_fieldsets(request, inline_instances[0].model))
  404. self.assertEqual(fieldsets[0][1]['fields'], ['day'])
  405. # radio_fields behavior ###########################################
  406. def test_default_foreign_key_widget(self):
  407. # First, without any radio_fields specified, the widgets for ForeignKey
  408. # and fields with choices specified ought to be a basic Select widget.
  409. # ForeignKey widgets in the admin are wrapped with RelatedFieldWidgetWrapper so
  410. # they need to be handled properly when type checking. For Select fields, all of
  411. # the choices lists have a first entry of dashes.
  412. cma = ModelAdmin(Concert, self.site)
  413. cmafa = cma.get_form(request)
  414. self.assertEqual(type(cmafa.base_fields['main_band'].widget.widget), Select)
  415. self.assertEqual(
  416. list(cmafa.base_fields['main_band'].widget.choices),
  417. [('', '---------'), (self.band.id, 'The Doors')])
  418. self.assertEqual(type(cmafa.base_fields['opening_band'].widget.widget), Select)
  419. self.assertEqual(
  420. list(cmafa.base_fields['opening_band'].widget.choices),
  421. [('', '---------'), (self.band.id, 'The Doors')]
  422. )
  423. self.assertEqual(type(cmafa.base_fields['day'].widget), Select)
  424. self.assertEqual(
  425. list(cmafa.base_fields['day'].widget.choices),
  426. [('', '---------'), (1, 'Fri'), (2, 'Sat')]
  427. )
  428. self.assertEqual(type(cmafa.base_fields['transport'].widget), Select)
  429. self.assertEqual(
  430. list(cmafa.base_fields['transport'].widget.choices),
  431. [('', '---------'), (1, 'Plane'), (2, 'Train'), (3, 'Bus')])
  432. def test_foreign_key_as_radio_field(self):
  433. # Now specify all the fields as radio_fields. Widgets should now be
  434. # RadioSelect, and the choices list should have a first entry of 'None' if
  435. # blank=True for the model field. Finally, the widget should have the
  436. # 'radiolist' attr, and 'inline' as well if the field is specified HORIZONTAL.
  437. class ConcertAdmin(ModelAdmin):
  438. radio_fields = {
  439. 'main_band': HORIZONTAL,
  440. 'opening_band': VERTICAL,
  441. 'day': VERTICAL,
  442. 'transport': HORIZONTAL,
  443. }
  444. cma = ConcertAdmin(Concert, self.site)
  445. cmafa = cma.get_form(request)
  446. self.assertEqual(type(cmafa.base_fields['main_band'].widget.widget), AdminRadioSelect)
  447. self.assertEqual(cmafa.base_fields['main_band'].widget.attrs, {'class': 'radiolist inline'})
  448. self.assertEqual(
  449. list(cmafa.base_fields['main_band'].widget.choices),
  450. [(self.band.id, 'The Doors')]
  451. )
  452. self.assertEqual(type(cmafa.base_fields['opening_band'].widget.widget), AdminRadioSelect)
  453. self.assertEqual(cmafa.base_fields['opening_band'].widget.attrs, {'class': 'radiolist'})
  454. self.assertEqual(
  455. list(cmafa.base_fields['opening_band'].widget.choices),
  456. [('', 'None'), (self.band.id, 'The Doors')]
  457. )
  458. self.assertEqual(type(cmafa.base_fields['day'].widget), AdminRadioSelect)
  459. self.assertEqual(cmafa.base_fields['day'].widget.attrs, {'class': 'radiolist'})
  460. self.assertEqual(list(cmafa.base_fields['day'].widget.choices), [(1, 'Fri'), (2, 'Sat')])
  461. self.assertEqual(type(cmafa.base_fields['transport'].widget), AdminRadioSelect)
  462. self.assertEqual(cmafa.base_fields['transport'].widget.attrs, {'class': 'radiolist inline'})
  463. self.assertEqual(
  464. list(cmafa.base_fields['transport'].widget.choices),
  465. [('', 'None'), (1, 'Plane'), (2, 'Train'), (3, 'Bus')]
  466. )
  467. class AdminConcertForm(forms.ModelForm):
  468. class Meta:
  469. model = Concert
  470. exclude = ('transport',)
  471. class ConcertAdmin(ModelAdmin):
  472. form = AdminConcertForm
  473. ma = ConcertAdmin(Concert, self.site)
  474. self.assertEqual(list(ma.get_form(request).base_fields), ['main_band', 'opening_band', 'day'])
  475. class AdminConcertForm(forms.ModelForm):
  476. extra = forms.CharField()
  477. class Meta:
  478. model = Concert
  479. fields = ['extra', 'transport']
  480. class ConcertAdmin(ModelAdmin):
  481. form = AdminConcertForm
  482. ma = ConcertAdmin(Concert, self.site)
  483. self.assertEqual(list(ma.get_form(request).base_fields), ['extra', 'transport'])
  484. class ConcertInline(TabularInline):
  485. form = AdminConcertForm
  486. model = Concert
  487. fk_name = 'main_band'
  488. can_delete = True
  489. class BandAdmin(ModelAdmin):
  490. inlines = [ConcertInline]
  491. ma = BandAdmin(Band, self.site)
  492. self.assertEqual(
  493. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  494. ['extra', 'transport', 'id', 'DELETE', 'main_band']
  495. )
  496. def test_log_actions(self):
  497. ma = ModelAdmin(Band, self.site)
  498. mock_request = MockRequest()
  499. mock_request.user = User.objects.create(username='bill')
  500. content_type = get_content_type_for_model(self.band)
  501. tests = (
  502. (ma.log_addition, ADDITION, {'added': {}}),
  503. (ma.log_change, CHANGE, {'changed': {'fields': ['name', 'bio']}}),
  504. (ma.log_deletion, DELETION, str(self.band)),
  505. )
  506. for method, flag, message in tests:
  507. with self.subTest(name=method.__name__):
  508. created = method(mock_request, self.band, message)
  509. fetched = LogEntry.objects.filter(action_flag=flag).latest('id')
  510. self.assertEqual(created, fetched)
  511. self.assertEqual(fetched.action_flag, flag)
  512. self.assertEqual(fetched.content_type, content_type)
  513. self.assertEqual(fetched.object_id, str(self.band.pk))
  514. self.assertEqual(fetched.user, mock_request.user)
  515. if flag == DELETION:
  516. self.assertEqual(fetched.change_message, '')
  517. self.assertEqual(fetched.object_repr, message)
  518. else:
  519. self.assertEqual(fetched.change_message, str(message))
  520. self.assertEqual(fetched.object_repr, str(self.band))
  521. def test_get_autocomplete_fields(self):
  522. class NameAdmin(ModelAdmin):
  523. search_fields = ['name']
  524. class SongAdmin(ModelAdmin):
  525. autocomplete_fields = ['featuring']
  526. fields = ['featuring', 'band']
  527. class OtherSongAdmin(SongAdmin):
  528. def get_autocomplete_fields(self, request):
  529. return ['band']
  530. self.site.register(Band, NameAdmin)
  531. try:
  532. # Uses autocomplete_fields if not overridden.
  533. model_admin = SongAdmin(Song, self.site)
  534. form = model_admin.get_form(request)()
  535. self.assertIsInstance(form.fields['featuring'].widget.widget, AutocompleteSelectMultiple)
  536. # Uses overridden get_autocomplete_fields
  537. model_admin = OtherSongAdmin(Song, self.site)
  538. form = model_admin.get_form(request)()
  539. self.assertIsInstance(form.fields['band'].widget.widget, AutocompleteSelect)
  540. finally:
  541. self.site.unregister(Band)
  542. def test_get_deleted_objects(self):
  543. mock_request = MockRequest()
  544. mock_request.user = User.objects.create_superuser(username='bob', email='bob@test.com', password='test')
  545. ma = ModelAdmin(Band, self.site)
  546. deletable_objects, model_count, perms_needed, protected = ma.get_deleted_objects([self.band], request)
  547. self.assertEqual(deletable_objects, ['Band: The Doors'])
  548. self.assertEqual(model_count, {'bands': 1})
  549. self.assertEqual(perms_needed, set())
  550. self.assertEqual(protected, [])
  551. class ModelAdminPermissionTests(SimpleTestCase):
  552. class MockUser:
  553. def has_module_perms(self, app_label):
  554. return app_label == 'modeladmin'
  555. class MockViewUser(MockUser):
  556. def has_perm(self, perm):
  557. return perm == 'modeladmin.view_band'
  558. class MockAddUser(MockUser):
  559. def has_perm(self, perm):
  560. return perm == 'modeladmin.add_band'
  561. class MockChangeUser(MockUser):
  562. def has_perm(self, perm):
  563. return perm == 'modeladmin.change_band'
  564. class MockDeleteUser(MockUser):
  565. def has_perm(self, perm):
  566. return perm == 'modeladmin.delete_band'
  567. def test_has_view_permission(self):
  568. """
  569. has_view_permission() returns True for users who can view objects and
  570. False for users who can't.
  571. """
  572. ma = ModelAdmin(Band, AdminSite())
  573. request = MockRequest()
  574. request.user = self.MockViewUser()
  575. self.assertIs(ma.has_view_permission(request), True)
  576. request.user = self.MockAddUser()
  577. self.assertIs(ma.has_view_permission(request), False)
  578. request.user = self.MockChangeUser()
  579. self.assertIs(ma.has_view_permission(request), True)
  580. request.user = self.MockDeleteUser()
  581. self.assertIs(ma.has_view_permission(request), False)
  582. def test_has_add_permission(self):
  583. """
  584. has_add_permission returns True for users who can add objects and
  585. False for users who can't.
  586. """
  587. ma = ModelAdmin(Band, AdminSite())
  588. request = MockRequest()
  589. request.user = self.MockViewUser()
  590. self.assertFalse(ma.has_add_permission(request))
  591. request.user = self.MockAddUser()
  592. self.assertTrue(ma.has_add_permission(request))
  593. request.user = self.MockChangeUser()
  594. self.assertFalse(ma.has_add_permission(request))
  595. request.user = self.MockDeleteUser()
  596. self.assertFalse(ma.has_add_permission(request))
  597. def test_inline_has_add_permission_uses_obj(self):
  598. class ConcertInline(TabularInline):
  599. model = Concert
  600. def has_add_permission(self, request, obj):
  601. return bool(obj)
  602. class BandAdmin(ModelAdmin):
  603. inlines = [ConcertInline]
  604. ma = BandAdmin(Band, AdminSite())
  605. request = MockRequest()
  606. request.user = self.MockAddUser()
  607. self.assertEqual(ma.get_inline_instances(request), [])
  608. band = Band(name='The Doors', bio='', sign_date=date(1965, 1, 1))
  609. inline_instances = ma.get_inline_instances(request, band)
  610. self.assertEqual(len(inline_instances), 1)
  611. self.assertIsInstance(inline_instances[0], ConcertInline)
  612. def test_has_change_permission(self):
  613. """
  614. has_change_permission returns True for users who can edit objects and
  615. False for users who can't.
  616. """
  617. ma = ModelAdmin(Band, AdminSite())
  618. request = MockRequest()
  619. request.user = self.MockViewUser()
  620. self.assertIs(ma.has_change_permission(request), False)
  621. request.user = self.MockAddUser()
  622. self.assertFalse(ma.has_change_permission(request))
  623. request.user = self.MockChangeUser()
  624. self.assertTrue(ma.has_change_permission(request))
  625. request.user = self.MockDeleteUser()
  626. self.assertFalse(ma.has_change_permission(request))
  627. def test_has_delete_permission(self):
  628. """
  629. has_delete_permission returns True for users who can delete objects and
  630. False for users who can't.
  631. """
  632. ma = ModelAdmin(Band, AdminSite())
  633. request = MockRequest()
  634. request.user = self.MockViewUser()
  635. self.assertIs(ma.has_delete_permission(request), False)
  636. request.user = self.MockAddUser()
  637. self.assertFalse(ma.has_delete_permission(request))
  638. request.user = self.MockChangeUser()
  639. self.assertFalse(ma.has_delete_permission(request))
  640. request.user = self.MockDeleteUser()
  641. self.assertTrue(ma.has_delete_permission(request))
  642. def test_has_module_permission(self):
  643. """
  644. as_module_permission returns True for users who have any permission
  645. for the module and False for users who don't.
  646. """
  647. ma = ModelAdmin(Band, AdminSite())
  648. request = MockRequest()
  649. request.user = self.MockViewUser()
  650. self.assertIs(ma.has_module_permission(request), True)
  651. request.user = self.MockAddUser()
  652. self.assertTrue(ma.has_module_permission(request))
  653. request.user = self.MockChangeUser()
  654. self.assertTrue(ma.has_module_permission(request))
  655. request.user = self.MockDeleteUser()
  656. self.assertTrue(ma.has_module_permission(request))
  657. original_app_label = ma.opts.app_label
  658. ma.opts.app_label = 'anotherapp'
  659. try:
  660. request.user = self.MockViewUser()
  661. self.assertIs(ma.has_module_permission(request), False)
  662. request.user = self.MockAddUser()
  663. self.assertFalse(ma.has_module_permission(request))
  664. request.user = self.MockChangeUser()
  665. self.assertFalse(ma.has_module_permission(request))
  666. request.user = self.MockDeleteUser()
  667. self.assertFalse(ma.has_module_permission(request))
  668. finally:
  669. ma.opts.app_label = original_app_label