1
0
mirror of https://github.com/django/django.git synced 2025-03-31 19:46:42 +00:00

Fixed #34380 -- Allowed specifying a default URL scheme in forms.URLField.

This also deprecates "http" as the default scheme.
This commit is contained in:
Coen van der Kamp 2023-03-08 20:12:34 +01:00 committed by Mariusz Felisiak
parent 070cbac0db
commit 7bbbadc693
10 changed files with 132 additions and 20 deletions

View File

@ -10,6 +10,7 @@ import operator
import os import os
import re import re
import uuid import uuid
import warnings
from decimal import Decimal, DecimalException from decimal import Decimal, DecimalException
from io import BytesIO from io import BytesIO
from urllib.parse import urlsplit, urlunsplit from urllib.parse import urlsplit, urlunsplit
@ -42,6 +43,7 @@ from django.forms.widgets import (
) )
from django.utils import formats from django.utils import formats
from django.utils.dateparse import parse_datetime, parse_duration from django.utils.dateparse import parse_datetime, parse_duration
from django.utils.deprecation import RemovedInDjango60Warning
from django.utils.duration import duration_string from django.utils.duration import duration_string
from django.utils.ipv6 import clean_ipv6_address from django.utils.ipv6 import clean_ipv6_address
from django.utils.regex_helper import _lazy_re_compile from django.utils.regex_helper import _lazy_re_compile
@ -753,7 +755,19 @@ class URLField(CharField):
} }
default_validators = [validators.URLValidator()] default_validators = [validators.URLValidator()]
def __init__(self, **kwargs): def __init__(self, *, assume_scheme=None, **kwargs):
if assume_scheme is None:
warnings.warn(
"The default scheme will be changed from 'http' to 'https' in Django "
"6.0. Pass the forms.URLField.assume_scheme argument to silence this "
"warning.",
RemovedInDjango60Warning,
stacklevel=2,
)
assume_scheme = "http"
# RemovedInDjango60Warning: When the deprecation ends, replace with:
# self.assume_scheme = assume_scheme or "https"
self.assume_scheme = assume_scheme
super().__init__(strip=True, **kwargs) super().__init__(strip=True, **kwargs)
def to_python(self, value): def to_python(self, value):
@ -773,8 +787,8 @@ class URLField(CharField):
if value: if value:
url_fields = split_url(value) url_fields = split_url(value)
if not url_fields[0]: if not url_fields[0]:
# If no URL scheme given, assume http:// # If no URL scheme given, add a scheme.
url_fields[0] = "http" url_fields[0] = self.assume_scheme
if not url_fields[1]: if not url_fields[1]:
# Assume that if no domain is provided, that the path segment # Assume that if no domain is provided, that the path segment
# contains the domain. # contains the domain.

View File

@ -32,6 +32,9 @@ details on these changes.
* The ``ForeignObject.get_reverse_joining_columns()`` method will be removed. * The ``ForeignObject.get_reverse_joining_columns()`` method will be removed.
* The default scheme for ``forms.URLField`` will change from ``"http"`` to
``"https"``.
.. _deprecation-removed-in-5.1: .. _deprecation-removed-in-5.1:
5.1 5.1

View File

@ -1071,8 +1071,18 @@ For each field, we describe the default widget used if you don't specify
given value is a valid URL. given value is a valid URL.
* Error message keys: ``required``, ``invalid`` * Error message keys: ``required``, ``invalid``
Has the optional arguments ``max_length``, ``min_length``, and Has the optional arguments ``max_length``, ``min_length``, ``empty_value``
``empty_value`` which work just as they do for :class:`CharField`. which work just as they do for :class:`CharField`, and ``assume_scheme``
that defaults to ``"http"``.
.. versionchanged:: 5.0
The ``assume_scheme`` argument was added.
.. deprecated:: 5.0
The default value for ``assume_scheme`` will change from ``"http"`` to
``"https"`` in Django 6.0.
``UUIDField`` ``UUIDField``
------------- -------------

