1
0
mirror of https://github.com/django/django.git synced 2024-12-22 17:16:24 +00:00
django/tests/modeladmin/tests.py
Simon Charette 57307bbc7d Fixed #35666 -- Documented stacklevel usage and testing, and adjusted test suite accordingly.
Over the years we've had multiple instances of hit and misses when
emitting warnings: either setting the wrong stacklevel or not setting
it at all.

This work adds assertions for the existing warnings that were declaring
the correct stacklevel, but were lacking tests for it.
2024-08-28 11:44:05 -03:00

1211 lines
43 KiB
Python

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, ())
# form/fields/fieldsets interaction ##############################
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):
# 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.
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):
# get_fieldsets() is called when figuring out form fields (#18681).
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):
"""
A lookup_allowed allows a parameter whose field lookup doesn't exist.
(#21129).
"""
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)
# Reverse OneToOneField
self.assertIs(
ma.lookup_allowed(
"employee__employeeinfo__description", "test_value", request
),
True,
)
# OneToOneField and ForeignKey
self.assertIs(
ma.lookup_allowed("employee__department__code", "test_value", request),
True,
)
@isolate_apps("modeladmin")
def test_lookup_allowed_for_local_fk_fields(self):
class Country(models.Model):
pass
class Place(models.Model):
country = models.ForeignKey(Country, models.CASCADE)
class PlaceAdmin(ModelAdmin):
pass
ma = PlaceAdmin(Place, self.site)
cases = [
("country", "1"),
("country__exact", "1"),
("country__id", "1"),
("country__id__exact", "1"),
("country__isnull", True),
("country__isnull", False),
("country__id__isnull", False),
]
for lookup, lookup_value in cases:
with self.subTest(lookup=lookup):
self.assertIs(ma.lookup_allowed(lookup, lookup_value, request), True)
@isolate_apps("modeladmin")
def test_lookup_allowed_non_autofield_primary_key(self):
class Country(models.Model):
id = models.CharField(max_length=2, primary_key=True)
class Place(models.Model):
country = models.ForeignKey(Country, models.CASCADE)
class PlaceAdmin(ModelAdmin):
list_filter = ["country"]
ma = PlaceAdmin(Place, self.site)
self.assertIs(ma.lookup_allowed("country__id__exact", "DE", 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):
# If fields is specified, fieldsets_add and fieldsets_change should
# just stick the fields into a formsets structure and return it.
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):
# 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`.
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"])
# Using `fieldsets`.
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"])
# Using `exclude`.
class BandAdmin(ModelAdmin):
exclude = ["bio"]
ma = BandAdmin(Band, self.site)
self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])
# You can also pass a tuple to `exclude`.
class BandAdmin(ModelAdmin):
exclude = ("bio",)
ma = BandAdmin(Band, self.site)
self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])
# Using `fields` and `exclude`.
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):
"""
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
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"])
# With InlineModelAdmin
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)
# `name` shouldn't appear in base_fields because it's part of
# readonly_fields.
self.assertEqual(list(ma.get_form(request).base_fields), ["bio", "sign_date"])
# But it should appear in get_fields()/fieldsets() so it can be
# displayed as read-only.
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):
"""
The custom ModelForm's `Meta.exclude` is overridden if
`ModelAdmin.exclude` or `InlineModelAdmin.exclude` are defined (#14496).
"""
# With ModelAdmin
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"])
# With InlineModelAdmin
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):
# 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.
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):
"""
The `exclude` kwarg passed to `ModelAdmin.get_form()` overrides all
other declarations (#8999).
"""
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):
"""
The `exclude` kwarg passed to `InlineModelAdmin.get_formset()`
overrides all other declarations (#8999).
"""
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):
"""
The autocomplete_fields, raw_id_fields, and radio_fields widgets may
overridden by specifying a widget in get_formset().
"""
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
)
# Without the override this would be ForeignKeyRawIdWidget.
self.assertIsInstance(band_widget, Select)
def test_queryset_override(self):
# If the queryset of a ModelChoiceField in a custom form is overridden,
# RelatedFieldWidgetWrapper doesn't mess that up.
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 data-context="available-source" '
'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 data-context="available-source" '
'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):
"""
`obj` is passed from `InlineModelAdmin.get_fieldsets()` to
`InlineModelAdmin.get_formset()`.
"""
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"])
# radio_fields behavior ###########################################
def test_default_foreign_key_widget(self):
# 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.
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):
# 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.
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", "data-context": "available-source"},
)
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", "data-context": "available-source"},
)
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)
# RemovedInDjango60Warning.
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) as ctx:
created = ma.log_deletion(mock_request, self.band, str(self.band))
self.assertEqual(ctx.filename, __file__)
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))
# RemovedInDjango60Warning.
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) as ctx:
ima.log_deletions(mock_request, queryset)
self.assertEqual(ctx.filename, __file__)
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:
# Uses autocomplete_fields if not overridden.
model_admin = SongAdmin(Song, self.site)
form = model_admin.get_form(request)()
self.assertIsInstance(
form.fields["featuring"].widget.widget, AutocompleteSelectMultiple
)
# Uses overridden get_autocomplete_fields
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):
"""
ModelAdmin.get_deleted_objects() uses ModelAdmin.has_delete_permission()
for permissions checking.
"""
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):
"""
has_view_permission() returns True for users who can view objects and
False for users who can't.
"""
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):
"""
has_add_permission returns True for users who can add objects and
False for users who can't.
"""
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):
"""
has_change_permission returns True for users who can edit objects and
False for users who can't.
"""
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):
"""
has_delete_permission returns True for users who can delete objects and
False for users who can't.
"""
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):
"""
as_module_permission returns True for users who have any permission
for the module and False for users who don't.
"""
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