Code

Ticket #4418: newforms-admin-media.5651.diff

File newforms-admin-media.5651.diff, 43.1 KB (added by russellm, 7 years ago)

Improved media descriptors for newforms-admin; against [5651]

Line 
1Index: django/contrib/admin/options.py
2===================================================================
3--- django/contrib/admin/options.py     (revision 5651)
4+++ django/contrib/admin/options.py     (working copy)
5@@ -2,6 +2,7 @@
6 from django import newforms as forms
7 from django.newforms.formsets import all_valid
8 from django.newforms.models import inline_formset
9+from django.newforms.widgets import Media, MediaDefiningClass
10 from django.contrib.admin import widgets
11 from django.core.exceptions import ImproperlyConfigured, PermissionDenied
12 from django.db import models
13@@ -48,6 +49,13 @@
14     def first_field(self):
15         for bf in self.form:
16             return bf
17+           
18+    def _media(self):
19+        media = self.form.media
20+        for fs in self.fieldsets:
21+            media = media + fs.media
22+        return media
23+    media = property(_media)
24 
25 class Fieldset(object):
26     def __init__(self, name=None, fields=(), classes=(), description=None):
27@@ -55,6 +63,13 @@
28         self.classes = u' '.join(classes)
29         self.description = description
30 
31+    def _media(self):
32+        from django.conf import settings
33+        if 'collapse' in self.classes:
34+            return Media(js=['%sjs/admin/CollapsedFieldsets.js' % settings.ADMIN_MEDIA_PREFIX])
35+        return Media()
36+    media = property(_media)
37+   
38 class BoundFieldset(object):
39     def __init__(self, form, fieldset):
40         self.form, self.fieldset = form, fieldset
41@@ -123,12 +138,12 @@
42 
43         # For DateFields, add a custom CSS class.
44         if isinstance(db_field, models.DateField):
45-            kwargs['widget'] = forms.TextInput(attrs={'class': 'vDateField', 'size': '10'})
46+            kwargs['widget'] = widgets.AdminDateWidget
47             return db_field.formfield(**kwargs)
48 
49         # For TimeFields, add a custom CSS class.
50         if isinstance(db_field, models.TimeField):
51-            kwargs['widget'] = forms.TextInput(attrs={'class': 'vTimeField', 'size': '8'})
52+            kwargs['widget'] = widgets.AdminTimeWidget
53             return db_field.formfield(**kwargs)
54 
55         # For ForeignKey or ManyToManyFields, use a special widget.
56@@ -148,7 +163,8 @@
57 
58 class ModelAdmin(BaseModelAdmin):
59     "Encapsulates all admin options and functionality for a given model."
60-
61+    __metaclass__ = MediaDefiningClass
62+   
63     list_display = ('__str__',)
64     list_display_links = ()
65     list_filter = ()
66@@ -159,7 +175,6 @@
67     save_as = False
68     save_on_top = False
69     ordering = None
70-    js = None
71     prepopulated_fields = {}
72     filter_vertical = ()
73     filter_horizontal = ()
74@@ -194,38 +209,20 @@
75         else:
76             return self.change_view(request, unquote(url))
77 
78-    def javascript(self, request, fieldsets):
79-        """
80-        Returns a list of URLs to include via <script> statements.
81+    def _media(self):
82+        from django.conf import settings
83 
84-        The URLs can be absolute ('/js/admin/') or explicit
85-        ('http://example.com/foo.js').
86-        """
87-        from django.conf import settings
88         js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
89         if self.prepopulated_fields:
90             js.append('js/urlify.js')
91-        if self.opts.has_field_type(models.DateTimeField) or self.opts.has_field_type(models.TimeField) or self.opts.has_field_type(models.DateField):
92-            js.extend(['js/calendar.js', 'js/admin/DateTimeShortcuts.js'])
93         if self.opts.get_ordered_objects():
94             js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js'])
95-        if self.js:
96-            js.extend(self.js)
97         if self.filter_vertical or self.filter_horizontal:
98             js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
99-        for fs in fieldsets:
100-            if 'collapse' in fs.classes:
101-                js.append('js/admin/CollapsedFieldsets.js')
102-                break
103-        prefix = settings.ADMIN_MEDIA_PREFIX
104-        return ['%s%s' % (prefix, url) for url in js]
105-
106-    def javascript_add(self, request):
107-        return self.javascript(request, self.fieldsets_add(request))
108-
109-    def javascript_change(self, request, obj):
110-        return self.javascript(request, self.fieldsets_change(request, obj))
111-
112+       
113+        return Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
114+    media = property(_media)
115+   
116     def fieldsets(self, request):
117         """
118         Generator that yields Fieldset objects for use on add and change admin
119@@ -244,13 +241,11 @@
120 
121     def fieldsets_add(self, request):
122         "Hook for specifying Fieldsets for the add form."
123-        for fs in self.fieldsets(request):
124-            yield fs
125+        return list(self.fieldsets(request))
126 
127     def fieldsets_change(self, request, obj):
128         "Hook for specifying Fieldsets for the change form."
129-        for fs in self.fieldsets(request):
130-            yield fs
131+        return list(self.fieldsets(request))
132 
133     def has_add_permission(self, request):
134         "Returns True if the given request has permission to add an object."
135@@ -433,12 +428,17 @@
136                 inline_formset = FormSet()
137                 inline_formsets.append(inline_formset)
138 
139+        adminForm = AdminForm(form, list(self.fieldsets_add(request)), self.prepopulated_fields)
140+        media = self.media + adminForm.media
141+        for fs in inline_formsets:
142+            media = media + fs.media
143+           
144         c = template.RequestContext(request, {
145             'title': _('Add %s') % opts.verbose_name,
146-            'adminform': AdminForm(form, self.fieldsets_add(request), self.prepopulated_fields),
147+            'adminform': adminForm,
148             'is_popup': request.REQUEST.has_key('_popup'),
149             'show_delete': False,
150-            'javascript_imports': self.javascript_add(request),
151+            'media': media,
152             'bound_inlines': [BoundInline(i, fs) for i, fs in zip(self.inlines, inline_formsets)],
153         })
154         return render_change_form(self, model, model.AddManipulator(), c, add=True)
155@@ -500,13 +500,19 @@
156                         #related.get_accessor_name())
157                 #orig_list = func()
158                 #oldform.order_objects.extend(orig_list)
159+               
160+        adminForm = AdminForm(form, self.fieldsets_change(request, obj), self.prepopulated_fields)       
161+        media = self.media + adminForm.media
162+        for fs in inline_formsets:
163+            media = media + fs.media
164+           
165         c = template.RequestContext(request, {
166             'title': _('Change %s') % opts.verbose_name,
167-            'adminform': AdminForm(form, self.fieldsets_change(request, obj), self.prepopulated_fields),
168+            'adminform': adminForm,
169             'object_id': object_id,
170             'original': obj,
171             'is_popup': request.REQUEST.has_key('_popup'),
172-            'javascript_imports': self.javascript_change(request, obj),
173+            'media': media,
174             'bound_inlines': [BoundInline(i, fs) for i, fs in zip(self.inlines, inline_formsets)],
175         })
176         return render_change_form(self, model, model.ChangeManipulator(object_id), c, change=True)
177Index: django/contrib/admin/widgets.py
178===================================================================
179--- django/contrib/admin/widgets.py     (revision 5651)
180+++ django/contrib/admin/widgets.py     (working copy)
181@@ -5,6 +5,7 @@
182 from django import newforms as forms
183 from django.utils.text import capfirst
184 from django.utils.translation import ugettext as _
185+from django.conf import settings
186 
187 class FilteredSelectMultiple(forms.SelectMultiple):
188     """
189@@ -28,13 +29,28 @@
190             (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), settings.ADMIN_MEDIA_PREFIX))
191         return u''.join(output)
192 
193+class AdminDateWidget(forms.TextInput):
194+    class Media:
195+        js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js",
196+              settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js")
197+       
198+    def __init__(self, attrs={}):
199+        super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'})
200+
201+class AdminTimeWidget(forms.TextInput):
202+    class Media:
203+        js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js",
204+              settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js")
205+
206+    def __init__(self, attrs={}):
207+        super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'})
208+   
209 class AdminSplitDateTime(forms.SplitDateTimeWidget):
210     """
211     A SplitDateTime Widget that has some admin-specific styling.
212     """
213     def __init__(self, attrs=None):
214-        widgets = [forms.TextInput(attrs={'class': 'vDateField', 'size': '10'}),
215-                   forms.TextInput(attrs={'class': 'vTimeField', 'size': '8'})]
216+        widgets = [AdminDateWidget, AdminTimeWidget]
217         # Note that we're calling MultiWidget, not SplitDateTimeWidget, because
218         # we want to define widgets.
219         forms.MultiWidget.__init__(self, widgets, attrs)
220Index: django/contrib/admin/templates/admin/auth/user/change_password.html
221===================================================================
222--- django/contrib/admin/templates/admin/auth/user/change_password.html (revision 5651)
223+++ django/contrib/admin/templates/admin/auth/user/change_password.html (working copy)
224@@ -2,7 +2,6 @@
225 {% load i18n admin_modify adminmedia %}
226 {% block extrahead %}{{ block.super }}
227 <script type="text/javascript" src="../../../../jsi18n/"></script>
228-{% for js in javascript_imports %}{% include_admin_script js %}{% endfor %}
229 {% endblock %}
230 {% block stylesheet %}{% admin_media_prefix %}css/forms.css{% endblock %}
231 {% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %}
232Index: django/contrib/admin/templates/admin/change_form.html
233===================================================================
234--- django/contrib/admin/templates/admin/change_form.html       (revision 5651)
235+++ django/contrib/admin/templates/admin/change_form.html       (working copy)
236@@ -3,8 +3,7 @@
237 
238 {% block extrahead %}{{ block.super }}
239 <script type="text/javascript" src="../../../jsi18n/"></script>
240-{% for js in javascript_imports %}<script type="text/javascript" src="{{ js }}"></script>
241-{% endfor %}
242+{{ media }}
243 {% endblock %}
244 
245 {% block stylesheet %}{% admin_media_prefix %}css/forms.css{% endblock %}
246Index: django/newforms/formsets.py
247===================================================================
248--- django/newforms/formsets.py (revision 5651)
249+++ django/newforms/formsets.py (working copy)
250@@ -1,6 +1,6 @@
251 from forms import Form, ValidationError
252 from fields import IntegerField, BooleanField
253-from widgets import HiddenInput
254+from widgets import HiddenInput, Media
255 
256 # special field names
257 FORM_COUNT_FIELD_NAME = 'COUNT'
258@@ -149,6 +149,15 @@
259         self.full_clean()
260         return self._is_valid
261 
262+    def _get_media(self):
263+        # All the forms on a FormSet are the same, so you only need to
264+        # interrogate the first form for media.
265+        if self.forms:
266+            return self.forms[0].media
267+        else:
268+            return Media()
269+    media = property(_get_media)
270+   
271 def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False):
272     """Return a FormSet for the given form class."""
273     attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable}
274Index: django/newforms/forms.py
275===================================================================
276--- django/newforms/forms.py    (revision 5651)
277+++ django/newforms/forms.py    (working copy)
278@@ -9,7 +9,7 @@
279 from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode
280 
281 from fields import Field
282-from widgets import TextInput, Textarea
283+from widgets import Media, media_property, TextInput, Textarea
284 from util import flatatt, ErrorDict, ErrorList, ValidationError
285 
286 __all__ = ('BaseForm', 'Form')
287@@ -37,6 +37,7 @@
288     """
289     Metaclass that converts Field attributes to a dictionary called
290     'base_fields', taking into account parent class 'base_fields' as well.
291+    Also integrates any additional media definitions
292     """
293     def __new__(cls, name, bases, attrs):
294         fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
295@@ -50,8 +51,12 @@
296                 fields = base.base_fields.items() + fields
297 
298         attrs['base_fields'] = SortedDictFromList(fields)
299-        return type.__new__(cls, name, bases, attrs)
300 
301+        new_class = type.__new__(cls, name, bases, attrs)
302+        if 'media' not in attrs:
303+            new_class.media = media_property(new_class)
304+        return new_class
305+
306 class BaseForm(StrAndUnicode):
307     # This is the main implementation of all the Form logic. Note that this
308     # class is different than Form. See the comments by the Form class for more
309@@ -234,6 +239,16 @@
310         self.is_bound = False
311         self.__errors = None
312 
313+    def _get_media(self):
314+        """
315+        Provide a description of all media required to render the widgets on this form
316+        """
317+        media = Media()
318+        for field in self.fields.values():
319+            media = media + field.widget.media
320+        return media
321+    media = property(_get_media)
322+   
323 class Form(BaseForm):
324     "A collection of Fields, plus their associated data."
325     # This is a separate class from BaseForm in order to abstract the way
326Index: django/newforms/widgets.py
327===================================================================
328--- django/newforms/widgets.py  (revision 5651)
329+++ django/newforms/widgets.py  (working copy)
330@@ -8,6 +8,7 @@
331     from sets import Set as set   # Python 2.3 fallback
332 
333 from itertools import chain
334+from django.conf import settings
335 from django.utils.datastructures import MultiValueDict
336 from django.utils.html import escape
337 from django.utils.translation import ugettext
338@@ -15,14 +16,113 @@
339 from util import flatatt
340 
341 __all__ = (
342-    'Widget', 'TextInput', 'PasswordInput',
343+    'Media', 'Widget', 'TextInput', 'PasswordInput',
344     'HiddenInput', 'MultipleHiddenInput',
345     'FileInput', 'Textarea', 'CheckboxInput',
346     'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
347     'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
348 )
349 
350+MEDIA_TYPES = ('css','js')
351+
352+class Media(StrAndUnicode):
353+    def __init__(self, media=None, **kwargs):
354+        if media:
355+            media_attrs = media.__dict__
356+        else:
357+            media_attrs = kwargs
358+           
359+        self._css = {}
360+        self._js = []
361+       
362+        for name in MEDIA_TYPES:
363+            getattr(self, 'add_' + name)(media_attrs.get(name, None))
364+
365+        # Any leftover attributes must be invalid.
366+        # if media_attrs != {}:
367+        #     raise TypeError, "'class Media' has invalid attribute(s): %s" % ','.join(media_attrs.keys())
368+       
369+    def __unicode__(self):
370+        return self.render()
371+       
372+    def render(self):
373+        return u'\n'.join(chain(*[getattr(self, 'render_' + name)() for name in MEDIA_TYPES]))
374+       
375+    def render_js(self):
376+        return [u'<script type="text/javascript" src="%s"></script>' % self.absolute_path(path) for path in self._js]
377+       
378+    def render_css(self):
379+        # To keep rendering order consistent, we can't just iterate over items().
380+        # We need to sort the keys, and iterate over the sorted list.
381+        media = self._css.keys()
382+        media.sort()
383+        return chain(*[
384+            [u'<link href="%s" type="text/css" media="%s" rel="stylesheet" />' % (self.absolute_path(path), medium)
385+                    for path in self._css[medium]]
386+                for medium in media])
387+       
388+    def absolute_path(self, path):
389+        return (path.startswith(u'http://') or path.startswith(u'https://')) and path or u''.join([settings.MEDIA_URL,path])
390+
391+    def __getitem__(self, name):
392+        "Returns a Media object that only contains media of the given type"
393+        if name in MEDIA_TYPES:
394+            return Media(**{name: getattr(self, '_' + name)})
395+        raise KeyError('Unknown media type "%s"' % name)
396+
397+    def add_js(self, data):
398+        if data:   
399+            self._js.extend([path for path in data if path not in self._js])
400+           
401+    def add_css(self, data):
402+        if data:
403+            for medium, paths in data.items():
404+                self._css.setdefault(medium, []).extend([path for path in paths if path not in self._css[medium]])
405+
406+    def __add__(self, other):
407+        combined = Media()
408+        for name in MEDIA_TYPES:
409+            getattr(combined, 'add_' + name)(getattr(self, '_' + name, None))
410+            getattr(combined, 'add_' + name)(getattr(other, '_' + name, None))
411+        return combined
412+
413+def media_property(cls):
414+    def _media(self):
415+        # Get the media property of the superclass, if it exists
416+        if hasattr(super(cls, self), 'media'):
417+            base = super(cls, self).media
418+        else:
419+            base = Media()
420+       
421+        # Get the media definition for this class   
422+        definition = getattr(cls, 'Media', None)
423+        if definition:
424+            extend = getattr(definition, 'extend', True)
425+            if extend:
426+                if extend == True:
427+                    m = base
428+                else:
429+                    m = Media()
430+                    for medium in extend:
431+                        m = m + base[medium]
432+                    m = m + Media(definition)
433+                return m + Media(definition)
434+            else:
435+                 return Media(definition)
436+        else:
437+            return base
438+    return property(_media)
439+   
440+class MediaDefiningClass(type):
441+    "Metaclass for classes that can have media definitions"
442+    def __new__(cls, name, bases, attrs):           
443+        new_class = type.__new__(cls, name, bases, attrs)
444+        if 'media' not in attrs:
445+            new_class.media = media_property(new_class)
446+        return new_class
447+       
448 class Widget(object):
449+    __metaclass__ = MediaDefiningClass
450     is_hidden = False          # Determines whether this corresponds to an <input type="hidden">.
451 
452     def __init__(self, attrs=None):
453@@ -377,6 +477,14 @@
454         """
455         raise NotImplementedError('Subclasses must implement this method.')
456 
457+    def _get_media(self):
458+        "Media for a multiwidget is the combination of all media of the subwidgets"
459+        media = Media()
460+        for w in self.widgets:
461+            media = media + w.media
462+        return media
463+    media = property(_get_media)
464+   
465 class SplitDateTimeWidget(MultiWidget):
466     """
467     A Widget that splits datetime input into two <input type="text"> boxes.
468@@ -389,3 +497,4 @@
469         if value:
470             return [value.date(), value.time()]
471         return [None, None]
472+   
473\ No newline at end of file
474Index: tests/regressiontests/forms/media.py
475===================================================================
476--- tests/regressiontests/forms/media.py        (revision 0)
477+++ tests/regressiontests/forms/media.py        (revision 0)
478@@ -0,0 +1,357 @@
479+# -*- coding: utf-8 -*-
480+# Tests for the media handling on widgets and forms
481+
482+media_tests = r"""
483+>>> from django.newforms import TextInput, Media, TextInput, CharField, Form, MultiWidget
484+>>> from django.conf import settings
485+>>> settings.MEDIA_URL = 'http://media.example.com'
486+
487+# Check construction of media objects
488+>>> 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'))
489+>>> print m
490+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
491+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
492+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
493+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
494+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
495+
496+>>> class Foo:
497+...     css = {
498+...        'all': ('/path/to/css1','/path/to/css2')
499+...     }
500+...     js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3')
501+>>> m3 = Media(Foo)
502+>>> print m3
503+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
504+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
505+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
506+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
507+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
508+
509+>>> m3 = Media(Foo)
510+>>> print m3
511+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
512+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
513+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
514+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
515+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
516+
517+# A widget can exist without a media definition
518+>>> class MyWidget(TextInput):
519+...     pass
520+
521+>>> w = MyWidget()
522+>>> print w.media
523+<BLANKLINE>
524+
525+###############################################################
526+# DSL Class-based media definitions
527+###############################################################
528+
529+# A widget can define media if it needs to.
530+# Any absolute path will be preserved; relative paths are combined
531+# with the value of settings.MEDIA_URL
532+>>> class MyWidget1(TextInput):
533+...     class Media:
534+...         css = {
535+...            'all': ('/path/to/css1','/path/to/css2')
536+...         }
537+...         js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3')
538+
539+>>> w1 = MyWidget1()
540+>>> print w1.media
541+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
542+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
543+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
544+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
545+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
546+
547+# Media objects can be interrogated by media type
548+>>> print w1.media['css']
549+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
550+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
551+
552+>>> print w1.media['js']
553+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
554+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
555+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
556+
557+# Media objects can be combined. Any given media resource will appear only
558+# once. Duplicated media definitions are ignored.
559+>>> class MyWidget2(TextInput):
560+...     class Media:
561+...         css = {
562+...            'all': ('/path/to/css2','/path/to/css3')
563+...         }
564+...         js = ('/path/to/js1','/path/to/js4')
565+
566+>>> class MyWidget3(TextInput):
567+...     class Media:
568+...         css = {
569+...            'all': ('/path/to/css3','/path/to/css1')
570+...         }
571+...         js = ('/path/to/js1','/path/to/js4')
572+
573+>>> w2 = MyWidget2()
574+>>> w3 = MyWidget3()
575+>>> print w1.media + w2.media + w3.media
576+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
577+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
578+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
579+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
580+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
581+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
582+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
583+
584+# Check that media addition hasn't affected the original objects
585+>>> print w1.media
586+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
587+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
588+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
589+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
590+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
591+
592+###############################################################
593+# Property-based media definitions
594+###############################################################
595+
596+# Widget media can be defined as a property
597+>>> class MyWidget4(TextInput):
598+...     def _media(self):
599+...         return Media(css={'all': ('/some/path',)}, js = ('/some/js',))
600+...     media = property(_media)
601+
602+>>> w4 = MyWidget4()
603+>>> print w4.media
604+<link href="http://media.example.com/some/path" type="text/css" media="all" rel="stylesheet" />
605+<script type="text/javascript" src="http://media.example.com/some/js"></script>
606+
607+# Media properties can reference the media of their parents
608+>>> class MyWidget5(MyWidget4):
609+...     def _media(self):
610+...         return super(MyWidget5, self).media + Media(css={'all': ('/other/path',)}, js = ('/other/js',))
611+...     media = property(_media)
612+
613+>>> w5 = MyWidget5()
614+>>> print w5.media
615+<link href="http://media.example.com/some/path" type="text/css" media="all" rel="stylesheet" />
616+<link href="http://media.example.com/other/path" type="text/css" media="all" rel="stylesheet" />
617+<script type="text/javascript" src="http://media.example.com/some/js"></script>
618+<script type="text/javascript" src="http://media.example.com/other/js"></script>
619+
620+# Media properties can reference the media of their parents,
621+# even if the parent media was defined using a class
622+>>> class MyWidget6(MyWidget1):
623+...     def _media(self):
624+...         return super(MyWidget6, self).media + Media(css={'all': ('/other/path',)}, js = ('/other/js',))
625+...     media = property(_media)
626+
627+>>> w6 = MyWidget6()
628+>>> print w6.media
629+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
630+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
631+<link href="http://media.example.com/other/path" type="text/css" media="all" rel="stylesheet" />
632+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
633+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
634+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
635+<script type="text/javascript" src="http://media.example.com/other/js"></script>
636+
637+###############################################################
638+# Inheritance of media
639+###############################################################
640+
641+# If a widget extends another but provides no media definition, it inherits the parent widget's media
642+>>> class MyWidget7(MyWidget1):
643+...     pass
644+
645+>>> w7 = MyWidget7()
646+>>> print w7.media
647+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
648+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
649+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
650+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
651+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
652+
653+# If a widget extends another but defines media, it extends the parent widget's media by default
654+>>> class MyWidget8(MyWidget1):
655+...     class Media:
656+...         css = {
657+...            'all': ('/path/to/css3','/path/to/css1')
658+...         }
659+...         js = ('/path/to/js1','/path/to/js4')
660+
661+>>> w8 = MyWidget8()
662+>>> print w8.media
663+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
664+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
665+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
666+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
667+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
668+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
669+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
670+
671+# If a widget extends another but defines media, it extends the parents widget's media,
672+# even if the parent defined media using a property.
673+>>> class MyWidget9(MyWidget4):
674+...     class Media:
675+...         css = {
676+...             'all': ('/other/path',)
677+...         }
678+...         js = ('/other/js',)
679+
680+>>> w9 = MyWidget9()
681+>>> print w9.media
682+<link href="http://media.example.com/some/path" type="text/css" media="all" rel="stylesheet" />
683+<link href="http://media.example.com/other/path" type="text/css" media="all" rel="stylesheet" />
684+<script type="text/javascript" src="http://media.example.com/some/js"></script>
685+<script type="text/javascript" src="http://media.example.com/other/js"></script>
686+
687+# A widget can disable media inheritance by specifying 'extend=False'
688+>>> class MyWidget10(MyWidget1):
689+...     class Media:
690+...         extend = False
691+...         css = {
692+...            'all': ('/path/to/css3','/path/to/css1')
693+...         }
694+...         js = ('/path/to/js1','/path/to/js4')
695+
696+>>> w10 = MyWidget10()
697+>>> print w10.media
698+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
699+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
700+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
701+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
702+
703+# A widget can explicitly enable full media inheritance by specifying 'extend=True'
704+>>> class MyWidget11(MyWidget1):
705+...     class Media:
706+...         extend = True
707+...         css = {
708+...            'all': ('/path/to/css3','/path/to/css1')
709+...         }
710+...         js = ('/path/to/js1','/path/to/js4')
711+
712+>>> w11 = MyWidget11()
713+>>> print w11.media
714+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
715+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
716+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
717+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
718+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
719+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
720+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
721+
722+# A widget can enable inheritance of one media type by specifying extend as a tuple
723+>>> class MyWidget12(MyWidget1):
724+...     class Media:
725+...         extend = ('css',)
726+...         css = {
727+...            'all': ('/path/to/css3','/path/to/css1')
728+...         }
729+...         js = ('/path/to/js1','/path/to/js4')
730+
731+>>> w12 = MyWidget12()
732+>>> print w12.media
733+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
734+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
735+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
736+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
737+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
738+
739+###############################################################
740+# Multi-media handling for CSS
741+###############################################################
742+
743+# A widget can define CSS media for multiple output media types
744+>>> class MultimediaWidget(TextInput):
745+...     class Media:
746+...         css = {
747+...            'screen, print': ('/file1','/file2'),
748+...            'screen': ('/file3',),
749+...            'print': ('/file4',)
750+...         }
751+...         js = ('/path/to/js1','/path/to/js4')
752+
753+>>> multimedia = MultimediaWidget()
754+>>> print multimedia.media
755+<link href="http://media.example.com/file4" type="text/css" media="print" rel="stylesheet" />
756+<link href="http://media.example.com/file3" type="text/css" media="screen" rel="stylesheet" />
757+<link href="http://media.example.com/file1" type="text/css" media="screen, print" rel="stylesheet" />
758+<link href="http://media.example.com/file2" type="text/css" media="screen, print" rel="stylesheet" />
759+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
760+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
761+
762+###############################################################
763+# Multiwidget media handling
764+###############################################################
765+
766+# MultiWidgets have a default media definition that gets all the
767+# media from the component widgets
768+>>> class MyMultiWidget(MultiWidget):
769+...     def __init__(self, attrs=None):
770+...         widgets = [MyWidget1, MyWidget2, MyWidget3]
771+...         super(MyMultiWidget, self).__init__(widgets, attrs)
772+           
773+>>> mymulti = MyMultiWidget()
774+>>> print mymulti.media   
775+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
776+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
777+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
778+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
779+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
780+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
781+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
782+
783+###############################################################
784+# Media processing for forms
785+###############################################################
786+
787+# You can ask a form for the media required by its widgets.
788+>>> class MyForm(Form):
789+...     field1 = CharField(max_length=20, widget=MyWidget1())
790+...     field2 = CharField(max_length=20, widget=MyWidget2())
791+>>> f1 = MyForm()
792+>>> print f1.media
793+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
794+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
795+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
796+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
797+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
798+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
799+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
800+
801+# Form media can be combined to produce a single media definition.
802+>>> class AnotherForm(Form):
803+...     field3 = CharField(max_length=20, widget=MyWidget3())
804+>>> f2 = AnotherForm()
805+>>> print f1.media + f2.media
806+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
807+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
808+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
809+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
810+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
811+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
812+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
813+
814+# Forms can also define media, following the same rules as widgets.
815+>>> class FormWithMedia(Form):
816+...     field1 = CharField(max_length=20, widget=MyWidget1())
817+...     field2 = CharField(max_length=20, widget=MyWidget2())
818+...     class Media:
819+...         js = ('/some/form/javascript',)
820+...         css = {
821+...             'all': ('/some/form/css',)
822+...         }
823+>>> f3 = FormWithMedia()
824+>>> print f3.media
825+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
826+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
827+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
828+<link href="http://media.example.com/some/form/css" type="text/css" media="all" rel="stylesheet" />
829+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
830+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
831+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
832+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
833+<script type="text/javascript" src="http://media.example.com/some/form/javascript"></script>
834+
835+"""
836\ No newline at end of file
837Index: tests/regressiontests/forms/tests.py
838===================================================================
839--- tests/regressiontests/forms/tests.py        (revision 5651)
840+++ tests/regressiontests/forms/tests.py        (working copy)
841@@ -2,6 +2,7 @@
842 from localflavor import localflavor_tests
843 from regressions import regression_tests
844 from formsets import formset_tests
845+from media import media_tests
846 
847 form_tests = r"""
848 >>> from django.newforms import *
849@@ -3698,6 +3699,7 @@
850     'localflavor': localflavor_tests,
851     'regressions': regression_tests,
852     'formset_tests': formset_tests,
853+    'media_tests': media_tests,
854 }
855 
856 if __name__ == "__main__":
857Index: docs/newforms.txt
858===================================================================
859--- docs/newforms.txt   (revision 5651)
860+++ docs/newforms.txt   (working copy)
861@@ -919,7 +919,7 @@
862 ~~~~~~~~~~
863 
864 The ``widget`` argument lets you specify a ``Widget`` class to use when
865-rendering this ``Field``. See "Widgets" below for more information.
866+rendering this ``Field``. See "Widgets"_ below for more information.
867 
868 ``help_text``
869 ~~~~~~~~~~~~~
870@@ -1325,6 +1325,124 @@
871         senders = MultiEmailField()
872         cc_myself = forms.BooleanField()
873 
874+Widgets
875+=======
876+
877+A widget is Django's representation of a HTML form widget. The widget
878+handles the rendering of the HTML, and the extraction of data from a GET/POST
879+dictionary that corresponds to the widget.
880+
881+Django provides a representation of all the basic HTML widgets, plus some
882+commonly used groups of widgets:
883+
884+    ============================  ===========================================
885+    Widget                        HTML Equivalent
886+    ============================  ===========================================
887+    ``TextInput``                 ``<input type='text' ...``
888+    ``PasswordInput``             ``<input type='password' ...``
889+    ``HiddenInput``               ``<input type='hidden' ...``
890+    ``MultipleHiddenInput``       Multiple ``<input type='hidden' ...``
891+                                  instances.
892+    ``FileInput``                 ``<input type='file' ...``
893+    ``Textarea``                  ``<textarea>...</textarea>``
894+    ``CheckboxInput``             ``<input type='checkbox' ...``
895+
896+    ``Select``                    ``<select><option ...``
897+
898+    ``NullBooleanSelect``         Select widget with options 'Unknown',
899+                                  'Yes' and 'No'
900+
901+    ``SelectMultiple``            ``<select multiple='multiple'><option ...``
902+
903+    ``RadioSelect``               ``<ul><li><input type='radio' ...``
904+
905+    ``CheckboxSelectMultiple``    ``<ul><li><input type='checkbox' ...``
906+
907+    ``MultiWidget``               Wrapper around multiple other widgets
908+
909+    ``SplitDateTimeWidget``       Wrapper around two ``TextInput`` widgets
910+
911+Specifying widgets
912+------------------
913+
914+Whenever you specify a field on a form, Django will use a default widget
915+that is appropriate to the type of data that is to be displayed.
916+
917+However, if you want to use a different widget, you can - just use the
918+'widget' argument on the field definition. For example::
919+
920+    class MyForm(forms.Form):
921+        name = forms.CharField(max_length=20, widget=forms.HiddenInput)
922+
923+This would specify a form with a CharField that uses a HiddenInput widget,
924+rather than the default TextInput widget.
925+
926+Customizing widget instances
927+----------------------------
928+ - sometimes you may want to associate a CSS class with a widget,
929+or modify the number of rows or columns displayed in a text area.
930+
931+When you specify a widget, you can provide a list of attributes that
932+will be used when rendering the widget. The attributes are
933+Django also provides a mechanism to customize the rendering of
934+
935+Django will then use these attributes
936+
937+For example, if you want to associate a specific CSS class with the
938+HTML generated by the widget, you can provide the CSS class as an
939+attribute definition::
940+
941+    class MyForm(forms.Form):
942+        name = forms.CharField(
943+                   max_length=20,
944+                   widget=forms.TextInput(attrs={'class':'special'})
945+
946+These attributes will be
947+
948+Customized Widgets
949+------------------
950+
951+If you need to reuse a particular set of customized widget attributes, it
952+may be worth defining your own widget that encapsulates those customized
953+
954+Sometimes, a particular set of customized attributes will be s
955+The widgets implemented in Django cover all the possibilities for HTML form
956+input. While this set of widgets is entirely functional, it doesn't necessarily
957+represent the best possible user interface for every application.
958+
959+
960+
961+For example, consider the case of a DateField in a model. By default,
962+a DateField on a form will be rendered using a TextInput widget. While this
963+is entirely functional, a calendar is a much better user interface for
964+inputting dates than a simple text field.
965+
966+To get a calendar input on your page, you will need to use JavaScript
967+
968+..note:
969+    Django has deliberately not blessed any one JavaScript toolkit.
970+    There are many options out there, and they all have their relative
971+    strengths and weaknesses.
972+
973+Whenever you specify
974+
975+However, if you're going to be using
976+
977+If you want to spice up your application, one approach you can take is
978+to define some customized
979+
980+user interface for a
981+
982+One way to improve the user interface for DateFields would be to define
983+a customized CalendarInput widget that displays a
984+
985+is to define and use a customized widget
986+
987+For the record - this is exactly what the Django Admin application does.
988+
989+The Django Admin application defines a number of customized widgets, and
990+uses those widgets in place of the Django defaults.
991+
992 Generating forms for models
993 ===========================
994 
995@@ -1633,6 +1751,21 @@
996 you shouldn't use these shortcuts. Creating a ``Form`` class the "long" way
997 isn't that difficult, after all.
998 
999+Media
1000+=====
1001+
1002+In a 'previous section'_
1003+provides a mechanism to simplify the definition and collation of the
1004+media requirements
1005+
1006+.. 'previous section'
1007+However, the CalendarInput widget requires more than just HTML - it
1008+also requires CSS and JavaScript files to be fully functional.
1009+
1010+
1011+. you may wish to display a calendar for the input of
1012+dates, rather than a simple text fie
1013+
1014 More coming soon
1015 ================
1016