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