Code

Ticket #4418: media.diff

File media.diff, 9.8 KB (added by russellm, 7 years ago)

Adds a Media descriptor to newforms widgets

Line 
1Index: django/newforms/forms.py
2===================================================================
3--- django/newforms/forms.py    (revision 5380)
4+++ django/newforms/forms.py    (working copy)
5@@ -9,7 +9,7 @@
6 from django.utils.encoding import StrAndUnicode
7 
8 from fields import Field
9-from widgets import TextInput, Textarea
10+from widgets import Media, TextInput, Textarea
11 from util import flatatt, ErrorDict, ErrorList, ValidationError
12 
13 __all__ = ('BaseForm', 'Form')
14@@ -209,6 +209,16 @@
15         """
16         return self.cleaned_data
17 
18+    def _get_media(self):
19+        """
20+        Provide a description of all media required to render the widgets on this form
21+        """
22+        m = Media()
23+        for field in self.fields.values():
24+            m = m + field.widget.media
25+        return m
26+    media = property(_get_media)
27+   
28 class Form(BaseForm):
29     "A collection of Fields, plus their associated data."
30     # This is a separate class from BaseForm in order to abstract the way
31Index: django/newforms/widgets.py
32===================================================================
33--- django/newforms/widgets.py  (revision 5380)
34+++ django/newforms/widgets.py  (working copy)
35@@ -8,6 +8,7 @@
36     from sets import Set as set # Python 2.3 fallback
37 from itertools import chain
38 
39+from django.conf import settings
40 from django.utils.datastructures import MultiValueDict
41 from django.utils.html import escape
42 from django.utils.translation import gettext
43@@ -16,14 +17,74 @@
44 from util import flatatt
45 
46 __all__ = (
47-    'Widget', 'TextInput', 'PasswordInput',
48+    'Media', 'Widget', 'TextInput', 'PasswordInput',
49     'HiddenInput', 'MultipleHiddenInput',
50     'FileInput', 'Textarea', 'CheckboxInput',
51     'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
52     'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
53 )
54 
55+MEDIA_TYPES = ('css','js')
56+
57+class Media(StrAndUnicode):
58+    def __init__(self, media=None, **kwargs):
59+        if media:
60+            media_attrs = media.__dict__
61+            del media_attrs['__module__']
62+            del media_attrs['__doc__']
63+        else:
64+            media_attrs = kwargs
65+           
66+        for attr_name in MEDIA_TYPES:
67+            setattr(self, '_' + attr_name, media_attrs.pop(attr_name, []))
68+
69+        # Any leftover attributes must be invalid.
70+        if media_attrs != {}:
71+            raise TypeError, "'class Media' has invalid attribute(s): %s" % ','.join(media_attrs.keys())
72+       
73+    def __unicode__(self):
74+        return self.render()
75+       
76+    def render(self):
77+        output = []
78+        for name in MEDIA_TYPES:
79+            for path in getattr(self, '_' + name):
80+                output.append(getattr(self, 'render_' + name)(path))
81+        return u'\n'.join(output)
82+       
83+    def render_js(self, path):
84+        return u'<script type="text/javascript" src="%s" />' % self.absolute_path(path)
85+       
86+    def render_css(self, path):
87+        return u'<link href="%s" type="text/css" rel="stylesheet" />' % self.absolute_path(path)
88+
89+    def absolute_path(self, path):
90+        return path.startswith('http://') and path or u''.join([settings.MEDIA_URL,path])
91+
92+    def __getitem__(self, name):
93+        "Returns a Media object that only contains media of the given type"
94+        if name in MEDIA_TYPES:
95+            return Media(**{name: getattr(self, '_' + name)})
96+        raise KeyError('Unknown media type "%s"' % name)
97+
98+    def __add__(self, other):
99+        "Combine two media objects to produce the union of all media"
100+        combined = {}
101+        for name in MEDIA_TYPES:
102+            combined[name] = list(getattr(self, '_' + name, [])) + \
103+                [m for m in getattr(other, '_' + name, []) if m not in getattr(self, '_' + name, []) ]
104+        return Media(**combined)
105+       
106+class WidgetBase(type):
107+    "Metaclass for all widgets"
108+    def __new__(cls, name, bases, attrs):       
109+        media = Media(attrs.pop('Media', None))
110+        new_class = type.__new__(cls, name, bases, attrs)
111+        setattr(new_class, 'media', media)
112+        return new_class
113+       
114 class Widget(object):
115+    __metaclass__ = WidgetBase
116     is_hidden = False          # Determines whether this corresponds to an <input type="hidden">.
117 
118     def __init__(self, attrs=None):
119Index: tests/regressiontests/forms/media.py
120===================================================================
121--- tests/regressiontests/forms/media.py        (revision 0)
122+++ tests/regressiontests/forms/media.py        (revision 0)
123@@ -0,0 +1,109 @@
124+# -*- coding: utf-8 -*-
125+# Tests for the media handling on widgets and forms
126+
127+media_tests = r"""
128+>>> from django.newforms import TextInput, Media, TextInput, CharField, Form
129+>>> from django.conf import settings
130+>>> settings.MEDIA_URL = 'http://media.example.com'
131+
132+# A widget can exist without a media definition
133+>>> class MyWidget(TextInput):
134+...     pass
135+
136+>>> w = MyWidget()
137+>>> print w.media
138+<BLANKLINE>
139+
140+# A widget can define media if it needs to.
141+# Any absolute path will be preserved; relative paths are combined
142+# with the value of settings.MEDIA_URL
143+>>> class MyWidget1(TextInput):
144+...     class Media:
145+...         css = ('/path/to/css1','/path/to/css2')
146+...         js = ('/path/to/js1','http://media.other.com/path/to/js2')
147+
148+>>> w1 = MyWidget1()
149+>>> print w1.media
150+<link href="http://media.example.com/path/to/css1" type="text/css" rel="stylesheet" />
151+<link href="http://media.example.com/path/to/css2" type="text/css" rel="stylesheet" />
152+<script type="text/javascript" src="http://media.example.com/path/to/js1" />
153+<script type="text/javascript" src="http://media.other.com/path/to/js2" />
154+
155+# Media objects can be interrogated by media type
156+>>> print w1.media['css']
157+<link href="http://media.example.com/path/to/css1" type="text/css" rel="stylesheet" />
158+<link href="http://media.example.com/path/to/css2" type="text/css" rel="stylesheet" />
159+
160+>>> print w1.media['js']
161+<script type="text/javascript" src="http://media.example.com/path/to/js1" />
162+<script type="text/javascript" src="http://media.other.com/path/to/js2" />
163+
164+# Media objects can be combined. Any given media resource will appear only
165+# once. Duplicated media definitions are ignored.
166+>>> class MyWidget2(TextInput):
167+...     class Media:
168+...         css = ('/path/to/css2','/path/to/css3')
169+...         js = ('/path/to/js1','/path/to/js3')
170+
171+>>> class MyWidget3(TextInput):
172+...     class Media:
173+...         css = ('/path/to/css3','/path/to/css1')
174+...         js = ('/path/to/js1','/path/to/js3')
175+
176+>>> w2 = MyWidget2()
177+>>> w3 = MyWidget3()
178+>>> print w1.media + w2.media + w3.media
179+<link href="http://media.example.com/path/to/css1" type="text/css" rel="stylesheet" />
180+<link href="http://media.example.com/path/to/css2" type="text/css" rel="stylesheet" />
181+<link href="http://media.example.com/path/to/css3" type="text/css" rel="stylesheet" />
182+<script type="text/javascript" src="http://media.example.com/path/to/js1" />
183+<script type="text/javascript" src="http://media.other.com/path/to/js2" />
184+<script type="text/javascript" src="http://media.example.com/path/to/js3" />
185+
186+# If a widget extends another, media must be redefined
187+>>> class MyWidget4(MyWidget1):
188+...     pass
189+
190+>>> w4 = MyWidget4()
191+>>> print w4.media
192+<BLANKLINE>
193+
194+# If a widget extends another, media from the parent widget is ignored
195+>>> class MyWidget5(MyWidget1):
196+...     class Media:
197+...         css = ('/path/to/css3','/path/to/css1')
198+...         js = ('/path/to/js1','/path/to/js3')
199+
200+>>> w5 = MyWidget5()
201+>>> print w5.media
202+<link href="http://media.example.com/path/to/css3" type="text/css" rel="stylesheet" />
203+<link href="http://media.example.com/path/to/css1" type="text/css" rel="stylesheet" />
204+<script type="text/javascript" src="http://media.example.com/path/to/js1" />
205+<script type="text/javascript" src="http://media.example.com/path/to/js3" />
206+
207+# You can ask a form for the media required by its widgets.
208+>>> class MyForm(Form):
209+...     field1 = CharField(max_length=20, widget=MyWidget1())
210+...     field2 = CharField(max_length=20, widget=MyWidget2())
211+>>> f1 = MyForm()
212+>>> print f1.media
213+<link href="http://media.example.com/path/to/css1" type="text/css" rel="stylesheet" />
214+<link href="http://media.example.com/path/to/css2" type="text/css" rel="stylesheet" />
215+<link href="http://media.example.com/path/to/css3" type="text/css" rel="stylesheet" />
216+<script type="text/javascript" src="http://media.example.com/path/to/js1" />
217+<script type="text/javascript" src="http://media.other.com/path/to/js2" />
218+<script type="text/javascript" src="http://media.example.com/path/to/js3" />
219+
220+# Form media can be combined to produce a single media definition.
221+>>> class AnotherForm(Form):
222+...     field3 = CharField(max_length=20, widget=MyWidget3())
223+>>> f2 = AnotherForm()
224+>>> print f1.media + f2.media
225+<link href="http://media.example.com/path/to/css1" type="text/css" rel="stylesheet" />
226+<link href="http://media.example.com/path/to/css2" type="text/css" rel="stylesheet" />
227+<link href="http://media.example.com/path/to/css3" type="text/css" rel="stylesheet" />
228+<script type="text/javascript" src="http://media.example.com/path/to/js1" />
229+<script type="text/javascript" src="http://media.other.com/path/to/js2" />
230+<script type="text/javascript" src="http://media.example.com/path/to/js3" />
231+
232+"""
233\ No newline at end of file
234Index: tests/regressiontests/forms/tests.py
235===================================================================
236--- tests/regressiontests/forms/tests.py        (revision 5380)
237+++ tests/regressiontests/forms/tests.py        (working copy)
238@@ -1,6 +1,7 @@
239 # -*- coding: utf-8 -*-
240 from localflavor import localflavor_tests
241 from regressions import regression_tests
242+from media import media_tests
243 
244 form_tests = r"""
245 >>> from django.newforms import *
246@@ -3677,6 +3678,7 @@
247 """
248 
249 __test__ = {
250+    'media_tests': media_tests,
251     'form_tests': form_tests,
252     'localflavor': localflavor_tests,
253     'regressions': regression_tests,