From a4931cd75a1780923b02e43475ba5447df3adb31 Mon Sep 17 00:00:00 2001
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Date: Tue, 28 Nov 2023 20:04:21 +0100
Subject: [PATCH] Refs #34380 -- Added FORMS_URLFIELD_ASSUME_HTTPS transitional
 setting.

This allows early adoption of the new default "https".
---
 django/conf/__init__.py                       | 17 ++++++++
 django/conf/global_settings.py                |  5 +++
 django/forms/fields.py                        | 22 +++++++----
 docs/internals/deprecation.txt                |  2 +
 docs/ref/forms/fields.txt                     |  4 +-
 docs/ref/settings.txt                         | 15 +++++++
 docs/releases/5.0.txt                         |  6 ++-
 .../forms_tests/field_tests/test_urlfield.py  | 39 ++++++++++++++++++-
 tests/model_forms/tests.py                    | 15 ++++++-
 9 files changed, 113 insertions(+), 12 deletions(-)

diff --git a/django/conf/__init__.py b/django/conf/__init__.py
index 6b5f044e344..5568d7cc83c 100644
--- a/django/conf/__init__.py
+++ b/django/conf/__init__.py
@@ -16,12 +16,18 @@ from pathlib import Path
 import django
 from django.conf import global_settings
 from django.core.exceptions import ImproperlyConfigured
+from django.utils.deprecation import RemovedInDjango60Warning
 from django.utils.functional import LazyObject, empty
 
 ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE"
 DEFAULT_STORAGE_ALIAS = "default"
 STATICFILES_STORAGE_ALIAS = "staticfiles"
 