View File

@ -245,6 +245,9 @@ Forms
:ref:`Choices classes <field-choices-enum-types>` directly instead of :ref:`Choices classes <field-choices-enum-types>` directly instead of
requiring expansion with the ``choices`` attribute. requiring expansion with the ``choices`` attribute.
* The new ``assume_scheme`` argument for :class:`~django.forms.URLField` allows
specifying a default URL scheme.
Generic Views Generic Views
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
@ -403,6 +406,9 @@ Miscellaneous
* The ``ForeignObject.get_reverse_joining_columns()`` method is deprecated. * The ``ForeignObject.get_reverse_joining_columns()`` method is deprecated.
* The default scheme for ``forms.URLField`` will change from ``"http"`` to
``"https"`` in Django 6.0.
Features removed in 5.0 Features removed in 5.0
======================= =======================

View File

@ -26,6 +26,7 @@ from django.forms.utils import ErrorList
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import ( from django.test import (
TestCase, TestCase,
ignore_warnings,
modify_settings, modify_settings,
override_settings, override_settings,
skipUnlessDBFeature, skipUnlessDBFeature,
@ -34,6 +35,7 @@ from django.test.utils import override_script_prefix
from django.urls import NoReverseMatch, resolve, reverse from django.urls import NoReverseMatch, resolve, reverse
from django.utils import formats, translation from django.utils import formats, translation
from django.utils.cache import get_max_age from django.utils.cache import get_max_age
from django.utils.deprecation import RemovedInDjango60Warning
from django.utils.encoding import iri_to_uri from django.utils.encoding import iri_to_uri
from django.utils.html import escape from django.utils.html import escape
from django.utils.http import urlencode from django.utils.http import urlencode
@ -6555,6 +6557,7 @@ class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
def setUp(self): def setUp(self):
self.client.force_login(self.superuser) self.client.force_login(self.superuser)
@ignore_warnings(category=RemovedInDjango60Warning)
def test_readonly_get(self): def test_readonly_get(self):
response = self.client.get(reverse("admin:admin_views_post_add")) response = self.client.get(reverse("admin:admin_views_post_add"))
self.assertNotContains(response, 'name="posted"') self.assertNotContains(response, 'name="posted"')
@ -6615,6 +6618,7 @@ class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
) )
self.assertContains(response, "%d amount of cool" % p.pk) self.assertContains(response, "%d amount of cool" % p.pk)
@ignore_warnings(category=RemovedInDjango60Warning)
def test_readonly_text_field(self): def test_readonly_text_field(self):
p = Post.objects.create( p = Post.objects.create(
title="Readonly test", title="Readonly test",
@ -6634,6 +6638,7 @@ class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
# Checking readonly field in inline. # Checking readonly field in inline.
self.assertContains(response, "test<br>link") self.assertContains(response, "test<br>link")
@ignore_warnings(category=RemovedInDjango60Warning)
def test_readonly_post(self): def test_readonly_post(self):
data = { data = {
"title": "Django Got Readonly Fields", "title": "Django Got Readonly Fields",
@ -6774,6 +6779,7 @@ class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
field = self.get_admin_readonly_field(response, "plotdetails") field = self.get_admin_readonly_field(response, "plotdetails")
self.assertEqual(field.contents(), "-") # default empty value self.assertEqual(field.contents(), "-") # default empty value
@ignore_warnings(category=RemovedInDjango60Warning)
def test_readonly_field_overrides(self): def test_readonly_field_overrides(self):
""" """
Regression test for #22087 - ModelForm Meta overrides are ignored by Regression test for #22087 - ModelForm Meta overrides are ignored by
@ -7233,6 +7239,7 @@ class CSSTest(TestCase):
def setUp(self): def setUp(self):
self.client.force_login(self.superuser) self.client.force_login(self.superuser)
@ignore_warnings(category=RemovedInDjango60Warning)
def test_field_prefix_css_classes(self): def test_field_prefix_css_classes(self):
""" """
Fields have a CSS class name with a 'field-' prefix. Fields have a CSS class name with a 'field-' prefix.

View File

@ -22,9 +22,10 @@ from django.db.models import (
ManyToManyField, ManyToManyField,
UUIDField, UUIDField,
) )
from django.test import SimpleTestCase, TestCase, override_settings from django.test import SimpleTestCase, TestCase, ignore_warnings, override_settings
from django.urls import reverse from django.urls import reverse
from django.utils import translation from django.utils import translation
from django.utils.deprecation import RemovedInDjango60Warning
from .models import ( from .models import (
Advisor, Advisor,
@ -106,6 +107,7 @@ class AdminFormfieldForDBFieldTests(SimpleTestCase):
def test_TextField(self): def test_TextField(self):
self.assertFormfield(Event, "description", widgets.AdminTextareaWidget) self.assertFormfield(Event, "description", widgets.AdminTextareaWidget)
@ignore_warnings(category=RemovedInDjango60Warning)
def test_URLField(self): def test_URLField(self):
self.assertFormfield(Event, "link", widgets.AdminURLFieldWidget) self.assertFormfield(Event, "link", widgets.AdminURLFieldWidget)
@ -320,6 +322,7 @@ class AdminForeignKeyRawIdWidget(TestDataMixin, TestCase):
def setUp(self): def setUp(self):
self.client.force_login(self.superuser) self.client.force_login(self.superuser)
@ignore_warnings(category=RemovedInDjango60Warning)
def test_nonexistent_target_id(self): def test_nonexistent_target_id(self):
band = Band.objects.create(name="Bogey Blues") band = Band.objects.create(name="Bogey Blues")
pk = band.pk pk = band.pk
@ -335,6 +338,7 @@ class AdminForeignKeyRawIdWidget(TestDataMixin, TestCase):
"Select a valid choice. That choice is not one of the available choices.", "Select a valid choice. That choice is not one of the available choices.",
) )
@ignore_warnings(category=RemovedInDjango60Warning)
def test_invalid_target_id(self): def test_invalid_target_id(self):
for test_str in ("Iñtërnâtiônàlizætiøn", "1234'", -1234): 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. # This should result in an error message, not a server exception.
@ -1610,6 +1614,7 @@ class HorizontalVerticalFilterSeleniumTests(AdminWidgetSeleniumTestCase):
self.assertCountSeleniumElements("#id_students_to > option", 2) self.assertCountSeleniumElements("#id_students_to > option", 2)
@ignore_warnings(category=RemovedInDjango60Warning)
class AdminRawIdWidgetSeleniumTests(AdminWidgetSeleniumTestCase): class AdminRawIdWidgetSeleniumTests(AdminWidgetSeleniumTestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()

View File

@ -1,10 +1,12 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms import URLField from django.forms import URLField
from django.test import SimpleTestCase from django.test import SimpleTestCase, ignore_warnings
from django.utils.deprecation import RemovedInDjango60Warning
from . import FormFieldAssertionsMixin from . import FormFieldAssertionsMixin
@ignore_warnings(category=RemovedInDjango60Warning)
class URLFieldTest(FormFieldAssertionsMixin, SimpleTestCase): class URLFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
def test_urlfield_widget(self): def test_urlfield_widget(self):
f = URLField() f = URLField()
@ -26,7 +28,9 @@ class URLFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
f.clean("http://abcdefghijklmnopqrstuvwxyz.com") f.clean("http://abcdefghijklmnopqrstuvwxyz.com")
def test_urlfield_clean(self): def test_urlfield_clean(self):
f = URLField(required=False) # RemovedInDjango60Warning: When the deprecation ends, remove the
# assume_scheme argument.
f = URLField(required=False, assume_scheme="https")
tests = [ tests = [
("http://localhost", "http://localhost"), ("http://localhost", "http://localhost"),
("http://example.com", "http://example.com"), ("http://example.com", "http://example.com"),
@ -38,8 +42,8 @@ class URLFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
"http://example.com?some_param=some_value", "http://example.com?some_param=some_value",
"http://example.com?some_param=some_value", "http://example.com?some_param=some_value",
), ),
("valid-with-hyphens.com", "http://valid-with-hyphens.com"), ("valid-with-hyphens.com", "https://valid-with-hyphens.com"),
("subdomain.domain.com", "http://subdomain.domain.com"), ("subdomain.domain.com", "https://subdomain.domain.com"),
("http://200.8.9.10", "http://200.8.9.10"), ("http://200.8.9.10", "http://200.8.9.10"),
("http://200.8.9.10:8000/test", "http://200.8.9.10:8000/test"), ("http://200.8.9.10:8000/test", "http://200.8.9.10:8000/test"),
("http://valid-----hyphens.com", "http://valid-----hyphens.com"), ("http://valid-----hyphens.com", "http://valid-----hyphens.com"),
@ -49,7 +53,7 @@ class URLFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
), ),
( (
"www.example.com/s/http://code.djangoproject.com/ticket/13804", "www.example.com/s/http://code.djangoproject.com/ticket/13804",
"http://www.example.com/s/http://code.djangoproject.com/ticket/13804", "https://www.example.com/s/http://code.djangoproject.com/ticket/13804",
), ),
# Normalization. # Normalization.
("http://example.com/ ", "http://example.com/"), ("http://example.com/ ", "http://example.com/"),
@ -135,3 +139,24 @@ class URLFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
msg = "__init__() got multiple values for keyword argument 'strip'" msg = "__init__() got multiple values for keyword argument 'strip'"
with self.assertRaisesMessage(TypeError, msg): with self.assertRaisesMessage(TypeError, msg):
URLField(strip=False) URLField(strip=False)
def test_urlfield_assume_scheme(self):
f = URLField()
# RemovedInDjango60Warning: When the deprecation ends, replace with:
# "https://example.com"
self.assertEqual(f.clean("example.com"), "http://example.com")
f = URLField(assume_scheme="http")
self.assertEqual(f.clean("example.com"), "http://example.com")
f = URLField(assume_scheme="https")
self.assertEqual(f.clean("example.com"), "https://example.com")
class URLFieldAssumeSchemeDeprecationTest(FormFieldAssertionsMixin, SimpleTestCase):
def test_urlfield_raises_warning(self):
msg = (
"The default scheme will be changed from 'http' to 'https' in Django 6.0. "
"Pass the forms.URLField.assume_scheme argument to silence this warning."
)
with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
f = URLField()
self.assertEqual(f.clean("example.com"), "http://example.com")

View File

@ -23,7 +23,8 @@ from django.forms import (
utils, utils,
) )
from django.template import Context, Template from django.template import Context, Template
from django.test import SimpleTestCase, TestCase from django.test import SimpleTestCase, TestCase, ignore_warnings
from django.utils.deprecation import RemovedInDjango60Warning
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from ..models import ChoiceModel from ..models import ChoiceModel
@ -167,7 +168,8 @@ class FormsErrorMessagesTestCase(SimpleTestCase, AssertFormErrorsMixin):
"invalid": "INVALID", "invalid": "INVALID",
"max_length": '"%(value)s" has more than %(limit_value)d characters.', "max_length": '"%(value)s" has more than %(limit_value)d characters.',
} }
f = URLField(error_messages=e, max_length=17) with ignore_warnings(category=RemovedInDjango60Warning):
f = URLField(error_messages=e, max_length=17)
self.assertFormErrors(["REQUIRED"], f.clean, "") self.assertFormErrors(["REQUIRED"], f.clean, "")
self.assertFormErrors(["INVALID"], f.clean, "abc.c") self.assertFormErrors(["INVALID"], f.clean, "abc.c")
self.assertFormErrors( self.assertFormErrors(

View File

@ -5,8 +5,15 @@ from django.contrib.contenttypes.admin import GenericTabularInline
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.forms.formsets import DEFAULT_MAX_NUM from django.forms.formsets import DEFAULT_MAX_NUM
from django.forms.models import ModelForm from django.forms.models import ModelForm
from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings from django.test import (
RequestFactory,
SimpleTestCase,
TestCase,
ignore_warnings,
override_settings,
)
from django.urls import reverse from django.urls import reverse
from django.utils.deprecation import RemovedInDjango60Warning
from .admin import MediaInline, MediaPermanentInline from .admin import MediaInline, MediaPermanentInline
from .admin import site as admin_site from .admin import site as admin_site
@ -21,6 +28,7 @@ class TestDataMixin:
) )
@ignore_warnings(category=RemovedInDjango60Warning)
@override_settings(ROOT_URLCONF="generic_inline_admin.urls") @override_settings(ROOT_URLCONF="generic_inline_admin.urls")
class GenericAdminViewTest(TestDataMixin, TestCase): class GenericAdminViewTest(TestDataMixin, TestCase):
def setUp(self): def setUp(self):
@ -95,6 +103,7 @@ class GenericAdminViewTest(TestDataMixin, TestCase):
self.assertEqual(response.status_code, 302) # redirect somewhere self.assertEqual(response.status_code, 302) # redirect somewhere
@ignore_warnings(category=RemovedInDjango60Warning)
@override_settings(ROOT_URLCONF="generic_inline_admin.urls") @override_settings(ROOT_URLCONF="generic_inline_admin.urls")
class GenericInlineAdminParametersTest(TestDataMixin, TestCase): class GenericInlineAdminParametersTest(TestDataMixin, TestCase):
factory = RequestFactory() factory = RequestFactory()
@ -296,6 +305,7 @@ class GenericInlineAdminWithUniqueTogetherTest(TestDataMixin, TestCase):
@override_settings(ROOT_URLCONF="generic_inline_admin.urls") @override_settings(ROOT_URLCONF="generic_inline_admin.urls")
class NoInlineDeletionTest(SimpleTestCase): class NoInlineDeletionTest(SimpleTestCase):
@ignore_warnings(category=RemovedInDjango60Warning)
def test_no_deletion(self): def test_no_deletion(self):
inline = MediaPermanentInline(EpisodePermanent, admin_site) inline = MediaPermanentInline(EpisodePermanent, admin_site)
fake_request = object() fake_request = object()
@ -321,6 +331,7 @@ class GenericInlineModelAdminTest(SimpleTestCase):
def setUp(self): def setUp(self):
self.site = AdminSite() self.site = AdminSite()
@ignore_warnings(category=RemovedInDjango60Warning)
def test_get_formset_kwargs(self): def test_get_formset_kwargs(self):
media_inline = MediaInline(Media, AdminSite()) media_inline = MediaInline(Media, AdminSite())
@ -360,6 +371,7 @@ class GenericInlineModelAdminTest(SimpleTestCase):
["keywords", "id", "DELETE"], ["keywords", "id", "DELETE"],
) )
@ignore_warnings(category=RemovedInDjango60Warning)
def test_custom_form_meta_exclude(self): def test_custom_form_meta_exclude(self):
""" """
The custom ModelForm's `Meta.exclude` is respected by The custom ModelForm's `Meta.exclude` is respected by
@ -403,6 +415,7 @@ class GenericInlineModelAdminTest(SimpleTestCase):
["description", "keywords", "id", "DELETE"], ["description", "keywords", "id", "DELETE"],
) )
@ignore_warnings(category=RemovedInDjango60Warning)
def test_get_fieldsets(self): def test_get_fieldsets(self):
# get_fieldsets is called when figuring out form fields. # get_fieldsets is called when figuring out form fields.
# Refs #18681. # Refs #18681.

View File

@ -21,8 +21,9 @@ from django.forms.models import (
modelform_factory, modelform_factory,
) )
from django.template import Context, Template from django.template import Context, Template
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature from django.test import SimpleTestCase, TestCase, ignore_warnings, skipUnlessDBFeature
from django.test.utils import isolate_apps from django.test.utils import isolate_apps
from django.utils.deprecation import RemovedInDjango60Warning
from .models import ( from .models import (
Article, Article,
@ -369,6 +370,7 @@ class ModelFormBaseTest(TestCase):
obj = form.save() obj = form.save()
self.assertEqual(obj.name, "") self.assertEqual(obj.name, "")
@ignore_warnings(category=RemovedInDjango60Warning)
def test_save_blank_null_unique_charfield_saves_null(self): def test_save_blank_null_unique_charfield_saves_null(self):
form_class = modelform_factory( form_class = modelform_factory(
model=NullableUniqueCharFieldModel, fields="__all__" model=NullableUniqueCharFieldModel, fields="__all__"
@ -907,6 +909,13 @@ class ModelFormBaseTest(TestCase):
self.assertEqual(m2.date_published, datetime.date(2010, 1, 1)) self.assertEqual(m2.date_published, datetime.date(2010, 1, 1))
# RemovedInDjango60Warning.
# It's a temporary workaround for the deprecation period.
class HttpsURLField(forms.URLField):
def __init__(self, **kwargs):
super().__init__(assume_scheme="https", **kwargs)
class FieldOverridesByFormMetaForm(forms.ModelForm): class FieldOverridesByFormMetaForm(forms.ModelForm):
class Meta: class Meta:
model = Category model = Category
@ -930,7 +939,7 @@ class FieldOverridesByFormMetaForm(forms.ModelForm):
} }
} }
field_classes = { field_classes = {
"url": forms.URLField, "url": HttpsURLField,
} }
@ -2857,6 +2866,7 @@ class ModelOtherFieldTests(SimpleTestCase):
}, },
) )
@ignore_warnings(category=RemovedInDjango60Warning)
def test_url_on_modelform(self): def test_url_on_modelform(self):
"Check basic URL field validation on model forms" "Check basic URL field validation on model forms"
@ -2881,6 +2891,19 @@ class ModelOtherFieldTests(SimpleTestCase):
) )
self.assertTrue(HomepageForm({"url": "http://example.com/foo/bar"}).is_valid()) self.assertTrue(HomepageForm({"url": "http://example.com/foo/bar"}).is_valid())
def test_url_modelform_assume_scheme_warning(self):
msg = (
"The default scheme will be changed from 'http' to 'https' in Django "
"6.0. Pass the forms.URLField.assume_scheme argument to silence this "
"warning."
)
with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
class HomepageForm(forms.ModelForm):
class Meta:
model = Homepage
fields = "__all__"
def test_modelform_non_editable_field(self): def test_modelform_non_editable_field(self):
""" """
When explicitly including a non-editable field in a ModelForm, the When explicitly including a non-editable field in a ModelForm, the
@ -2900,23 +2923,27 @@ class ModelOtherFieldTests(SimpleTestCase):
model = Article model = Article
fields = ("headline", "created") fields = ("headline", "created")
def test_http_prefixing(self): def test_https_prefixing(self):
""" """
If the http:// prefix is omitted on form input, the field adds it again. If the https:// prefix is omitted on form input, the field adds it
again.
""" """
class HomepageForm(forms.ModelForm): class HomepageForm(forms.ModelForm):
# RemovedInDjango60Warning.
url = forms.URLField(assume_scheme="https")
class Meta: class Meta:
model = Homepage model = Homepage
fields = "__all__" fields = "__all__"
form = HomepageForm({"url": "example.com"}) form = HomepageForm({"url": "example.com"})
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data["url"], "http://example.com") self.assertEqual(form.cleaned_data["url"], "https://example.com")
form = HomepageForm({"url": "example.com/test"}) form = HomepageForm({"url": "example.com/test"})
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data["url"], "http://example.com/test") self.assertEqual(form.cleaned_data["url"], "https://example.com/test")
class OtherModelFormTests(TestCase): class OtherModelFormTests(TestCase):