a smoke test to ensure get on the addview works a smoke test to ensure get on the changeview works a smoke test to ensure post on addview works inline data a smoke test to ensure post on editview works inline data create a model with an attached media object via gfk we can t load content via a fixture since the genericforeignkey relies on content type ids which will vary depending on what other tests have been run thus we do it here with one initial form extra default at 3 there should be 4 forms with extra0 there should be one form with extra5 and maxnum2 there should be only 2 forms with extra3 and minnum2 there should be five forms inline data create a formset with default arguments create a formset with custom keyword arguments the custom modelform s meta exclude is respected when used in conjunction with genericinlinemodeladmin readonlyfields and when no modeladmin exclude is defined the custom modelform s meta exclude is respected by genericinlinemodeladmin getformset and overridden if modeladmin exclude or genericinlinemodeladmin exclude are defined refs 15907 first with genericinlinemodeladmin then only with modelform getfieldsets is called when figuring out form fields refs 18681 getformsetswithinlines returns the correct tuples a smoke test to ensure get on the add_view works a smoke test to ensure get on the change_view works a smoke test to ensure post on add_view works inline data redirect somewhere a smoke test to ensure post on edit_view works inline data redirect somewhere create a model with an attached media object via gfk we can t load content via a fixture since the genericforeignkey relies on content type ids which will vary depending on what other tests have been run thus we do it here with one initial form extra default at 3 there should be 4 forms with extra 0 there should be one form with extra 5 and max_num 2 there should be only 2 forms with extra 3 and min_num 2 there should be five forms inline data redirect somewhere create a formset with default arguments create a formset with custom keyword arguments the custom modelform s meta exclude is respected when used in conjunction with genericinlinemodeladmin readonly_fields and when no modeladmin exclude is defined the custom modelform s meta exclude is respected by genericinlinemodeladmin get_formset and overridden if modeladmin exclude or genericinlinemodeladmin exclude are defined refs 15907 first with genericinlinemodeladmin then only with modelform get_fieldsets is called when figuring out form fields refs 18681 get_formsets_with_inlines returns the correct tuples
from django.contrib import admin
from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import User
from django.contrib.contenttypes.admin import GenericTabularInline
from django.contrib.contenttypes.models import ContentType
from django.forms.formsets import DEFAULT_MAX_NUM
from django.forms.models import ModelForm
from django.test import (
    RequestFactory,
    SimpleTestCase,
    TestCase,
    ignore_warnings,
    override_settings,
)
from django.urls import reverse
from django.utils.deprecation import RemovedInDjango60Warning

from .admin import MediaInline, MediaPermanentInline
from .admin import site as admin_site
from .models import Category, Episode, EpisodePermanent, Media, PhoneNumber


class TestDataMixin:
    @classmethod
    def setUpTestData(cls):
        cls.superuser = User.objects.create_superuser(
            username="super", password="secret", email="super@example.com"
        )


@ignore_warnings(category=RemovedInDjango60Warning)
@override_settings(ROOT_URLCONF="generic_inline_admin.urls")
class GenericAdminViewTest(TestDataMixin, TestCase):
    def setUp(self):
        self.client.force_login(self.superuser)

        e = Episode.objects.create(name="This Week in Django")
        self.episode_pk = e.pk
        m = Media(content_object=e, url="http://example.com/podcast.mp3")
        m.save()
        self.mp3_media_pk = m.pk

        m = Media(content_object=e, url="http://example.com/logo.png")
        m.save()
        self.png_media_pk = m.pk

    def test_basic_add_GET(self):
        
        response = self.client.get(reverse("admin:generic_inline_admin_episode_add"))
        self.assertEqual(response.status_code, 200)

    def test_basic_edit_GET(self):
        
        response = self.client.get(
            reverse(
                "admin:generic_inline_admin_episode_change", args=(self.episode_pk,)
            )
        )
        self.assertEqual(response.status_code, 200)

    def test_basic_add_POST(self):
        
        post_data = {
            "name": "This Week in Django",
            
            "generic_inline_admin-media-content_type-object_id-TOTAL_FORMS": "1",
            "generic_inline_admin-media-content_type-object_id-INITIAL_FORMS": "0",
            "generic_inline_admin-media-content_type-object_id-MAX_NUM_FORMS": "0",
        }
        response = self.client.post(
            reverse("admin:generic_inline_admin_episode_add"), post_data
        )
        self.assertEqual(response.status_code, 302)  

    def test_basic_edit_POST(self):
        
        prefix = "generic_inline_admin-media-content_type-object_id"
        post_data = {
            "name": "This Week in Django",
            
            f"{prefix}-TOTAL_FORMS": "3",
            f"{prefix}-INITIAL_FORMS": "2",
            f"{prefix}-MAX_NUM_FORMS": "0",
            f"{prefix}-0-id": str(self.mp3_media_pk),
            f"{prefix}-0-url": "http://example.com/podcast.mp3",
            f"{prefix}-1-id": str(self.png_media_pk),
            f"{prefix}-1-url": "http://example.com/logo.png",
            f"{prefix}-2-id": "",
            f"{prefix}-2-url": "",
        }
        url = reverse(
            "admin:generic_inline_admin_episode_change", args=(self.episode_pk,)
        )
        response = self.client.post(url, post_data)
        self.assertEqual(response.status_code, 302)  


@ignore_warnings(category=RemovedInDjango60Warning)
@override_settings(ROOT_URLCONF="generic_inline_admin.urls")
class GenericInlineAdminParametersTest(TestDataMixin, TestCase):
    factory = RequestFactory()

    def setUp(self):
        self.client.force_login(self.superuser)

    def _create_object(self, model):
        
        e = model.objects.create(name="This Week in Django")
        Media.objects.create(content_object=e, url="http://example.com/podcast.mp3")
        return e

    def test_no_param(self):
        
        e = self._create_object(Episode)
        response = self.client.get(
            reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
        )
        formset = response.context["inline_admin_formsets"][0].formset
        self.assertEqual(formset.total_form_count(), 4)
        self.assertEqual(formset.initial_form_count(), 1)

    def test_extra_param(self):
        

        class ExtraInline(GenericTabularInline):
            model = Media
            extra = 0

        modeladmin = admin.ModelAdmin(Episode, admin_site)
        modeladmin.inlines = [ExtraInline]

        e = self._create_object(Episode)
        request = self.factory.get(
            reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
        )
        request.user = User(username="super", is_superuser=True)
        response = modeladmin.changeform_view(request, object_id=str(e.pk))
        formset = response.context_data["inline_admin_formsets"][0].formset
        self.assertEqual(formset.total_form_count(), 1)
        self.assertEqual(formset.initial_form_count(), 1)

    def test_max_num_param(self):
        

        class MaxNumInline(GenericTabularInline):
            model = Media
            extra = 5
            max_num = 2

        modeladmin = admin.ModelAdmin(Episode, admin_site)
        modeladmin.inlines = [MaxNumInline]

        e = self._create_object(Episode)
        request = self.factory.get(
            reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
        )
        request.user = User(username="super", is_superuser=True)
        response = modeladmin.changeform_view(request, object_id=str(e.pk))
        formset = response.context_data["inline_admin_formsets"][0].formset
        self.assertEqual(formset.total_form_count(), 2)
        self.assertEqual(formset.initial_form_count(), 1)

    def test_min_num_param(self):
        

        class MinNumInline(GenericTabularInline):
            model = Media
            extra = 3
            min_num = 2

        modeladmin = admin.ModelAdmin(Episode, admin_site)
        modeladmin.inlines = [MinNumInline]

        e = self._create_object(Episode)
        request = self.factory.get(
            reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
        )
        request.user = User(username="super", is_superuser=True)
        response = modeladmin.changeform_view(request, object_id=str(e.pk))
        formset = response.context_data["inline_admin_formsets"][0].formset
        self.assertEqual(formset.total_form_count(), 5)
        self.assertEqual(formset.initial_form_count(), 1)

    def test_get_extra(self):
        class GetExtraInline(GenericTabularInline):
            model = Media
            extra = 4

            def get_extra(self, request, obj):
                return 2

        modeladmin = admin.ModelAdmin(Episode, admin_site)
        modeladmin.inlines = [GetExtraInline]
        e = self._create_object(Episode)
        request = self.factory.get(
            reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
        )
        request.user = User(username="super", is_superuser=True)
        response = modeladmin.changeform_view(request, object_id=str(e.pk))
        formset = response.context_data["inline_admin_formsets"][0].formset

        self.assertEqual(formset.extra, 2)

    def test_get_min_num(self):
        class GetMinNumInline(GenericTabularInline):
            model = Media
            min_num = 5

            def get_min_num(self, request, obj):
                return 2

        modeladmin = admin.ModelAdmin(Episode, admin_site)
        modeladmin.inlines = [GetMinNumInline]
        e = self._create_object(Episode)
        request = self.factory.get(
            reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
        )
        request.user = User(username="super", is_superuser=True)
        response = modeladmin.changeform_view(request, object_id=str(e.pk))
        formset = response.context_data["inline_admin_formsets"][0].formset

        self.assertEqual(formset.min_num, 2)

    def test_get_max_num(self):
        class GetMaxNumInline(GenericTabularInline):
            model = Media
            extra = 5

            def get_max_num(self, request, obj):
                return 2

        modeladmin = admin.ModelAdmin(Episode, admin_site)
        modeladmin.inlines = [GetMaxNumInline]
        e = self._create_object(Episode)
        request = self.factory.get(
            reverse("admin:generic_inline_admin_episode_change", args=(e.pk,))
        )
        request.user = User(username="super", is_superuser=True)
        response = modeladmin.changeform_view(request, object_id=str(e.pk))
        formset = response.context_data["inline_admin_formsets"][0].formset

        self.assertEqual(formset.max_num, 2)


@override_settings(ROOT_URLCONF="generic_inline_admin.urls")
class GenericInlineAdminWithUniqueTogetherTest(TestDataMixin, TestCase):
    def setUp(self):
        self.client.force_login(self.superuser)

    def test_add(self):
        category_id = Category.objects.create(name="male").pk
        prefix = "generic_inline_admin-phonenumber-content_type-object_id"
        post_data = {
            "name": "John Doe",
            
            f"{prefix}-TOTAL_FORMS": "1",
            f"{prefix}-INITIAL_FORMS": "0",
            f"{prefix}-MAX_NUM_FORMS": "0",
            f"{prefix}-0-id": "",
            f"{prefix}-0-phone_number": "555-555-5555",
            f"{prefix}-0-category": str(category_id),
        }
        response = self.client.get(reverse("admin:generic_inline_admin_contact_add"))
        self.assertEqual(response.status_code, 200)
        response = self.client.post(
            reverse("admin:generic_inline_admin_contact_add"), post_data
        )
        self.assertEqual(response.status_code, 302)  

    def test_delete(self):
        from .models import Contact

        c = Contact.objects.create(name="foo")
        PhoneNumber.objects.create(
            object_id=c.id,
            content_type=ContentType.objects.get_for_model(Contact),
            phone_number="555-555-5555",
        )
        response = self.client.post(
            reverse("admin:generic_inline_admin_contact_delete", args=[c.pk])
        )
        self.assertContains(response, "Are you sure you want to delete")


@override_settings(ROOT_URLCONF="generic_inline_admin.urls")
class NoInlineDeletionTest(SimpleTestCase):
    @ignore_warnings(category=RemovedInDjango60Warning)
    def test_no_deletion(self):
        inline = MediaPermanentInline(EpisodePermanent, admin_site)
        fake_request = object()
        formset = inline.get_formset(fake_request)
        self.assertFalse(formset.can_delete)


class MockRequest:
    pass


class MockSuperUser:
    def has_perm(self, perm, obj=None):
        return True


request = MockRequest()
request.user = MockSuperUser()


@override_settings(ROOT_URLCONF="generic_inline_admin.urls")
class GenericInlineModelAdminTest(SimpleTestCase):
    def setUp(self):
        self.site = AdminSite()

    @ignore_warnings(category=RemovedInDjango60Warning)
    def test_get_formset_kwargs(self):
        media_inline = MediaInline(Media, AdminSite())

        
        formset = media_inline.get_formset(request)
        self.assertEqual(formset.max_num, DEFAULT_MAX_NUM)
        self.assertIs(formset.can_order, False)

        
        formset = media_inline.get_formset(request, max_num=100, can_order=True)
        self.assertEqual(formset.max_num, 100)
        self.assertIs(formset.can_order, True)

    def test_custom_form_meta_exclude_with_readonly(self):
        

        class MediaForm(ModelForm):
            class Meta:
                model = Media
                exclude = ["url"]

        class MediaInline(GenericTabularInline):
            readonly_fields = ["description"]
            form = MediaForm
            model = Media

        class EpisodeAdmin(admin.ModelAdmin):
            inlines = [MediaInline]

        ma = EpisodeAdmin(Episode, self.site)
        self.assertEqual(
            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
            ["keywords", "id", "DELETE"],
        )

    @ignore_warnings(category=RemovedInDjango60Warning)
    def test_custom_form_meta_exclude(self):
        
        

        class MediaForm(ModelForm):
            class Meta:
                model = Media
                exclude = ["url"]

        class MediaInline(GenericTabularInline):
            exclude = ["description"]
            form = MediaForm
            model = Media

        class EpisodeAdmin(admin.ModelAdmin):
            inlines = [MediaInline]

        ma = EpisodeAdmin(Episode, self.site)
        self.assertEqual(
            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
            ["url", "keywords", "id", "DELETE"],
        )

        

        class MediaInline(GenericTabularInline):
            form = MediaForm
            model = Media

        class EpisodeAdmin(admin.ModelAdmin):
            inlines = [MediaInline]

        ma = EpisodeAdmin(Episode, self.site)
        self.assertEqual(
            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
            ["description", "keywords", "id", "DELETE"],
        )

    @ignore_warnings(category=RemovedInDjango60Warning)
    def test_get_fieldsets(self):
        
        
        class MediaForm(ModelForm):
            class Meta:
                model = Media
                fields = "__all__"

        class MediaInline(GenericTabularInline):
            form = MediaForm
            model = Media
            can_delete = False

            def get_fieldsets(self, request, obj=None):
                return [(None, {"fields": ["url", "description"]})]

        ma = MediaInline(Media, self.site)
        form = ma.get_formset(None).form
        self.assertEqual(form._meta.fields, ["url", "description"])

    def test_get_formsets_with_inlines_returns_tuples(self):
        

        class MediaForm(ModelForm):
            class Meta:
                model = Media
                exclude = ["url"]

        class MediaInline(GenericTabularInline):
            form = MediaForm
            model = Media

        class AlternateInline(GenericTabularInline):
            form = MediaForm
            model = Media

        class EpisodeAdmin(admin.ModelAdmin):
            inlines = [AlternateInline, MediaInline]

        ma = EpisodeAdmin(Episode, self.site)
        inlines = ma.get_inline_instances(request)
        for (formset, inline), other_inline in zip(
            ma.get_formsets_with_inlines(request), inlines
        ):
            self.assertIsInstance(formset, other_inline.get_formset(request).__class__)

    def test_get_inline_instances_override_get_inlines(self):
        class MediaInline(GenericTabularInline):
            model = Media

        class AlternateInline(GenericTabularInline):
            model = Media

        class EpisodeAdmin(admin.ModelAdmin):
            inlines = (AlternateInline, MediaInline)

            def get_inlines(self, request, obj):
                if hasattr(request, "name"):
                    if request.name == "alternate":
                        return self.inlines[:1]
                    elif request.name == "media":
                        return self.inlines[1:2]
                return []

        ma = EpisodeAdmin(Episode, self.site)
        self.assertEqual(ma.get_inlines(request, None), [])
        self.assertEqual(ma.get_inline_instances(request), [])
        for name, inline_class in (
            ("alternate", AlternateInline),
            ("media", MediaInline),
        ):
            request.name = name
            self.assertEqual(ma.get_inlines(request, None), (inline_class,))
            self.assertEqual(type(ma.get_inline_instances(request)[0]), inline_class)