+# RemovedInDjango60Warning.
+FORMS_URLFIELD_ASSUME_HTTPS_DEPRECATED_MSG = (
+    "The FORMS_URLFIELD_ASSUME_HTTPS transitional setting is deprecated."
+)
+
 
 class SettingsReference(str):
     """
@@ -180,6 +186,12 @@ class Settings:
                 setattr(self, setting, setting_value)
                 self._explicit_settings.add(setting)
 
+        if self.is_overridden("FORMS_URLFIELD_ASSUME_HTTPS"):
+            warnings.warn(
+                FORMS_URLFIELD_ASSUME_HTTPS_DEPRECATED_MSG,
+                RemovedInDjango60Warning,
+            )
+
         if hasattr(time, "tzset") and self.TIME_ZONE:
             # When we can, attempt to validate the timezone. If we can't find
             # this file, no check happens and it's harmless.
@@ -224,6 +236,11 @@ class UserSettingsHolder:
 
     def __setattr__(self, name, value):
         self._deleted.discard(name)
+        if name == "FORMS_URLFIELD_ASSUME_HTTPS":
+            warnings.warn(
+                FORMS_URLFIELD_ASSUME_HTTPS_DEPRECATED_MSG,
+                RemovedInDjango60Warning,
+            )
         super().__setattr__(name, value)
 
     def __delattr__(self, name):
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 6b91c6a7167..8e1d2ace09c 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -216,6 +216,11 @@ TEMPLATES = []
 # Default form rendering class.
 FORM_RENDERER = "django.forms.renderers.DjangoTemplates"
 
+# RemovedInDjango60Warning: It's a transitional setting helpful in early
+# adoption of "https" as the new default value of forms.URLField.assume_scheme.
+# Set to True to assume "https" during the Django 5.x release cycle.
+FORMS_URLFIELD_ASSUME_HTTPS = False
+
 # Default email address to use for various automated correspondence from
 # the site managers.
 DEFAULT_FROM_EMAIL = "webmaster@localhost"
diff --git a/django/forms/fields.py b/django/forms/fields.py
index d1ba8af6548..62d68985c07 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -15,6 +15,7 @@ from decimal import Decimal, DecimalException
 from io import BytesIO
 from urllib.parse import urlsplit, urlunsplit
 
+from django.conf import settings
 from django.core import validators
 from django.core.exceptions import ValidationError
 from django.forms.boundfield import BoundField
@@ -762,14 +763,19 @@ class URLField(CharField):
 
     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"
+            if settings.FORMS_URLFIELD_ASSUME_HTTPS:
+                assume_scheme = "https"
+            else:
+                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, or set the FORMS_URLFIELD_ASSUME_HTTPS "
+                    "transitional setting to True to opt into using 'https' as the new "
+                    "default scheme.",
+                    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
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index dd6712e936c..edda364b73f 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -53,6 +53,8 @@ details on these changes.
 * ``get_prefetcher()`` and ``prefetch_related_objects()`` will no longer
   fallback to ``get_prefetch_queryset()``.
 
+* The ``FORMS_URLFIELD_ASSUME_HTTPS`` transitional setting will be removed.
+
 See the :ref:`Django 5.1 release notes <deprecated-features-5.1>` for more
 details on these changes.
 
diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
index 0f32df0ebb5..a3e0bf1aba8 100644
--- a/docs/ref/forms/fields.txt
+++ b/docs/ref/forms/fields.txt
@@ -1155,7 +1155,9 @@ For each field, we describe the default widget used if you don't specify
     .. deprecated:: 5.0
 
         The default value for ``assume_scheme`` will change from ``"http"`` to
-        ``"https"`` in Django 6.0.
+        ``"https"`` in Django 6.0. Set :setting:`FORMS_URLFIELD_ASSUME_HTTPS`
+        transitional setting to ``True`` to opt into using ``"https"`` during
+        the Django 5.x release cycle.
 
 ``UUIDField``
 -------------
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index 67f64827123..5d3e893cc78 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -1675,6 +1675,20 @@ renderers are:
 * ``'``:class:`django.forms.renderers.Jinja2`\ ``'``
 * ``'``:class:`django.forms.renderers.TemplatesSetting`\ ``'``
 
+.. setting:: FORMS_URLFIELD_ASSUME_HTTPS
+
+``FORMS_URLFIELD_ASSUME_HTTPS``
+-------------------------------
+
+.. versionadded:: 5.0
+.. deprecated:: 5.0
+
+Default: ``False``
+
+Set this transitional setting to ``True`` to opt into using ``"https"`` as the
+new default value of :attr:`URLField.assume_scheme
+<django.forms.URLField.assume_scheme>` during the Django 5.x release cycle.
+
 .. setting:: FORMAT_MODULE_PATH
 
 ``FORMAT_MODULE_PATH``
@@ -3635,6 +3649,7 @@ File uploads
 Forms
 -----
 * :setting:`FORM_RENDERER`
+* :setting:`FORMS_URLFIELD_ASSUME_HTTPS`
 
 Globalization (``i18n``/``l10n``)
 ---------------------------------
diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt
index b74a0f00c79..ebddec38e69 100644
--- a/docs/releases/5.0.txt
+++ b/docs/releases/5.0.txt
@@ -612,7 +612,11 @@ Miscellaneous
 * 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.
+  ``"https"`` in Django 6.0. Set :setting:`FORMS_URLFIELD_ASSUME_HTTPS`
+  transitional setting to ``True`` to opt into assuming ``"https"`` during the
+  Django 5.x release cycle.
+
+* ``FORMS_URLFIELD_ASSUME_HTTPS`` transitional setting is deprecated.
 
 * Support for calling ``format_html()`` without passing args or kwargs will be
   removed.
diff --git a/tests/forms_tests/field_tests/test_urlfield.py b/tests/forms_tests/field_tests/test_urlfield.py
index 2cd1a82694e..8ba78420640 100644
--- a/tests/forms_tests/field_tests/test_urlfield.py
+++ b/tests/forms_tests/field_tests/test_urlfield.py
@@ -1,3 +1,7 @@
+import sys
+from types import ModuleType
+
+from django.conf import FORMS_URLFIELD_ASSUME_HTTPS_DEPRECATED_MSG, Settings, settings
 from django.core.exceptions import ValidationError
 from django.forms import URLField
 from django.test import SimpleTestCase, ignore_warnings
@@ -155,8 +159,41 @@ class URLFieldAssumeSchemeDeprecationTest(FormFieldAssertionsMixin, SimpleTestCa
     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."
+            "Pass the forms.URLField.assume_scheme argument to silence this warning, "
+            "or set the FORMS_URLFIELD_ASSUME_HTTPS transitional setting to True to "
+            "opt into using 'https' as the new default scheme."
         )
         with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
             f = URLField()
             self.assertEqual(f.clean("example.com"), "http://example.com")
+
+    @ignore_warnings(category=RemovedInDjango60Warning)
+    def test_urlfield_forms_urlfield_assume_https(self):
+        with self.settings(FORMS_URLFIELD_ASSUME_HTTPS=True):
+            f = URLField()
+            self.assertEqual(f.clean("example.com"), "https://example.com")
+            f = URLField(assume_scheme="http")
+            self.assertEqual(f.clean("example.com"), "http://example.com")
+
+    def test_override_forms_urlfield_assume_https_setting_warning(self):
+        msg = FORMS_URLFIELD_ASSUME_HTTPS_DEPRECATED_MSG
+        with self.assertRaisesMessage(RemovedInDjango60Warning, msg):
+            # Changing FORMS_URLFIELD_ASSUME_HTTPS via self.settings() raises a
+            # deprecation warning.
+            with self.settings(FORMS_URLFIELD_ASSUME_HTTPS=True):
+                pass
+
+    def test_settings_init_forms_urlfield_assume_https_warning(self):
+        settings_module = ModuleType("fake_settings_module")
+        settings_module.FORMS_URLFIELD_ASSUME_HTTPS = True
+        sys.modules["fake_settings_module"] = settings_module
+        msg = FORMS_URLFIELD_ASSUME_HTTPS_DEPRECATED_MSG
+        try:
+            with self.assertRaisesMessage(RemovedInDjango60Warning, msg):
+                Settings("fake_settings_module")
+        finally:
+            del sys.modules["fake_settings_module"]
+
+    def test_access_forms_urlfield_assume_https(self):
+        # Warning is not raised on access.
+        self.assertEqual(settings.FORMS_URLFIELD_ASSUME_HTTPS, False)
diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py
index f7062711062..3f927cb0534 100644
--- a/tests/model_forms/tests.py
+++ b/tests/model_forms/tests.py
@@ -2930,7 +2930,8 @@ class ModelOtherFieldTests(SimpleTestCase):
         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."
+            "warning, or set the FORMS_URLFIELD_ASSUME_HTTPS transitional setting to "
+            "True to opt into using 'https' as the new default scheme."
         )
         with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
 
@@ -2939,6 +2940,18 @@ class ModelOtherFieldTests(SimpleTestCase):
                     model = Homepage
                     fields = "__all__"
 
+    def test_url_modelform_assume_scheme_early_adopt_https(self):
+        msg = "The FORMS_URLFIELD_ASSUME_HTTPS transitional setting is deprecated."
+        with (
+            self.assertWarnsMessage(RemovedInDjango60Warning, msg),
+            self.settings(FORMS_URLFIELD_ASSUME_HTTPS=True),
+        ):
+
+            class HomepageForm(forms.ModelForm):
+                class Meta:
+                    model = Homepage
+                    fields = "__all__"
+
     def test_modelform_non_editable_field(self):
         """
         When explicitly including a non-editable field in a ModelForm, the