formfieldsfieldsets interaction fieldsetsadd and fieldsetschange should return a special data structure that is used in the templates they should generate the right thing whether we have specified a custom form the fields argument or nothing at all here s the default case there are no custom formaddformchange methods no fields argument and no fieldsets argument getfieldsets is called when figuring out form fields 18681 a lookupallowed allows a parameter whose field lookup doesn t exist 21129 reverse onetoonefield onetoonefield and foreignkey if fields is specified fieldsetsadd and fieldsetschange should just stick the fields into a formsets structure and return it if fields or fieldsets is specified it should exclude fields on the form class to the fields specified this may cause errors to be raised in the db layer if required model fields aren t in fields fieldsets but that s preferable to ghost errors where a field in the form class isn t being displayed because it s not in fieldsfieldsets using fields using fieldsets using exclude you can also pass a tuple to exclude using fields and exclude the custom modelform s meta exclude is respected when used in conjunction with modeladmin readonlyfields and when no modeladmin exclude is defined 14496 with modeladmin with inlinemodeladmin name shouldn t appear in basefields because it s part of readonlyfields but it should appear in getfieldsfieldsets so it can be displayed as readonly the custom modelform s meta exclude is overridden if modeladmin exclude or inlinemodeladmin exclude are defined 14496 with modeladmin with inlinemodeladmin if a form is specified it should use it allowing custom validation to work properly this won t break any of the admin widgets or media the exclude kwarg passed to modeladmin getform overrides all other declarations 8999 the exclude kwarg passed to inlinemodeladmin getformset overrides all other declarations 8999 the autocompletefields rawidfields and radiofields widgets may overridden by specifying a widget in getformset without the override this would be foreignkeyrawidwidget if the queryset of a modelchoicefield in a custom form is overridden relatedfieldwidgetwrapper doesn t mess that up obj is passed from inlinemodeladmin getfieldsets to inlinemodeladmin getformset radiofields behavior first without any radiofields specified the widgets for foreignkey and fields with choices specified ought to be a basic select widget foreignkey widgets in the admin are wrapped with relatedfieldwidgetwrapper so they need to be handled properly when type checking for select fields all of the choices lists have a first entry of dashes now specify all the fields as radiofields widgets should now be radioselect and the choices list should have a first entry of none if blanktrue for the model field finally the widget should have the radiolist attr and inline as well if the field is specified horizontal removedindjango60warning removedindjango60warning uses autocompletefields if not overridden uses overridden getautocompletefields modeladmin getdeletedobjects uses modeladmin hasdeletepermission for permissions checking hasviewpermission returns true for users who can view objects and false for users who can t hasaddpermission returns true for users who can add objects and false for users who can t haschangepermission returns true for users who can edit objects and false for users who can t hasdeletepermission returns true for users who can delete objects and false for users who can t asmodulepermission returns true for users who have any permission for the module and false for users who don t form fields fieldsets interaction fieldsets_add and fieldsets_change should return a special data structure that is used in the templates they should generate the right thing whether we have specified a custom form the fields argument or nothing at all here s the default case there are no custom form_add form_change methods no fields argument and no fieldsets argument get_fieldsets is called when figuring out form fields 18681 a lookup_allowed allows a parameter whose field lookup doesn t exist 21129 reverse onetoonefield onetoonefield and foreignkey if fields is specified fieldsets_add and fieldsets_change should just stick the fields into a formsets structure and return it if fields or fieldsets is specified it should exclude fields on the form class to the fields specified this may cause errors to be raised in the db layer if required model fields aren t in fields fieldsets but that s preferable to ghost errors where a field in the form class isn t being displayed because it s not in fields fieldsets using fields using fieldsets using exclude you can also pass a tuple to exclude using fields and exclude the custom modelform s meta exclude is respected when used in conjunction with modeladmin readonly_fields and when no modeladmin exclude is defined 14496 with modeladmin with inlinemodeladmin name shouldn t appear in base_fields because it s part of readonly_fields but it should appear in get_fields fieldsets so it can be displayed as read only the custom modelform s meta exclude is overridden if modeladmin exclude or inlinemodeladmin exclude are defined 14496 with modeladmin with inlinemodeladmin if a form is specified it should use it allowing custom validation to work properly this won t break any of the admin widgets or media the exclude kwarg passed to modeladmin get_form overrides all other declarations 8999 the exclude kwarg passed to inlinemodeladmin get_formset overrides all other declarations 8999 the autocomplete_fields raw_id_fields and radio_fields widgets may overridden by specifying a widget in get_formset without the override this would be foreignkeyrawidwidget if the queryset of a modelchoicefield in a custom form is overridden relatedfieldwidgetwrapper doesn t mess that up obj is passed from inlinemodeladmin get_fieldsets to inlinemodeladmin get_formset radio_fields behavior first without any radio_fields specified the widgets for foreignkey and fields with choices specified ought to be a basic select widget foreignkey widgets in the admin are wrapped with relatedfieldwidgetwrapper so they need to be handled properly when type checking for select fields all of the choices lists have a first entry of dashes now specify all the fields as radio_fields widgets should now be radioselect and the choices list should have a first entry of none if blank true for the model field finally the widget should have the radiolist attr and inline as well if the field is specified horizontal removedindjango60warning removedindjango60warning uses autocomplete_fields if not overridden uses overridden get_autocomplete_fields modeladmin get_deleted_objects uses modeladmin has_delete_permission for permissions checking has_view_permission returns true for users who can view objects and false for users who can t has_add_permission returns true for users who can add objects and false for users who can t has_change_permission returns true for users who can edit objects and false for users who can t has_delete_permission returns true for users who can delete objects and false for users who can t as_module_permission returns true for users who have any permission for the module and false for users who don t
from datetime import date

from django import forms
from django.contrib.admin.models import ADDITION, CHANGE, DELETION, LogEntry
from django.contrib.admin.options import (
    HORIZONTAL,
    VERTICAL,
    ModelAdmin,
    TabularInline,
    get_content_type_for_model,
)
from django.contrib.admin.sites import AdminSite
from django.contrib.admin.widgets import (
    AdminDateWidget,
    AdminRadioSelect,
    AutocompleteSelect,
    AutocompleteSelectMultiple,
)
from django.contrib.auth.models import User
from django.db import models
from django.forms.widgets import Select
from django.test import RequestFactory, SimpleTestCase, TestCase
from django.test.utils import isolate_apps
from django.utils.deprecation import RemovedInDjango60Warning

from .models import Band, Concert, Song


class MockRequest:
    pass


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


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


class ModelAdminTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.band = Band.objects.create(
            name="The Doors",
            bio="",
            sign_date=date(1965, 1, 1),
        )

    def setUp(self):
        self.site = AdminSite()

    def test_modeladmin_str(self):
        ma = ModelAdmin(Band, self.site)
        self.assertEqual(str(ma), "modeladmin.ModelAdmin")

    def test_default_attributes(self):
        ma = ModelAdmin(Band, self.site)
        self.assertEqual(ma.actions, ())
        self.assertEqual(ma.inlines, ())

    

    def test_default_fields(self):
        ma = ModelAdmin(Band, self.site)
        self.assertEqual(
            list(ma.get_form(request).base_fields), ["name", "bio", "sign_date"]
        )
        self.assertEqual(list(ma.get_fields(request)), ["name", "bio", "sign_date"])
        self.assertEqual(
            list(ma.get_fields(request, self.band)), ["name", "bio", "sign_date"]
        )
        self.assertIsNone(ma.get_exclude(request, self.band))

    def test_default_fieldsets(self):
        
        
        
        
        
        
        ma = ModelAdmin(Band, self.site)
        self.assertEqual(
            ma.get_fieldsets(request),
            [(None, {"fields": ["name", "bio", "sign_date"]})],
        )
        self.assertEqual(
            ma.get_fieldsets(request, self.band),
            [(None, {"fields": ["name", "bio", "sign_date"]})],
        )

    def test_get_fieldsets(self):
        
        class BandAdmin(ModelAdmin):
            def get_fieldsets(self, request, obj=None):
                return [(None, {"fields": ["name", "bio"]})]

        ma = BandAdmin(Band, self.site)
        form = ma.get_form(None)
        self.assertEqual(form._meta.fields, ["name", "bio"])

        class InlineBandAdmin(TabularInline):
            model = Concert
            fk_name = "main_band"
            can_delete = False

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

        ma = InlineBandAdmin(Band, self.site)
        form = ma.get_formset(None).form
        self.assertEqual(form._meta.fields, ["day", "transport"])

    def test_lookup_allowed_allows_nonexistent_lookup(self):
        

        class BandAdmin(ModelAdmin):
            fields = ["name"]

        ma = BandAdmin(Band, self.site)
        self.assertIs(
            ma.lookup_allowed("name__nonexistent", "test_value", request),
            True,
        )

    @isolate_apps("modeladmin")
    def test_lookup_allowed_onetoone(self):
        class Department(models.Model):
            code = models.CharField(max_length=4, unique=True)

        class Employee(models.Model):
            department = models.ForeignKey(Department, models.CASCADE, to_field="code")

        class EmployeeProfile(models.Model):
            employee = models.OneToOneField(Employee, models.CASCADE)

        class EmployeeInfo(models.Model):
            employee = models.OneToOneField(Employee, models.CASCADE)
            description = models.CharField(max_length=100)

        class EmployeeProfileAdmin(ModelAdmin):
            list_filter = [
                "employee__employeeinfo__description",
                "employee__department__code",
            ]

        ma = EmployeeProfileAdmin(EmployeeProfile, self.site)
        
        self.assertIs(
            ma.lookup_allowed(
                "employee__employeeinfo__description", "test_value", request
            ),
            True,
        )
        
        self.assertIs(
            ma.lookup_allowed("employee__department__code", "test_value", request),
            True,
        )

    @isolate_apps("modeladmin")
    def test_lookup_allowed_foreign_primary(self):
        class Country(models.Model):
            name = models.CharField(max_length=256)

        class Place(models.Model):
            country = models.ForeignKey(Country, models.CASCADE)

        class Restaurant(models.Model):
            place = models.OneToOneField(Place, models.CASCADE, primary_key=True)

        class Waiter(models.Model):
            restaurant = models.ForeignKey(Restaurant, models.CASCADE)

        class WaiterAdmin(ModelAdmin):
            list_filter = [
                "restaurant__place__country",
                "restaurant__place__country__name",
            ]

        ma = WaiterAdmin(Waiter, self.site)
        self.assertIs(
            ma.lookup_allowed("restaurant__place__country", "1", request),
            True,
        )
        self.assertIs(
            ma.lookup_allowed("restaurant__place__country__id__exact", "1", request),
            True,
        )
        self.assertIs(
            ma.lookup_allowed(
                "restaurant__place__country__name", "test_value", request
            ),
            True,
        )

    def test_lookup_allowed_considers_dynamic_list_filter(self):
        class ConcertAdmin(ModelAdmin):
            list_filter = ["main_band__sign_date"]

            def get_list_filter(self, request):
                if getattr(request, "user", None):
                    return self.list_filter + ["main_band__name"]
                return self.list_filter

        model_admin = ConcertAdmin(Concert, self.site)
        request_band_name_filter = RequestFactory().get(
            "/", {"main_band__name": "test"}
        )
        self.assertIs(
            model_admin.lookup_allowed(
                "main_band__sign_date", "?", request_band_name_filter
            ),
            True,
        )
        self.assertIs(
            model_admin.lookup_allowed(
                "main_band__name", "?", request_band_name_filter
            ),
            False,
        )
        request_with_superuser = request
        self.assertIs(
            model_admin.lookup_allowed(
                "main_band__sign_date", "?", request_with_superuser
            ),
            True,
        )
        self.assertIs(
            model_admin.lookup_allowed("main_band__name", "?", request_with_superuser),
            True,
        )

    def test_lookup_allowed_without_request_deprecation(self):
        class ConcertAdmin(ModelAdmin):
            list_filter = ["main_band__sign_date"]

            def get_list_filter(self, request):
                return self.list_filter + ["main_band__name"]

            def lookup_allowed(self, lookup, value):
                return True

        model_admin = ConcertAdmin(Concert, self.site)
        msg = (
            "`request` must be added to the signature of ModelAdminTests."
            "test_lookup_allowed_without_request_deprecation.<locals>."
            "ConcertAdmin.lookup_allowed()."
        )
        request_band_name_filter = RequestFactory().get(
            "/", {"main_band__name": "test"}
        )
        request_band_name_filter.user = User.objects.create_superuser(
            username="bob", email="bob@test.com", password="test"
        )
        with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
            changelist = model_admin.get_changelist_instance(request_band_name_filter)
            filterspec = changelist.get_filters(request_band_name_filter)[0][0]
            self.assertEqual(filterspec.title, "sign date")
            filterspec = changelist.get_filters(request_band_name_filter)[0][1]
            self.assertEqual(filterspec.title, "name")
            self.assertSequenceEqual(filterspec.lookup_choices, [self.band.name])

    def test_field_arguments(self):
        
        
        class BandAdmin(ModelAdmin):
            fields = ["name"]

        ma = BandAdmin(Band, self.site)

        self.assertEqual(list(ma.get_fields(request)), ["name"])
        self.assertEqual(list(ma.get_fields(request, self.band)), ["name"])
        self.assertEqual(ma.get_fieldsets(request), [(None, {"fields": ["name"]})])
        self.assertEqual(
            ma.get_fieldsets(request, self.band), [(None, {"fields": ["name"]})]
        )

    def test_field_arguments_restricted_on_form(self):
        
        
        
        
        

        
        class BandAdmin(ModelAdmin):
            fields = ["name"]

        ma = BandAdmin(Band, self.site)
        self.assertEqual(list(ma.get_form(request).base_fields), ["name"])
        self.assertEqual(list(ma.get_form(request, self.band).base_fields), ["name"])

        
        class BandAdmin(ModelAdmin):
            fieldsets = [(None, {"fields": ["name"]})]

        ma = BandAdmin(Band, self.site)
        self.assertEqual(list(ma.get_form(request).base_fields), ["name"])
        self.assertEqual(list(ma.get_form(request, self.band).base_fields), ["name"])

        
        class BandAdmin(ModelAdmin):
            exclude = ["bio"]

        ma = BandAdmin(Band, self.site)
        self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])

        
        class BandAdmin(ModelAdmin):
            exclude = ("bio",)

        ma = BandAdmin(Band, self.site)
        self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])

        
        class BandAdmin(ModelAdmin):
            fields = ["name", "bio"]
            exclude = ["bio"]

        ma = BandAdmin(Band, self.site)
        self.assertEqual(list(ma.get_form(request).base_fields), ["name"])

    def test_custom_form_meta_exclude_with_readonly(self):
        

        
        class AdminBandForm(forms.ModelForm):
            class Meta:
                model = Band
                exclude = ["bio"]

        class BandAdmin(ModelAdmin):
            readonly_fields = ["name"]
            form = AdminBandForm

        ma = BandAdmin(Band, self.site)
        self.assertEqual(list(ma.get_form(request).base_fields), ["sign_date"])

        
        class AdminConcertForm(forms.ModelForm):
            class Meta:
                model = Concert
                exclude = ["day"]

        class ConcertInline(TabularInline):
            readonly_fields = ["transport"]
            form = AdminConcertForm
            fk_name = "main_band"
            model = Concert

        class BandAdmin(ModelAdmin):
            inlines = [ConcertInline]

        ma = BandAdmin(Band, self.site)
        self.assertEqual(
            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
            ["main_band", "opening_band", "id", "DELETE"],
        )

    def test_custom_formfield_override_readonly(self):
        class AdminBandForm(forms.ModelForm):
            name = forms.CharField()

            class Meta:
                exclude = ()
                model = Band

        class BandAdmin(ModelAdmin):
            form = AdminBandForm
            readonly_fields = ["name"]

        ma = BandAdmin(Band, self.site)

        
        
        self.assertEqual(list(ma.get_form(request).base_fields), ["bio", "sign_date"])
        
        
        self.assertEqual(list(ma.get_fields(request)), ["bio", "sign_date", "name"])
        self.assertEqual(
            list(ma.get_fieldsets(request)),
            [(None, {"fields": ["bio", "sign_date", "name"]})],
        )

    def test_custom_form_meta_exclude(self):
        

        
        class AdminBandForm(forms.ModelForm):
            class Meta:
                model = Band
                exclude = ["bio"]

        class BandAdmin(ModelAdmin):
            exclude = ["name"]
            form = AdminBandForm

        ma = BandAdmin(Band, self.site)
        self.assertEqual(list(ma.get_form(request).base_fields), ["bio", "sign_date"])

        
        class AdminConcertForm(forms.ModelForm):
            class Meta:
                model = Concert
                exclude = ["day"]

        class ConcertInline(TabularInline):
            exclude = ["transport"]
            form = AdminConcertForm
            fk_name = "main_band"
            model = Concert

        class BandAdmin(ModelAdmin):
            inlines = [ConcertInline]

        ma = BandAdmin(Band, self.site)
        self.assertEqual(
            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
            ["main_band", "opening_band", "day", "id", "DELETE"],
        )

    def test_overriding_get_exclude(self):
        class BandAdmin(ModelAdmin):
            def get_exclude(self, request, obj=None):
                return ["name"]

        self.assertEqual(
            list(BandAdmin(Band, self.site).get_form(request).base_fields),
            ["bio", "sign_date"],
        )

    def test_get_exclude_overrides_exclude(self):
        class BandAdmin(ModelAdmin):
            exclude = ["bio"]

            def get_exclude(self, request, obj=None):
                return ["name"]

        self.assertEqual(
            list(BandAdmin(Band, self.site).get_form(request).base_fields),
            ["bio", "sign_date"],
        )

    def test_get_exclude_takes_obj(self):
        class BandAdmin(ModelAdmin):
            def get_exclude(self, request, obj=None):
                if obj:
                    return ["sign_date"]
                return ["name"]

        self.assertEqual(
            list(BandAdmin(Band, self.site).get_form(request, self.band).base_fields),
            ["name", "bio"],
        )

    def test_custom_form_validation(self):
        
        
        class AdminBandForm(forms.ModelForm):
            delete = forms.BooleanField()

        class BandAdmin(ModelAdmin):
            form = AdminBandForm

        ma = BandAdmin(Band, self.site)
        self.assertEqual(
            list(ma.get_form(request).base_fields),
            ["name", "bio", "sign_date", "delete"],
        )
        self.assertEqual(
            type(ma.get_form(request).base_fields["sign_date"].widget), AdminDateWidget
        )

    def test_form_exclude_kwarg_override(self):
        

        class AdminBandForm(forms.ModelForm):
            class Meta:
                model = Band
                exclude = ["name"]

        class BandAdmin(ModelAdmin):
            exclude = ["sign_date"]
            form = AdminBandForm

            def get_form(self, request, obj=None, **kwargs):
                kwargs["exclude"] = ["bio"]
                return super().get_form(request, obj, **kwargs)

        ma = BandAdmin(Band, self.site)
        self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])

    def test_formset_exclude_kwarg_override(self):
        

        class AdminConcertForm(forms.ModelForm):
            class Meta:
                model = Concert
                exclude = ["day"]

        class ConcertInline(TabularInline):
            exclude = ["transport"]
            form = AdminConcertForm
            fk_name = "main_band"
            model = Concert

            def get_formset(self, request, obj=None, **kwargs):
                kwargs["exclude"] = ["opening_band"]
                return super().get_formset(request, obj, **kwargs)

        class BandAdmin(ModelAdmin):
            inlines = [ConcertInline]

        ma = BandAdmin(Band, self.site)
        self.assertEqual(
            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
            ["main_band", "day", "transport", "id", "DELETE"],
        )

    def test_formset_overriding_get_exclude_with_form_fields(self):
        class AdminConcertForm(forms.ModelForm):
            class Meta:
                model = Concert
                fields = ["main_band", "opening_band", "day", "transport"]

        class ConcertInline(TabularInline):
            form = AdminConcertForm
            fk_name = "main_band"
            model = Concert

            def get_exclude(self, request, obj=None):
                return ["opening_band"]

        class BandAdmin(ModelAdmin):
            inlines = [ConcertInline]

        ma = BandAdmin(Band, self.site)
        self.assertEqual(
            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
            ["main_band", "day", "transport", "id", "DELETE"],
        )

    def test_formset_overriding_get_exclude_with_form_exclude(self):
        class AdminConcertForm(forms.ModelForm):
            class Meta:
                model = Concert
                exclude = ["day"]

        class ConcertInline(TabularInline):
            form = AdminConcertForm
            fk_name = "main_band"
            model = Concert

            def get_exclude(self, request, obj=None):
                return ["opening_band"]

        class BandAdmin(ModelAdmin):
            inlines = [ConcertInline]

        ma = BandAdmin(Band, self.site)
        self.assertEqual(
            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
            ["main_band", "day", "transport", "id", "DELETE"],
        )

    def test_raw_id_fields_widget_override(self):
        

        class ConcertInline(TabularInline):
            model = Concert
            fk_name = "main_band"
            raw_id_fields = ("opening_band",)

            def get_formset(self, request, obj=None, **kwargs):
                kwargs["widgets"] = {"opening_band": Select}
                return super().get_formset(request, obj, **kwargs)

        class BandAdmin(ModelAdmin):
            inlines = [ConcertInline]

        ma = BandAdmin(Band, self.site)
        band_widget = (
            list(ma.get_formsets_with_inlines(request))[0][0]()
            .forms[0]
            .fields["opening_band"]
            .widget
        )
        
        self.assertIsInstance(band_widget, Select)

    def test_queryset_override(self):
        
        
        band2 = Band.objects.create(
            name="The Beatles", bio="", sign_date=date(1962, 1, 1)
        )

        ma = ModelAdmin(Concert, self.site)
        form = ma.get_form(request)()

        self.assertHTMLEqual(
            str(form["main_band"]),
            '<div class="related-widget-wrapper" data-model-ref="band">'
            '<select name="main_band" id="id_main_band" required>'
            '<option value="" selected>---------</option>'
            '<option value="%d">The Beatles</option>'
            '<option value="%d">The Doors</option>'
            "</select></div>" % (band2.id, self.band.id),
        )

        class AdminConcertForm(forms.ModelForm):
            def __init__(self, *args, **kwargs):
                super().__init__(*args, **kwargs)
                self.fields["main_band"].queryset = Band.objects.filter(
                    name="The Doors"
                )

        class ConcertAdminWithForm(ModelAdmin):
            form = AdminConcertForm

        ma = ConcertAdminWithForm(Concert, self.site)
        form = ma.get_form(request)()

        self.assertHTMLEqual(
            str(form["main_band"]),
            '<div class="related-widget-wrapper" data-model-ref="band">'
            '<select name="main_band" id="id_main_band" required>'
            '<option value="" selected>---------</option>'
            '<option value="%d">The Doors</option>'
            "</select></div>" % self.band.id,
        )

    def test_regression_for_ticket_15820(self):
        

        class CustomConcertForm(forms.ModelForm):
            class Meta:
                model = Concert
                fields = ["day"]

        class ConcertInline(TabularInline):
            model = Concert
            fk_name = "main_band"

            def get_formset(self, request, obj=None, **kwargs):
                if obj:
                    kwargs["form"] = CustomConcertForm
                return super().get_formset(request, obj, **kwargs)

        class BandAdmin(ModelAdmin):
            inlines = [ConcertInline]

        Concert.objects.create(main_band=self.band, opening_band=self.band, day=1)
        ma = BandAdmin(Band, self.site)
        inline_instances = ma.get_inline_instances(request)
        fieldsets = list(inline_instances[0].get_fieldsets(request))
        self.assertEqual(
            fieldsets[0][1]["fields"], ["main_band", "opening_band", "day", "transport"]
        )
        fieldsets = list(
            inline_instances[0].get_fieldsets(request, inline_instances[0].model)
        )
        self.assertEqual(fieldsets[0][1]["fields"], ["day"])

    

    def test_default_foreign_key_widget(self):
        
        
        
        
        
        cma = ModelAdmin(Concert, self.site)
        cmafa = cma.get_form(request)

        self.assertEqual(type(cmafa.base_fields["main_band"].widget.widget), Select)
        self.assertEqual(
            list(cmafa.base_fields["main_band"].widget.choices),
            [("", "---------"), (self.band.id, "The Doors")],
        )

        self.assertEqual(type(cmafa.base_fields["opening_band"].widget.widget), Select)
        self.assertEqual(
            list(cmafa.base_fields["opening_band"].widget.choices),
            [("", "---------"), (self.band.id, "The Doors")],
        )
        self.assertEqual(type(cmafa.base_fields["day"].widget), Select)
        self.assertEqual(
            list(cmafa.base_fields["day"].widget.choices),
            [("", "---------"), (1, "Fri"), (2, "Sat")],
        )
        self.assertEqual(type(cmafa.base_fields["transport"].widget), Select)
        self.assertEqual(
            list(cmafa.base_fields["transport"].widget.choices),
            [("", "---------"), (1, "Plane"), (2, "Train"), (3, "Bus")],
        )

    def test_foreign_key_as_radio_field(self):
        
        
        
        
        class ConcertAdmin(ModelAdmin):
            radio_fields = {
                "main_band": HORIZONTAL,
                "opening_band": VERTICAL,
                "day": VERTICAL,
                "transport": HORIZONTAL,
            }

        cma = ConcertAdmin(Concert, self.site)
        cmafa = cma.get_form(request)

        self.assertEqual(
            type(cmafa.base_fields["main_band"].widget.widget), AdminRadioSelect
        )
        self.assertEqual(
            cmafa.base_fields["main_band"].widget.attrs, {"class": "radiolist inline"}
        )
        self.assertEqual(
            list(cmafa.base_fields["main_band"].widget.choices),
            [(self.band.id, "The Doors")],
        )

        self.assertEqual(
            type(cmafa.base_fields["opening_band"].widget.widget), AdminRadioSelect
        )
        self.assertEqual(
            cmafa.base_fields["opening_band"].widget.attrs, {"class": "radiolist"}
        )
        self.assertEqual(
            list(cmafa.base_fields["opening_band"].widget.choices),
            [("", "None"), (self.band.id, "The Doors")],
        )
        self.assertEqual(type(cmafa.base_fields["day"].widget), AdminRadioSelect)
        self.assertEqual(cmafa.base_fields["day"].widget.attrs, {"class": "radiolist"})
        self.assertEqual(
            list(cmafa.base_fields["day"].widget.choices), [(1, "Fri"), (2, "Sat")]
        )

        self.assertEqual(type(cmafa.base_fields["transport"].widget), AdminRadioSelect)
        self.assertEqual(
            cmafa.base_fields["transport"].widget.attrs, {"class": "radiolist inline"}
        )
        self.assertEqual(
            list(cmafa.base_fields["transport"].widget.choices),
            [("", "None"), (1, "Plane"), (2, "Train"), (3, "Bus")],
        )

        class AdminConcertForm(forms.ModelForm):
            class Meta:
                model = Concert
                exclude = ("transport",)

        class ConcertAdmin(ModelAdmin):
            form = AdminConcertForm

        ma = ConcertAdmin(Concert, self.site)
        self.assertEqual(
            list(ma.get_form(request).base_fields), ["main_band", "opening_band", "day"]
        )

        class AdminConcertForm(forms.ModelForm):
            extra = forms.CharField()

            class Meta:
                model = Concert
                fields = ["extra", "transport"]

        class ConcertAdmin(ModelAdmin):
            form = AdminConcertForm

        ma = ConcertAdmin(Concert, self.site)
        self.assertEqual(list(ma.get_form(request).base_fields), ["extra", "transport"])

        class ConcertInline(TabularInline):
            form = AdminConcertForm
            model = Concert
            fk_name = "main_band"
            can_delete = True

        class BandAdmin(ModelAdmin):
            inlines = [ConcertInline]

        ma = BandAdmin(Band, self.site)
        self.assertEqual(
            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
            ["extra", "transport", "id", "DELETE", "main_band"],
        )

    def test_log_actions(self):
        ma = ModelAdmin(Band, self.site)
        mock_request = MockRequest()
        mock_request.user = User.objects.create(username="bill")
        content_type = get_content_type_for_model(self.band)
        tests = (
            (ma.log_addition, ADDITION, {"added": {}}),
            (ma.log_change, CHANGE, {"changed": {"fields": ["name", "bio"]}}),
        )
        for method, flag, message in tests:
            with self.subTest(name=method.__name__):
                created = method(mock_request, self.band, message)
                fetched = LogEntry.objects.filter(action_flag=flag).latest("id")
                self.assertEqual(created, fetched)
                self.assertEqual(fetched.action_flag, flag)
                self.assertEqual(fetched.content_type, content_type)
                self.assertEqual(fetched.object_id, str(self.band.pk))
                self.assertEqual(fetched.user, mock_request.user)
                self.assertEqual(fetched.change_message, str(message))
                self.assertEqual(fetched.object_repr, str(self.band))

    def test_log_deletions(self):
        ma = ModelAdmin(Band, self.site)
        mock_request = MockRequest()
        mock_request.user = User.objects.create(username="akash")
        content_type = get_content_type_for_model(self.band)
        Band.objects.create(
            name="The Beatles",
            bio="A legendary rock band from Liverpool.",
            sign_date=date(1962, 1, 1),
        )
        Band.objects.create(
            name="Mohiner Ghoraguli",
            bio="A progressive rock band from Calcutta.",
            sign_date=date(1975, 1, 1),
        )
        queryset = Band.objects.all().order_by("-id")[:3]
        self.assertEqual(len(queryset), 3)
        with self.assertNumQueries(1):
            ma.log_deletions(mock_request, queryset)
        logs = (
            LogEntry.objects.filter(action_flag=DELETION)
            .order_by("id")
            .values_list(
                "user_id",
                "content_type",
                "object_id",
                "object_repr",
                "action_flag",
                "change_message",
            )
        )
        expected_log_values = [
            (
                mock_request.user.id,
                content_type.id,
                str(obj.pk),
                str(obj),
                DELETION,
                "",
            )
            for obj in queryset
        ]
        self.assertSequenceEqual(logs, expected_log_values)

    
    def test_log_deletion(self):
        ma = ModelAdmin(Band, self.site)
        mock_request = MockRequest()
        mock_request.user = User.objects.create(username="bill")
        content_type = get_content_type_for_model(self.band)
        msg = "ModelAdmin.log_deletion() is deprecated. Use log_deletions() instead."
        with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
            created = ma.log_deletion(mock_request, self.band, str(self.band))
        fetched = LogEntry.objects.filter(action_flag=DELETION).latest("id")
        self.assertEqual(created, fetched)
        self.assertEqual(fetched.action_flag, DELETION)
        self.assertEqual(fetched.content_type, content_type)
        self.assertEqual(fetched.object_id, str(self.band.pk))
        self.assertEqual(fetched.user, mock_request.user)
        self.assertEqual(fetched.change_message, "")
        self.assertEqual(fetched.object_repr, str(self.band))

    
    def test_log_deletion_fallback(self):
        class InheritedModelAdmin(ModelAdmin):
            def log_deletion(self, request, obj, object_repr):
                return super().log_deletion(request, obj, object_repr)

        ima = InheritedModelAdmin(Band, self.site)
        mock_request = MockRequest()
        mock_request.user = User.objects.create(username="akash")
        content_type = get_content_type_for_model(self.band)
        Band.objects.create(
            name="The Beatles",
            bio="A legendary rock band from Liverpool.",
            sign_date=date(1962, 1, 1),
        )
        Band.objects.create(
            name="Mohiner Ghoraguli",
            bio="A progressive rock band from Calcutta.",
            sign_date=date(1975, 1, 1),
        )
        queryset = Band.objects.all().order_by("-id")[:3]
        self.assertEqual(len(queryset), 3)
        msg = (
            "The usage of log_deletion() is deprecated. Implement log_deletions() "
            "instead."
        )
        with self.assertNumQueries(3):
            with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
                ima.log_deletions(mock_request, queryset)
        logs = (
            LogEntry.objects.filter(action_flag=DELETION)
            .order_by("id")
            .values_list(
                "user_id",
                "content_type",
                "object_id",
                "object_repr",
                "action_flag",
                "change_message",
            )
        )
        expected_log_values = [
            (
                mock_request.user.id,
                content_type.id,
                str(obj.pk),
                str(obj),
                DELETION,
                "",
            )
            for obj in queryset
        ]
        self.assertSequenceEqual(logs, expected_log_values)

    def test_get_autocomplete_fields(self):
        class NameAdmin(ModelAdmin):
            search_fields = ["name"]

        class SongAdmin(ModelAdmin):
            autocomplete_fields = ["featuring"]
            fields = ["featuring", "band"]

        class OtherSongAdmin(SongAdmin):
            def get_autocomplete_fields(self, request):
                return ["band"]

        self.site.register(Band, NameAdmin)
        try:
            
            model_admin = SongAdmin(Song, self.site)
            form = model_admin.get_form(request)()
            self.assertIsInstance(
                form.fields["featuring"].widget.widget, AutocompleteSelectMultiple
            )
            
            model_admin = OtherSongAdmin(Song, self.site)
            form = model_admin.get_form(request)()
            self.assertIsInstance(form.fields["band"].widget.widget, AutocompleteSelect)
        finally:
            self.site.unregister(Band)

    def test_get_deleted_objects(self):
        mock_request = MockRequest()
        mock_request.user = User.objects.create_superuser(
            username="bob", email="bob@test.com", password="test"
        )
        self.site.register(Band, ModelAdmin)
        ma = self.site.get_model_admin(Band)
        (
            deletable_objects,
            model_count,
            perms_needed,
            protected,
        ) = ma.get_deleted_objects([self.band], request)
        self.assertEqual(deletable_objects, ["Band: The Doors"])
        self.assertEqual(model_count, {"bands": 1})
        self.assertEqual(perms_needed, set())
        self.assertEqual(protected, [])

    def test_get_deleted_objects_with_custom_has_delete_permission(self):
        
        mock_request = MockRequest()
        mock_request.user = User.objects.create_superuser(
            username="bob", email="bob@test.com", password="test"
        )

        class TestModelAdmin(ModelAdmin):
            def has_delete_permission(self, request, obj=None):
                return False

        self.site.register(Band, TestModelAdmin)
        ma = self.site.get_model_admin(Band)
        (
            deletable_objects,
            model_count,
            perms_needed,
            protected,
        ) = ma.get_deleted_objects([self.band], request)
        self.assertEqual(deletable_objects, ["Band: The Doors"])
        self.assertEqual(model_count, {"bands": 1})
        self.assertEqual(perms_needed, {"band"})
        self.assertEqual(protected, [])

    def test_modeladmin_repr(self):
        ma = ModelAdmin(Band, self.site)
        self.assertEqual(
            repr(ma),
            "<ModelAdmin: model=Band site=AdminSite(name='admin')>",
        )


class ModelAdminPermissionTests(SimpleTestCase):
    class MockUser:
        def has_module_perms(self, app_label):
            return app_label == "modeladmin"

    class MockViewUser(MockUser):
        def has_perm(self, perm, obj=None):
            return perm == "modeladmin.view_band"

    class MockAddUser(MockUser):
        def has_perm(self, perm, obj=None):
            return perm == "modeladmin.add_band"

    class MockChangeUser(MockUser):
        def has_perm(self, perm, obj=None):
            return perm == "modeladmin.change_band"

    class MockDeleteUser(MockUser):
        def has_perm(self, perm, obj=None):
            return perm == "modeladmin.delete_band"

    def test_has_view_permission(self):
        
        ma = ModelAdmin(Band, AdminSite())
        request = MockRequest()
        request.user = self.MockViewUser()
        self.assertIs(ma.has_view_permission(request), True)
        request.user = self.MockAddUser()
        self.assertIs(ma.has_view_permission(request), False)
        request.user = self.MockChangeUser()
        self.assertIs(ma.has_view_permission(request), True)
        request.user = self.MockDeleteUser()
        self.assertIs(ma.has_view_permission(request), False)

    def test_has_add_permission(self):
        
        ma = ModelAdmin(Band, AdminSite())
        request = MockRequest()
        request.user = self.MockViewUser()
        self.assertFalse(ma.has_add_permission(request))
        request.user = self.MockAddUser()
        self.assertTrue(ma.has_add_permission(request))
        request.user = self.MockChangeUser()
        self.assertFalse(ma.has_add_permission(request))
        request.user = self.MockDeleteUser()
        self.assertFalse(ma.has_add_permission(request))

    def test_inline_has_add_permission_uses_obj(self):
        class ConcertInline(TabularInline):
            model = Concert

            def has_add_permission(self, request, obj):
                return bool(obj)

        class BandAdmin(ModelAdmin):
            inlines = [ConcertInline]

        ma = BandAdmin(Band, AdminSite())
        request = MockRequest()
        request.user = self.MockAddUser()
        self.assertEqual(ma.get_inline_instances(request), [])
        band = Band(name="The Doors", bio="", sign_date=date(1965, 1, 1))
        inline_instances = ma.get_inline_instances(request, band)
        self.assertEqual(len(inline_instances), 1)
        self.assertIsInstance(inline_instances[0], ConcertInline)

    def test_has_change_permission(self):
        
        ma = ModelAdmin(Band, AdminSite())
        request = MockRequest()
        request.user = self.MockViewUser()
        self.assertIs(ma.has_change_permission(request), False)
        request.user = self.MockAddUser()
        self.assertFalse(ma.has_change_permission(request))
        request.user = self.MockChangeUser()
        self.assertTrue(ma.has_change_permission(request))
        request.user = self.MockDeleteUser()
        self.assertFalse(ma.has_change_permission(request))

    def test_has_delete_permission(self):
        
        ma = ModelAdmin(Band, AdminSite())
        request = MockRequest()
        request.user = self.MockViewUser()
        self.assertIs(ma.has_delete_permission(request), False)
        request.user = self.MockAddUser()
        self.assertFalse(ma.has_delete_permission(request))
        request.user = self.MockChangeUser()
        self.assertFalse(ma.has_delete_permission(request))
        request.user = self.MockDeleteUser()
        self.assertTrue(ma.has_delete_permission(request))

    def test_has_module_permission(self):
        
        ma = ModelAdmin(Band, AdminSite())
        request = MockRequest()
        request.user = self.MockViewUser()
        self.assertIs(ma.has_module_permission(request), True)
        request.user = self.MockAddUser()
        self.assertTrue(ma.has_module_permission(request))
        request.user = self.MockChangeUser()
        self.assertTrue(ma.has_module_permission(request))
        request.user = self.MockDeleteUser()
        self.assertTrue(ma.has_module_permission(request))

        original_app_label = ma.opts.app_label
        ma.opts.app_label = "anotherapp"
        try:
            request.user = self.MockViewUser()
            self.assertIs(ma.has_module_permission(request), False)
            request.user = self.MockAddUser()
            self.assertFalse(ma.has_module_permission(request))
            request.user = self.MockChangeUser()
            self.assertFalse(ma.has_module_permission(request))
            request.user = self.MockDeleteUser()
            self.assertFalse(ma.has_module_permission(request))
        finally:
            ma.opts.app_label = original_app_label