tests.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. from django.contrib import admin
  2. from django.contrib.admin.sites import AdminSite
  3. from django.contrib.auth.models import User
  4. from django.contrib.contenttypes.admin import GenericTabularInline
  5. from django.contrib.contenttypes.models import ContentType
  6. from django.forms.formsets import DEFAULT_MAX_NUM
  7. from django.forms.models import ModelForm
  8. from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings
  9. from django.urls import reverse
  10. from .admin import MediaInline, MediaPermanentInline
  11. from .admin import site as admin_site
  12. from .models import Category, Episode, EpisodePermanent, Media, PhoneNumber
  13. class TestDataMixin:
  14. @classmethod
  15. def setUpTestData(cls):
  16. cls.superuser = User.objects.create_superuser(
  17. username="super", password="secret", email="super@example.com"
  18. )
  19. @override_settings(ROOT_URLCONF="generic_inline_admin.urls")
  20. class GenericAdminViewTest(TestDataMixin, TestCase):
  21. def setUp(self):
  22. self.client.force_login(self.superuser)
  23. e = Episode.objects.create(name="This Week in Django")
  24. self.episode_pk = e.pk
  25. m = Media(content_object=e, url="http://example.com/podcast.mp3")
  26. m.save()
  27. self.mp3_media_pk = m.pk
  28. m = Media(content_object=e, url="http://example.com/logo.png")
  29. m.save()
  30. self.png_media_pk = m.pk
  31. def test_basic_add_GET(self):
  32. """
  33. A smoke test to ensure GET on the add_view works.
  34. """
  35. response = self.client.get(reverse("admin:generic_inline_admin_episode_add"))
  36. self.assertEqual(response.status_code, 200)
  37. def test_basic_edit_GET(self):
  38. """
  39. A smoke test to ensure GET on the change_view works.
  40. """
  41. response = self.client.get(
  42. reverse(
  43. "admin:generic_inline_admin_episode_change", args=(self.episode_pk,)
  44. )
  45. )
  46. self.assertEqual(response.status_code, 200)
  47. def test_basic_add_POST(self):
  48. """
  49. A smoke test to ensure POST on add_view works.
  50. """
  51. post_data = {
  52. "name": "This Week in Django",
  53. # inline data
  54. "generic_inline_admin-media-content_type-object_id-TOTAL_FORMS": "1",
  55. "generic_inline_admin-media-content_type-object_id-INITIAL_FORMS": "0",
  56. "generic_inline_admin-media-content_type-object_id-MAX_NUM_FORMS": "0",
  57. }
  58. response = self.client.post(
  59. reverse("admin:generic_inline_admin_episode_add"), post_data
  60. )
  61. self.assertEqual(response.status_code, 302) # redirect somewhere
  62. def test_basic_edit_POST(self):
  63. """
  64. A smoke test to ensure POST on edit_view works.
  65. """
  66. prefix = "generic_inline_admin-media-content_type-object_id"
  67. post_data = {
  68. "name": "This Week in Django",
  69. # inline data
  70. f"{prefix}-TOTAL_FORMS": "3",
  71. f"{prefix}-INITIAL_FORMS": "2",
  72. f"{prefix}-MAX_NUM_FORMS": "0",
  73. f"{prefix}-0-id": str(self.mp3_media_pk),
  74. f"{prefix}-0-url": "http://example.com/podcast.mp3",
  75. f"{prefix}-1-id": str(self.png_media_pk),
  76. f"{prefix}-1-url": "http://example.com/logo.png",
  77. f"{prefix}-2-id": "",
  78. f"{prefix}-2-url": "",
  79. }
  80. url = reverse(
  81. "admin:generic_inline_admin_episode_change", args=(self.episode_pk,)
  82. )
  83. response = self.client.post(url, post_data)
  84. self.assertEqual(response.status_code, 302) # redirect somewhere
  85. @override_settings(ROOT_URLCONF="generic_inline_admin.urls")
  86. class GenericInlineAdminParametersTest(TestDataMixin, TestCase):
  87. factory = RequestFactory()
  88. def setUp(self):
  89. self.client.force_login(self.superuser)
  90. def _create_object(self, model):
  91. """
  92. Create a model with an attached Media object via GFK. We can't
  93. load content via a fixture (since the GenericForeignKey relies on
  94. content type IDs, which will vary depending on what other tests
  95. have been run), thus we do it here.
  96. """
  97. e = model.objects.create(name="This Week in Django")
  98. Media.objects.create(content_object=e, url="http://example.com/podcast.mp3")
  99. return e
  100. def test_no_param(self):
  101. """
  102. With one initial form, extra (default) at 3, there should be 4 forms.
  103. """
  104. e = self._create_object(Episode)
  105. response = self.client.get(
  106. reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
  107. )
  108. formset = response.context["inline_admin_formsets"][0].formset
  109. self.assertEqual(formset.total_form_count(), 4)
  110. self.assertEqual(formset.initial_form_count(), 1)
  111. def test_extra_param(self):
  112. """
  113. With extra=0, there should be one form.
  114. """
  115. class ExtraInline(GenericTabularInline):
  116. model = Media
  117. extra = 0
  118. modeladmin = admin.ModelAdmin(Episode, admin_site)
  119. modeladmin.inlines = [ExtraInline]
  120. e = self._create_object(Episode)
  121. request = self.factory.get(
  122. reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
  123. )
  124. request.user = User(username="super", is_superuser=True)
  125. response = modeladmin.changeform_view(request, object_id=str(e.pk))
  126. formset = response.context_data["inline_admin_formsets"][0].formset
  127. self.assertEqual(formset.total_form_count(), 1)
  128. self.assertEqual(formset.initial_form_count(), 1)
  129. def test_max_num_param(self):
  130. """
  131. With extra=5 and max_num=2, there should be only 2 forms.
  132. """
  133. class MaxNumInline(GenericTabularInline):
  134. model = Media
  135. extra = 5
  136. max_num = 2
  137. modeladmin = admin.ModelAdmin(Episode, admin_site)
  138. modeladmin.inlines = [MaxNumInline]
  139. e = self._create_object(Episode)
  140. request = self.factory.get(
  141. reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
  142. )
  143. request.user = User(username="super", is_superuser=True)
  144. response = modeladmin.changeform_view(request, object_id=str(e.pk))
  145. formset = response.context_data["inline_admin_formsets"][0].formset
  146. self.assertEqual(formset.total_form_count(), 2)
  147. self.assertEqual(formset.initial_form_count(), 1)
  148. def test_min_num_param(self):
  149. """
  150. With extra=3 and min_num=2, there should be five forms.
  151. """
  152. class MinNumInline(GenericTabularInline):
  153. model = Media
  154. extra = 3
  155. min_num = 2
  156. modeladmin = admin.ModelAdmin(Episode, admin_site)
  157. modeladmin.inlines = [MinNumInline]
  158. e = self._create_object(Episode)
  159. request = self.factory.get(
  160. reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
  161. )
  162. request.user = User(username="super", is_superuser=True)
  163. response = modeladmin.changeform_view(request, object_id=str(e.pk))
  164. formset = response.context_data["inline_admin_formsets"][0].formset
  165. self.assertEqual(formset.total_form_count(), 5)
  166. self.assertEqual(formset.initial_form_count(), 1)
  167. def test_get_extra(self):
  168. class GetExtraInline(GenericTabularInline):
  169. model = Media
  170. extra = 4
  171. def get_extra(self, request, obj):
  172. return 2
  173. modeladmin = admin.ModelAdmin(Episode, admin_site)
  174. modeladmin.inlines = [GetExtraInline]
  175. e = self._create_object(Episode)
  176. request = self.factory.get(
  177. reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
  178. )
  179. request.user = User(username="super", is_superuser=True)
  180. response = modeladmin.changeform_view(request, object_id=str(e.pk))
  181. formset = response.context_data["inline_admin_formsets"][0].formset
  182. self.assertEqual(formset.extra, 2)
  183. def test_get_min_num(self):
  184. class GetMinNumInline(GenericTabularInline):
  185. model = Media
  186. min_num = 5
  187. def get_min_num(self, request, obj):
  188. return 2
  189. modeladmin = admin.ModelAdmin(Episode, admin_site)
  190. modeladmin.inlines = [GetMinNumInline]
  191. e = self._create_object(Episode)
  192. request = self.factory.get(
  193. reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
  194. )
  195. request.user = User(username="super", is_superuser=True)
  196. response = modeladmin.changeform_view(request, object_id=str(e.pk))
  197. formset = response.context_data["inline_admin_formsets"][0].formset
  198. self.assertEqual(formset.min_num, 2)
  199. def test_get_max_num(self):
  200. class GetMaxNumInline(GenericTabularInline):
  201. model = Media
  202. extra = 5
  203. def get_max_num(self, request, obj):
  204. return 2
  205. modeladmin = admin.ModelAdmin(Episode, admin_site)
  206. modeladmin.inlines = [GetMaxNumInline]
  207. e = self._create_object(Episode)
  208. request = self.factory.get(
  209. reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
  210. )
  211. request.user = User(username="super", is_superuser=True)
  212. response = modeladmin.changeform_view(request, object_id=str(e.pk))
  213. formset = response.context_data["inline_admin_formsets"][0].formset
  214. self.assertEqual(formset.max_num, 2)
  215. @override_settings(ROOT_URLCONF="generic_inline_admin.urls")
  216. class GenericInlineAdminWithUniqueTogetherTest(TestDataMixin, TestCase):
  217. def setUp(self):
  218. self.client.force_login(self.superuser)
  219. def test_add(self):
  220. category_id = Category.objects.create(name="male").pk
  221. prefix = "generic_inline_admin-phonenumber-content_type-object_id"
  222. post_data = {
  223. "name": "John Doe",
  224. # inline data
  225. f"{prefix}-TOTAL_FORMS": "1",
  226. f"{prefix}-INITIAL_FORMS": "0",
  227. f"{prefix}-MAX_NUM_FORMS": "0",
  228. f"{prefix}-0-id": "",
  229. f"{prefix}-0-phone_number": "555-555-5555",
  230. f"{prefix}-0-category": str(category_id),
  231. }
  232. response = self.client.get(reverse("admin:generic_inline_admin_contact_add"))
  233. self.assertEqual(response.status_code, 200)
  234. response = self.client.post(
  235. reverse("admin:generic_inline_admin_contact_add"), post_data
  236. )
  237. self.assertEqual(response.status_code, 302) # redirect somewhere
  238. def test_delete(self):
  239. from .models import Contact
  240. c = Contact.objects.create(name="foo")
  241. PhoneNumber.objects.create(
  242. object_id=c.id,
  243. content_type=ContentType.objects.get_for_model(Contact),
  244. phone_number="555-555-5555",
  245. )
  246. response = self.client.post(
  247. reverse("admin:generic_inline_admin_contact_delete", args=[c.pk])
  248. )
  249. self.assertContains(response, "Are you sure you want to delete")
  250. @override_settings(ROOT_URLCONF="generic_inline_admin.urls")
  251. class NoInlineDeletionTest(SimpleTestCase):
  252. def test_no_deletion(self):
  253. inline = MediaPermanentInline(EpisodePermanent, admin_site)
  254. fake_request = object()
  255. formset = inline.get_formset(fake_request)
  256. self.assertFalse(formset.can_delete)
  257. class MockRequest:
  258. pass
  259. class MockSuperUser:
  260. def has_perm(self, perm, obj=None):
  261. return True
  262. request = MockRequest()
  263. request.user = MockSuperUser()
  264. @override_settings(ROOT_URLCONF="generic_inline_admin.urls")
  265. class GenericInlineModelAdminTest(SimpleTestCase):
  266. def setUp(self):
  267. self.site = AdminSite()
  268. def test_get_formset_kwargs(self):
  269. media_inline = MediaInline(Media, AdminSite())
  270. # Create a formset with default arguments
  271. formset = media_inline.get_formset(request)
  272. self.assertEqual(formset.max_num, DEFAULT_MAX_NUM)
  273. self.assertIs(formset.can_order, False)
  274. # Create a formset with custom keyword arguments
  275. formset = media_inline.get_formset(request, max_num=100, can_order=True)
  276. self.assertEqual(formset.max_num, 100)
  277. self.assertIs(formset.can_order, True)
  278. def test_custom_form_meta_exclude_with_readonly(self):
  279. """
  280. The custom ModelForm's `Meta.exclude` is respected when
  281. used in conjunction with `GenericInlineModelAdmin.readonly_fields`
  282. and when no `ModelAdmin.exclude` is defined.
  283. """
  284. class MediaForm(ModelForm):
  285. class Meta:
  286. model = Media
  287. exclude = ["url"]
  288. class MediaInline(GenericTabularInline):
  289. readonly_fields = ["description"]
  290. form = MediaForm
  291. model = Media
  292. class EpisodeAdmin(admin.ModelAdmin):
  293. inlines = [MediaInline]
  294. ma = EpisodeAdmin(Episode, self.site)
  295. self.assertEqual(
  296. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  297. ["keywords", "id", "DELETE"],
  298. )
  299. def test_custom_form_meta_exclude(self):
  300. """
  301. The custom ModelForm's `Meta.exclude` is respected by
  302. `GenericInlineModelAdmin.get_formset`, and overridden if
  303. `ModelAdmin.exclude` or `GenericInlineModelAdmin.exclude` are defined.
  304. Refs #15907.
  305. """
  306. # First with `GenericInlineModelAdmin` -----------------
  307. class MediaForm(ModelForm):
  308. class Meta:
  309. model = Media
  310. exclude = ["url"]
  311. class MediaInline(GenericTabularInline):
  312. exclude = ["description"]
  313. form = MediaForm
  314. model = Media
  315. class EpisodeAdmin(admin.ModelAdmin):
  316. inlines = [MediaInline]
  317. ma = EpisodeAdmin(Episode, self.site)
  318. self.assertEqual(
  319. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  320. ["url", "keywords", "id", "DELETE"],
  321. )
  322. # Then, only with `ModelForm` -----------------
  323. class MediaInline(GenericTabularInline):
  324. form = MediaForm
  325. model = Media
  326. class EpisodeAdmin(admin.ModelAdmin):
  327. inlines = [MediaInline]
  328. ma = EpisodeAdmin(Episode, self.site)
  329. self.assertEqual(
  330. list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
  331. ["description", "keywords", "id", "DELETE"],
  332. )
  333. def test_get_fieldsets(self):
  334. # get_fieldsets is called when figuring out form fields.
  335. # Refs #18681.
  336. class MediaForm(ModelForm):
  337. class Meta:
  338. model = Media
  339. fields = "__all__"
  340. class MediaInline(GenericTabularInline):
  341. form = MediaForm
  342. model = Media
  343. can_delete = False
  344. def get_fieldsets(self, request, obj=None):
  345. return [(None, {"fields": ["url", "description"]})]
  346. ma = MediaInline(Media, self.site)
  347. form = ma.get_formset(None).form
  348. self.assertEqual(form._meta.fields, ["url", "description"])
  349. def test_get_formsets_with_inlines_returns_tuples(self):
  350. """
  351. get_formsets_with_inlines() returns the correct tuples.
  352. """
  353. class MediaForm(ModelForm):
  354. class Meta:
  355. model = Media
  356. exclude = ["url"]
  357. class MediaInline(GenericTabularInline):
  358. form = MediaForm
  359. model = Media
  360. class AlternateInline(GenericTabularInline):
  361. form = MediaForm
  362. model = Media
  363. class EpisodeAdmin(admin.ModelAdmin):
  364. inlines = [AlternateInline, MediaInline]
  365. ma = EpisodeAdmin(Episode, self.site)
  366. inlines = ma.get_inline_instances(request)
  367. for (formset, inline), other_inline in zip(
  368. ma.get_formsets_with_inlines(request), inlines
  369. ):
  370. self.assertIsInstance(formset, other_inline.get_formset(request).__class__)
  371. def test_get_inline_instances_override_get_inlines(self):
  372. class MediaInline(GenericTabularInline):
  373. model = Media
  374. class AlternateInline(GenericTabularInline):
  375. model = Media
  376. class EpisodeAdmin(admin.ModelAdmin):
  377. inlines = (AlternateInline, MediaInline)
  378. def get_inlines(self, request, obj):
  379. if hasattr(request, "name"):
  380. if request.name == "alternate":
  381. return self.inlines[:1]
  382. elif request.name == "media":
  383. return self.inlines[1:2]
  384. return []
  385. ma = EpisodeAdmin(Episode, self.site)
  386. self.assertEqual(ma.get_inlines(request, None), [])
  387. self.assertEqual(ma.get_inline_instances(request), [])
  388. for name, inline_class in (
  389. ("alternate", AlternateInline),
  390. ("media", MediaInline),
  391. ):
  392. request.name = name
  393. self.assertEqual(ma.get_inlines(request, None), (inline_class,))
  394. self.assertEqual(type(ma.get_inline_instances(request)[0]), inline_class)