tests.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053
  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,
  6. VERTICAL,
  7. ModelAdmin,
  8. TabularInline,
  9. get_content_type_for_model,
  10. )
  11. from django.contrib.admin.sites import AdminSite
  12. from django.contrib.admin.widgets import (
  13. AdminDateWidget,
  14. AdminRadioSelect,
  15. AutocompleteSelect,
  16. AutocompleteSelectMultiple,
  17. )
  18. from django.contrib.auth.models import User
  19. from django.db import models
  20. from django.forms.widgets import Select
  21. from django.test import RequestFactory, SimpleTestCase, TestCase
  22. from django.test.utils import isolate_apps
  23. from django.utils.deprecation import RemovedInDjango60Warning
  24. from .models import Band, Concert, Song
  25. class MockRequest:
  26. pass
  27. class MockSuperUser:
  28. def has_perm(self, perm, obj=None):
  29. return True
  30. request = MockRequest()
  31. request.user = MockSuperUser()
  32. class ModelAdminTests(TestCase):
  33. @classmethod
  34. def setUpTestData(cls):
  35. cls.band = Band.objects.create(
  36. name="The Doors",
  37. bio="",
  38. sign_date=date(1965, 1, 1),
  39. )
  40. def setUp(self):
  41. self.site = AdminSite()
  42. def test_modeladmin_str(self):
  43. ma = ModelAdmin(Band, self.site)
  44. self.assertEqual(str(ma), "modeladmin.ModelAdmin")
  45. def test_default_attributes(self):
  46. ma = ModelAdmin(Band, self.site)
  47. self.assertEqual(ma.actions, ())
  48. self.assertEqual(ma.inlines, ())
  49. # form/fields/fieldsets interaction ##############################
  50. def test_default_fields(self):
  51. ma = ModelAdmin(Band, self.site)
  52. self.assertEqual(
  53. list(ma.get_form(request).base_fields), ["name", "bio", "sign_date"]
  54. )
  55. self.assertEqual(list(ma.get_fields(request)), ["name", "bio", "sign_date"])
  56. self.assertEqual(
  57. list(ma.get_fields(request, self.band)), ["name", "bio", "sign_date"]
  58. )
  59. self.assertIsNone(ma.get_exclude(request, self.band))
  60. def test_default_fieldsets(self):
  61. # fieldsets_add and fieldsets_change should return a special data structure that
  62. # is used in the templates. They should generate the "right thing" whether we
  63. # have specified a custom form, the fields argument, or nothing at all.
  64. #
  65. # Here's the default case. There are no custom form_add/form_change methods,
  66. # no fields argument, and no fieldsets argument.
  67. ma = ModelAdmin(Band, self.site)
  68. self.assertEqual(
  69. ma.get_fieldsets(request),
  70. [(None, {"fields": ["name", "bio", "sign_date"]})],
  71. )
  72. self.assertEqual(
  73. ma.get_fieldsets(request, self.band),
  74. [(None, {"fields": ["name", "bio", "sign_date"]})],
  75. )
  76. def test_get_fieldsets(self):
  77. # get_fieldsets() is called when figuring out form fields (#18681).
  78. class BandAdmin(ModelAdmin):
  79. def get_fieldsets(self, request, obj=None):
  80. return [(None, {"fields": ["name", "bio"]})]
  81. ma = BandAdmin(Band, self.site)
  82. form = ma.get_form(None)
  83. self.assertEqual(form._meta.fields, ["name", "bio"])
  84. class InlineBandAdmin(TabularInline):
  85. model = Concert
  86. fk_name = "main_band"
  87. can_delete = False
  88. def get_fieldsets(self, request, obj=None):
  89. return [(None, {"fields": ["day", "transport"]})]
  90. ma = InlineBandAdmin(Band, self.site)
  91. form = ma.get_formset(None).form
  92. self.assertEqual(form._meta.fields, ["day", "transport"])
  93. def test_lookup_allowed_allows_nonexistent_lookup(self):
  94. """
  95. A lookup_allowed allows a parameter whose field lookup doesn't exist.
  96. (#21129).
  97. """
  98. class BandAdmin(ModelAdmin):
  99. fields = ["name"]
  100. ma = BandAdmin(Band, self.site)
  101. self.assertIs(
  102. ma.lookup_allowed("name__nonexistent", "test_value", request),
  103. True,
  104. )
  105. @isolate_apps("modeladmin")
  106. def test_lookup_allowed_onetoone(self):
  107. class Department(models.Model):
  108. code = models.CharField(max_length=4, unique=True)
  109. class Employee(models.Model):
  110. department = models.ForeignKey(Department, models.CASCADE, to_field="code")
  111. class EmployeeProfile(models.Model):
  112. employee = models.OneToOneField(Employee, models.CASCADE)
  113. class EmployeeInfo(models.Model):
  114. employee = models.OneToOneField(Employee, models.CASCADE)
  115. description = models.CharField(max_length=100)
  116. class EmployeeProfileAdmin(ModelAdmin):
  117. list_filter = [
  118. "employee__employeeinfo__description",
  119. "employee__department__code",
  120. ]
  121. ma = EmployeeProfileAdmin(EmployeeProfile, self.site)
  122. # Reverse OneToOneField
  123. self.assertIs(
  124. ma.lookup_allowed(
  125. "employee__employeeinfo__description", "test_value", request
  126. ),
  127. True,
  128. )
  129. # OneToOneField and ForeignKey
  130. self.assertIs(
  131. ma.lookup_allowed("employee__department__code", "test_value", request),
  132. True,
  133. )
  134. @isolate_apps("modeladmin")
  135. def test_lookup_allowed_foreign_primary(self):
  136. class Country(models.Model):
  137. name = models.CharField(max_length=256)
  138. class Place(models.Model):
  139. country = models.ForeignKey(Country, models.CASCADE)
  140. class Restaurant(models.Model):
  141. place = models.OneToOneField(Place, models.CASCADE, primary_key=True)
  142. class Waiter(models.Model):
  143. restaurant = models.ForeignKey(Restaurant, models.CASCADE)
  144. class WaiterAdmin(ModelAdmin):
  145. list_filter = [
  146. "restaurant__place__country",
  147. "restaurant__place__country__name",
  148. ]
  149. ma = WaiterAdmin(Waiter, self.site)
  150. self.assertIs(
  151. ma.lookup_allowed("restaurant__place__country", "1", request),
  152. True,
  153. )
  154. self.assertIs(
  155. ma.lookup_allowed("restaurant__place__country__id__exact", "1", request),
  156. True,
  157. )
  158. self.assertIs(
  159. ma.lookup_allowed(
  160. "restaurant__place__country__name", "test_value", request
  161. ),
  162. True,
  163. )
  164. def test_lookup_allowed_considers_dynamic_list_filter(self):
  165. class ConcertAdmin(ModelAdmin):
  166. list_filter = ["main_band__sign_date"]
  167. def get_list_filter(self, request):
  168. if getattr(request, "user", None):
  169. return self.list_filter + ["main_band__name"]
  170. return self.list_filter
  171. model_admin = ConcertAdmin(Concert, self.site)
  172. request_band_name_filter = RequestFactory().get(
  173. "/", {"main_band__name": "test"}
  174. )
  175. self.assertIs(
  176. model_admin.lookup_allowed(
  177. "main_band__sign_date", "?", request_band_name_filter
  178. ),
  179. True,
  180. )
  181. self.assertIs(
  182. model_admin.lookup_allowed(
  183. "main_band__name", "?", request_band_name_filter
  184. ),
  185. False,
  186. )
  187. request_with_superuser = request
  188. self.assertIs(
  189. model_admin.lookup_allowed(
  190. "main_band__sign_date", "?", request_with_superuser
  191. ),
  192. True,
  193. )
  194. self.assertIs(
  195. model_admin.lookup_allowed("main_band__name", "?", request_with_superuser),
  196. True,
  197. )
  198. def test_lookup_allowed_without_request_deprecation(self):
  199. class ConcertAdmin(ModelAdmin):
  200. list_filter = ["main_band__sign_date"]
  201. def get_list_filter(self, request):
  202. return self.list_filter + ["main_band__name"]
  203. def lookup_allowed(self, lookup, value):
  204. return True
  205. model_admin = ConcertAdmin(Concert, self.site)
  206. msg = (
  207. "`request` must be added to the signature of ModelAdminTests."
  208. "test_lookup_allowed_without_request_deprecation.<locals>."
  209. "ConcertAdmin.lookup_allowed()."
  210. )
  211. request_band_name_filter = RequestFactory().get(
  212. "/", {"main_band__name": "test"}
  213. )
  214. request_band_name_filter.user = User.objects.create_superuser(
  215. username="bob", email="bob@test.com", password="test"
  216. )
  217. with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
  218. changelist = model_admin.get_changelist_instance(request_band_name_filter)
  219. filterspec = changelist.get_filters(request_band_name_filter)[0][0]
  220. self.assertEqual(filterspec.title, "sign date")
  221. filterspec = changelist.get_filters(request_band_name_filter)[0][1]
  222. self.assertEqual(filterspec.title, "name")
  223. self.assertSequenceEqual(filterspec.lookup_choices, [self.band.name])
  224. def test_field_arguments(self):
  225. # If fields is specified, fieldsets_add and fieldsets_change should
  226. # just stick the fields into a formsets structure and return it.
  227. class BandAdmin(ModelAdmin):
  228. fields = ["name"]
  229. ma = BandAdmin(Band, self.site)
  230. self.assertEqual(list(ma.get_fields(request)), ["name"])
  231. self.assertEqual(list(ma.get_fields(request, self.band)), ["name"])
  232. self.assertEqual(ma.get_fieldsets(request), [(None, {"fields": ["name"]})])
  233. self.assertEqual(
  234. ma.get_fieldsets(request, self.band), [(None, {"fields": ["name"]})]
  235. )
  236. def test_field_arguments_restricted_on_form(self):
  237. # If fields or fieldsets is specified, it should exclude fields on the
  238. # Form class to the fields specified. This may cause errors to be
  239. # raised in the db layer if required model fields aren't in fields/
  240. # fieldsets, but that's preferable to ghost errors where a field in the
  241. # Form class isn't being displayed because it's not in fields/fieldsets.
  242. # Using `fields`.
  243. class BandAdmin(ModelAdmin):
  244. fields = ["name"]
  245. ma = BandAdmin(Band, self.site)
  246. self.assertEqual(list(ma.get_form(request).base_fields), ["name"])
  247. self.assertEqual(list(ma.get_form(request, self.band).base_fields), ["name"])
  248. # Using `fieldsets`.
  249. class BandAdmin(ModelAdmin):
  250. fieldsets = [(None, {"fields": ["name"]})]
  251. ma = BandAdmin(Band, self.site)
  252. self.assertEqual(list(ma.get_form(request).base_fields), ["name"])
  253. self.assertEqual(list(ma.get_form(request, self.band).base_fields), ["name"])
  254. # Using `exclude`.
  255. class BandAdmin(ModelAdmin):
  256. exclude = ["bio"]
  257. ma = BandAdmin(Band, self.site)
  258. self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])
  259. # You can also pass a tuple to `exclude`.
  260. class BandAdmin(ModelAdmin):
  261. exclude = ("bio",)
  262. ma = BandAdmin(Band, self.site)
  263. self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])
  264. # Using `fields` and `exclude`.
  265. class BandAdmin(ModelAdmin):
  266. fields = ["name", "bio"]
  267. exclude = ["bio"]
  268. ma = BandAdmin(Band, self.site)
  269. self.assertEqual(list(ma.get_form(request).base_fields), ["name"])
  270. def test_custom_form_meta_exclude_with_readonly(self):
  271. """
  272. The custom ModelForm's `Meta.exclude` is respected when used in
  273. conjunction with `ModelAdmin.readonly_fields` and when no
  274. `ModelAdmin.exclude` is defined (#14496).
  275. """
  276. # With ModelAdmin
  277. class AdminBandForm(forms.ModelForm):
  278. class Meta:
  279. model = Band
  280. exclude = ["bio"]
  281. class BandAdmin(ModelAdmin):
  282. readonly_fields = ["name"]
  283. form = AdminBandForm
  284. ma = BandAdmin(Band, self.site)
  285. self.assertEqual(list(ma.get_form(request).base_fields), ["sign_date"])
  286. # With InlineModelAdmin
  287. class AdminConcertForm(forms.ModelForm):
  288. class Meta:
  289. model = Concert
  290. exclude = ["day"]
  291. class ConcertInline(TabularInline):
  292. readonly_fields = ["transport"]
  293. form = AdminConcertForm
  294. fk_name = "main_band"
  295. model = Concert
  296. class BandAdmin(ModelAdmin):
  297. inlines = [ConcertInline]
  298. ma = BandAdmin(Band, self.site)
  299. self.assertEqual(
  300. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  301. ["main_band", "opening_band", "id", "DELETE"],
  302. )
  303. def test_custom_formfield_override_readonly(self):
  304. class AdminBandForm(forms.ModelForm):
  305. name = forms.CharField()
  306. class Meta:
  307. exclude = ()
  308. model = Band
  309. class BandAdmin(ModelAdmin):
  310. form = AdminBandForm
  311. readonly_fields = ["name"]
  312. ma = BandAdmin(Band, self.site)
  313. # `name` shouldn't appear in base_fields because it's part of
  314. # readonly_fields.
  315. self.assertEqual(list(ma.get_form(request).base_fields), ["bio", "sign_date"])
  316. # But it should appear in get_fields()/fieldsets() so it can be
  317. # displayed as read-only.
  318. self.assertEqual(list(ma.get_fields(request)), ["bio", "sign_date", "name"])
  319. self.assertEqual(
  320. list(ma.get_fieldsets(request)),
  321. [(None, {"fields": ["bio", "sign_date", "name"]})],
  322. )
  323. def test_custom_form_meta_exclude(self):
  324. """
  325. The custom ModelForm's `Meta.exclude` is overridden if
  326. `ModelAdmin.exclude` or `InlineModelAdmin.exclude` are defined (#14496).
  327. """
  328. # With ModelAdmin
  329. class AdminBandForm(forms.ModelForm):
  330. class Meta:
  331. model = Band
  332. exclude = ["bio"]
  333. class BandAdmin(ModelAdmin):
  334. exclude = ["name"]
  335. form = AdminBandForm
  336. ma = BandAdmin(Band, self.site)
  337. self.assertEqual(list(ma.get_form(request).base_fields), ["bio", "sign_date"])
  338. # With InlineModelAdmin
  339. class AdminConcertForm(forms.ModelForm):
  340. class Meta:
  341. model = Concert
  342. exclude = ["day"]
  343. class ConcertInline(TabularInline):
  344. exclude = ["transport"]
  345. form = AdminConcertForm
  346. fk_name = "main_band"
  347. model = Concert
  348. class BandAdmin(ModelAdmin):
  349. inlines = [ConcertInline]
  350. ma = BandAdmin(Band, self.site)
  351. self.assertEqual(
  352. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  353. ["main_band", "opening_band", "day", "id", "DELETE"],
  354. )
  355. def test_overriding_get_exclude(self):
  356. class BandAdmin(ModelAdmin):
  357. def get_exclude(self, request, obj=None):
  358. return ["name"]
  359. self.assertEqual(
  360. list(BandAdmin(Band, self.site).get_form(request).base_fields),
  361. ["bio", "sign_date"],
  362. )
  363. def test_get_exclude_overrides_exclude(self):
  364. class BandAdmin(ModelAdmin):
  365. exclude = ["bio"]
  366. def get_exclude(self, request, obj=None):
  367. return ["name"]
  368. self.assertEqual(
  369. list(BandAdmin(Band, self.site).get_form(request).base_fields),
  370. ["bio", "sign_date"],
  371. )
  372. def test_get_exclude_takes_obj(self):
  373. class BandAdmin(ModelAdmin):
  374. def get_exclude(self, request, obj=None):
  375. if obj:
  376. return ["sign_date"]
  377. return ["name"]
  378. self.assertEqual(
  379. list(BandAdmin(Band, self.site).get_form(request, self.band).base_fields),
  380. ["name", "bio"],
  381. )
  382. def test_custom_form_validation(self):
  383. # If a form is specified, it should use it allowing custom validation
  384. # to work properly. This won't break any of the admin widgets or media.
  385. class AdminBandForm(forms.ModelForm):
  386. delete = forms.BooleanField()
  387. class BandAdmin(ModelAdmin):
  388. form = AdminBandForm
  389. ma = BandAdmin(Band, self.site)
  390. self.assertEqual(
  391. list(ma.get_form(request).base_fields),
  392. ["name", "bio", "sign_date", "delete"],
  393. )
  394. self.assertEqual(
  395. type(ma.get_form(request).base_fields["sign_date"].widget), AdminDateWidget
  396. )
  397. def test_form_exclude_kwarg_override(self):
  398. """
  399. The `exclude` kwarg passed to `ModelAdmin.get_form()` overrides all
  400. other declarations (#8999).
  401. """
  402. class AdminBandForm(forms.ModelForm):
  403. class Meta:
  404. model = Band
  405. exclude = ["name"]
  406. class BandAdmin(ModelAdmin):
  407. exclude = ["sign_date"]
  408. form = AdminBandForm
  409. def get_form(self, request, obj=None, **kwargs):
  410. kwargs["exclude"] = ["bio"]
  411. return super().get_form(request, obj, **kwargs)
  412. ma = BandAdmin(Band, self.site)
  413. self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])
  414. def test_formset_exclude_kwarg_override(self):
  415. """
  416. The `exclude` kwarg passed to `InlineModelAdmin.get_formset()`
  417. overrides all other declarations (#8999).
  418. """
  419. class AdminConcertForm(forms.ModelForm):
  420. class Meta:
  421. model = Concert
  422. exclude = ["day"]
  423. class ConcertInline(TabularInline):
  424. exclude = ["transport"]
  425. form = AdminConcertForm
  426. fk_name = "main_band"
  427. model = Concert
  428. def get_formset(self, request, obj=None, **kwargs):
  429. kwargs["exclude"] = ["opening_band"]
  430. return super().get_formset(request, obj, **kwargs)
  431. class BandAdmin(ModelAdmin):
  432. inlines = [ConcertInline]
  433. ma = BandAdmin(Band, self.site)
  434. self.assertEqual(
  435. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  436. ["main_band", "day", "transport", "id", "DELETE"],
  437. )
  438. def test_formset_overriding_get_exclude_with_form_fields(self):
  439. class AdminConcertForm(forms.ModelForm):
  440. class Meta:
  441. model = Concert
  442. fields = ["main_band", "opening_band", "day", "transport"]
  443. class ConcertInline(TabularInline):
  444. form = AdminConcertForm
  445. fk_name = "main_band"
  446. model = Concert
  447. def get_exclude(self, request, obj=None):
  448. return ["opening_band"]
  449. class BandAdmin(ModelAdmin):
  450. inlines = [ConcertInline]
  451. ma = BandAdmin(Band, self.site)
  452. self.assertEqual(
  453. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  454. ["main_band", "day", "transport", "id", "DELETE"],
  455. )
  456. def test_formset_overriding_get_exclude_with_form_exclude(self):
  457. class AdminConcertForm(forms.ModelForm):
  458. class Meta:
  459. model = Concert
  460. exclude = ["day"]
  461. class ConcertInline(TabularInline):
  462. form = AdminConcertForm
  463. fk_name = "main_band"
  464. model = Concert
  465. def get_exclude(self, request, obj=None):
  466. return ["opening_band"]
  467. class BandAdmin(ModelAdmin):
  468. inlines = [ConcertInline]
  469. ma = BandAdmin(Band, self.site)
  470. self.assertEqual(
  471. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  472. ["main_band", "day", "transport", "id", "DELETE"],
  473. )
  474. def test_raw_id_fields_widget_override(self):
  475. """
  476. The autocomplete_fields, raw_id_fields, and radio_fields widgets may
  477. overridden by specifying a widget in get_formset().
  478. """
  479. class ConcertInline(TabularInline):
  480. model = Concert
  481. fk_name = "main_band"
  482. raw_id_fields = ("opening_band",)
  483. def get_formset(self, request, obj=None, **kwargs):
  484. kwargs["widgets"] = {"opening_band": Select}
  485. return super().get_formset(request, obj, **kwargs)
  486. class BandAdmin(ModelAdmin):
  487. inlines = [ConcertInline]
  488. ma = BandAdmin(Band, self.site)
  489. band_widget = (
  490. list(ma.get_formsets_with_inlines(request))[0][0]()
  491. .forms[0]
  492. .fields["opening_band"]
  493. .widget
  494. )
  495. # Without the override this would be ForeignKeyRawIdWidget.
  496. self.assertIsInstance(band_widget, Select)
  497. def test_queryset_override(self):
  498. # If the queryset of a ModelChoiceField in a custom form is overridden,
  499. # RelatedFieldWidgetWrapper doesn't mess that up.
  500. band2 = Band.objects.create(
  501. name="The Beatles", bio="", sign_date=date(1962, 1, 1)
  502. )
  503. ma = ModelAdmin(Concert, self.site)
  504. form = ma.get_form(request)()
  505. self.assertHTMLEqual(
  506. str(form["main_band"]),
  507. '<div class="related-widget-wrapper" data-model-ref="band">'
  508. '<select name="main_band" id="id_main_band" required>'
  509. '<option value="" selected>---------</option>'
  510. '<option value="%d">The Beatles</option>'
  511. '<option value="%d">The Doors</option>'
  512. "</select></div>" % (band2.id, self.band.id),
  513. )
  514. class AdminConcertForm(forms.ModelForm):
  515. def __init__(self, *args, **kwargs):
  516. super().__init__(*args, **kwargs)
  517. self.fields["main_band"].queryset = Band.objects.filter(
  518. name="The Doors"
  519. )
  520. class ConcertAdminWithForm(ModelAdmin):
  521. form = AdminConcertForm
  522. ma = ConcertAdminWithForm(Concert, self.site)
  523. form = ma.get_form(request)()
  524. self.assertHTMLEqual(
  525. str(form["main_band"]),
  526. '<div class="related-widget-wrapper" data-model-ref="band">'
  527. '<select name="main_band" id="id_main_band" required>'
  528. '<option value="" selected>---------</option>'
  529. '<option value="%d">The Doors</option>'
  530. "</select></div>" % self.band.id,
  531. )
  532. def test_regression_for_ticket_15820(self):
  533. """
  534. `obj` is passed from `InlineModelAdmin.get_fieldsets()` to
  535. `InlineModelAdmin.get_formset()`.
  536. """
  537. class CustomConcertForm(forms.ModelForm):
  538. class Meta:
  539. model = Concert
  540. fields = ["day"]
  541. class ConcertInline(TabularInline):
  542. model = Concert
  543. fk_name = "main_band"
  544. def get_formset(self, request, obj=None, **kwargs):
  545. if obj:
  546. kwargs["form"] = CustomConcertForm
  547. return super().get_formset(request, obj, **kwargs)
  548. class BandAdmin(ModelAdmin):
  549. inlines = [ConcertInline]
  550. Concert.objects.create(main_band=self.band, opening_band=self.band, day=1)
  551. ma = BandAdmin(Band, self.site)
  552. inline_instances = ma.get_inline_instances(request)
  553. fieldsets = list(inline_instances[0].get_fieldsets(request))
  554. self.assertEqual(
  555. fieldsets[0][1]["fields"], ["main_band", "opening_band", "day", "transport"]
  556. )
  557. fieldsets = list(
  558. inline_instances[0].get_fieldsets(request, inline_instances[0].model)
  559. )
  560. self.assertEqual(fieldsets[0][1]["fields"], ["day"])
  561. # radio_fields behavior ###########################################
  562. def test_default_foreign_key_widget(self):
  563. # First, without any radio_fields specified, the widgets for ForeignKey
  564. # and fields with choices specified ought to be a basic Select widget.
  565. # ForeignKey widgets in the admin are wrapped with RelatedFieldWidgetWrapper so
  566. # they need to be handled properly when type checking. For Select fields, all of
  567. # the choices lists have a first entry of dashes.
  568. cma = ModelAdmin(Concert, self.site)
  569. cmafa = cma.get_form(request)
  570. self.assertEqual(type(cmafa.base_fields["main_band"].widget.widget), Select)
  571. self.assertEqual(
  572. list(cmafa.base_fields["main_band"].widget.choices),
  573. [("", "---------"), (self.band.id, "The Doors")],
  574. )
  575. self.assertEqual(type(cmafa.base_fields["opening_band"].widget.widget), Select)
  576. self.assertEqual(
  577. list(cmafa.base_fields["opening_band"].widget.choices),
  578. [("", "---------"), (self.band.id, "The Doors")],
  579. )
  580. self.assertEqual(type(cmafa.base_fields["day"].widget), Select)
  581. self.assertEqual(
  582. list(cmafa.base_fields["day"].widget.choices),
  583. [("", "---------"), (1, "Fri"), (2, "Sat")],
  584. )
  585. self.assertEqual(type(cmafa.base_fields["transport"].widget), Select)
  586. self.assertEqual(
  587. list(cmafa.base_fields["transport"].widget.choices),
  588. [("", "---------"), (1, "Plane"), (2, "Train"), (3, "Bus")],
  589. )
  590. def test_foreign_key_as_radio_field(self):
  591. # Now specify all the fields as radio_fields. Widgets should now be
  592. # RadioSelect, and the choices list should have a first entry of 'None' if
  593. # blank=True for the model field. Finally, the widget should have the
  594. # 'radiolist' attr, and 'inline' as well if the field is specified HORIZONTAL.
  595. class ConcertAdmin(ModelAdmin):
  596. radio_fields = {
  597. "main_band": HORIZONTAL,
  598. "opening_band": VERTICAL,
  599. "day": VERTICAL,
  600. "transport": HORIZONTAL,
  601. }
  602. cma = ConcertAdmin(Concert, self.site)
  603. cmafa = cma.get_form(request)
  604. self.assertEqual(
  605. type(cmafa.base_fields["main_band"].widget.widget), AdminRadioSelect
  606. )
  607. self.assertEqual(
  608. cmafa.base_fields["main_band"].widget.attrs, {"class": "radiolist inline"}
  609. )
  610. self.assertEqual(
  611. list(cmafa.base_fields["main_band"].widget.choices),
  612. [(self.band.id, "The Doors")],
  613. )
  614. self.assertEqual(
  615. type(cmafa.base_fields["opening_band"].widget.widget), AdminRadioSelect
  616. )
  617. self.assertEqual(
  618. cmafa.base_fields["opening_band"].widget.attrs, {"class": "radiolist"}
  619. )
  620. self.assertEqual(
  621. list(cmafa.base_fields["opening_band"].widget.choices),
  622. [("", "None"), (self.band.id, "The Doors")],
  623. )
  624. self.assertEqual(type(cmafa.base_fields["day"].widget), AdminRadioSelect)
  625. self.assertEqual(cmafa.base_fields["day"].widget.attrs, {"class": "radiolist"})
  626. self.assertEqual(
  627. list(cmafa.base_fields["day"].widget.choices), [(1, "Fri"), (2, "Sat")]
  628. )
  629. self.assertEqual(type(cmafa.base_fields["transport"].widget), AdminRadioSelect)
  630. self.assertEqual(
  631. cmafa.base_fields["transport"].widget.attrs, {"class": "radiolist inline"}
  632. )
  633. self.assertEqual(
  634. list(cmafa.base_fields["transport"].widget.choices),
  635. [("", "None"), (1, "Plane"), (2, "Train"), (3, "Bus")],
  636. )
  637. class AdminConcertForm(forms.ModelForm):
  638. class Meta:
  639. model = Concert
  640. exclude = ("transport",)
  641. class ConcertAdmin(ModelAdmin):
  642. form = AdminConcertForm
  643. ma = ConcertAdmin(Concert, self.site)
  644. self.assertEqual(
  645. list(ma.get_form(request).base_fields), ["main_band", "opening_band", "day"]
  646. )
  647. class AdminConcertForm(forms.ModelForm):
  648. extra = forms.CharField()
  649. class Meta:
  650. model = Concert
  651. fields = ["extra", "transport"]
  652. class ConcertAdmin(ModelAdmin):
  653. form = AdminConcertForm
  654. ma = ConcertAdmin(Concert, self.site)
  655. self.assertEqual(list(ma.get_form(request).base_fields), ["extra", "transport"])
  656. class ConcertInline(TabularInline):
  657. form = AdminConcertForm
  658. model = Concert
  659. fk_name = "main_band"
  660. can_delete = True
  661. class BandAdmin(ModelAdmin):
  662. inlines = [ConcertInline]
  663. ma = BandAdmin(Band, self.site)
  664. self.assertEqual(
  665. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  666. ["extra", "transport", "id", "DELETE", "main_band"],
  667. )
  668. def test_log_actions(self):
  669. ma = ModelAdmin(Band, self.site)
  670. mock_request = MockRequest()
  671. mock_request.user = User.objects.create(username="bill")
  672. content_type = get_content_type_for_model(self.band)
  673. tests = (
  674. (ma.log_addition, ADDITION, {"added": {}}),
  675. (ma.log_change, CHANGE, {"changed": {"fields": ["name", "bio"]}}),
  676. (ma.log_deletion, DELETION, str(self.band)),
  677. )
  678. for method, flag, message in tests:
  679. with self.subTest(name=method.__name__):
  680. created = method(mock_request, self.band, message)
  681. fetched = LogEntry.objects.filter(action_flag=flag).latest("id")
  682. self.assertEqual(created, fetched)
  683. self.assertEqual(fetched.action_flag, flag)
  684. self.assertEqual(fetched.content_type, content_type)
  685. self.assertEqual(fetched.object_id, str(self.band.pk))
  686. self.assertEqual(fetched.user, mock_request.user)
  687. if flag == DELETION:
  688. self.assertEqual(fetched.change_message, "")
  689. self.assertEqual(fetched.object_repr, message)
  690. else:
  691. self.assertEqual(fetched.change_message, str(message))
  692. self.assertEqual(fetched.object_repr, str(self.band))
  693. def test_get_autocomplete_fields(self):
  694. class NameAdmin(ModelAdmin):
  695. search_fields = ["name"]
  696. class SongAdmin(ModelAdmin):
  697. autocomplete_fields = ["featuring"]
  698. fields = ["featuring", "band"]
  699. class OtherSongAdmin(SongAdmin):
  700. def get_autocomplete_fields(self, request):
  701. return ["band"]
  702. self.site.register(Band, NameAdmin)
  703. try:
  704. # Uses autocomplete_fields if not overridden.
  705. model_admin = SongAdmin(Song, self.site)
  706. form = model_admin.get_form(request)()
  707. self.assertIsInstance(
  708. form.fields["featuring"].widget.widget, AutocompleteSelectMultiple
  709. )
  710. # Uses overridden get_autocomplete_fields
  711. model_admin = OtherSongAdmin(Song, self.site)
  712. form = model_admin.get_form(request)()
  713. self.assertIsInstance(form.fields["band"].widget.widget, AutocompleteSelect)
  714. finally:
  715. self.site.unregister(Band)
  716. def test_get_deleted_objects(self):
  717. mock_request = MockRequest()
  718. mock_request.user = User.objects.create_superuser(
  719. username="bob", email="bob@test.com", password="test"
  720. )
  721. self.site.register(Band, ModelAdmin)
  722. ma = self.site.get_model_admin(Band)
  723. (
  724. deletable_objects,
  725. model_count,
  726. perms_needed,
  727. protected,
  728. ) = ma.get_deleted_objects([self.band], request)
  729. self.assertEqual(deletable_objects, ["Band: The Doors"])
  730. self.assertEqual(model_count, {"bands": 1})
  731. self.assertEqual(perms_needed, set())
  732. self.assertEqual(protected, [])
  733. def test_get_deleted_objects_with_custom_has_delete_permission(self):
  734. """
  735. ModelAdmin.get_deleted_objects() uses ModelAdmin.has_delete_permission()
  736. for permissions checking.
  737. """
  738. mock_request = MockRequest()
  739. mock_request.user = User.objects.create_superuser(
  740. username="bob", email="bob@test.com", password="test"
  741. )
  742. class TestModelAdmin(ModelAdmin):
  743. def has_delete_permission(self, request, obj=None):
  744. return False
  745. self.site.register(Band, TestModelAdmin)
  746. ma = self.site.get_model_admin(Band)
  747. (
  748. deletable_objects,
  749. model_count,
  750. perms_needed,
  751. protected,
  752. ) = ma.get_deleted_objects([self.band], request)
  753. self.assertEqual(deletable_objects, ["Band: The Doors"])
  754. self.assertEqual(model_count, {"bands": 1})
  755. self.assertEqual(perms_needed, {"band"})
  756. self.assertEqual(protected, [])
  757. def test_modeladmin_repr(self):
  758. ma = ModelAdmin(Band, self.site)
  759. self.assertEqual(
  760. repr(ma),
  761. "<ModelAdmin: model=Band site=AdminSite(name='admin')>",
  762. )
  763. class ModelAdminPermissionTests(SimpleTestCase):
  764. class MockUser:
  765. def has_module_perms(self, app_label):
  766. return app_label == "modeladmin"
  767. class MockViewUser(MockUser):
  768. def has_perm(self, perm, obj=None):
  769. return perm == "modeladmin.view_band"
  770. class MockAddUser(MockUser):
  771. def has_perm(self, perm, obj=None):
  772. return perm == "modeladmin.add_band"
  773. class MockChangeUser(MockUser):
  774. def has_perm(self, perm, obj=None):
  775. return perm == "modeladmin.change_band"
  776. class MockDeleteUser(MockUser):
  777. def has_perm(self, perm, obj=None):
  778. return perm == "modeladmin.delete_band"
  779. def test_has_view_permission(self):
  780. """
  781. has_view_permission() returns True for users who can view objects and
  782. False for users who can't.
  783. """
  784. ma = ModelAdmin(Band, AdminSite())
  785. request = MockRequest()
  786. request.user = self.MockViewUser()
  787. self.assertIs(ma.has_view_permission(request), True)
  788. request.user = self.MockAddUser()
  789. self.assertIs(ma.has_view_permission(request), False)
  790. request.user = self.MockChangeUser()
  791. self.assertIs(ma.has_view_permission(request), True)
  792. request.user = self.MockDeleteUser()
  793. self.assertIs(ma.has_view_permission(request), False)
  794. def test_has_add_permission(self):
  795. """
  796. has_add_permission returns True for users who can add objects and
  797. False for users who can't.
  798. """
  799. ma = ModelAdmin(Band, AdminSite())
  800. request = MockRequest()
  801. request.user = self.MockViewUser()
  802. self.assertFalse(ma.has_add_permission(request))
  803. request.user = self.MockAddUser()
  804. self.assertTrue(ma.has_add_permission(request))
  805. request.user = self.MockChangeUser()
  806. self.assertFalse(ma.has_add_permission(request))
  807. request.user = self.MockDeleteUser()
  808. self.assertFalse(ma.has_add_permission(request))
  809. def test_inline_has_add_permission_uses_obj(self):
  810. class ConcertInline(TabularInline):
  811. model = Concert
  812. def has_add_permission(self, request, obj):
  813. return bool(obj)
  814. class BandAdmin(ModelAdmin):
  815. inlines = [ConcertInline]
  816. ma = BandAdmin(Band, AdminSite())
  817. request = MockRequest()
  818. request.user = self.MockAddUser()
  819. self.assertEqual(ma.get_inline_instances(request), [])
  820. band = Band(name="The Doors", bio="", sign_date=date(1965, 1, 1))
  821. inline_instances = ma.get_inline_instances(request, band)
  822. self.assertEqual(len(inline_instances), 1)
  823. self.assertIsInstance(inline_instances[0], ConcertInline)
  824. def test_has_change_permission(self):
  825. """
  826. has_change_permission returns True for users who can edit objects and
  827. False for users who can't.
  828. """
  829. ma = ModelAdmin(Band, AdminSite())
  830. request = MockRequest()
  831. request.user = self.MockViewUser()
  832. self.assertIs(ma.has_change_permission(request), False)
  833. request.user = self.MockAddUser()
  834. self.assertFalse(ma.has_change_permission(request))
  835. request.user = self.MockChangeUser()
  836. self.assertTrue(ma.has_change_permission(request))
  837. request.user = self.MockDeleteUser()
  838. self.assertFalse(ma.has_change_permission(request))
  839. def test_has_delete_permission(self):
  840. """
  841. has_delete_permission returns True for users who can delete objects and
  842. False for users who can't.
  843. """
  844. ma = ModelAdmin(Band, AdminSite())
  845. request = MockRequest()
  846. request.user = self.MockViewUser()
  847. self.assertIs(ma.has_delete_permission(request), False)
  848. request.user = self.MockAddUser()
  849. self.assertFalse(ma.has_delete_permission(request))
  850. request.user = self.MockChangeUser()
  851. self.assertFalse(ma.has_delete_permission(request))
  852. request.user = self.MockDeleteUser()
  853. self.assertTrue(ma.has_delete_permission(request))
  854. def test_has_module_permission(self):
  855. """
  856. as_module_permission returns True for users who have any permission
  857. for the module and False for users who don't.
  858. """
  859. ma = ModelAdmin(Band, AdminSite())
  860. request = MockRequest()
  861. request.user = self.MockViewUser()
  862. self.assertIs(ma.has_module_permission(request), True)
  863. request.user = self.MockAddUser()
  864. self.assertTrue(ma.has_module_permission(request))
  865. request.user = self.MockChangeUser()
  866. self.assertTrue(ma.has_module_permission(request))
  867. request.user = self.MockDeleteUser()
  868. self.assertTrue(ma.has_module_permission(request))
  869. original_app_label = ma.opts.app_label
  870. ma.opts.app_label = "anotherapp"
  871. try:
  872. request.user = self.MockViewUser()
  873. self.assertIs(ma.has_module_permission(request), False)
  874. request.user = self.MockAddUser()
  875. self.assertFalse(ma.has_module_permission(request))
  876. request.user = self.MockChangeUser()
  877. self.assertFalse(ma.has_module_permission(request))
  878. request.user = self.MockDeleteUser()
  879. self.assertFalse(ma.has_module_permission(request))
  880. finally:
  881. ma.opts.app_label = original_app_label