mirror of
https://github.com/django/django.git
synced 2024-12-23 01:25:58 +00:00
8db593de05
Regression in f80669d2f5
.
Thanks Sarah Boyce for the review.
1205 lines
43 KiB
Python
1205 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 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):
|
|
"""
|
|
`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"}
|
|
)
|
|
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)
|
|
|
|
# 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):
|
|
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))
|
|
|
|
# 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):
|
|
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:
|
|
# 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
|