Fixed #28377 -- Made combining form Media retain relative asset order.
Thanks Florian Apolloner, Mariusz Felisiak, and Tim Graham for reviews.
This commit is contained in:
parent
f86b6f351d
commit
c19b56f633
|
@ -304,7 +304,7 @@ class InlineAdminFormSet:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media(self):
|
def media(self):
|
||||||
media = self.opts.media + self.formset.media
|
media = self.formset.media + self.opts.media
|
||||||
for fs in self:
|
for fs in self:
|
||||||
media = media + fs.media
|
media = media + fs.media
|
||||||
return media
|
return media
|
||||||
|
|
|
@ -579,9 +579,9 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
def media(self):
|
def media(self):
|
||||||
extra = '' if settings.DEBUG else '.min'
|
extra = '' if settings.DEBUG else '.min'
|
||||||
js = [
|
js = [
|
||||||
'core.js',
|
|
||||||
'vendor/jquery/jquery%s.js' % extra,
|
'vendor/jquery/jquery%s.js' % extra,
|
||||||
'jquery.init.js',
|
'jquery.init.js',
|
||||||
|
'core.js',
|
||||||
'admin/RelatedObjectLookups.js',
|
'admin/RelatedObjectLookups.js',
|
||||||
'actions%s.js' % extra,
|
'actions%s.js' % extra,
|
||||||
'urlify.js',
|
'urlify.js',
|
||||||
|
|
|
@ -4,6 +4,7 @@ Form Widget classes specific to the Django admin site.
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.db.models.deletion import CASCADE
|
from django.db.models.deletion import CASCADE
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.urls.exceptions import NoReverseMatch
|
from django.urls.exceptions import NoReverseMatch
|
||||||
|
@ -22,7 +23,14 @@ class FilteredSelectMultiple(forms.SelectMultiple):
|
||||||
"""
|
"""
|
||||||
@property
|
@property
|
||||||
def media(self):
|
def media(self):
|
||||||
js = ["core.js", "SelectBox.js", "SelectFilter2.js"]
|
extra = '' if settings.DEBUG else '.min'
|
||||||
|
js = [
|
||||||
|
'vendor/jquery/jquery%s.js' % extra,
|
||||||
|
'jquery.init.js',
|
||||||
|
'core.js',
|
||||||
|
'SelectBox.js',
|
||||||
|
'SelectFilter2.js',
|
||||||
|
]
|
||||||
return forms.Media(js=["admin/js/%s" % path for path in js])
|
return forms.Media(js=["admin/js/%s" % path for path in js])
|
||||||
|
|
||||||
def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
|
def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
|
||||||
|
@ -43,7 +51,13 @@ class FilteredSelectMultiple(forms.SelectMultiple):
|
||||||
class AdminDateWidget(forms.DateInput):
|
class AdminDateWidget(forms.DateInput):
|
||||||
@property
|
@property
|
||||||
def media(self):
|
def media(self):
|
||||||
js = ["calendar.js", "admin/DateTimeShortcuts.js"]
|
extra = '' if settings.DEBUG else '.min'
|
||||||
|
js = [
|
||||||
|
'vendor/jquery/jquery%s.js' % extra,
|
||||||
|
'jquery.init.js',
|
||||||
|
'calendar.js',
|
||||||
|
'admin/DateTimeShortcuts.js',
|
||||||
|
]
|
||||||
return forms.Media(js=["admin/js/%s" % path for path in js])
|
return forms.Media(js=["admin/js/%s" % path for path in js])
|
||||||
|
|
||||||
def __init__(self, attrs=None, format=None):
|
def __init__(self, attrs=None, format=None):
|
||||||
|
@ -56,7 +70,13 @@ class AdminDateWidget(forms.DateInput):
|
||||||
class AdminTimeWidget(forms.TimeInput):
|
class AdminTimeWidget(forms.TimeInput):
|
||||||
@property
|
@property
|
||||||
def media(self):
|
def media(self):
|
||||||
js = ["calendar.js", "admin/DateTimeShortcuts.js"]
|
extra = '' if settings.DEBUG else '.min'
|
||||||
|
js = [
|
||||||
|
'vendor/jquery/jquery%s.js' % extra,
|
||||||
|
'jquery.init.js',
|
||||||
|
'calendar.js',
|
||||||
|
'admin/DateTimeShortcuts.js',
|
||||||
|
]
|
||||||
return forms.Media(js=["admin/js/%s" % path for path in js])
|
return forms.Media(js=["admin/js/%s" % path for path in js])
|
||||||
|
|
||||||
def __init__(self, attrs=None, format=None):
|
def __init__(self, attrs=None, format=None):
|
||||||
|
|
|
@ -2,6 +2,7 @@ from django.contrib.admin import ModelAdmin
|
||||||
from django.contrib.gis.admin.widgets import OpenLayersWidget
|
from django.contrib.gis.admin.widgets import OpenLayersWidget
|
||||||
from django.contrib.gis.db import models
|
from django.contrib.gis.db import models
|
||||||
from django.contrib.gis.gdal import OGRGeomType
|
from django.contrib.gis.gdal import OGRGeomType
|
||||||
|
from django.forms import Media
|
||||||
|
|
||||||
spherical_mercator_srid = 3857
|
spherical_mercator_srid = 3857
|
||||||
|
|
||||||
|
@ -46,10 +47,7 @@ class GeoModelAdmin(ModelAdmin):
|
||||||
@property
|
@property
|
||||||
def media(self):
|
def media(self):
|
||||||
"Injects OpenLayers JavaScript into the admin."
|
"Injects OpenLayers JavaScript into the admin."
|
||||||
media = super().media
|
return super().media + Media(js=[self.openlayers_url] + self.extra_js)
|
||||||
media.add_js([self.openlayers_url])
|
|
||||||
media.add_js(self.extra_js)
|
|
||||||
return media
|
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -5,6 +5,7 @@ HTML Widget classes
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
import warnings
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
|
@ -33,19 +34,23 @@ __all__ = (
|
||||||
MEDIA_TYPES = ('css', 'js')
|
MEDIA_TYPES = ('css', 'js')
|
||||||
|
|
||||||
|
|
||||||
|
class MediaOrderConflictWarning(RuntimeWarning):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@html_safe
|
@html_safe
|
||||||
class Media:
|
class Media:
|
||||||
def __init__(self, media=None, **kwargs):
|
def __init__(self, media=None, css=None, js=None):
|
||||||
if media:
|
if media is not None:
|
||||||
media_attrs = media.__dict__
|
css = getattr(media, 'css', {})
|
||||||
|
js = getattr(media, 'js', [])
|
||||||
else:
|
else:
|
||||||
media_attrs = kwargs
|
if css is None:
|
||||||
|
css = {}
|
||||||
self._css = {}
|
if js is None:
|
||||||
self._js = []
|
js = []
|
||||||
|
self._css = css
|
||||||
for name in MEDIA_TYPES:
|
self._js = js
|
||||||
getattr(self, 'add_' + name)(media_attrs.get(name))
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.render()
|
return self.render()
|
||||||
|
@ -88,24 +93,48 @@ class Media:
|
||||||
return Media(**{str(name): getattr(self, '_' + name)})
|
return Media(**{str(name): getattr(self, '_' + name)})
|
||||||
raise KeyError('Unknown media type "%s"' % name)
|
raise KeyError('Unknown media type "%s"' % name)
|
||||||
|
|
||||||
def add_js(self, data):
|
@staticmethod
|
||||||
if data:
|
def merge(list_1, list_2):
|
||||||
for path in data:
|
"""
|
||||||
if path not in self._js:
|
Merge two lists while trying to keep the relative order of the elements.
|
||||||
self._js.append(path)
|
Warn if the lists have the same two elements in a different relative
|
||||||
|
order.
|
||||||
|
|
||||||
def add_css(self, data):
|
For static assets it can be important to have them included in the DOM
|
||||||
if data:
|
in a certain order. In JavaScript you may not be able to reference a
|
||||||
for medium, paths in data.items():
|
global or in CSS you might want to override a style.
|
||||||
for path in paths:
|
"""
|
||||||
if not self._css.get(medium) or path not in self._css[medium]:
|
# Start with a copy of list_1.
|
||||||
self._css.setdefault(medium, []).append(path)
|
combined_list = list(list_1)
|
||||||
|
last_insert_index = len(list_1)
|
||||||
|
# Walk list_2 in reverse, inserting each element into combined_list if
|
||||||
|
# it doesn't already exist.
|
||||||
|
for path in reversed(list_2):
|
||||||
|
try:
|
||||||
|
# Does path already exist in the list?
|
||||||
|
index = combined_list.index(path)
|
||||||
|
except ValueError:
|
||||||
|
# Add path to combined_list since it doesn't exist.
|
||||||
|
combined_list.insert(last_insert_index, path)
|
||||||
|
else:
|
||||||
|
if index > last_insert_index:
|
||||||
|
warnings.warn(
|
||||||
|
'Detected duplicate Media files in an opposite order:\n'
|
||||||
|
'%s\n%s' % (combined_list[last_insert_index], combined_list[index]),
|
||||||
|
MediaOrderConflictWarning,
|
||||||
|
)
|
||||||
|
# path already exists in the list. Update last_insert_index so
|
||||||
|
# that the following elements are inserted in front of this one.
|
||||||
|
last_insert_index = index
|
||||||
|
return combined_list
|
||||||
|
|
||||||
def __add__(self, other):
|
def __add__(self, other):
|
||||||
combined = Media()
|
combined = Media()
|
||||||
for name in MEDIA_TYPES:
|
combined._js = self.merge(self._js, other._js)
|
||||||
getattr(combined, 'add_' + name)(getattr(self, '_' + name, None))
|
combined._css = {
|
||||||
getattr(combined, 'add_' + name)(getattr(other, '_' + name, None))
|
medium: self.merge(self._css.get(medium, []), other._css.get(medium, []))
|
||||||
|
for medium in self._css.keys() | other._css.keys()
|
||||||
|
}
|
||||||
return combined
|
return combined
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -554,6 +554,11 @@ Miscellaneous
|
||||||
* Renamed ``BaseExpression._output_field`` to ``output_field``. You may need
|
* Renamed ``BaseExpression._output_field`` to ``output_field``. You may need
|
||||||
to update custom expressions.
|
to update custom expressions.
|
||||||
|
|
||||||
|
* In older versions, forms and formsets combine their ``Media`` with widget
|
||||||
|
``Media`` by concatenating the two. The combining now tries to :ref:`preserve
|
||||||
|
the relative order of elements in each list <form-media-asset-order>`.
|
||||||
|
``MediaOrderConflictWarning`` is issued if the order can't be preserved.
|
||||||
|
|
||||||
.. _deprecated-features-2.0:
|
.. _deprecated-features-2.0:
|
||||||
|
|
||||||
Features deprecated in 2.0
|
Features deprecated in 2.0
|
||||||
|
|
|
@ -305,6 +305,42 @@ specified by both::
|
||||||
<script type="text/javascript" src="http://static.example.com/actions.js"></script>
|
<script type="text/javascript" src="http://static.example.com/actions.js"></script>
|
||||||
<script type="text/javascript" src="http://static.example.com/whizbang.js"></script>
|
<script type="text/javascript" src="http://static.example.com/whizbang.js"></script>
|
||||||
|
|
||||||
|
.. _form-media-asset-order:
|
||||||
|
|
||||||
|
Order of assets
|
||||||
|
---------------
|
||||||
|
|
||||||
|
The order in which assets are inserted into the DOM if often important. For
|
||||||
|
example, you may have a script that depends on jQuery. Therefore, combining
|
||||||
|
``Media`` objects attempts to preserve the relative order in which assets are
|
||||||
|
defined in each ``Media`` class.
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
|
>>> from django import forms
|
||||||
|
>>> class CalendarWidget(forms.TextInput):
|
||||||
|
... class Media:
|
||||||
|
... js = ('jQuery.js', 'calendar.js', 'noConflict.js')
|
||||||
|
>>> class TimeWidget(forms.TextInput):
|
||||||
|
... class Media:
|
||||||
|
... js = ('jQuery.js', 'time.js', 'noConflict.js')
|
||||||
|
>>> w1 = CalendarWidget()
|
||||||
|
>>> w2 = TimeWidget()
|
||||||
|
>>> print(w1.media + w2.media)
|
||||||
|
<script type="text/javascript" src="http://static.example.com/jQuery.js"></script>
|
||||||
|
<script type="text/javascript" src="http://static.example.com/calendar.js"></script>
|
||||||
|
<script type="text/javascript" src="http://static.example.com/time.js"></script>
|
||||||
|
<script type="text/javascript" src="http://static.example.com/noConflict.js"></script>
|
||||||
|
|
||||||
|
Combining ``Media`` objects with assets in a conflicting order results in a
|
||||||
|
``MediaOrderConflictWarning``.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
|
||||||
|
In older versions, the assets of ``Media`` objects are concatenated rather
|
||||||
|
than merged in a way that tries to preserve the relative ordering of the
|
||||||
|
elements in each list.
|
||||||
|
|
||||||
``Media`` on Forms
|
``Media`` on Forms
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django.forms import CharField, Form, Media, MultiWidget, TextInput
|
from django.forms import CharField, Form, Media, MultiWidget, TextInput
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
from django.test import SimpleTestCase, override_settings
|
from django.test import SimpleTestCase, override_settings
|
||||||
|
@ -106,7 +108,7 @@ class FormsMediaTestCase(SimpleTestCase):
|
||||||
class MyWidget3(TextInput):
|
class MyWidget3(TextInput):
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
'all': ('/path/to/css3', 'path/to/css1')
|
'all': ('path/to/css1', '/path/to/css3')
|
||||||
}
|
}
|
||||||
js = ('/path/to/js1', '/path/to/js4')
|
js = ('/path/to/js1', '/path/to/js4')
|
||||||
|
|
||||||
|
@ -237,9 +239,9 @@ class FormsMediaTestCase(SimpleTestCase):
|
||||||
w8 = MyWidget8()
|
w8 = MyWidget8()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
str(w8.media),
|
str(w8.media),
|
||||||
"""<link href="http://media.example.com/static/path/to/css1" type="text/css" media="all" rel="stylesheet" />
|
"""<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" />
|
||||||
|
<link href="http://media.example.com/static/path/to/css1" type="text/css" media="all" rel="stylesheet" />
|
||||||
<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" />
|
<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" />
|
||||||
<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" />
|
|
||||||
<script type="text/javascript" src="/path/to/js1"></script>
|
<script type="text/javascript" src="/path/to/js1"></script>
|
||||||
<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
|
<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
|
||||||
<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
|
<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
|
||||||
|
@ -312,9 +314,9 @@ class FormsMediaTestCase(SimpleTestCase):
|
||||||
w11 = MyWidget11()
|
w11 = MyWidget11()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
str(w11.media),
|
str(w11.media),
|
||||||
"""<link href="http://media.example.com/static/path/to/css1" type="text/css" media="all" rel="stylesheet" />
|
"""<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" />
|
||||||
|
<link href="http://media.example.com/static/path/to/css1" type="text/css" media="all" rel="stylesheet" />
|
||||||
<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" />
|
<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" />
|
||||||
<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" />
|
|
||||||
<script type="text/javascript" src="/path/to/js1"></script>
|
<script type="text/javascript" src="/path/to/js1"></script>
|
||||||
<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
|
<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
|
||||||
<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
|
<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
|
||||||
|
@ -341,9 +343,9 @@ class FormsMediaTestCase(SimpleTestCase):
|
||||||
w12 = MyWidget12()
|
w12 = MyWidget12()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
str(w12.media),
|
str(w12.media),
|
||||||
"""<link href="http://media.example.com/static/path/to/css1" type="text/css" media="all" rel="stylesheet" />
|
"""<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" />
|
||||||
|
<link href="http://media.example.com/static/path/to/css1" type="text/css" media="all" rel="stylesheet" />
|
||||||
<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" />
|
<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" />
|
||||||
<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" />
|
|
||||||
<script type="text/javascript" src="/path/to/js1"></script>
|
<script type="text/javascript" src="/path/to/js1"></script>
|
||||||
<script type="text/javascript" src="/path/to/js4"></script>"""
|
<script type="text/javascript" src="/path/to/js4"></script>"""
|
||||||
)
|
)
|
||||||
|
@ -396,7 +398,7 @@ class FormsMediaTestCase(SimpleTestCase):
|
||||||
class MyWidget3(TextInput):
|
class MyWidget3(TextInput):
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
'all': ('/path/to/css3', 'path/to/css1')
|
'all': ('path/to/css1', '/path/to/css3')
|
||||||
}
|
}
|
||||||
js = ('/path/to/js1', '/path/to/js4')
|
js = ('/path/to/js1', '/path/to/js4')
|
||||||
|
|
||||||
|
@ -441,7 +443,7 @@ class FormsMediaTestCase(SimpleTestCase):
|
||||||
class MyWidget3(TextInput):
|
class MyWidget3(TextInput):
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
'all': ('/path/to/css3', 'path/to/css1')
|
'all': ('path/to/css1', '/path/to/css3')
|
||||||
}
|
}
|
||||||
js = ('/path/to/js1', '/path/to/js4')
|
js = ('/path/to/js1', '/path/to/js4')
|
||||||
|
|
||||||
|
@ -518,3 +520,25 @@ class FormsMediaTestCase(SimpleTestCase):
|
||||||
media = Media(css={'all': ['/path/to/css']}, js=['/path/to/js'])
|
media = Media(css={'all': ['/path/to/css']}, js=['/path/to/js'])
|
||||||
self.assertTrue(hasattr(Media, '__html__'))
|
self.assertTrue(hasattr(Media, '__html__'))
|
||||||
self.assertEqual(str(media), media.__html__())
|
self.assertEqual(str(media), media.__html__())
|
||||||
|
|
||||||
|
def test_merge(self):
|
||||||
|
test_values = (
|
||||||
|
(([1, 2], [3, 4]), [1, 2, 3, 4]),
|
||||||
|
(([1, 2], [2, 3]), [1, 2, 3]),
|
||||||
|
(([2, 3], [1, 2]), [1, 2, 3]),
|
||||||
|
(([1, 3], [2, 3]), [1, 2, 3]),
|
||||||
|
(([1, 2], [1, 3]), [1, 2, 3]),
|
||||||
|
(([1, 2], [3, 2]), [1, 3, 2]),
|
||||||
|
)
|
||||||
|
for (list1, list2), expected in test_values:
|
||||||
|
with self.subTest(list1=list1, list2=list2):
|
||||||
|
self.assertEqual(Media.merge(list1, list2), expected)
|
||||||
|
|
||||||
|
def test_merge_warning(self):
|
||||||
|
with warnings.catch_warnings(record=True) as w:
|
||||||
|
warnings.simplefilter('always')
|
||||||
|
self.assertEqual(Media.merge([1, 2], [2, 1]), [1, 2])
|
||||||
|
self.assertEqual(
|
||||||
|
str(w[-1].message),
|
||||||
|
'Detected duplicate Media files in an opposite order:\n1\n2'
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue