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'), '

Currently:' 'http://example.com
' 'Change:

' ) def test_render_idn(self): w = widgets.AdminURLFieldWidget() self.assertHTMLEqual( w.render('test', 'http://example-äüö.com'), '

Currently: ' 'http://example-äüö.com
' 'Change:

' ) def test_render_quoting(self): """ WARNING: This test doesn't use assertHTMLEqual since it will get rid of some escapes which are tested here! """ HREF_RE = re.compile('href="([^"]+)"') VALUE_RE = re.compile('value="([^"]+)"') TEXT_RE = re.compile(']+>([^>]+)') w = widgets.AdminURLFieldWidget() output = w.render('test', 'http://example.com/some-text') self.assertEqual( HREF_RE.search(output)[1], 'http://example.com/%3Csometag%3Esome-text%3C/sometag%3E', ) self.assertEqual( TEXT_RE.search(output)[1], 'http://example.com/<sometag>some-text</sometag>', ) self.assertEqual( VALUE_RE.search(output)[1], 'http://example.com/<sometag>some-text</sometag>', ) output = w.render('test', 'http://example-äüö.com/some-text') self.assertEqual( HREF_RE.search(output)[1], 'http://xn--example--7za4pnc.com/%3Csometag%3Esome-text%3C/sometag%3E', ) self.assertEqual( TEXT_RE.search(output)[1], 'http://example-äüö.com/<sometag>some-text</sometag>', ) self.assertEqual( VALUE_RE.search(output)[1], 'http://example-äüö.com/<sometag>some-text</sometag>', ) output = w.render('test', 'http://www.example.com/%C3%A4">"') self.assertEqual( HREF_RE.search(output)[1], 'http://www.example.com/%C3%A4%22%3E%3Cscript%3Ealert(%22XSS!%22)%3C/script%3E%22', ) self.assertEqual( TEXT_RE.search(output)[1], 'http://www.example.com/%C3%A4"><script>' 'alert("XSS!")</script>"' ) self.assertEqual( VALUE_RE.search(output)[1], 'http://www.example.com/%C3%A4"><script>alert("XSS!")</script>"', ) class AdminUUIDWidgetTests(SimpleTestCase): def test_attrs(self): w = widgets.AdminUUIDInputWidget() self.assertHTMLEqual( w.render('test', '550e8400-e29b-41d4-a716-446655440000'), '', ) w = widgets.AdminUUIDInputWidget(attrs={'class': 'myUUIDInput'}) self.assertHTMLEqual( w.render('test', '550e8400-e29b-41d4-a716-446655440000'), '', ) @override_settings(ROOT_URLCONF='admin_widgets.urls') class AdminFileWidgetTests(TestDataMixin, TestCase): @classmethod def setUpTestData(cls): super().setUpTestData() band = Band.objects.create(name='Linkin Park') cls.album = band.album_set.create( name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg' ) def test_render(self): w = widgets.AdminFileWidget() self.assertHTMLEqual( w.render('test', self.album.cover_art), '

Currently: albums\hybrid_theory.jpg ' '' ' ' '
' 'Change:

' % { 'STORAGE_URL': default_storage.url(''), }, ) self.assertHTMLEqual( w.render('test', SimpleUploadedFile('test', b'content')), '', ) def test_render_required(self): widget = widgets.AdminFileWidget() widget.is_required = True self.assertHTMLEqual( widget.render('test', self.album.cover_art), '

Currently: albums\hybrid_theory.jpg
' 'Change:

' % { 'STORAGE_URL': default_storage.url(''), }, ) def test_render_disabled(self): widget = widgets.AdminFileWidget(attrs={'disabled': True}) self.assertHTMLEqual( widget.render('test', self.album.cover_art), '

Currently: albums\hybrid_theory.jpg ' '' '' '
' 'Change:

' % { 'STORAGE_URL': default_storage.url(''), }, ) def test_readonly_fields(self): """ File widgets should render as a link when they're marked "read only." """ self.client.force_login(self.superuser) response = self.client.get(reverse('admin:admin_widgets_album_change', args=(self.album.id,))) self.assertContains( response, '
' r'albums\hybrid_theory.jpg
' % {'STORAGE_URL': default_storage.url('')}, html=True, ) self.assertNotContains( response, '', html=True, ) response = self.client.get(reverse('admin:admin_widgets_album_add')) self.assertContains( response, '
', html=True, ) @override_settings(ROOT_URLCONF='admin_widgets.urls') class ForeignKeyRawIdWidgetTest(TestCase): def test_render(self): band = Band.objects.create(name='Linkin Park') band.album_set.create( name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg' ) rel_uuid = Album._meta.get_field('band').remote_field w = widgets.ForeignKeyRawIdWidget(rel_uuid, widget_admin_site) self.assertHTMLEqual( w.render('test', band.uuid, attrs={}), '' ' ' 'Linkin Park' '' % {'banduuid': band.uuid, 'bandpk': band.pk} ) rel_id = ReleaseEvent._meta.get_field('album').remote_field w = widgets.ForeignKeyRawIdWidget(rel_id, widget_admin_site) self.assertHTMLEqual( w.render('test', None, attrs={}), '' '', ) def test_relations_to_non_primary_key(self): # ForeignKeyRawIdWidget works with fields which aren't related to # the model's primary key. apple = Inventory.objects.create(barcode=86, name='Apple') Inventory.objects.create(barcode=22, name='Pear') core = Inventory.objects.create( barcode=87, name='Core', parent=apple ) rel = Inventory._meta.get_field('parent').remote_field w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site) self.assertHTMLEqual( w.render('test', core.parent_id, attrs={}), '' '' ' ' 'Apple' % {'pk': apple.pk} ) def test_fk_related_model_not_in_admin(self): # FK to a model not registered with admin site. Raw ID widget should # have no magnifying glass link. See #16542 big_honeycomb = Honeycomb.objects.create(location='Old tree') big_honeycomb.bee_set.create() rel = Bee._meta.get_field('honeycomb').remote_field w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site) self.assertHTMLEqual( w.render('honeycomb_widget', big_honeycomb.pk, attrs={}), '' ' %(hcomb)s' % {'hcombpk': big_honeycomb.pk, 'hcomb': big_honeycomb} ) def test_fk_to_self_model_not_in_admin(self): # FK to self, not registered with admin site. Raw ID widget should have # no magnifying glass link. See #16542 subject1 = Individual.objects.create(name='Subject #1') Individual.objects.create(name='Child', parent=subject1) rel = Individual._meta.get_field('parent').remote_field w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site) self.assertHTMLEqual( w.render('individual_widget', subject1.pk, attrs={}), '' ' %(subj1)s' % {'subj1pk': subject1.pk, 'subj1': subject1} ) def test_proper_manager_for_label_lookup(self): # see #9258 rel = Inventory._meta.get_field('parent').remote_field w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site) hidden = Inventory.objects.create( barcode=93, name='Hidden', hidden=True ) child_of_hidden = Inventory.objects.create( barcode=94, name='Child of hidden', parent=hidden ) self.assertHTMLEqual( w.render('test', child_of_hidden.parent_id, attrs={}), '' '' ' ' 'Hidden' % {'pk': hidden.pk} ) def test_render_unsafe_limit_choices_to(self): rel = UnsafeLimitChoicesTo._meta.get_field('band').remote_field w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site) self.assertHTMLEqual( w.render('test', None), '\n' '' ) def test_render_fk_as_pk_model(self): rel = VideoStream._meta.get_field('release_event').remote_field w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site) self.assertHTMLEqual( w.render('test', None), '\n' '' ) @override_settings(ROOT_URLCONF='admin_widgets.urls') class ManyToManyRawIdWidgetTest(TestCase): def test_render(self): band = Band.objects.create(name='Linkin Park') m1 = Member.objects.create(name='Chester') m2 = Member.objects.create(name='Mike') band.members.add(m1, m2) rel = Band._meta.get_field('members').remote_field w = widgets.ManyToManyRawIdWidget(rel, widget_admin_site) self.assertHTMLEqual( w.render('test', [m1.pk, m2.pk], attrs={}), ( '' '' ) % {'m1pk': m1.pk, 'm2pk': m2.pk} ) self.assertHTMLEqual( w.render('test', [m1.pk]), ( '' '' ) % {'m1pk': m1.pk} ) def test_m2m_related_model_not_in_admin(self): # M2M relationship with model not registered with admin site. Raw ID # widget should have no magnifying glass link. See #16542 consultor1 = Advisor.objects.create(name='Rockstar Techie') c1 = Company.objects.create(name='Doodle') c2 = Company.objects.create(name='Pear') consultor1.companies.add(c1, c2) rel = Advisor._meta.get_field('companies').remote_field w = widgets.ManyToManyRawIdWidget(rel, widget_admin_site) self.assertHTMLEqual( w.render('company_widget1', [c1.pk, c2.pk], attrs={}), '' % {'c1pk': c1.pk, 'c2pk': c2.pk} ) self.assertHTMLEqual( w.render('company_widget2', [c1.pk]), '' % {'c1pk': c1.pk} ) @override_settings(ROOT_URLCONF='admin_widgets.urls') class RelatedFieldWidgetWrapperTests(SimpleTestCase): def test_no_can_add_related(self): rel = Individual._meta.get_field('parent').remote_field w = widgets.AdminRadioSelect() # Used to fail with a name error. w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site) self.assertFalse(w.can_add_related) def test_select_multiple_widget_cant_change_delete_related(self): rel = Individual._meta.get_field('parent').remote_field widget = forms.SelectMultiple() wrapper = widgets.RelatedFieldWidgetWrapper( widget, rel, widget_admin_site, can_add_related=True, can_change_related=True, can_delete_related=True, ) self.assertTrue(wrapper.can_add_related) self.assertFalse(wrapper.can_change_related) self.assertFalse(wrapper.can_delete_related) def test_on_delete_cascade_rel_cant_delete_related(self): rel = Individual._meta.get_field('soulmate').remote_field widget = forms.Select() wrapper = widgets.RelatedFieldWidgetWrapper( widget, rel, widget_admin_site, can_add_related=True, can_change_related=True, can_delete_related=True, ) self.assertTrue(wrapper.can_add_related) self.assertTrue(wrapper.can_change_related) self.assertFalse(wrapper.can_delete_related) def test_custom_widget_render(self): class CustomWidget(forms.Select): def render(self, *args, **kwargs): return 'custom render output' rel = Album._meta.get_field('band').remote_field widget = CustomWidget() wrapper = widgets.RelatedFieldWidgetWrapper( widget, rel, widget_admin_site, can_add_related=True, can_change_related=True, can_delete_related=True, ) output = wrapper.render('name', 'value') self.assertIn('custom render output', output) def test_widget_delegates_value_omitted_from_data(self): class CustomWidget(forms.Select): def value_omitted_from_data(self, data, files, name): return False rel = Album._meta.get_field('band').remote_field widget = CustomWidget() wrapper = widgets.RelatedFieldWidgetWrapper(widget, rel, widget_admin_site) self.assertIs(wrapper.value_omitted_from_data({}, {}, 'band'), False) def test_widget_is_hidden(self): rel = Album._meta.get_field('band').remote_field widget = forms.HiddenInput() widget.choices = () wrapper = widgets.RelatedFieldWidgetWrapper(widget, rel, widget_admin_site) self.assertIs(wrapper.is_hidden, True) context = wrapper.get_context('band', None, {}) self.assertIs(context['is_hidden'], True) output = wrapper.render('name', 'value') # Related item links are hidden. self.assertNotIn('