Fixed #29490 -- Added support for object-based Media CSS and JS paths.
This commit is contained in:
parent
cda81b79f2
commit
4c76ffc2d6
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -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
|
||||
=================
|
||||
|
||||
|
|
|
@ -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>',
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue