1. from datetime import date
    
  2. 
    
  3. from django import forms
    
  4. from django.contrib.admin.models import ADDITION, CHANGE, DELETION, LogEntry
    
  5. from django.contrib.admin.options import (
    
  6.     HORIZONTAL,
    
  7.     VERTICAL,
    
  8.     ModelAdmin,
    
  9.     TabularInline,
    
  10.     get_content_type_for_model,
    
  11. )
    
  12. from django.contrib.admin.sites import AdminSite
    
  13. from django.contrib.admin.widgets import (
    
  14.     AdminDateWidget,
    
  15.     AdminRadioSelect,
    
  16.     AutocompleteSelect,
    
  17.     AutocompleteSelectMultiple,
    
  18. )
    
  19. from django.contrib.auth.models import User
    
  20. from django.db import models
    
  21. from django.forms.widgets import Select
    
  22. from django.test import SimpleTestCase, TestCase
    
  23. from django.test.utils import isolate_apps
    
  24. 
    
  25. from .models import Band, Concert, Song
    
  26. 
    
  27. 
    
  28. class MockRequest:
    
  29.     pass
    
  30. 
    
  31. 
    
  32. class MockSuperUser:
    
  33.     def has_perm(self, perm, obj=None):
    
  34.         return True
    
  35. 
    
  36. 
    
  37. request = MockRequest()
    
  38. request.user = MockSuperUser()
    
  39. 
    
  40. 
    
  41. class ModelAdminTests(TestCase):
    
  42.     @classmethod
    
  43.     def setUpTestData(cls):
    
  44.         cls.band = Band.objects.create(
    
  45.             name="The Doors",
    
  46.             bio="",
    
  47.             sign_date=date(1965, 1, 1),
    
  48.         )
    
  49. 
    
  50.     def setUp(self):
    
  51.         self.site = AdminSite()
    
  52. 
    
  53.     def test_modeladmin_str(self):
    
  54.         ma = ModelAdmin(Band, self.site)
    
  55.         self.assertEqual(str(ma), "modeladmin.ModelAdmin")
    
  56. 
    
  57.     def test_default_attributes(self):
    
  58.         ma = ModelAdmin(Band, self.site)
    
  59.         self.assertEqual(ma.actions, ())
    
  60.         self.assertEqual(ma.inlines, ())
    
  61. 
    
  62.     # form/fields/fieldsets interaction ##############################
    
  63. 
    
  64.     def test_default_fields(self):
    
  65.         ma = ModelAdmin(Band, self.site)
    
  66.         self.assertEqual(
    
  67.             list(ma.get_form(request).base_fields), ["name", "bio", "sign_date"]
    
  68.         )
    
  69.         self.assertEqual(list(ma.get_fields(request)), ["name", "bio", "sign_date"])
    
  70.         self.assertEqual(
    
  71.             list(ma.get_fields(request, self.band)), ["name", "bio", "sign_date"]
    
  72.         )
    
  73.         self.assertIsNone(ma.get_exclude(request, self.band))
    
  74. 
    
  75.     def test_default_fieldsets(self):
    
  76.         # fieldsets_add and fieldsets_change should return a special data structure that
    
  77.         # is used in the templates. They should generate the "right thing" whether we
    
  78.         # have specified a custom form, the fields argument, or nothing at all.
    
  79.         #
    
  80.         # Here's the default case. There are no custom form_add/form_change methods,
    
  81.         # no fields argument, and no fieldsets argument.
    
  82.         ma = ModelAdmin(Band, self.site)
    
  83.         self.assertEqual(
    
  84.             ma.get_fieldsets(request),
    
  85.             [(None, {"fields": ["name", "bio", "sign_date"]})],
    
  86.         )
    
  87.         self.assertEqual(
    
  88.             ma.get_fieldsets(request, self.band),
    
  89.             [(None, {"fields": ["name", "bio", "sign_date"]})],
    
  90.         )
    
  91. 
    
  92.     def test_get_fieldsets(self):
    
  93.         # get_fieldsets() is called when figuring out form fields (#18681).
    
  94.         class BandAdmin(ModelAdmin):
    
  95.             def get_fieldsets(self, request, obj=None):
    
  96.                 return [(None, {"fields": ["name", "bio"]})]
    
  97. 
    
  98.         ma = BandAdmin(Band, self.site)
    
  99.         form = ma.get_form(None)
    
  100.         self.assertEqual(form._meta.fields, ["name", "bio"])
    
  101. 
    
  102.         class InlineBandAdmin(TabularInline):
    
  103.             model = Concert
    
  104.             fk_name = "main_band"
    
  105.             can_delete = False
    
  106. 
    
  107.             def get_fieldsets(self, request, obj=None):
    
  108.                 return [(None, {"fields": ["day", "transport"]})]
    
  109. 
    
  110.         ma = InlineBandAdmin(Band, self.site)
    
  111.         form = ma.get_formset(None).form
    
  112.         self.assertEqual(form._meta.fields, ["day", "transport"])
    
  113. 
    
  114.     def test_lookup_allowed_allows_nonexistent_lookup(self):
    
  115.         """
    
  116.         A lookup_allowed allows a parameter whose field lookup doesn't exist.
    
  117.         (#21129).
    
  118.         """
    
  119. 
    
  120.         class BandAdmin(ModelAdmin):
    
  121.             fields = ["name"]
    
  122. 
    
  123.         ma = BandAdmin(Band, self.site)
    
  124.         self.assertTrue(ma.lookup_allowed("name__nonexistent", "test_value"))
    
  125. 
    
  126.     @isolate_apps("modeladmin")
    
  127.     def test_lookup_allowed_onetoone(self):
    
  128.         class Department(models.Model):
    
  129.             code = models.CharField(max_length=4, unique=True)
    
  130. 
    
  131.         class Employee(models.Model):
    
  132.             department = models.ForeignKey(Department, models.CASCADE, to_field="code")
    
  133. 
    
  134.         class EmployeeProfile(models.Model):
    
  135.             employee = models.OneToOneField(Employee, models.CASCADE)
    
  136. 
    
  137.         class EmployeeInfo(models.Model):
    
  138.             employee = models.OneToOneField(Employee, models.CASCADE)
    
  139.             description = models.CharField(max_length=100)
    
  140. 
    
  141.         class EmployeeProfileAdmin(ModelAdmin):
    
  142.             list_filter = [
    
  143.                 "employee__employeeinfo__description",
    
  144.                 "employee__department__code",
    
  145.             ]
    
  146. 
    
  147.         ma = EmployeeProfileAdmin(EmployeeProfile, self.site)
    
  148.         # Reverse OneToOneField
    
  149.         self.assertIs(
    
  150.             ma.lookup_allowed("employee__employeeinfo__description", "test_value"), True
    
  151.         )
    
  152.         # OneToOneField and ForeignKey
    
  153.         self.assertIs(
    
  154.             ma.lookup_allowed("employee__department__code", "test_value"), True
    
  155.         )
    
  156. 
    
  157.     def test_field_arguments(self):
    
  158.         # If fields is specified, fieldsets_add and fieldsets_change should
    
  159.         # just stick the fields into a formsets structure and return it.
    
  160.         class BandAdmin(ModelAdmin):
    
  161.             fields = ["name"]
    
  162. 
    
  163.         ma = BandAdmin(Band, self.site)
    
  164. 
    
  165.         self.assertEqual(list(ma.get_fields(request)), ["name"])
    
  166.         self.assertEqual(list(ma.get_fields(request, self.band)), ["name"])
    
  167.         self.assertEqual(ma.get_fieldsets(request), [(None, {"fields": ["name"]})])
    
  168.         self.assertEqual(
    
  169.             ma.get_fieldsets(request, self.band), [(None, {"fields": ["name"]})]
    
  170.         )
    
  171. 
    
  172.     def test_field_arguments_restricted_on_form(self):
    
  173.         # If fields or fieldsets is specified, it should exclude fields on the
    
  174.         # Form class to the fields specified. This may cause errors to be
    
  175.         # raised in the db layer if required model fields aren't in fields/
    
  176.         # fieldsets, but that's preferable to ghost errors where a field in the
    
  177.         # Form class isn't being displayed because it's not in fields/fieldsets.
    
  178. 
    
  179.         # Using `fields`.
    
  180.         class BandAdmin(ModelAdmin):
    
  181.             fields = ["name"]
    
  182. 
    
  183.         ma = BandAdmin(Band, self.site)
    
  184.         self.assertEqual(list(ma.get_form(request).base_fields), ["name"])
    
  185.         self.assertEqual(list(ma.get_form(request, self.band).base_fields), ["name"])
    
  186. 
    
  187.         # Using `fieldsets`.
    
  188.         class BandAdmin(ModelAdmin):
    
  189.             fieldsets = [(None, {"fields": ["name"]})]
    
  190. 
    
  191.         ma = BandAdmin(Band, self.site)
    
  192.         self.assertEqual(list(ma.get_form(request).base_fields), ["name"])
    
  193.         self.assertEqual(list(ma.get_form(request, self.band).base_fields), ["name"])
    
  194. 
    
  195.         # Using `exclude`.
    
  196.         class BandAdmin(ModelAdmin):
    
  197.             exclude = ["bio"]
    
  198. 
    
  199.         ma = BandAdmin(Band, self.site)
    
  200.         self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])
    
  201. 
    
  202.         # You can also pass a tuple to `exclude`.
    
  203.         class BandAdmin(ModelAdmin):
    
  204.             exclude = ("bio",)
    
  205. 
    
  206.         ma = BandAdmin(Band, self.site)
    
  207.         self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])
    
  208. 
    
  209.         # Using `fields` and `exclude`.
    
  210.         class BandAdmin(ModelAdmin):
    
  211.             fields = ["name", "bio"]
    
  212.             exclude = ["bio"]
    
  213. 
    
  214.         ma = BandAdmin(Band, self.site)
    
  215.         self.assertEqual(list(ma.get_form(request).base_fields), ["name"])
    
  216. 
    
  217.     def test_custom_form_meta_exclude_with_readonly(self):
    
  218.         """
    
  219.         The custom ModelForm's `Meta.exclude` is respected when used in
    
  220.         conjunction with `ModelAdmin.readonly_fields` and when no
    
  221.         `ModelAdmin.exclude` is defined (#14496).
    
  222.         """
    
  223. 
    
  224.         # With ModelAdmin
    
  225.         class AdminBandForm(forms.ModelForm):
    
  226.             class Meta:
    
  227.                 model = Band
    
  228.                 exclude = ["bio"]
    
  229. 
    
  230.         class BandAdmin(ModelAdmin):
    
  231.             readonly_fields = ["name"]
    
  232.             form = AdminBandForm
    
  233. 
    
  234.         ma = BandAdmin(Band, self.site)
    
  235.         self.assertEqual(list(ma.get_form(request).base_fields), ["sign_date"])
    
  236. 
    
  237.         # With InlineModelAdmin
    
  238.         class AdminConcertForm(forms.ModelForm):
    
  239.             class Meta:
    
  240.                 model = Concert
    
  241.                 exclude = ["day"]
    
  242. 
    
  243.         class ConcertInline(TabularInline):
    
  244.             readonly_fields = ["transport"]
    
  245.             form = AdminConcertForm
    
  246.             fk_name = "main_band"
    
  247.             model = Concert
    
  248. 
    
  249.         class BandAdmin(ModelAdmin):
    
  250.             inlines = [ConcertInline]
    
  251. 
    
  252.         ma = BandAdmin(Band, self.site)
    
  253.         self.assertEqual(
    
  254.             list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
    
  255.             ["main_band", "opening_band", "id", "DELETE"],
    
  256.         )
    
  257. 
    
  258.     def test_custom_formfield_override_readonly(self):
    
  259.         class AdminBandForm(forms.ModelForm):
    
  260.             name = forms.CharField()
    
  261. 
    
  262.             class Meta:
    
  263.                 exclude = ()
    
  264.                 model = Band
    
  265. 
    
  266.         class BandAdmin(ModelAdmin):
    
  267.             form = AdminBandForm
    
  268.             readonly_fields = ["name"]
    
  269. 
    
  270.         ma = BandAdmin(Band, self.site)
    
  271. 
    
  272.         # `name` shouldn't appear in base_fields because it's part of
    
  273.         # readonly_fields.
    
  274.         self.assertEqual(list(ma.get_form(request).base_fields), ["bio", "sign_date"])
    
  275.         # But it should appear in get_fields()/fieldsets() so it can be
    
  276.         # displayed as read-only.
    
  277.         self.assertEqual(list(ma.get_fields(request)), ["bio", "sign_date", "name"])
    
  278.         self.assertEqual(
    
  279.             list(ma.get_fieldsets(request)),
    
  280.             [(None, {"fields": ["bio", "sign_date", "name"]})],
    
  281.         )
    
  282. 
    
  283.     def test_custom_form_meta_exclude(self):
    
  284.         """
    
  285.         The custom ModelForm's `Meta.exclude` is overridden if
    
  286.         `ModelAdmin.exclude` or `InlineModelAdmin.exclude` are defined (#14496).
    
  287.         """
    
  288. 
    
  289.         # With ModelAdmin
    
  290.         class AdminBandForm(forms.ModelForm):
    
  291.             class Meta:
    
  292.                 model = Band
    
  293.                 exclude = ["bio"]
    
  294. 
    
  295.         class BandAdmin(ModelAdmin):
    
  296.             exclude = ["name"]
    
  297.             form = AdminBandForm
    
  298. 
    
  299.         ma = BandAdmin(Band, self.site)
    
  300.         self.assertEqual(list(ma.get_form(request).base_fields), ["bio", "sign_date"])
    
  301. 
    
  302.         # With InlineModelAdmin
    
  303.         class AdminConcertForm(forms.ModelForm):
    
  304.             class Meta:
    
  305.                 model = Concert
    
  306.                 exclude = ["day"]
    
  307. 
    
  308.         class ConcertInline(TabularInline):
    
  309.             exclude = ["transport"]
    
  310.             form = AdminConcertForm
    
  311.             fk_name = "main_band"
    
  312.             model = Concert
    
  313. 
    
  314.         class BandAdmin(ModelAdmin):
    
  315.             inlines = [ConcertInline]
    
  316. 
    
  317.         ma = BandAdmin(Band, self.site)
    
  318.         self.assertEqual(
    
  319.             list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
    
  320.             ["main_band", "opening_band", "day", "id", "DELETE"],
    
  321.         )
    
  322. 
    
  323.     def test_overriding_get_exclude(self):
    
  324.         class BandAdmin(ModelAdmin):
    
  325.             def get_exclude(self, request, obj=None):
    
  326.                 return ["name"]
    
  327. 
    
  328.         self.assertEqual(
    
  329.             list(BandAdmin(Band, self.site).get_form(request).base_fields),
    
  330.             ["bio", "sign_date"],
    
  331.         )
    
  332. 
    
  333.     def test_get_exclude_overrides_exclude(self):
    
  334.         class BandAdmin(ModelAdmin):
    
  335.             exclude = ["bio"]
    
  336. 
    
  337.             def get_exclude(self, request, obj=None):
    
  338.                 return ["name"]
    
  339. 
    
  340.         self.assertEqual(
    
  341.             list(BandAdmin(Band, self.site).get_form(request).base_fields),
    
  342.             ["bio", "sign_date"],
    
  343.         )
    
  344. 
    
  345.     def test_get_exclude_takes_obj(self):
    
  346.         class BandAdmin(ModelAdmin):
    
  347.             def get_exclude(self, request, obj=None):
    
  348.                 if obj:
    
  349.                     return ["sign_date"]
    
  350.                 return ["name"]
    
  351. 
    
  352.         self.assertEqual(
    
  353.             list(BandAdmin(Band, self.site).get_form(request, self.band).base_fields),
    
  354.             ["name", "bio"],
    
  355.         )
    
  356. 
    
  357.     def test_custom_form_validation(self):
    
  358.         # If a form is specified, it should use it allowing custom validation
    
  359.         # to work properly. This won't break any of the admin widgets or media.
    
  360.         class AdminBandForm(forms.ModelForm):
    
  361.             delete = forms.BooleanField()
    
  362. 
    
  363.         class BandAdmin(ModelAdmin):
    
  364.             form = AdminBandForm
    
  365. 
    
  366.         ma = BandAdmin(Band, self.site)
    
  367.         self.assertEqual(
    
  368.             list(ma.get_form(request).base_fields),
    
  369.             ["name", "bio", "sign_date", "delete"],
    
  370.         )
    
  371.         self.assertEqual(
    
  372.             type(ma.get_form(request).base_fields["sign_date"].widget), AdminDateWidget
    
  373.         )
    
  374. 
    
  375.     def test_form_exclude_kwarg_override(self):
    
  376.         """
    
  377.         The `exclude` kwarg passed to `ModelAdmin.get_form()` overrides all
    
  378.         other declarations (#8999).
    
  379.         """
    
  380. 
    
  381.         class AdminBandForm(forms.ModelForm):
    
  382.             class Meta:
    
  383.                 model = Band
    
  384.                 exclude = ["name"]
    
  385. 
    
  386.         class BandAdmin(ModelAdmin):
    
  387.             exclude = ["sign_date"]
    
  388.             form = AdminBandForm
    
  389. 
    
  390.             def get_form(self, request, obj=None, **kwargs):
    
  391.                 kwargs["exclude"] = ["bio"]
    
  392.                 return super().get_form(request, obj, **kwargs)
    
  393. 
    
  394.         ma = BandAdmin(Band, self.site)
    
  395.         self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])
    
  396. 
    
  397.     def test_formset_exclude_kwarg_override(self):
    
  398.         """
    
  399.         The `exclude` kwarg passed to `InlineModelAdmin.get_formset()`
    
  400.         overrides all other declarations (#8999).
    
  401.         """
    
  402. 
    
  403.         class AdminConcertForm(forms.ModelForm):
    
  404.             class Meta:
    
  405.                 model = Concert
    
  406.                 exclude = ["day"]
    
  407. 
    
  408.         class ConcertInline(TabularInline):
    
  409.             exclude = ["transport"]
    
  410.             form = AdminConcertForm
    
  411.             fk_name = "main_band"
    
  412.             model = Concert
    
  413. 
    
  414.             def get_formset(self, request, obj=None, **kwargs):
    
  415.                 kwargs["exclude"] = ["opening_band"]
    
  416.                 return super().get_formset(request, obj, **kwargs)
    
  417. 
    
  418.         class BandAdmin(ModelAdmin):
    
  419.             inlines = [ConcertInline]
    
  420. 
    
  421.         ma = BandAdmin(Band, self.site)
    
  422.         self.assertEqual(
    
  423.             list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
    
  424.             ["main_band", "day", "transport", "id", "DELETE"],
    
  425.         )
    
  426. 
    
  427.     def test_formset_overriding_get_exclude_with_form_fields(self):
    
  428.         class AdminConcertForm(forms.ModelForm):
    
  429.             class Meta:
    
  430.                 model = Concert
    
  431.                 fields = ["main_band", "opening_band", "day", "transport"]
    
  432. 
    
  433.         class ConcertInline(TabularInline):
    
  434.             form = AdminConcertForm
    
  435.             fk_name = "main_band"
    
  436.             model = Concert
    
  437. 
    
  438.             def get_exclude(self, request, obj=None):
    
  439.                 return ["opening_band"]
    
  440. 
    
  441.         class BandAdmin(ModelAdmin):
    
  442.             inlines = [ConcertInline]
    
  443. 
    
  444.         ma = BandAdmin(Band, self.site)
    
  445.         self.assertEqual(
    
  446.             list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
    
  447.             ["main_band", "day", "transport", "id", "DELETE"],
    
  448.         )
    
  449. 
    
  450.     def test_formset_overriding_get_exclude_with_form_exclude(self):
    
  451.         class AdminConcertForm(forms.ModelForm):
    
  452.             class Meta:
    
  453.                 model = Concert
    
  454.                 exclude = ["day"]
    
  455. 
    
  456.         class ConcertInline(TabularInline):
    
  457.             form = AdminConcertForm
    
  458.             fk_name = "main_band"
    
  459.             model = Concert
    
  460. 
    
  461.             def get_exclude(self, request, obj=None):
    
  462.                 return ["opening_band"]
    
  463. 
    
  464.         class BandAdmin(ModelAdmin):
    
  465.             inlines = [ConcertInline]
    
  466. 
    
  467.         ma = BandAdmin(Band, self.site)
    
  468.         self.assertEqual(
    
  469.             list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
    
  470.             ["main_band", "day", "transport", "id", "DELETE"],
    
  471.         )
    
  472. 
    
  473.     def test_raw_id_fields_widget_override(self):
    
  474.         """
    
  475.         The autocomplete_fields, raw_id_fields, and radio_fields widgets may
    
  476.         overridden by specifying a widget in get_formset().
    
  477.         """
    
  478. 
    
  479.         class ConcertInline(TabularInline):
    
  480.             model = Concert
    
  481.             fk_name = "main_band"
    
  482.             raw_id_fields = ("opening_band",)
    
  483. 
    
  484.             def get_formset(self, request, obj=None, **kwargs):
    
  485.                 kwargs["widgets"] = {"opening_band": Select}
    
  486.                 return super().get_formset(request, obj, **kwargs)
    
  487. 
    
  488.         class BandAdmin(ModelAdmin):
    
  489.             inlines = [ConcertInline]
    
  490. 
    
  491.         ma = BandAdmin(Band, self.site)
    
  492.         band_widget = (
    
  493.             list(ma.get_formsets_with_inlines(request))[0][0]()
    
  494.             .forms[0]
    
  495.             .fields["opening_band"]
    
  496.             .widget
    
  497.         )
    
  498.         # Without the override this would be ForeignKeyRawIdWidget.
    
  499.         self.assertIsInstance(band_widget, Select)
    
  500. 
    
  501.     def test_queryset_override(self):
    
  502.         # If the queryset of a ModelChoiceField in a custom form is overridden,
    
  503.         # RelatedFieldWidgetWrapper doesn't mess that up.
    
  504.         band2 = Band.objects.create(
    
  505.             name="The Beatles", bio="", sign_date=date(1962, 1, 1)
    
  506.         )
    
  507. 
    
  508.         ma = ModelAdmin(Concert, self.site)
    
  509.         form = ma.get_form(request)()
    
  510. 
    
  511.         self.assertHTMLEqual(
    
  512.             str(form["main_band"]),
    
  513.             '<div class="related-widget-wrapper" data-model-ref="band">'
    
  514.             '<select name="main_band" id="id_main_band" required>'
    
  515.             '<option value="" selected>---------</option>'
    
  516.             '<option value="%d">The Beatles</option>'
    
  517.             '<option value="%d">The Doors</option>'
    
  518.             "</select></div>" % (band2.id, self.band.id),
    
  519.         )
    
  520. 
    
  521.         class AdminConcertForm(forms.ModelForm):
    
  522.             def __init__(self, *args, **kwargs):
    
  523.                 super().__init__(*args, **kwargs)
    
  524.                 self.fields["main_band"].queryset = Band.objects.filter(
    
  525.                     name="The Doors"
    
  526.                 )
    
  527. 
    
  528.         class ConcertAdminWithForm(ModelAdmin):
    
  529.             form = AdminConcertForm
    
  530. 
    
  531.         ma = ConcertAdminWithForm(Concert, self.site)
    
  532.         form = ma.get_form(request)()
    
  533. 
    
  534.         self.assertHTMLEqual(
    
  535.             str(form["main_band"]),
    
  536.             '<div class="related-widget-wrapper" data-model-ref="band">'
    
  537.             '<select name="main_band" id="id_main_band" required>'
    
  538.             '<option value="" selected>---------</option>'
    
  539.             '<option value="%d">The Doors</option>'
    
  540.             "</select></div>" % self.band.id,
    
  541.         )
    
  542. 
    
  543.     def test_regression_for_ticket_15820(self):
    
  544.         """
    
  545.         `obj` is passed from `InlineModelAdmin.get_fieldsets()` to
    
  546.         `InlineModelAdmin.get_formset()`.
    
  547.         """
    
  548. 
    
  549.         class CustomConcertForm(forms.ModelForm):
    
  550.             class Meta:
    
  551.                 model = Concert
    
  552.                 fields = ["day"]
    
  553. 
    
  554.         class ConcertInline(TabularInline):
    
  555.             model = Concert
    
  556.             fk_name = "main_band"
    
  557. 
    
  558.             def get_formset(self, request, obj=None, **kwargs):
    
  559.                 if obj:
    
  560.                     kwargs["form"] = CustomConcertForm
    
  561.                 return super().get_formset(request, obj, **kwargs)
    
  562. 
    
  563.         class BandAdmin(ModelAdmin):
    
  564.             inlines = [ConcertInline]
    
  565. 
    
  566.         Concert.objects.create(main_band=self.band, opening_band=self.band, day=1)
    
  567.         ma = BandAdmin(Band, self.site)
    
  568.         inline_instances = ma.get_inline_instances(request)
    
  569.         fieldsets = list(inline_instances[0].get_fieldsets(request))
    
  570.         self.assertEqual(
    
  571.             fieldsets[0][1]["fields"], ["main_band", "opening_band", "day", "transport"]
    
  572.         )
    
  573.         fieldsets = list(
    
  574.             inline_instances[0].get_fieldsets(request, inline_instances[0].model)
    
  575.         )
    
  576.         self.assertEqual(fieldsets[0][1]["fields"], ["day"])
    
  577. 
    
  578.     # radio_fields behavior ###########################################
    
  579. 
    
  580.     def test_default_foreign_key_widget(self):
    
  581.         # First, without any radio_fields specified, the widgets for ForeignKey
    
  582.         # and fields with choices specified ought to be a basic Select widget.
    
  583.         # ForeignKey widgets in the admin are wrapped with RelatedFieldWidgetWrapper so
    
  584.         # they need to be handled properly when type checking. For Select fields, all of
    
  585.         # the choices lists have a first entry of dashes.
    
  586.         cma = ModelAdmin(Concert, self.site)
    
  587.         cmafa = cma.get_form(request)
    
  588. 
    
  589.         self.assertEqual(type(cmafa.base_fields["main_band"].widget.widget), Select)
    
  590.         self.assertEqual(
    
  591.             list(cmafa.base_fields["main_band"].widget.choices),
    
  592.             [("", "---------"), (self.band.id, "The Doors")],
    
  593.         )
    
  594. 
    
  595.         self.assertEqual(type(cmafa.base_fields["opening_band"].widget.widget), Select)
    
  596.         self.assertEqual(
    
  597.             list(cmafa.base_fields["opening_band"].widget.choices),
    
  598.             [("", "---------"), (self.band.id, "The Doors")],
    
  599.         )
    
  600.         self.assertEqual(type(cmafa.base_fields["day"].widget), Select)
    
  601.         self.assertEqual(
    
  602.             list(cmafa.base_fields["day"].widget.choices),
    
  603.             [("", "---------"), (1, "Fri"), (2, "Sat")],
    
  604.         )
    
  605.         self.assertEqual(type(cmafa.base_fields["transport"].widget), Select)
    
  606.         self.assertEqual(
    
  607.             list(cmafa.base_fields["transport"].widget.choices),
    
  608.             [("", "---------"), (1, "Plane"), (2, "Train"), (3, "Bus")],
    
  609.         )
    
  610. 
    
  611.     def test_foreign_key_as_radio_field(self):
    
  612.         # Now specify all the fields as radio_fields.  Widgets should now be
    
  613.         # RadioSelect, and the choices list should have a first entry of 'None' if
    
  614.         # blank=True for the model field.  Finally, the widget should have the
    
  615.         # 'radiolist' attr, and 'inline' as well if the field is specified HORIZONTAL.
    
  616.         class ConcertAdmin(ModelAdmin):
    
  617.             radio_fields = {
    
  618.                 "main_band": HORIZONTAL,
    
  619.                 "opening_band": VERTICAL,
    
  620.                 "day": VERTICAL,
    
  621.                 "transport": HORIZONTAL,
    
  622.             }
    
  623. 
    
  624.         cma = ConcertAdmin(Concert, self.site)
    
  625.         cmafa = cma.get_form(request)
    
  626. 
    
  627.         self.assertEqual(
    
  628.             type(cmafa.base_fields["main_band"].widget.widget), AdminRadioSelect
    
  629.         )
    
  630.         self.assertEqual(
    
  631.             cmafa.base_fields["main_band"].widget.attrs, {"class": "radiolist inline"}
    
  632.         )
    
  633.         self.assertEqual(
    
  634.             list(cmafa.base_fields["main_band"].widget.choices),
    
  635.             [(self.band.id, "The Doors")],
    
  636.         )
    
  637. 
    
  638.         self.assertEqual(
    
  639.             type(cmafa.base_fields["opening_band"].widget.widget), AdminRadioSelect
    
  640.         )
    
  641.         self.assertEqual(
    
  642.             cmafa.base_fields["opening_band"].widget.attrs, {"class": "radiolist"}
    
  643.         )
    
  644.         self.assertEqual(
    
  645.             list(cmafa.base_fields["opening_band"].widget.choices),
    
  646.             [("", "None"), (self.band.id, "The Doors")],
    
  647.         )
    
  648.         self.assertEqual(type(cmafa.base_fields["day"].widget), AdminRadioSelect)
    
  649.         self.assertEqual(cmafa.base_fields["day"].widget.attrs, {"class": "radiolist"})
    
  650.         self.assertEqual(
    
  651.             list(cmafa.base_fields["day"].widget.choices), [(1, "Fri"), (2, "Sat")]
    
  652.         )
    
  653. 
    
  654.         self.assertEqual(type(cmafa.base_fields["transport"].widget), AdminRadioSelect)
    
  655.         self.assertEqual(
    
  656.             cmafa.base_fields["transport"].widget.attrs, {"class": "radiolist inline"}
    
  657.         )
    
  658.         self.assertEqual(
    
  659.             list(cmafa.base_fields["transport"].widget.choices),
    
  660.             [("", "None"), (1, "Plane"), (2, "Train"), (3, "Bus")],
    
  661.         )
    
  662. 
    
  663.         class AdminConcertForm(forms.ModelForm):
    
  664.             class Meta:
    
  665.                 model = Concert
    
  666.                 exclude = ("transport",)
    
  667. 
    
  668.         class ConcertAdmin(ModelAdmin):
    
  669.             form = AdminConcertForm
    
  670. 
    
  671.         ma = ConcertAdmin(Concert, self.site)
    
  672.         self.assertEqual(
    
  673.             list(ma.get_form(request).base_fields), ["main_band", "opening_band", "day"]
    
  674.         )
    
  675. 
    
  676.         class AdminConcertForm(forms.ModelForm):
    
  677.             extra = forms.CharField()
    
  678. 
    
  679.             class Meta:
    
  680.                 model = Concert
    
  681.                 fields = ["extra", "transport"]
    
  682. 
    
  683.         class ConcertAdmin(ModelAdmin):
    
  684.             form = AdminConcertForm
    
  685. 
    
  686.         ma = ConcertAdmin(Concert, self.site)
    
  687.         self.assertEqual(list(ma.get_form(request).base_fields), ["extra", "transport"])
    
  688. 
    
  689.         class ConcertInline(TabularInline):
    
  690.             form = AdminConcertForm
    
  691.             model = Concert
    
  692.             fk_name = "main_band"
    
  693.             can_delete = True
    
  694. 
    
  695.         class BandAdmin(ModelAdmin):
    
  696.             inlines = [ConcertInline]
    
  697. 
    
  698.         ma = BandAdmin(Band, self.site)
    
  699.         self.assertEqual(
    
  700.             list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
    
  701.             ["extra", "transport", "id", "DELETE", "main_band"],
    
  702.         )
    
  703. 
    
  704.     def test_log_actions(self):
    
  705.         ma = ModelAdmin(Band, self.site)
    
  706.         mock_request = MockRequest()
    
  707.         mock_request.user = User.objects.create(username="bill")
    
  708.         content_type = get_content_type_for_model(self.band)
    
  709.         tests = (
    
  710.             (ma.log_addition, ADDITION, {"added": {}}),
    
  711.             (ma.log_change, CHANGE, {"changed": {"fields": ["name", "bio"]}}),
    
  712.             (ma.log_deletion, DELETION, str(self.band)),
    
  713.         )
    
  714.         for method, flag, message in tests:
    
  715.             with self.subTest(name=method.__name__):
    
  716.                 created = method(mock_request, self.band, message)
    
  717.                 fetched = LogEntry.objects.filter(action_flag=flag).latest("id")
    
  718.                 self.assertEqual(created, fetched)
    
  719.                 self.assertEqual(fetched.action_flag, flag)
    
  720.                 self.assertEqual(fetched.content_type, content_type)
    
  721.                 self.assertEqual(fetched.object_id, str(self.band.pk))
    
  722.                 self.assertEqual(fetched.user, mock_request.user)
    
  723.                 if flag == DELETION:
    
  724.                     self.assertEqual(fetched.change_message, "")
    
  725.                     self.assertEqual(fetched.object_repr, message)
    
  726.                 else:
    
  727.                     self.assertEqual(fetched.change_message, str(message))
    
  728.                     self.assertEqual(fetched.object_repr, str(self.band))
    
  729. 
    
  730.     def test_get_autocomplete_fields(self):
    
  731.         class NameAdmin(ModelAdmin):
    
  732.             search_fields = ["name"]
    
  733. 
    
  734.         class SongAdmin(ModelAdmin):
    
  735.             autocomplete_fields = ["featuring"]
    
  736.             fields = ["featuring", "band"]
    
  737. 
    
  738.         class OtherSongAdmin(SongAdmin):
    
  739.             def get_autocomplete_fields(self, request):
    
  740.                 return ["band"]
    
  741. 
    
  742.         self.site.register(Band, NameAdmin)
    
  743.         try:
    
  744.             # Uses autocomplete_fields if not overridden.
    
  745.             model_admin = SongAdmin(Song, self.site)
    
  746.             form = model_admin.get_form(request)()
    
  747.             self.assertIsInstance(
    
  748.                 form.fields["featuring"].widget.widget, AutocompleteSelectMultiple
    
  749.             )
    
  750.             # Uses overridden get_autocomplete_fields
    
  751.             model_admin = OtherSongAdmin(Song, self.site)
    
  752.             form = model_admin.get_form(request)()
    
  753.             self.assertIsInstance(form.fields["band"].widget.widget, AutocompleteSelect)
    
  754.         finally:
    
  755.             self.site.unregister(Band)
    
  756. 
    
  757.     def test_get_deleted_objects(self):
    
  758.         mock_request = MockRequest()
    
  759.         mock_request.user = User.objects.create_superuser(
    
  760.             username="bob", email="[email protected]", password="test"
    
  761.         )
    
  762.         self.site.register(Band, ModelAdmin)
    
  763.         ma = self.site._registry[Band]
    
  764.         (
    
  765.             deletable_objects,
    
  766.             model_count,
    
  767.             perms_needed,
    
  768.             protected,
    
  769.         ) = ma.get_deleted_objects([self.band], request)
    
  770.         self.assertEqual(deletable_objects, ["Band: The Doors"])
    
  771.         self.assertEqual(model_count, {"bands": 1})
    
  772.         self.assertEqual(perms_needed, set())
    
  773.         self.assertEqual(protected, [])
    
  774. 
    
  775.     def test_get_deleted_objects_with_custom_has_delete_permission(self):
    
  776.         """
    
  777.         ModelAdmin.get_deleted_objects() uses ModelAdmin.has_delete_permission()
    
  778.         for permissions checking.
    
  779.         """
    
  780.         mock_request = MockRequest()
    
  781.         mock_request.user = User.objects.create_superuser(
    
  782.             username="bob", email="[email protected]", password="test"
    
  783.         )
    
  784. 
    
  785.         class TestModelAdmin(ModelAdmin):
    
  786.             def has_delete_permission(self, request, obj=None):
    
  787.                 return False
    
  788. 
    
  789.         self.site.register(Band, TestModelAdmin)
    
  790.         ma = self.site._registry[Band]
    
  791.         (
    
  792.             deletable_objects,
    
  793.             model_count,
    
  794.             perms_needed,
    
  795.             protected,
    
  796.         ) = ma.get_deleted_objects([self.band], request)
    
  797.         self.assertEqual(deletable_objects, ["Band: The Doors"])
    
  798.         self.assertEqual(model_count, {"bands": 1})
    
  799.         self.assertEqual(perms_needed, {"band"})
    
  800.         self.assertEqual(protected, [])
    
  801. 
    
  802.     def test_modeladmin_repr(self):
    
  803.         ma = ModelAdmin(Band, self.site)
    
  804.         self.assertEqual(
    
  805.             repr(ma),
    
  806.             "<ModelAdmin: model=Band site=AdminSite(name='admin')>",
    
  807.         )
    
  808. 
    
  809. 
    
  810. class ModelAdminPermissionTests(SimpleTestCase):
    
  811.     class MockUser:
    
  812.         def has_module_perms(self, app_label):
    
  813.             return app_label == "modeladmin"
    
  814. 
    
  815.     class MockViewUser(MockUser):
    
  816.         def has_perm(self, perm, obj=None):
    
  817.             return perm == "modeladmin.view_band"
    
  818. 
    
  819.     class MockAddUser(MockUser):
    
  820.         def has_perm(self, perm, obj=None):
    
  821.             return perm == "modeladmin.add_band"
    
  822. 
    
  823.     class MockChangeUser(MockUser):
    
  824.         def has_perm(self, perm, obj=None):
    
  825.             return perm == "modeladmin.change_band"
    
  826. 
    
  827.     class MockDeleteUser(MockUser):
    
  828.         def has_perm(self, perm, obj=None):
    
  829.             return perm == "modeladmin.delete_band"
    
  830. 
    
  831.     def test_has_view_permission(self):
    
  832.         """
    
  833.         has_view_permission() returns True for users who can view objects and
    
  834.         False for users who can't.
    
  835.         """
    
  836.         ma = ModelAdmin(Band, AdminSite())
    
  837.         request = MockRequest()
    
  838.         request.user = self.MockViewUser()
    
  839.         self.assertIs(ma.has_view_permission(request), True)
    
  840.         request.user = self.MockAddUser()
    
  841.         self.assertIs(ma.has_view_permission(request), False)
    
  842.         request.user = self.MockChangeUser()
    
  843.         self.assertIs(ma.has_view_permission(request), True)
    
  844.         request.user = self.MockDeleteUser()
    
  845.         self.assertIs(ma.has_view_permission(request), False)
    
  846. 
    
  847.     def test_has_add_permission(self):
    
  848.         """
    
  849.         has_add_permission returns True for users who can add objects and
    
  850.         False for users who can't.
    
  851.         """
    
  852.         ma = ModelAdmin(Band, AdminSite())
    
  853.         request = MockRequest()
    
  854.         request.user = self.MockViewUser()
    
  855.         self.assertFalse(ma.has_add_permission(request))
    
  856.         request.user = self.MockAddUser()
    
  857.         self.assertTrue(ma.has_add_permission(request))
    
  858.         request.user = self.MockChangeUser()
    
  859.         self.assertFalse(ma.has_add_permission(request))
    
  860.         request.user = self.MockDeleteUser()
    
  861.         self.assertFalse(ma.has_add_permission(request))
    
  862. 
    
  863.     def test_inline_has_add_permission_uses_obj(self):
    
  864.         class ConcertInline(TabularInline):
    
  865.             model = Concert
    
  866. 
    
  867.             def has_add_permission(self, request, obj):
    
  868.                 return bool(obj)
    
  869. 
    
  870.         class BandAdmin(ModelAdmin):
    
  871.             inlines = [ConcertInline]
    
  872. 
    
  873.         ma = BandAdmin(Band, AdminSite())
    
  874.         request = MockRequest()
    
  875.         request.user = self.MockAddUser()
    
  876.         self.assertEqual(ma.get_inline_instances(request), [])
    
  877.         band = Band(name="The Doors", bio="", sign_date=date(1965, 1, 1))
    
  878.         inline_instances = ma.get_inline_instances(request, band)
    
  879.         self.assertEqual(len(inline_instances), 1)
    
  880.         self.assertIsInstance(inline_instances[0], ConcertInline)
    
  881. 
    
  882.     def test_has_change_permission(self):
    
  883.         """
    
  884.         has_change_permission returns True for users who can edit objects and
    
  885.         False for users who can't.
    
  886.         """
    
  887.         ma = ModelAdmin(Band, AdminSite())
    
  888.         request = MockRequest()
    
  889.         request.user = self.MockViewUser()
    
  890.         self.assertIs(ma.has_change_permission(request), False)
    
  891.         request.user = self.MockAddUser()
    
  892.         self.assertFalse(ma.has_change_permission(request))
    
  893.         request.user = self.MockChangeUser()
    
  894.         self.assertTrue(ma.has_change_permission(request))
    
  895.         request.user = self.MockDeleteUser()
    
  896.         self.assertFalse(ma.has_change_permission(request))
    
  897. 
    
  898.     def test_has_delete_permission(self):
    
  899.         """
    
  900.         has_delete_permission returns True for users who can delete objects and
    
  901.         False for users who can't.
    
  902.         """
    
  903.         ma = ModelAdmin(Band, AdminSite())
    
  904.         request = MockRequest()
    
  905.         request.user = self.MockViewUser()
    
  906.         self.assertIs(ma.has_delete_permission(request), False)
    
  907.         request.user = self.MockAddUser()
    
  908.         self.assertFalse(ma.has_delete_permission(request))
    
  909.         request.user = self.MockChangeUser()
    
  910.         self.assertFalse(ma.has_delete_permission(request))
    
  911.         request.user = self.MockDeleteUser()
    
  912.         self.assertTrue(ma.has_delete_permission(request))
    
  913. 
    
  914.     def test_has_module_permission(self):
    
  915.         """
    
  916.         as_module_permission returns True for users who have any permission
    
  917.         for the module and False for users who don't.
    
  918.         """
    
  919.         ma = ModelAdmin(Band, AdminSite())
    
  920.         request = MockRequest()
    
  921.         request.user = self.MockViewUser()
    
  922.         self.assertIs(ma.has_module_permission(request), True)
    
  923.         request.user = self.MockAddUser()
    
  924.         self.assertTrue(ma.has_module_permission(request))
    
  925.         request.user = self.MockChangeUser()
    
  926.         self.assertTrue(ma.has_module_permission(request))
    
  927.         request.user = self.MockDeleteUser()
    
  928.         self.assertTrue(ma.has_module_permission(request))
    
  929. 
    
  930.         original_app_label = ma.opts.app_label
    
  931.         ma.opts.app_label = "anotherapp"
    
  932.         try:
    
  933.             request.user = self.MockViewUser()
    
  934.             self.assertIs(ma.has_module_permission(request), False)
    
  935.             request.user = self.MockAddUser()
    
  936.             self.assertFalse(ma.has_module_permission(request))
    
  937.             request.user = self.MockChangeUser()
    
  938.             self.assertFalse(ma.has_module_permission(request))
    
  939.             request.user = self.MockDeleteUser()
    
  940.             self.assertFalse(ma.has_module_permission(request))
    
  941.         finally:
    
  942.             ma.opts.app_label = original_app_label