44See https://github.com/django/django/commit/c19b56f633e172b3c02094cbe12d28865ee57772
55and https://code.djangoproject.com/ticket/28377
66"""
7+ from collections import defaultdict , OrderedDict
78import warnings
89
910import django
1011import django .forms
12+ from django .utils .datastructures import OrderedSet as _OrderedSet
1113
1214try :
1315 from django .forms .widgets import MediaOrderConflictWarning
@@ -19,8 +21,57 @@ class MediaOrderConflictWarning(RuntimeWarning):
1921else :
2022 MergeSafeMedia = django .forms .Media
2123
24+ try :
25+ from django .utils .topological_sort import (
26+ CyclicDependencyError , stable_topological_sort )
27+ except ImportError :
28+ class CyclicDependencyError (ValueError ):
29+ pass
30+
31+ stable_topological_sort = None
32+
33+
34+ class OrderedSet (_OrderedSet ):
35+ def __sub__ (self , other ):
36+ return self .__class__ ([i for i in self if i not in other ])
37+
38+ def __str__ (self ):
39+ return "OrderedSet(%r)" % list (self )
40+
41+ def __repr__ (self ):
42+ return "OrderedSet(%r)" % list (self )
43+
44+
45+ if MergeSafeMedia is None or stable_topological_sort is None :
46+ def linearize_as_needed (l , dependency_graph ):
47+ # Algorithm: DFS Topological sort
48+ # https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
49+ l = list (dependency_graph )
50+
51+ temporary = set ()
52+ permanent = set ()
53+
54+ result = []
55+
56+ def visit (vertices ):
57+ for vertex in vertices :
58+ if vertex in permanent :
59+ pass
60+ elif vertex in temporary :
61+ raise CyclicDependencyError ('Cyclic dependency in graph' )
62+ else :
63+ temporary .add (vertex )
64+
65+ visit (dependency_graph [vertex ])
66+
67+ result .append (vertex )
68+
69+ temporary .remove (vertex )
70+ permanent .add (vertex )
71+
72+ visit (l )
73+ return result
2274
23- if MergeSafeMedia is None :
2475 class MergeSafeMedia (django .forms .Media ):
2576 def __init__ (self , media = None , css = None , js = None ):
2677 if media is not None :
@@ -31,59 +82,112 @@ def __init__(self, media=None, css=None, js=None):
3182 css = {}
3283 if js is None :
3384 js = []
34- self ._css = css
35- self ._js = js
85+ self ._css_lists = [css ]
86+ self ._js_lists = [js ]
87+
88+ @property
89+ def _css (self ):
90+ css = defaultdict (list )
91+ for css_list in self ._css_lists :
92+ for medium , sublist in css_list .items ():
93+ css [medium ].append (sublist )
94+ return {medium : self .merge (* lists ) for medium , lists in css .items ()}
95+
96+ @_css .setter
97+ def _css (self , data ):
98+ css_media = ensure_merge_safe_media (django .forms .Media (css = data ))
99+ self ._css_lists = css_media ._css_lists
100+
101+ @property
102+ def _js (self ):
103+ return self .merge (* self ._js_lists )
104+
105+ @_js .setter
106+ def _js (self , data ):
107+ js_media = ensure_merge_safe_media (django .forms .Media (js = data ))
108+ self ._js_lists = js_media ._js_lists
36109
37110 @staticmethod
38- def merge (list_1 , list_2 ):
111+ def merge (* lists ):
39112 """
40- Merge two lists while trying to keep the relative order of the elements.
41- Warn if the lists have the same two elements in a different relative
42- order.
113+ Merge lists while trying to keep the relative order of the elements.
114+ Warn if the lists have the same elements in a different relative order.
43115
44116 For static assets it can be important to have them included in the DOM
45117 in a certain order. In JavaScript you may not be able to reference a
46118 global or in CSS you might want to override a style.
47119 """
48- # Start with a copy of list_1.
49- combined_list = list (list_1 )
50- last_insert_index = len (list_1 )
51- # Walk list_2 in reverse, inserting each element into combined_list if
52- # it doesn't already exist.
53- for path in reversed (list_2 ):
54- try :
55- # Does path already exist in the list?
56- index = combined_list .index (path )
57- except ValueError :
58- # Add path to combined_list since it doesn't exist.
59- combined_list .insert (last_insert_index , path )
60- else :
61- if index > last_insert_index :
62- warnings .warn (
63- 'Detected duplicate Media files in an opposite order:\n '
64- '%s\n %s' % (combined_list [last_insert_index ], combined_list [index ]),
65- MediaOrderConflictWarning ,
66- )
67- # path already exists in the list. Update last_insert_index so
68- # that the following elements are inserted in front of this one.
69- last_insert_index = index
70- return combined_list
120+ dependency_graph = OrderedDict ()
121+ all_items = OrderedSet ()
122+
123+ for list_ in filter (None , lists ):
124+ head = list_ [0 ]
125+ # The first items depend on nothing but have to be part of the
126+ # dependency graph to be included in the result.
127+ dependency_graph .setdefault (head , OrderedSet ())
128+ for item in list_ :
129+ all_items .add (item )
130+ # No self dependencies
131+ if head != item :
132+ dependency_graph .setdefault (item , OrderedSet ())
133+ dependency_graph [item ].add (head )
134+ head = item
135+ try :
136+ return linearize_as_needed (all_items , dependency_graph )
137+ except CyclicDependencyError :
138+ warnings .warn (
139+ 'Detected duplicate Media files in an opposite order: {}' .format (
140+ ', ' .join (repr (l ) for l in lists )
141+ ), MediaOrderConflictWarning ,
142+ )
143+ return list (all_items )
71144
72145 def __add__ (self , other ):
73146 combined = MergeSafeMedia ()
74- combined ._js = self .merge (self ._js , other ._js )
75- combined ._css = {
76- medium : self .merge (self ._css .get (medium , []), other ._css .get (medium , []))
77- for medium in set (self ._css .keys ()) | set (other ._css .keys ())
78- }
147+ other_css_lists = getattr (other , '_css_lists' , None )
148+ if other_css_lists is None :
149+ other_css_lists = []
150+ for medium , css_list in other ._css .items ():
151+ for css_file in css_list :
152+ other_css_lists .append ({medium : [css_file ]})
153+ other_js_lists = getattr (other , '_js_lists' , None ) or [[js ] for js in other ._js ]
154+ combined ._css_lists = self ._css_lists + other_css_lists
155+ combined ._js_lists = self ._js_lists + other_js_lists
79156 return combined
80157
81158 def add_js (self , data ):
82159 if data :
83160 new_media = self + MergeSafeMedia (js = data )
84- self ._js = new_media ._js
161+ self ._js_lists = new_media ._js_lists
85162
86163 def add_css (self , data ):
87164 if data :
88165 new_media = self + MergeSafeMedia (css = data )
89- self ._css = new_media ._css
166+ self ._css_lists = new_media ._css_lists
167+
168+
169+ def ensure_merge_safe_media (media ):
170+ if isinstance (media , MergeSafeMedia ):
171+ return media
172+ safe_media = MergeSafeMedia ()
173+ for js in media ._js :
174+ safe_media += MergeSafeMedia (js = [js ])
175+ for medium , css_files in media ._css .items ():
176+ for css_file in css_files :
177+ safe_media += MergeSafeMedia (css = {medium : [css_file ]})
178+ return safe_media
179+
180+
181+ try :
182+ import polymorphic .admin .inlines
183+ import polymorphic .formsets .utils
184+ import polymorphic .formsets .models
185+ except ImportError :
186+ pass
187+ else :
188+ def add_media (dest , media ):
189+ return dest + media
190+
191+ polymorphic .formsets .utils .add_media = add_media
192+ polymorphic .formsets .utils .add_media = add_media
193+ polymorphic .formsets .models .add_media = add_media
0 commit comments