tests.py 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164
  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. )
  677. for method, flag, message in tests:
  678. with self.subTest(name=method.__name__):
  679. created = method(mock_request, self.band, message)
  680. fetched = LogEntry.objects.filter(action_flag=flag).latest("id")
  681. self.assertEqual(created, fetched)
  682. self.assertEqual(fetched.action_flag, flag)
  683. self.assertEqual(fetched.content_type, content_type)
  684. self.assertEqual(fetched.object_id, str(self.band.pk))
  685. self.assertEqual(fetched.user, mock_request.user)
  686. self.assertEqual(fetched.change_message, str(message))
  687. self.assertEqual(fetched.object_repr, str(self.band))
  688. def test_log_deletions(self):
  689. ma = ModelAdmin(Band, self.site)
  690. mock_request = MockRequest()
  691. mock_request.user = User.objects.create(username="akash")
  692. content_type = get_content_type_for_model(self.band)
  693. Band.objects.create(
  694. name="The Beatles",
  695. bio="A legendary rock band from Liverpool.",
  696. sign_date=date(1962, 1, 1),
  697. )
  698. Band.objects.create(
  699. name="Mohiner Ghoraguli",
  700. bio="A progressive rock band from Calcutta.",
  701. sign_date=date(1975, 1, 1),
  702. )
  703. queryset = Band.objects.all().order_by("-id")[:3]
  704. self.assertEqual(len(queryset), 3)
  705. with self.assertNumQueries(1):
  706. ma.log_deletions(mock_request, queryset)
  707. logs = (
  708. LogEntry.objects.filter(action_flag=DELETION)
  709. .order_by("id")
  710. .values_list(
  711. "user_id",
  712. "content_type",
  713. "object_id",
  714. "object_repr",
  715. "action_flag",
  716. "change_message",
  717. )
  718. )
  719. expected_log_values = [
  720. (
  721. mock_request.user.id,
  722. content_type.id,
  723. str(obj.pk),
  724. str(obj),
  725. DELETION,
  726. "",
  727. )
  728. for obj in queryset
  729. ]
  730. self.assertSequenceEqual(logs, expected_log_values)
  731. # RemovedInDjango60Warning.
  732. def test_log_deletion(self):
  733. ma = ModelAdmin(Band, self.site)
  734. mock_request = MockRequest()
  735. mock_request.user = User.objects.create(username="bill")
  736. content_type = get_content_type_for_model(self.band)
  737. msg = "ModelAdmin.log_deletion() is deprecated. Use log_deletions() instead."
  738. with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
  739. created = ma.log_deletion(mock_request, self.band, str(self.band))
  740. fetched = LogEntry.objects.filter(action_flag=DELETION).latest("id")
  741. self.assertEqual(created, fetched)
  742. self.assertEqual(fetched.action_flag, DELETION)
  743. self.assertEqual(fetched.content_type, content_type)
  744. self.assertEqual(fetched.object_id, str(self.band.pk))
  745. self.assertEqual(fetched.user, mock_request.user)
  746. self.assertEqual(fetched.change_message, "")
  747. self.assertEqual(fetched.object_repr, str(self.band))
  748. # RemovedInDjango60Warning.
  749. def test_log_deletion_fallback(self):
  750. class InheritedModelAdmin(ModelAdmin):
  751. def log_deletion(self, request, obj, object_repr):
  752. return super().log_deletion(request, obj, object_repr)
  753. ima = InheritedModelAdmin(Band, self.site)
  754. mock_request = MockRequest()
  755. mock_request.user = User.objects.create(username="akash")
  756. content_type = get_content_type_for_model(self.band)
  757. Band.objects.create(
  758. name="The Beatles",
  759. bio="A legendary rock band from Liverpool.",
  760. sign_date=date(1962, 1, 1),
  761. )
  762. Band.objects.create(
  763. name="Mohiner Ghoraguli",
  764. bio="A progressive rock band from Calcutta.",
  765. sign_date=date(1975, 1, 1),
  766. )
  767. queryset = Band.objects.all().order_by("-id")[:3]
  768. self.assertEqual(len(queryset), 3)
  769. msg = (
  770. "The usage of log_deletion() is deprecated. Implement log_deletions() "
  771. "instead."
  772. )
  773. with self.assertNumQueries(3):
  774. with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
  775. ima.log_deletions(mock_request, queryset)
  776. logs = (
  777. LogEntry.objects.filter(action_flag=DELETION)
  778. .order_by("id")
  779. .values_list(
  780. "user_id",
  781. "content_type",
  782. "object_id",
  783. "object_repr",
  784. "action_flag",
  785. "change_message",
  786. )
  787. )
  788. expected_log_values = [
  789. (
  790. mock_request.user.id,
  791. content_type.id,
  792. str(obj.pk),
  793. str(obj),
  794. DELETION,
  795. "",
  796. )
  797. for obj in queryset
  798. ]
  799. self.assertSequenceEqual(logs, expected_log_values)
  800. def test_get_autocomplete_fields(self):
  801. class NameAdmin(ModelAdmin):
  802. search_fields = ["name"]
  803. class SongAdmin(ModelAdmin):
  804. autocomplete_fields = ["featuring"]
  805. fields = ["featuring", "band"]
  806. class OtherSongAdmin(SongAdmin):
  807. def get_autocomplete_fields(self, request):
  808. return ["band"]
  809. self.site.register(Band, NameAdmin)
  810. try:
  811. # Uses autocomplete_fields if not overridden.
  812. model_admin = SongAdmin(Song, self.site)
  813. form = model_admin.get_form(request)()
  814. self.assertIsInstance(
  815. form.fields["featuring"].widget.widget, AutocompleteSelectMultiple
  816. )
  817. # Uses overridden get_autocomplete_fields
  818. model_admin = OtherSongAdmin(Song, self.site)
  819. form = model_admin.get_form(request)()
  820. self.assertIsInstance(form.fields["band"].widget.widget, AutocompleteSelect)
  821. finally:
  822. self.site.unregister(Band)
  823. def test_get_deleted_objects(self):
  824. mock_request = MockRequest()
  825. mock_request.user = User.objects.create_superuser(
  826. username="bob", email="bob@test.com", password="test"
  827. )
  828. self.site.register(Band, ModelAdmin)
  829. ma = self.site.get_model_admin(Band)
  830. (
  831. deletable_objects,
  832. model_count,
  833. perms_needed,
  834. protected,
  835. ) = ma.get_deleted_objects([self.band], request)
  836. self.assertEqual(deletable_objects, ["Band: The Doors"])
  837. self.assertEqual(model_count, {"bands": 1})
  838. self.assertEqual(perms_needed, set())
  839. self.assertEqual(protected, [])
  840. def test_get_deleted_objects_with_custom_has_delete_permission(self):
  841. """
  842. ModelAdmin.get_deleted_objects() uses ModelAdmin.has_delete_permission()
  843. for permissions checking.
  844. """
  845. mock_request = MockRequest()
  846. mock_request.user = User.objects.create_superuser(
  847. username="bob", email="bob@test.com", password="test"
  848. )
  849. class TestModelAdmin(ModelAdmin):
  850. def has_delete_permission(self, request, obj=None):
  851. return False
  852. self.site.register(Band, TestModelAdmin)
  853. ma = self.site.get_model_admin(Band)
  854. (
  855. deletable_objects,
  856. model_count,
  857. perms_needed,
  858. protected,
  859. ) = ma.get_deleted_objects([self.band], request)
  860. self.assertEqual(deletable_objects, ["Band: The Doors"])
  861. self.assertEqual(model_count, {"bands": 1})
  862. self.assertEqual(perms_needed, {"band"})
  863. self.assertEqual(protected, [])
  864. def test_modeladmin_repr(self):
  865. ma = ModelAdmin(Band, self.site)
  866. self.assertEqual(
  867. repr(ma),
  868. "<ModelAdmin: model=Band site=AdminSite(name='admin')>",
  869. )
  870. class ModelAdminPermissionTests(SimpleTestCase):
  871. class MockUser:
  872. def has_module_perms(self, app_label):
  873. return app_label == "modeladmin"
  874. class MockViewUser(MockUser):
  875. def has_perm(self, perm, obj=None):
  876. return perm == "modeladmin.view_band"
  877. class MockAddUser(MockUser):
  878. def has_perm(self, perm, obj=None):
  879. return perm == "modeladmin.add_band"
  880. class MockChangeUser(MockUser):
  881. def has_perm(self, perm, obj=None):
  882. return perm == "modeladmin.change_band"
  883. class MockDeleteUser(MockUser):
  884. def has_perm(self, perm, obj=None):
  885. return perm == "modeladmin.delete_band"
  886. def test_has_view_permission(self):
  887. """
  888. has_view_permission() returns True for users who can view objects and
  889. False for users who can't.
  890. """
  891. ma = ModelAdmin(Band, AdminSite())
  892. request = MockRequest()
  893. request.user = self.MockViewUser()
  894. self.assertIs(ma.has_view_permission(request), True)
  895. request.user = self.MockAddUser()
  896. self.assertIs(ma.has_view_permission(request), False)
  897. request.user = self.MockChangeUser()
  898. self.assertIs(ma.has_view_permission(request), True)
  899. request.user = self.MockDeleteUser()
  900. self.assertIs(ma.has_view_permission(request), False)
  901. def test_has_add_permission(self):
  902. """
  903. has_add_permission returns True for users who can add objects and
  904. False for users who can't.
  905. """
  906. ma = ModelAdmin(Band, AdminSite())
  907. request = MockRequest()
  908. request.user = self.MockViewUser()
  909. self.assertFalse(ma.has_add_permission(request))
  910. request.user = self.MockAddUser()
  911. self.assertTrue(ma.has_add_permission(request))
  912. request.user = self.MockChangeUser()
  913. self.assertFalse(ma.has_add_permission(request))
  914. request.user = self.MockDeleteUser()
  915. self.assertFalse(ma.has_add_permission(request))
  916. def test_inline_has_add_permission_uses_obj(self):
  917. class ConcertInline(TabularInline):
  918. model = Concert
  919. def has_add_permission(self, request, obj):
  920. return bool(obj)
  921. class BandAdmin(ModelAdmin):
  922. inlines = [ConcertInline]
  923. ma = BandAdmin(Band, AdminSite())
  924. request = MockRequest()
  925. request.user = self.MockAddUser()
  926. self.assertEqual(ma.get_inline_instances(request), [])
  927. band = Band(name="The Doors", bio="", sign_date=date(1965, 1, 1))
  928. inline_instances = ma.get_inline_instances(request, band)
  929. self.assertEqual(len(inline_instances), 1)
  930. self.assertIsInstance(inline_instances[0], ConcertInline)
  931. def test_has_change_permission(self):
  932. """
  933. has_change_permission returns True for users who can edit objects and
  934. False for users who can't.
  935. """
  936. ma = ModelAdmin(Band, AdminSite())
  937. request = MockRequest()
  938. request.user = self.MockViewUser()
  939. self.assertIs(ma.has_change_permission(request), False)
  940. request.user = self.MockAddUser()
  941. self.assertFalse(ma.has_change_permission(request))
  942. request.user = self.MockChangeUser()
  943. self.assertTrue(ma.has_change_permission(request))
  944. request.user = self.MockDeleteUser()
  945. self.assertFalse(ma.has_change_permission(request))
  946. def test_has_delete_permission(self):
  947. """
  948. has_delete_permission returns True for users who can delete objects and
  949. False for users who can't.
  950. """
  951. ma = ModelAdmin(Band, AdminSite())
  952. request = MockRequest()
  953. request.user = self.MockViewUser()
  954. self.assertIs(ma.has_delete_permission(request), False)
  955. request.user = self.MockAddUser()
  956. self.assertFalse(ma.has_delete_permission(request))
  957. request.user = self.MockChangeUser()
  958. self.assertFalse(ma.has_delete_permission(request))
  959. request.user = self.MockDeleteUser()
  960. self.assertTrue(ma.has_delete_permission(request))
  961. def test_has_module_permission(self):
  962. """
  963. as_module_permission returns True for users who have any permission
  964. for the module and False for users who don't.
  965. """
  966. ma = ModelAdmin(Band, AdminSite())
  967. request = MockRequest()
  968. request.user = self.MockViewUser()
  969. self.assertIs(ma.has_module_permission(request), True)
  970. request.user = self.MockAddUser()
  971. self.assertTrue(ma.has_module_permission(request))
  972. request.user = self.MockChangeUser()
  973. self.assertTrue(ma.has_module_permission(request))
  974. request.user = self.MockDeleteUser()
  975. self.assertTrue(ma.has_module_permission(request))
  976. original_app_label = ma.opts.app_label
  977. ma.opts.app_label = "anotherapp"
  978. try:
  979. request.user = self.MockViewUser()
  980. self.assertIs(ma.has_module_permission(request), False)
  981. request.user = self.MockAddUser()
  982. self.assertFalse(ma.has_module_permission(request))
  983. request.user = self.MockChangeUser()
  984. self.assertFalse(ma.has_module_permission(request))
  985. request.user = self.MockDeleteUser()
  986. self.assertFalse(ma.has_module_permission(request))
  987. finally:
  988. ma.opts.app_label = original_app_label