diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index 8c5122ad1d..208464b60a 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -101,7 +101,9 @@ class Media:
def render_js(self):
return [
- format_html('', self.absolute_path(path))
+ path.__html__()
+ if hasattr(path, "__html__")
+ else format_html('', self.absolute_path(path))
for path in self._js
]
@@ -111,7 +113,9 @@ class Media:
media = sorted(self._css)
return chain.from_iterable(
[
- format_html(
+ path.__html__()
+ if hasattr(path, "__html__")
+ else format_html(
'',
self.absolute_path(path),
medium,
diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index 02d51ed2a5..4b62cf09cf 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -192,6 +192,11 @@ Forms
* The new ``edit_only`` argument for :func:`.modelformset_factory` and
:func:`.inlineformset_factory` allows preventing new objects creation.
+* The ``js`` and ``css`` class attributes of :doc:`Media `
+ now allow using hashable objects, not only path strings, as long as those
+ objects implement the ``__html__()`` method (typically when decorated with
+ the :func:`~django.utils.html.html_safe` decorator).
+
Generic Views
~~~~~~~~~~~~~
diff --git a/docs/topics/forms/media.txt b/docs/topics/forms/media.txt
index 6ca7c66fde..7e5a04e3d9 100644
--- a/docs/topics/forms/media.txt
+++ b/docs/topics/forms/media.txt
@@ -206,7 +206,10 @@ return values for dynamic ``media`` properties.
Paths in asset definitions
==========================
-Paths used to specify assets can be either relative or absolute. If a
+Paths as strings
+----------------
+
+String paths used to specify assets can be either relative or absolute. If a
path starts with ``/``, ``http://`` or ``https://``, it will be
interpreted as an absolute path, and left as-is. All other paths will
be prepended with the value of the appropriate prefix. If the
@@ -254,6 +257,28 @@ Or if :mod:`~django.contrib.staticfiles` is configured using the
+Paths as objects
+----------------
+
+.. versionadded:: 4.1
+
+Asset paths may also be given as hashable objects implementing an
+``__html__()`` method. The ``__html__()`` method is typically added using the
+:func:`~django.utils.html.html_safe` decorator. The object is responsible for
+outputting the complete HTML ``' % (
+ ' integrity="{}"' if self.integrity else "{}"
+ )
+ return format_html(template, self.absolute_path(path), self.integrity)
+
+
+@override_settings(
+ STATIC_URL="http://media.example.com/static/",
+)
+class FormsMediaObjectTestCase(SimpleTestCase):
+ """Media handling when media are objects instead of raw strings."""
+
+ def test_construction(self):
+ m = Media(
+ css={"all": (CSS("path/to/css1", "all"), CSS("/path/to/css2", "all"))},
+ js=(
+ JS("/path/to/js1"),
+ JS("http://media.other.com/path/to/js2"),
+ JS(
+ "https://secure.other.com/path/to/js3",
+ integrity="9d947b87fdeb25030d56d01f7aa75800",
+ ),
+ ),
+ )
+ self.assertEqual(
+ str(m),
+ '\n'
+ '\n'
+ '\n'
+ '\n'
+ '',
+ )
+ self.assertEqual(
+ repr(m),
+ "Media(css={'all': ['path/to/css1', '/path/to/css2']}, "
+ "js=['/path/to/js1', 'http://media.other.com/path/to/js2', "
+ "'https://secure.other.com/path/to/js3'])",
+ )
+
+ def test_simplest_class(self):
+ @html_safe
+ class SimpleJS:
+ """The simplest possible asset class."""
+
+ def __str__(self):
+ return '\n'
+ '\n'
+ '\n'
+ '",
+ )
+
+ def test_media_deduplication(self):
+ # The deduplication doesn't only happen at the point of merging two or
+ # more media objects.
+ media = Media(
+ css={
+ "all": (
+ CSS("/path/to/css1", "all"),
+ CSS("/path/to/css1", "all"),
+ "/path/to/css1",
+ )
+ },
+ js=(JS("/path/to/js1"), JS("/path/to/js1"), "/path/to/js1"),
+ )
+ self.assertEqual(
+ str(media),
+ '\n'
+ '',
+ )