import gettext
import os
import re
from datetime import datetime, timedelta
from importlib import import_module
try:
import zoneinfo
except ImportError:
from backports import zoneinfo
from django import forms
from django.conf import settings
from django.contrib import admin
from django.contrib.admin import widgets
from django.contrib.admin.tests import AdminSeleniumTestCase
from django.contrib.auth.models import User
from django.core.files.storage import default_storage
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import (
CharField,
DateField,
DateTimeField,
ManyToManyField,
UUIDField,
)
from django.test import SimpleTestCase, TestCase, override_settings
from django.urls import reverse
from django.utils import translation
from .models import (
Advisor,
Album,
Band,
Bee,
Car,
Company,
Event,
Honeycomb,
Individual,
Inventory,
Member,
MyFileField,
Profile,
ReleaseEvent,
School,
Student,
UnsafeLimitChoicesTo,
VideoStream,
)
from .widgetadmin import site as widget_admin_site
class TestDataMixin:
@classmethod
def setUpTestData(cls):
cls.superuser = User.objects.create_superuser(
username="super", password="secret", email=None
)
cls.u2 = User.objects.create_user(username="testser", password="secret")
Car.objects.create(owner=cls.superuser, make="Volkswagen", model="Passat")
Car.objects.create(owner=cls.u2, make="BMW", model="M3")
class AdminFormfieldForDBFieldTests(SimpleTestCase):
"""
Tests for correct behavior of ModelAdmin.formfield_for_dbfield
"""
def assertFormfield(self, model, fieldname, widgetclass, **admin_overrides):
"""
Helper to call formfield_for_dbfield for a given model and field name
and verify that the returned formfield is appropriate.
"""
# Override any settings on the model admin
class MyModelAdmin(admin.ModelAdmin):
pass
for k in admin_overrides:
setattr(MyModelAdmin, k, admin_overrides[k])
# Construct the admin, and ask it for a formfield
ma = MyModelAdmin(model, admin.site)
ff = ma.formfield_for_dbfield(model._meta.get_field(fieldname), request=None)
# "unwrap" the widget wrapper, if needed
if isinstance(ff.widget, widgets.RelatedFieldWidgetWrapper):
widget = ff.widget.widget
else:
widget = ff.widget
self.assertIsInstance(widget, widgetclass)
# Return the formfield so that other tests can continue
return ff
def test_DateField(self):
self.assertFormfield(Event, "start_date", widgets.AdminDateWidget)
def test_DateTimeField(self):
self.assertFormfield(Member, "birthdate", widgets.AdminSplitDateTime)
def test_TimeField(self):
self.assertFormfield(Event, "start_time", widgets.AdminTimeWidget)
def test_TextField(self):
self.assertFormfield(Event, "description", widgets.AdminTextareaWidget)
def test_URLField(self):
self.assertFormfield(Event, "link", widgets.AdminURLFieldWidget)
def test_IntegerField(self):
self.assertFormfield(Event, "min_age", widgets.AdminIntegerFieldWidget)
def test_CharField(self):
self.assertFormfield(Member, "name", widgets.AdminTextInputWidget)
def test_EmailField(self):
self.assertFormfield(Member, "email", widgets.AdminEmailInputWidget)
def test_FileField(self):
self.assertFormfield(Album, "cover_art", widgets.AdminFileWidget)
def test_ForeignKey(self):
self.assertFormfield(Event, "main_band", forms.Select)
def test_raw_id_ForeignKey(self):
self.assertFormfield(
Event,
"main_band",
widgets.ForeignKeyRawIdWidget,
raw_id_fields=["main_band"],
)
def test_radio_fields_ForeignKey(self):
ff = self.assertFormfield(
Event,
"main_band",
widgets.AdminRadioSelect,
radio_fields={"main_band": admin.VERTICAL},
)
self.assertIsNone(ff.empty_label)
def test_many_to_many(self):
self.assertFormfield(Band, "members", forms.SelectMultiple)
def test_raw_id_many_to_many(self):
self.assertFormfield(
Band, "members", widgets.ManyToManyRawIdWidget, raw_id_fields=["members"]
)
def test_filtered_many_to_many(self):
self.assertFormfield(
Band, "members", widgets.FilteredSelectMultiple, filter_vertical=["members"]
)
def test_formfield_overrides(self):
self.assertFormfield(
Event,
"start_date",
forms.TextInput,
formfield_overrides={DateField: {"widget": forms.TextInput}},
)
def test_formfield_overrides_widget_instances(self):
"""
Widget instances in formfield_overrides are not shared between
different fields. (#19423)
"""
class BandAdmin(admin.ModelAdmin):
formfield_overrides = {
CharField: {"widget": forms.TextInput(attrs={"size": "10"})}
}
ma = BandAdmin(Band, admin.site)
f1 = ma.formfield_for_dbfield(Band._meta.get_field("name"), request=None)
f2 = ma.formfield_for_dbfield(Band._meta.get_field("style"), request=None)
self.assertNotEqual(f1.widget, f2.widget)
self.assertEqual(f1.widget.attrs["maxlength"], "100")
self.assertEqual(f2.widget.attrs["maxlength"], "20")
self.assertEqual(f2.widget.attrs["size"], "10")
def test_formfield_overrides_m2m_filter_widget(self):
"""
The autocomplete_fields, raw_id_fields, filter_vertical, and
filter_horizontal widgets for ManyToManyFields may be overridden by
specifying a widget in formfield_overrides.
"""
class BandAdmin(admin.ModelAdmin):
filter_vertical = ["members"]
formfield_overrides = {
ManyToManyField: {"widget": forms.CheckboxSelectMultiple},
}
ma = BandAdmin(Band, admin.site)
field = ma.formfield_for_dbfield(Band._meta.get_field("members"), request=None)
self.assertIsInstance(field.widget.widget, forms.CheckboxSelectMultiple)
def test_formfield_overrides_for_datetime_field(self):
"""
Overriding the widget for DateTimeField doesn't overrides the default
form_class for that field (#26449).
"""
class MemberAdmin(admin.ModelAdmin):
formfield_overrides = {
DateTimeField: {"widget": widgets.AdminSplitDateTime}
}
ma = MemberAdmin(Member, admin.site)
f1 = ma.formfield_for_dbfield(Member._meta.get_field("birthdate"), request=None)
self.assertIsInstance(f1.widget, widgets.AdminSplitDateTime)
self.assertIsInstance(f1, forms.SplitDateTimeField)
def test_formfield_overrides_for_custom_field(self):
"""
formfield_overrides works for a custom field class.
"""
class AlbumAdmin(admin.ModelAdmin):
formfield_overrides = {MyFileField: {"widget": forms.TextInput()}}
ma = AlbumAdmin(Member, admin.site)
f1 = ma.formfield_for_dbfield(
Album._meta.get_field("backside_art"), request=None
)
self.assertIsInstance(f1.widget, forms.TextInput)
def test_field_with_choices(self):
self.assertFormfield(Member, "gender", forms.Select)
def test_choices_with_radio_fields(self):
self.assertFormfield(
Member,
"gender",
widgets.AdminRadioSelect,
radio_fields={"gender": admin.VERTICAL},
)
def test_inheritance(self):
self.assertFormfield(Album, "backside_art", widgets.AdminFileWidget)
def test_m2m_widgets(self):
"""m2m fields help text as it applies to admin app (#9321)."""
class AdvisorAdmin(admin.ModelAdmin):
filter_vertical = ["companies"]
self.assertFormfield(
Advisor,
"companies",
widgets.FilteredSelectMultiple,
filter_vertical=["companies"],
)
ma = AdvisorAdmin(Advisor, admin.site)
f = ma.formfield_for_dbfield(Advisor._meta.get_field("companies"), request=None)
self.assertEqual(
f.help_text,
"Hold down “Control”, or “Command” on a Mac, to select more than one.",
)
@override_settings(ROOT_URLCONF="admin_widgets.urls")
class AdminFormfieldForDBFieldWithRequestTests(TestDataMixin, TestCase):
def test_filter_choices_by_request_user(self):
"""
Ensure the user can only see their own cars in the foreign key dropdown.
"""
self.client.force_login(self.superuser)
response = self.client.get(reverse("admin:admin_widgets_cartire_add"))
self.assertNotContains(response, "BMW M3")
self.assertContains(response, "Volkswagen Passat")
@override_settings(ROOT_URLCONF="admin_widgets.urls")
class AdminForeignKeyWidgetChangeList(TestDataMixin, TestCase):
def setUp(self):
self.client.force_login(self.superuser)
def test_changelist_ForeignKey(self):
response = self.client.get(reverse("admin:admin_widgets_car_changelist"))
self.assertContains(response, "/auth/user/add/")
@override_settings(ROOT_URLCONF="admin_widgets.urls")
class AdminForeignKeyRawIdWidget(TestDataMixin, TestCase):
def setUp(self):
self.client.force_login(self.superuser)
def test_nonexistent_target_id(self):
band = Band.objects.create(name="Bogey Blues")
pk = band.pk
band.delete()
post_data = {
"main_band": str(pk),
}
# Try posting with a nonexistent pk in a raw id field: this
# should result in an error message, not a server exception.
response = self.client.post(reverse("admin:admin_widgets_event_add"), post_data)
self.assertContains(
response,
"Select a valid choice. That choice is not one of the available choices.",
)
def test_invalid_target_id(self):
for test_str in ("Iñtërnâtiônàlizætiøn", "1234'", -1234):
# This should result in an error message, not a server exception.
response = self.client.post(
reverse("admin:admin_widgets_event_add"), {"main_band": test_str}
)
self.assertContains(
response,
"Select a valid choice. That choice is not one of the available choices.",
)
def test_url_params_from_lookup_dict_any_iterable(self):
lookup1 = widgets.url_params_from_lookup_dict({"color__in": ("red", "blue")})
lookup2 = widgets.url_params_from_lookup_dict({"color__in": ["red", "blue"]})
self.assertEqual(lookup1, {"color__in": "red,blue"})
self.assertEqual(lookup1, lookup2)
def test_url_params_from_lookup_dict_callable(self):
def my_callable():
return "works"
lookup1 = widgets.url_params_from_lookup_dict({"myfield": my_callable})
lookup2 = widgets.url_params_from_lookup_dict({"myfield": my_callable()})
self.assertEqual(lookup1, lookup2)
def test_label_and_url_for_value_invalid_uuid(self):
field = Bee._meta.get_field("honeycomb")
self.assertIsInstance(field.target_field, UUIDField)
widget = widgets.ForeignKeyRawIdWidget(field.remote_field, admin.site)
self.assertEqual(widget.label_and_url_for_value("invalid-uuid"), ("", ""))
class FilteredSelectMultipleWidgetTest(SimpleTestCase):
def test_render(self):
# Backslash in verbose_name to ensure it is JavaScript escaped.
w = widgets.FilteredSelectMultiple("test\\", False)
self.assertHTMLEqual(
w.render("test", "test"),
'',
)
def test_stacked_render(self):
# Backslash in verbose_name to ensure it is JavaScript escaped.
w = widgets.FilteredSelectMultiple("test\\", True)
self.assertHTMLEqual(
w.render("test", "test"),
'',
)
class AdminDateWidgetTest(SimpleTestCase):
def test_attrs(self):
w = widgets.AdminDateWidget()
self.assertHTMLEqual(
w.render("test", datetime(2007, 12, 1, 9, 30)),
'',
)
# pass attrs to widget
w = widgets.AdminDateWidget(attrs={"size": 20, "class": "myDateField"})
self.assertHTMLEqual(
w.render("test", datetime(2007, 12, 1, 9, 30)),
'',
)
class AdminTimeWidgetTest(SimpleTestCase):
def test_attrs(self):
w = widgets.AdminTimeWidget()
self.assertHTMLEqual(
w.render("test", datetime(2007, 12, 1, 9, 30)),
'',
)
# pass attrs to widget
w = widgets.AdminTimeWidget(attrs={"size": 20, "class": "myTimeField"})
self.assertHTMLEqual(
w.render("test", datetime(2007, 12, 1, 9, 30)),
'',
)
class AdminSplitDateTimeWidgetTest(SimpleTestCase):
def test_render(self):
w = widgets.AdminSplitDateTime()
self.assertHTMLEqual(
w.render("test", datetime(2007, 12, 1, 9, 30)),
'
'
'Date: '
'Time:
',
)
def test_localization(self):
w = widgets.AdminSplitDateTime()
with translation.override("de-at"):
w.is_localized = True
self.assertHTMLEqual(
w.render("test", datetime(2007, 12, 1, 9, 30)),
'
'
'Datum: '
'Zeit:
',
)
class AdminURLWidgetTest(SimpleTestCase):
def test_get_context_validates_url(self):
w = widgets.AdminURLFieldWidget()
for invalid in ["", "/not/a/full/url/", 'javascript:alert("Danger XSS!")']:
with self.subTest(url=invalid):
self.assertFalse(w.get_context("name", invalid, {})["url_valid"])
self.assertTrue(w.get_context("name", "http://example.com", {})["url_valid"])
def test_render(self):
w = widgets.AdminURLFieldWidget()
self.assertHTMLEqual(
w.render("test", ""), ''
)
self.assertHTMLEqual(
w.render("test", "http://example.com"),
'