summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaude Paroz <claude@2xlibre.net>2022-01-22 17:21:57 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-02-10 08:48:27 +0100
commit4c76ffc2d6c77c850b4bef8d9acc197d11c47937 (patch)
tree943f449e397f362057c3f57906e7bdcc49fcbb2f
parentcda81b79f212e0666782393c52ad19c2790c9446 (diff)
Fixed #29490 -- Added support for object-based Media CSS and JS paths.
-rw-r--r--django/forms/widgets.py8
-rw-r--r--docs/releases/4.1.txt5
-rw-r--r--docs/topics/forms/media.txt27
-rw-r--r--tests/forms_tests/tests/test_media.py159
4 files changed, 196 insertions, 3 deletions
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('<script src="{}"></script>', self.absolute_path(path))
+ path.__html__()
+ if hasattr(path, "__html__")
+ else format_html('<script src="{}"></script>', 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(
'<link href="{}" media="{}" rel="stylesheet">',
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 </topics/forms/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
<script src="https://static.example.com/animations.27e20196a850.js"></script>
<script src="http://othersite.com/actions.js"></script>
+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 ``<script>`` or ``<link>`` tag content::
+
+ >>> from django import forms
+ >>> from django.utils.html import html_safe
+ >>>
+ >>> @html_safe
+ >>> class JSPath:
+ ... def __str__(self):
+ ... return '<script src="https://example.org/asset.js" rel="stylesheet">'
+
+ >>> class SomeWidget(forms.TextInput):
+ ... class Media:
+ ... js = (JSPath(),)
+
``Media`` objects
=================
diff --git a/tests/forms_tests/tests/test_media.py b/tests/forms_tests/tests/test_media.py
index 4338321af8..171b70e508 100644
--- a/tests/forms_tests/tests/test_media.py
+++ b/tests/forms_tests/tests/test_media.py
@@ -1,6 +1,8 @@
from django.forms import CharField, Form, Media, MultiWidget, TextInput
from django.template import Context, Template
+from django.templatetags.static import static
from django.test import SimpleTestCase, override_settings
+from django.utils.html import format_html, html_safe
@override_settings(
@@ -710,3 +712,160 @@ class FormsMediaTestCase(SimpleTestCase):
merged = media + empty_media
self.assertEqual(merged._css_lists, [{"screen": ["a.css"]}])
self.assertEqual(merged._js_lists, [["a"]])
+
+
+@html_safe
+class Asset:
+ def __init__(self, path):
+ self.path = path
+
+ def __eq__(self, other):
+ return (self.__class__ == other.__class__ and self.path == other.path) or (
+ other.__class__ == str and self.path == other
+ )
+
+ def __hash__(self):
+ return hash(self.path)
+
+ def __str__(self):
+ return self.absolute_path(self.path)
+
+ def absolute_path(self, path):
+ """
+ Given a relative or absolute path to a static asset, return an absolute
+ path. An absolute path will be returned unchanged while a relative path
+ will be passed to django.templatetags.static.static().
+ """
+ if path.startswith(("http://", "https://", "/")):
+ return path
+ return static(path)
+
+ def __repr__(self):
+ return f"{self.path!r}"
+
+
+class CSS(Asset):
+ def __init__(self, path, medium):
+ super().__init__(path)
+ self.medium = medium
+
+ def __str__(self):
+ path = super().__str__()
+ return format_html(
+ '<link href="{}" media="{}" rel="stylesheet">',
+ self.absolute_path(path),
+ self.medium,
+ )
+
+
+class JS(Asset):
+ def __init__(self, path, integrity=None):
+ super().__init__(path)
+ self.integrity = integrity or ""
+
+ def __str__(self, integrity=None):
+ path = super().__str__()
+ template = '<script src="{}"%s></script>' % (
+ ' 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),
+ '<link href="http://media.example.com/static/path/to/css1" media="all" '
+ 'rel="stylesheet">\n'
+ '<link href="/path/to/css2" media="all" rel="stylesheet">\n'
+ '<script src="/path/to/js1"></script>\n'
+ '<script src="http://media.other.com/path/to/js2"></script>\n'
+ '<script src="https://secure.other.com/path/to/js3" '
+ 'integrity="9d947b87fdeb25030d56d01f7aa75800"></script>',
+ )
+ 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 '<script src="https://example.org/asset.js" rel="stylesheet">'
+
+ m = Media(js=(SimpleJS(),))
+ self.assertEqual(
+ str(m),
+ '<script src="https://example.org/asset.js" rel="stylesheet">',
+ )
+
+ def test_combine_media(self):
+ class MyWidget1(TextInput):
+ class Media:
+ css = {"all": (CSS("path/to/css1", "all"), "/path/to/css2")}
+ js = (
+ "/path/to/js1",
+ "http://media.other.com/path/to/js2",
+ "https://secure.other.com/path/to/js3",
+ JS("/path/to/js4", integrity="9d947b87fdeb25030d56d01f7aa75800"),
+ )
+
+ class MyWidget2(TextInput):
+ class Media:
+ css = {"all": (CSS("/path/to/css2", "all"), "/path/to/css3")}
+ js = (JS("/path/to/js1"), "/path/to/js4")
+
+ w1 = MyWidget1()
+ w2 = MyWidget2()
+ self.assertEqual(
+ str(w1.media + w2.media),
+ '<link href="http://media.example.com/static/path/to/css1" media="all" '
+ 'rel="stylesheet">\n'
+ '<link href="/path/to/css2" media="all" rel="stylesheet">\n'
+ '<link href="/path/to/css3" media="all" rel="stylesheet">\n'
+ '<script src="/path/to/js1"></script>\n'
+ '<script src="http://media.other.com/path/to/js2"></script>\n'
+ '<script src="https://secure.other.com/path/to/js3"></script>\n'
+ '<script src="/path/to/js4" integrity="9d947b87fdeb25030d56d01f7aa75800">'
+ "</script>",
+ )
+
+ 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),
+ '<link href="/path/to/css1" media="all" rel="stylesheet">\n'
+ '<script src="/path/to/js1"></script>',
+ )