Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

newforms-admin: Fixed #4418 -- Added the ability for widgets, forms, …

…and Admin declarations to have media definitions.

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@5926 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 549198b714ba0b032904639af0c388e739261fe6 1 parent 28e3503
Russell Keith-Magee authored August 18, 2007
78  django/contrib/admin/options.py
@@ -2,6 +2,7 @@
2 2
 from django import newforms as forms
3 3
 from django.newforms.formsets import all_valid
4 4
 from django.newforms.models import inline_formset
  5
+from django.newforms.widgets import Media, MediaDefiningClass
5 6
 from django.contrib.admin import widgets
6 7
 from django.core.exceptions import ImproperlyConfigured, PermissionDenied
7 8
 from django.db import models
@@ -48,6 +49,13 @@ def __iter__(self):
48 49
     def first_field(self):
49 50
         for bf in self.form:
50 51
             return bf
  52
+            
  53
+    def _media(self):
  54
+        media = self.form.media
  55
+        for fs in self.fieldsets:
  56
+            media = media + fs.media
  57
+        return media
  58
+    media = property(_media)
51 59
 
52 60
 class Fieldset(object):
53 61
     def __init__(self, name=None, fields=(), classes=(), description=None):
@@ -55,6 +63,13 @@ def __init__(self, name=None, fields=(), classes=(), description=None):
55 63
         self.classes = u' '.join(classes)
56 64
         self.description = description
57 65
 
  66
+    def _media(self):
  67
+        from django.conf import settings
  68
+        if 'collapse' in self.classes:
  69
+            return Media(js=['%sjs/admin/CollapsedFieldsets.js' % settings.ADMIN_MEDIA_PREFIX])
  70
+        return Media()
  71
+    media = property(_media)
  72
+    
58 73
 class BoundFieldset(object):
59 74
     def __init__(self, form, fieldset):
60 75
         self.form, self.fieldset = form, fieldset
@@ -123,12 +138,12 @@ def formfield_for_dbfield(self, db_field, **kwargs):
123 138
 
124 139
         # For DateFields, add a custom CSS class.
125 140
         if isinstance(db_field, models.DateField):
126  
-            kwargs['widget'] = forms.TextInput(attrs={'class': 'vDateField', 'size': '10'})
  141
+            kwargs['widget'] = widgets.AdminDateWidget
127 142
             return db_field.formfield(**kwargs)
128 143
 
129 144
         # For TimeFields, add a custom CSS class.
130 145
         if isinstance(db_field, models.TimeField):
131  
-            kwargs['widget'] = forms.TextInput(attrs={'class': 'vTimeField', 'size': '8'})
  146
+            kwargs['widget'] = widgets.AdminTimeWidget
132 147
             return db_field.formfield(**kwargs)
133 148
 
134 149
         # For ForeignKey or ManyToManyFields, use a special widget.
@@ -148,7 +163,8 @@ def formfield_for_dbfield(self, db_field, **kwargs):
148 163
 
149 164
 class ModelAdmin(BaseModelAdmin):
150 165
     "Encapsulates all admin options and functionality for a given model."
151  
-
  166
+    __metaclass__ = MediaDefiningClass
  167
+    
152 168
     list_display = ('__str__',)
153 169
     list_display_links = ()
154 170
     list_filter = ()
@@ -159,7 +175,6 @@ class ModelAdmin(BaseModelAdmin):
159 175
     save_as = False
160 176
     save_on_top = False
161 177
     ordering = None
162  
-    js = None
163 178
     prepopulated_fields = {}
164 179
     filter_vertical = ()
165 180
     filter_horizontal = ()
@@ -194,38 +209,20 @@ def __call__(self, request, url):
194 209
         else:
195 210
             return self.change_view(request, unquote(url))
196 211
 
197  
-    def javascript(self, request, fieldsets):
198  
-        """
199  
-        Returns a list of URLs to include via <script> statements.
200  
-
201  
-        The URLs can be absolute ('/js/admin/') or explicit
202  
-        ('http://example.com/foo.js').
203  
-        """
  212
+    def _media(self):
204 213
         from django.conf import settings
  214
+
205 215
         js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
206 216
         if self.prepopulated_fields:
207 217
             js.append('js/urlify.js')
208  
-        if self.opts.has_field_type(models.DateTimeField) or self.opts.has_field_type(models.TimeField) or self.opts.has_field_type(models.DateField):
209  
-            js.extend(['js/calendar.js', 'js/admin/DateTimeShortcuts.js'])
210 218
         if self.opts.get_ordered_objects():
211 219
             js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js'])
212  
-        if self.js:
213  
-            js.extend(self.js)
214 220
         if self.filter_vertical or self.filter_horizontal:
215 221
             js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
216  
-        for fs in fieldsets:
217  
-            if 'collapse' in fs.classes:
218  
-                js.append('js/admin/CollapsedFieldsets.js')
219  
-                break
220  
-        prefix = settings.ADMIN_MEDIA_PREFIX
221  
-        return ['%s%s' % (prefix, url) for url in js]
222  
-
223  
-    def javascript_add(self, request):
224  
-        return self.javascript(request, self.fieldsets_add(request))
225  
-
226  
-    def javascript_change(self, request, obj):
227  
-        return self.javascript(request, self.fieldsets_change(request, obj))
228  
-
  222
+        
  223
+        return Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
  224
+    media = property(_media)
  225
+    
229 226
     def fieldsets(self, request):
230 227
         """
231 228
         Generator that yields Fieldset objects for use on add and change admin
@@ -244,13 +241,11 @@ def fieldsets(self, request):
244 241
 
245 242
     def fieldsets_add(self, request):
246 243
         "Hook for specifying Fieldsets for the add form."
247  
-        for fs in self.fieldsets(request):
248  
-            yield fs
  244
+        return list(self.fieldsets(request))
249 245
 
250 246
     def fieldsets_change(self, request, obj):
251 247
         "Hook for specifying Fieldsets for the change form."
252  
-        for fs in self.fieldsets(request):
253  
-            yield fs
  248
+        return list(self.fieldsets(request))
254 249
 
255 250
     def has_add_permission(self, request):
256 251
         "Returns True if the given request has permission to add an object."
@@ -430,12 +425,17 @@ def add_view(self, request, form_url=''):
430 425
                 inline_formset = FormSet()
431 426
                 inline_formsets.append(inline_formset)
432 427
 
  428
+        adminForm = AdminForm(form, list(self.fieldsets_add(request)), self.prepopulated_fields)
  429
+        media = self.media + adminForm.media
  430
+        for fs in inline_formsets:
  431
+            media = media + fs.media
  432
+            
433 433
         c = template.RequestContext(request, {
434 434
             'title': _('Add %s') % opts.verbose_name,
435  
-            'adminform': AdminForm(form, self.fieldsets_add(request), self.prepopulated_fields),
  435
+            'adminform': adminForm,
436 436
             'is_popup': request.REQUEST.has_key('_popup'),
437 437
             'show_delete': False,
438  
-            'javascript_imports': self.javascript_add(request),
  438
+            'media': media,
439 439
             'bound_inlines': [BoundInline(i, fs) for i, fs in zip(self.inlines, inline_formsets)],
440 440
         })
441 441
         return render_change_form(self, model, model.AddManipulator(), c, add=True)
@@ -494,13 +494,19 @@ def change_view(self, request, object_id):
494 494
                         #related.get_accessor_name())
495 495
                 #orig_list = func()
496 496
                 #oldform.order_objects.extend(orig_list)
  497
+                
  498
+        adminForm = AdminForm(form, self.fieldsets_change(request, obj), self.prepopulated_fields)        
  499
+        media = self.media + adminForm.media
  500
+        for fs in inline_formsets:
  501
+            media = media + fs.media
  502
+            
497 503
         c = template.RequestContext(request, {
498 504
             'title': _('Change %s') % opts.verbose_name,
499  
-            'adminform': AdminForm(form, self.fieldsets_change(request, obj), self.prepopulated_fields),
  505
+            'adminform': adminForm,
500 506
             'object_id': object_id,
501 507
             'original': obj,
502 508
             'is_popup': request.REQUEST.has_key('_popup'),
503  
-            'javascript_imports': self.javascript_change(request, obj),
  509
+            'media': media,
504 510
             'bound_inlines': [BoundInline(i, fs) for i, fs in zip(self.inlines, inline_formsets)],
505 511
         })
506 512
         return render_change_form(self, model, model.ChangeManipulator(object_id), c, change=True)
1  django/contrib/admin/templates/admin/auth/user/change_password.html
@@ -2,7 +2,6 @@
2 2
 {% load i18n admin_modify adminmedia %}
3 3
 {% block extrahead %}{{ block.super }}
4 4
 <script type="text/javascript" src="../../../../jsi18n/"></script>
5  
-{% for js in javascript_imports %}{% include_admin_script js %}{% endfor %}
6 5
 {% endblock %}
7 6
 {% block stylesheet %}{% admin_media_prefix %}css/forms.css{% endblock %}
8 7
 {% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %}
3  django/contrib/admin/templates/admin/change_form.html
@@ -3,8 +3,7 @@
3 3
 
4 4
 {% block extrahead %}{{ block.super }}
5 5
 <script type="text/javascript" src="../../../jsi18n/"></script>
6  
-{% for js in javascript_imports %}<script type="text/javascript" src="{{ js }}"></script>
7  
-{% endfor %}
  6
+{{ media }}
8 7
 {% endblock %}
9 8
 
10 9
 {% block stylesheet %}{% admin_media_prefix %}css/forms.css{% endblock %}
20  django/contrib/admin/widgets.py
@@ -5,6 +5,7 @@
5 5
 from django import newforms as forms
6 6
 from django.utils.text import capfirst
7 7
 from django.utils.translation import ugettext as _
  8
+from django.conf import settings
8 9
 
9 10
 class FilteredSelectMultiple(forms.SelectMultiple):
10 11
     """
@@ -28,13 +29,28 @@ def render(self, name, value, attrs=None, choices=()):
28 29
             (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), settings.ADMIN_MEDIA_PREFIX))
29 30
         return u''.join(output)
30 31
 
  32
+class AdminDateWidget(forms.TextInput):
  33
+    class Media:
  34
+        js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", 
  35
+              settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js")
  36
+        
  37
+    def __init__(self, attrs={}):
  38
+        super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'})
  39
+
  40
+class AdminTimeWidget(forms.TextInput):
  41
+    class Media:
  42
+        js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", 
  43
+              settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js")
  44
+
  45
+    def __init__(self, attrs={}):
  46
+        super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'})
  47
+    
31 48
 class AdminSplitDateTime(forms.SplitDateTimeWidget):
32 49
     """
33 50
     A SplitDateTime Widget that has some admin-specific styling.
34 51
     """
35 52
     def __init__(self, attrs=None):
36  
-        widgets = [forms.TextInput(attrs={'class': 'vDateField', 'size': '10'}),
37  
-                   forms.TextInput(attrs={'class': 'vTimeField', 'size': '8'})]
  53
+        widgets = [AdminDateWidget, AdminTimeWidget]
38 54
         # Note that we're calling MultiWidget, not SplitDateTimeWidget, because
39 55
         # we want to define widgets.
40 56
         forms.MultiWidget.__init__(self, widgets, attrs)
19  django/newforms/forms.py
@@ -9,7 +9,7 @@
9 9
 from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode
10 10
 
11 11
 from fields import Field
12  
-from widgets import TextInput, Textarea
  12
+from widgets import Media, media_property, TextInput, Textarea
13 13
 from util import flatatt, ErrorDict, ErrorList, ValidationError
14 14
 
15 15
 __all__ = ('BaseForm', 'Form')
@@ -37,6 +37,7 @@ class DeclarativeFieldsMetaclass(type):
37 37
     """
38 38
     Metaclass that converts Field attributes to a dictionary called
39 39
     'base_fields', taking into account parent class 'base_fields' as well.
  40
+    Also integrates any additional media definitions
40 41
     """
41 42
     def __new__(cls, name, bases, attrs):
42 43
         fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
@@ -50,7 +51,11 @@ def __new__(cls, name, bases, attrs):
50 51
                 fields = base.base_fields.items() + fields
51 52
 
52 53
         attrs['base_fields'] = SortedDictFromList(fields)
53  
-        return type.__new__(cls, name, bases, attrs)
  54
+
  55
+        new_class = type.__new__(cls, name, bases, attrs)
  56
+        if 'media' not in attrs:
  57
+            new_class.media = media_property(new_class)
  58
+        return new_class
54 59
 
55 60
 class BaseForm(StrAndUnicode):
56 61
     # This is the main implementation of all the Form logic. Note that this
@@ -235,6 +240,16 @@ def reset(self):
235 240
         self.is_bound = False
236 241
         self.__errors = None
237 242
 
  243
+    def _get_media(self):
  244
+        """
  245
+        Provide a description of all media required to render the widgets on this form
  246
+        """
  247
+        media = Media()
  248
+        for field in self.fields.values():
  249
+            media = media + field.widget.media
  250
+        return media
  251
+    media = property(_get_media)
  252
+    
238 253
 class Form(BaseForm):
239 254
     "A collection of Fields, plus their associated data."
240 255
     # This is a separate class from BaseForm in order to abstract the way
11  django/newforms/formsets.py
... ...
@@ -1,6 +1,6 @@
1 1
 from forms import Form, ValidationError
2 2
 from fields import IntegerField, BooleanField
3  
-from widgets import HiddenInput
  3
+from widgets import HiddenInput, Media
4 4
 
5 5
 # special field names
6 6
 FORM_COUNT_FIELD_NAME = 'COUNT'
@@ -154,6 +154,15 @@ def is_valid(self):
154 154
         self.full_clean()
155 155
         return self._is_valid
156 156
 
  157
+    def _get_media(self):
  158
+        # All the forms on a FormSet are the same, so you only need to 
  159
+        # interrogate the first form for media.
  160
+        if self.forms:
  161
+            return self.forms[0].media
  162
+        else:
  163
+            return Media()
  164
+    media = property(_get_media)
  165
+    
157 166
 def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False):
158 167
     """Return a FormSet for the given form class."""
159 168
     attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable}
111  django/newforms/widgets.py
@@ -8,6 +8,7 @@
8 8
     from sets import Set as set   # Python 2.3 fallback
9 9
 
10 10
 from itertools import chain
  11
+from django.conf import settings
11 12
 from django.utils.datastructures import MultiValueDict
12 13
 from django.utils.html import escape
13 14
 from django.utils.translation import ugettext
@@ -15,14 +16,113 @@
15 16
 from util import flatatt
16 17
 
17 18
 __all__ = (
18  
-    'Widget', 'TextInput', 'PasswordInput',
  19
+    'Media', 'Widget', 'TextInput', 'PasswordInput',
19 20
     'HiddenInput', 'MultipleHiddenInput',
20 21
     'FileInput', 'Textarea', 'CheckboxInput',
21 22
     'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
22 23
     'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
23 24
 )
24 25
 
  26
+MEDIA_TYPES = ('css','js')
  27
+
  28
+class Media(StrAndUnicode):
  29
+    def __init__(self, media=None, **kwargs):
  30
+        if media:
  31
+            media_attrs = media.__dict__
  32
+        else:
  33
+            media_attrs = kwargs
  34
+            
  35
+        self._css = {}
  36
+        self._js = []
  37
+        
  38
+        for name in MEDIA_TYPES:
  39
+            getattr(self, 'add_' + name)(media_attrs.get(name, None))
  40
+
  41
+        # Any leftover attributes must be invalid.
  42
+        # if media_attrs != {}:
  43
+        #     raise TypeError, "'class Media' has invalid attribute(s): %s" % ','.join(media_attrs.keys())
  44
+        
  45
+    def __unicode__(self):
  46
+        return self.render()
  47
+        
  48
+    def render(self):
  49
+        return u'\n'.join(chain(*[getattr(self, 'render_' + name)() for name in MEDIA_TYPES]))
  50
+        
  51
+    def render_js(self):
  52
+        return [u'<script type="text/javascript" src="%s"></script>' % self.absolute_path(path) for path in self._js]
  53
+        
  54
+    def render_css(self):
  55
+        # To keep rendering order consistent, we can't just iterate over items().
  56
+        # We need to sort the keys, and iterate over the sorted list.
  57
+        media = self._css.keys()
  58
+        media.sort()
  59
+        return chain(*[
  60
+            [u'<link href="%s" type="text/css" media="%s" rel="stylesheet" />' % (self.absolute_path(path), medium) 
  61
+                    for path in self._css[medium]] 
  62
+                for medium in media])
  63
+        
  64
+    def absolute_path(self, path):
  65
+        return (path.startswith(u'http://') or path.startswith(u'https://')) and path or u''.join([settings.MEDIA_URL,path])
  66
+
  67
+    def __getitem__(self, name):
  68
+        "Returns a Media object that only contains media of the given type"
  69
+        if name in MEDIA_TYPES:
  70
+            return Media(**{name: getattr(self, '_' + name)})
  71
+        raise KeyError('Unknown media type "%s"' % name)
  72
+
  73
+    def add_js(self, data):
  74
+        if data:    
  75
+            self._js.extend([path for path in data if path not in self._js])
  76
+            
  77
+    def add_css(self, data):
  78
+        if data:
  79
+            for medium, paths in data.items():
  80
+                self._css.setdefault(medium, []).extend([path for path in paths if path not in self._css[medium]])
  81
+
  82
+    def __add__(self, other):
  83
+        combined = Media()
  84
+        for name in MEDIA_TYPES:
  85
+            getattr(combined, 'add_' + name)(getattr(self, '_' + name, None))
  86
+            getattr(combined, 'add_' + name)(getattr(other, '_' + name, None))
  87
+        return combined
  88
+
  89
+def media_property(cls):
  90
+    def _media(self):
  91
+        # Get the media property of the superclass, if it exists
  92
+        if hasattr(super(cls, self), 'media'):
  93
+            base = super(cls, self).media
  94
+        else:
  95
+            base = Media()
  96
+        
  97
+        # Get the media definition for this class    
  98
+        definition = getattr(cls, 'Media', None)
  99
+        if definition:
  100
+            extend = getattr(definition, 'extend', True)
  101
+            if extend:
  102
+                if extend == True:
  103
+                    m = base
  104
+                else:
  105
+                    m = Media()
  106
+                    for medium in extend:
  107
+                        m = m + base[medium]
  108
+                    m = m + Media(definition)
  109
+                return m + Media(definition)
  110
+            else:
  111
+                 return Media(definition)
  112
+        else:
  113
+            return base
  114
+    return property(_media)
  115
+    
  116
+class MediaDefiningClass(type):
  117
+    "Metaclass for classes that can have media definitions"
  118
+    def __new__(cls, name, bases, attrs):            
  119
+        new_class = type.__new__(cls, name, bases, attrs)
  120
+        if 'media' not in attrs:
  121
+            new_class.media = media_property(new_class)
  122
+        return new_class
  123
+        
25 124
 class Widget(object):
  125
+    __metaclass__ = MediaDefiningClass
26 126
     is_hidden = False          # Determines whether this corresponds to an <input type="hidden">.
27 127
 
28 128
     def __init__(self, attrs=None):
@@ -405,6 +505,14 @@ def decompress(self, value):
405 505
         """
406 506
         raise NotImplementedError('Subclasses must implement this method.')
407 507
 
  508
+    def _get_media(self):
  509
+        "Media for a multiwidget is the combination of all media of the subwidgets"
  510
+        media = Media()
  511
+        for w in self.widgets:
  512
+            media = media + w.media
  513
+        return media
  514
+    media = property(_get_media)
  515
+    
408 516
 class SplitDateTimeWidget(MultiWidget):
409 517
     """
410 518
     A Widget that splits datetime input into two <input type="text"> boxes.
@@ -417,3 +525,4 @@ def decompress(self, value):
417 525
         if value:
418 526
             return [value.date(), value.time()]
419 527
         return [None, None]
  528
+    
309  docs/newforms.txt
@@ -76,6 +76,9 @@ The library deals with these concepts:
76 76
     * **Form** -- A collection of fields that knows how to validate itself and
77 77
       display itself as HTML.
78 78
 
  79
+    * **Media** -- A definition of the CSS and JavaScript resources that are
  80
+      required to render a form.
  81
+      
79 82
 The library is decoupled from the other Django components, such as the database
80 83
 layer, views and templates. It relies only on Django settings, a couple of
81 84
 ``django.utils`` helper functions and Django's internationalization hooks (but
@@ -1940,6 +1943,312 @@ more than one model, or a form that contains fields that *aren't* on a model,
1940 1943
 you shouldn't use these shortcuts. Creating a ``Form`` class the "long" way
1941 1944
 isn't that difficult, after all.
1942 1945
 
  1946
+Media
  1947
+=====
  1948
+
  1949
+Rendering an attractive and easy-to-use web form requires more than just 
  1950
+HTML - it also requires CSS stylesheets, and if you want to use fancy 
  1951
+"Web2.0" widgets, you may also need to include some JavaScript on each 
  1952
+page. The exact combination of CSS and JavaScript that is required for 
  1953
+any given page will depend upon the widgets that are in use on that page.
  1954
+
  1955
+This is where Django media definitions come in. Django allows you to 
  1956
+associate different media files with the forms and widgets that require
  1957
+that media. For example, if you want to use a calendar to render DateFields, 
  1958
+you can define a custom Calendar widget. This widget can then be associated 
  1959
+with the CSS and Javascript that is required to render the calendar. When 
  1960
+the Calendar widget is used on a form, Django is able to identify the CSS and
  1961
+JavaScript files that are required, and provide the list of file names
  1962
+in a form suitable for easy inclusion on your web page.
  1963
+
  1964
+.. admonition:: Media and Django Admin
  1965
+
  1966
+    The Django Admin application defines a number of customized widgets
  1967
+    for calendars, filtered selections, and so on. These widgets define
  1968
+    media requirements, and the Django Admin uses the custom widgets
  1969
+    in place of the Django defaults. The Admin templates will only include
  1970
+    those media files that are required to render the widgets on any 
  1971
+    given page.
  1972
+    
  1973
+    If you like the widgets that the Django Admin application uses,
  1974
+    feel free to use them in your own application! They're all stored
  1975
+    in ``django.contrib.admin.widgets``.
  1976
+
  1977
+.. admonition:: Which JavaScript toolkit?
  1978
+
  1979
+    Many JavaScript toolkits exist, and many of them include widgets (such
  1980
+    as calendar widgets) that can be used to enhance your application. 
  1981
+    Django has deliberately avoided blessing any one JavaScript toolkit.
  1982
+    Each toolkit has its own relative strengths and weaknesses - use 
  1983
+    whichever toolkit suits your requirements. Django is able to integrate
  1984
+    with any JavaScript toolkit. 
  1985
+
  1986
+Media as a static definition
  1987
+----------------------------
  1988
+
  1989
+The easiest way to define media is as a static definition. Using this method,
  1990
+the media declaration is an inner class. The properties of the inner class
  1991
+define the media requirements. 
  1992
+
  1993
+Here's a simple example::
  1994
+
  1995
+    class CalendarWidget(forms.TextInput):
  1996
+        class Media:
  1997
+            css = {
  1998
+                'all': ('pretty.css',)
  1999
+            }
  2000
+            js = ('animations.js', 'actions.js')
  2001
+
  2002
+This code defines a ``CalendarWidget``, which will be based on ``TextInput``.
  2003
+Every time the CalendarWidget is used on a form, that form will be directed 
  2004
+to include the CSS file ``pretty.css``, and the JavaScript files 
  2005
+``animations.js`` and ``actions.js``. 
  2006
+
  2007
+This static media definition is converted at runtime into a widget property
  2008
+named ``media``. The media for a CalendarWidget instance can be retrieved 
  2009
+through this property::
  2010
+
  2011
+    >>> w = CalendarWidget()
  2012
+    >>> print w.media
  2013
+    <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
  2014
+    <script type="text/javascript" src="http://media.example.com/animations.js"></script>
  2015
+    <script type="text/javascript" src="http://media.example.com/actions.js"></script>
  2016
+
  2017
+Here's a list of all possible ``Media`` options. There are no required options.
  2018
+
  2019
+``css``
  2020
+~~~~~~~
  2021
+
  2022
+A dictionary describing the CSS files required for various forms of output 
  2023
+media. 
  2024
+
  2025
+The values in the dictionary should be a tuple/list of file names. See
  2026
+`the section on media paths`_ for details of how to specify paths to media 
  2027
+files.
  2028
+
  2029
+.. _the section on media paths: `Paths in media definitions`_
  2030
+
  2031
+The keys in the dictionary are the output media types. These are the same 
  2032
+types accepted by CSS files in media declarations: 'all', 'aural', 'braille',
  2033
+'embossed', 'handheld', 'print', 'projection', 'screen', 'tty' and 'tv'. If
  2034
+you need to have different stylesheets for different media types, provide
  2035
+a list of CSS files for each output medium. The following example would
  2036
+provide two CSS options -- one for the screen, and one for print::
  2037
+
  2038
+    class Media:
  2039
+        css = {
  2040
+            'screen': ('pretty.css',),
  2041
+            'print': ('newspaper.css',)
  2042
+        }
  2043
+
  2044
+If a group of CSS files are appropriate for multiple output media types, 
  2045
+the dictionary key can be a comma separated list of output media types. 
  2046
+In the following example, TV's and projectors will have the same media 
  2047
+requirements::
  2048
+
  2049
+    class Media:
  2050
+        css = {
  2051
+            'screen': ('pretty.css',),
  2052
+            'tv,projector': ('lo_res.css',),
  2053
+            'print': ('newspaper.css',)
  2054
+        }
  2055
+    
  2056
+If this last CSS definition were to be rendered, it would become the following HTML::
  2057
+
  2058
+    <link href="http://media.example.com/pretty.css" type="text/css" media="screen" rel="stylesheet" />
  2059
+    <link href="http://media.example.com/lo_res.css" type="text/css" media="tv,projector" rel="stylesheet" />
  2060
+    <link href="http://media.example.com/newspaper.css" type="text/css" media="print" rel="stylesheet" />
  2061
+                 
  2062
+``js``
  2063
+~~~~~~
  2064
+
  2065
+A tuple describing the required javascript files. See
  2066
+`the section on media paths`_ for details of how to specify paths to media 
  2067
+files.
  2068
+
  2069
+``extend``   
  2070
+~~~~~~~~~~
  2071
+
  2072
+A boolean defining inheritance behavior for media declarations. 
  2073
+
  2074
+By default, any object using a static media definition will inherit all the
  2075
+media associated with the parent widget. This occurs regardless of how the 
  2076
+parent defines its media requirements. For example, if we were to extend our
  2077
+basic Calendar widget from the example above:: 
  2078
+
  2079
+    class FancyCalendarWidget(CalendarWidget):
  2080
+        class Media:
  2081
+            css = {
  2082
+                'all': ('fancy.css',)
  2083
+            }
  2084
+            js = ('whizbang.js',)
  2085
+
  2086
+    >>> w = FancyCalendarWidget()
  2087
+    >>> print w.media
  2088
+    <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
  2089
+    <link href="http://media.example.com/fancy.css" type="text/css" media="all" rel="stylesheet" />
  2090
+    <script type="text/javascript" src="http://media.example.com/animations.js"></script>
  2091
+    <script type="text/javascript" src="http://media.example.com/actions.js"></script>
  2092
+    <script type="text/javascript" src="http://media.example.com/whizbang.js"></script>
  2093
+
  2094
+The FancyCalendar widget inherits all the media from it's parent widget. If 
  2095
+you don't want media to be inherited in this way, add an ``extend=False``
  2096
+declaration to the media declaration::
  2097
+
  2098
+    class FancyCalendar(Calendar):
  2099
+        class Media:
  2100
+            extend = False 
  2101
+            css = {
  2102
+                'all': ('fancy.css',)
  2103
+            }
  2104
+            js = ('whizbang.js',)
  2105
+
  2106
+    >>> w = FancyCalendarWidget()
  2107
+    >>> print w.media
  2108
+    <link href="http://media.example.com/fancy.css" type="text/css" media="all" rel="stylesheet" />
  2109
+    <script type="text/javascript" src="http://media.example.com/whizbang.js"></script>
  2110
+
  2111
+If you require even more control over media inheritance, define your media 
  2112
+using a `dynamic property`_. Dynamic properties give you complete control over
  2113
+which media files are inherited, and which are not.
  2114
+
  2115
+.. _dynamic property: `Media as a dynamic property`_
  2116
+    
  2117
+Media as a dynamic property
  2118
+---------------------------
  2119
+
  2120
+If you need to perform some more sophisticated manipulation of media
  2121
+requirements, you can define the media property directly. This is done
  2122
+by defining a model property that returns an instance of ``forms.Media``.
  2123
+The constructor for ``forms.Media`` accepts ``css`` and ``js`` keyword 
  2124
+arguments in the same format as that used in a static media definition.
  2125
+
  2126
+For example, the static media definition for our Calendar Widget could
  2127
+also be defined in a dynamic fashion::
  2128
+
  2129
+    class CalendarWidget(forms.TextInput):
  2130
+        def _media(self):
  2131
+            return forms.Media(css={'all': ('pretty.css',)},
  2132
+                               js=('animations.js', 'actions.js'))
  2133
+        media = property(_media)
  2134
+
  2135
+See the section on `Media objects`_ for more details on how to construct
  2136
+return values for dynamic media properties.
  2137
+
  2138
+Paths in media definitions
  2139
+--------------------------
  2140
+
  2141
+Paths used to specify media can be either relative or absolute. If a path 
  2142
+starts with 'http://' or 'https://', it will be interpreted as an absolute
  2143
+path, and left as-is. All other paths will be prepended with the value of
  2144
+``settings.MEDIA_URL``. For example, if the MEDIA_URL for your site was
  2145
+``http://media.example.com/``::
  2146
+
  2147
+    class CalendarWidget(forms.TextInput):
  2148
+        class Media:
  2149
+            js = ('animations.js', 'http://othersite.com/actions.js')
  2150
+
  2151
+    >>> w = CalendarWidget()
  2152
+    >>> print w.media
  2153
+    <script type="text/javascript" src="http://media.example.com/animations.js"></script>
  2154
+    <script type="text/javascript" src="http://othersite.com/actions.js"></script>
  2155
+
  2156
+Media objects
  2157
+-------------
  2158
+
  2159
+When you interrogate the media attribute of a widget or form, the value that
  2160
+is returned is a ``forms.Media`` object. As we have already seen, the string
  2161
+representation of a Media object is the HTML required to include media 
  2162
+in the ``<head>`` block of your HTML page. 
  2163
+
  2164
+However, Media objects have some other interesting properties.
  2165
+
  2166
+Media subsets
  2167
+~~~~~~~~~~~~~
  2168
+
  2169
+If you only want media of a particular type, you can use the subscript operator 
  2170
+to filter out a medium of interest. For example::
  2171
+
  2172
+    >>> w = CalendarWidget()
  2173
+    >>> print w.media
  2174
+    <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
  2175
+    <script type="text/javascript" src="http://media.example.com/animations.js"></script>
  2176
+    <script type="text/javascript" src="http://media.example.com/actions.js"></script>
  2177
+
  2178
+    >>> print w.media['css']
  2179
+    <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
  2180
+
  2181
+When you use the subscript operator, the value that is returned is a new 
  2182
+Media object -- but one that only contains the media of interest.
  2183
+
  2184
+Combining media objects
  2185
+~~~~~~~~~~~~~~~~~~~~~~~
  2186
+
  2187
+Media objects can also be added together. When two media objects are added,
  2188
+the resulting Media object contains the union of the media from both files::
  2189
+
  2190
+    class CalendarWidget(forms.TextInput):
  2191
+        class Media:
  2192
+            css = {
  2193
+                'all': ('pretty.css',)
  2194
+            }
  2195
+            js = ('animations.js', 'actions.js')
  2196
+
  2197
+    class OtherWidget(forms.TextInput):
  2198
+        class Media:
  2199
+            js = ('whizbang.js',)
  2200
+
  2201
+    >>> w1 = CalendarWidget()
  2202
+    >>> w2 = OtherWidget()
  2203
+    >>> print w1+w2
  2204
+    <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
  2205
+    <script type="text/javascript" src="http://media.example.com/animations.js"></script>
  2206
+    <script type="text/javascript" src="http://media.example.com/actions.js"></script>
  2207
+    <script type="text/javascript" src="http://media.example.com/whizbang.js"></script>
  2208
+    
  2209
+Media on Forms
  2210
+--------------
  2211
+
  2212
+Widgets aren't the only objects that can have media definitions -- forms
  2213
+can also define media. The rules for media definitions on forms are the
  2214
+same as the rules for widgets: declarations can be static or dynamic;
  2215
+path and inheritance rules for those declarations are exactly the same.
  2216
+
  2217
+Regardless of whether you define a media declaration, *all* Form objects 
  2218
+have a media property. The default value for this property is the result 
  2219
+of adding the media definitions for all widgets that are part of the form::
  2220
+
  2221
+    class ContactForm(forms.Form):
  2222
+        date = DateField(widget=CalendarWidget)
  2223
+        name = CharField(max_length=40, widget=OtherWidget)
  2224
+
  2225
+    >>> f = ContactForm()
  2226
+    >>> f.media
  2227
+    <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
  2228
+    <script type="text/javascript" src="http://media.example.com/animations.js"></script>
  2229
+    <script type="text/javascript" src="http://media.example.com/actions.js"></script>
  2230
+    <script type="text/javascript" src="http://media.example.com/whizbang.js"></script>
  2231
+
  2232
+If you want to associate additional media with a form -- for example, CSS for form 
  2233
+layout -- simply add a media declaration to the form::
  2234
+
  2235
+    class ContactForm(forms.Form):
  2236
+        date = DateField(widget=CalendarWidget)
  2237
+        name = CharField(max_length=40, widget=OtherWidget)
  2238
+
  2239
+        class Media:
  2240
+            css = {
  2241
+                'all': ('layout.css',)
  2242
+            }
  2243
+            
  2244
+    >>> f = ContactForm()
  2245
+    >>> f.media
  2246
+    <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
  2247
+    <link href="http://media.example.com/layout.css" type="text/css" media="all" rel="stylesheet" />
  2248
+    <script type="text/javascript" src="http://media.example.com/animations.js"></script>
  2249
+    <script type="text/javascript" src="http://media.example.com/actions.js"></script>
  2250
+    <script type="text/javascript" src="http://media.example.com/whizbang.js"></script>
  2251
+
1943 2252
 More coming soon
1944 2253
 ================
1945 2254
 
357  tests/regressiontests/forms/media.py
... ...
@@ -0,0 +1,357 @@
  1
+# -*- coding: utf-8 -*-
  2
+# Tests for the media handling on widgets and forms
  3
+
  4
+media_tests = r"""
  5
+>>> from django.newforms import TextInput, Media, TextInput, CharField, Form, MultiWidget
  6
+>>> from django.conf import settings
  7
+>>> settings.MEDIA_URL = 'http://media.example.com'
  8
+
  9
+# Check construction of media objects
  10
+>>> 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'))
  11
+>>> print m
  12
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  13
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  14
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  15
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  16
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  17
+
  18
+>>> class Foo:
  19
+...     css = {
  20
+...        'all': ('/path/to/css1','/path/to/css2')
  21
+...     }
  22
+...     js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3')
  23
+>>> m3 = Media(Foo)
  24
+>>> print m3
  25
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  26
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  27
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  28
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  29
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  30
+
  31
+>>> m3 = Media(Foo)
  32
+>>> print m3
  33
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  34
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  35
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  36
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  37
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  38
+
  39
+# A widget can exist without a media definition
  40
+>>> class MyWidget(TextInput):
  41
+...     pass
  42
+
  43
+>>> w = MyWidget()
  44
+>>> print w.media
  45
+<BLANKLINE>
  46
+
  47
+###############################################################
  48
+# DSL Class-based media definitions
  49
+###############################################################
  50
+
  51
+# A widget can define media if it needs to.
  52
+# Any absolute path will be preserved; relative paths are combined
  53
+# with the value of settings.MEDIA_URL
  54
+>>> class MyWidget1(TextInput):
  55
+...     class Media:
  56
+...         css = {
  57
+...            'all': ('/path/to/css1','/path/to/css2')
  58
+...         }
  59
+...         js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3')
  60
+
  61
+>>> w1 = MyWidget1()
  62
+>>> print w1.media
  63
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  64
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  65
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  66
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  67
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  68
+
  69
+# Media objects can be interrogated by media type
  70
+>>> print w1.media['css']
  71
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  72
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  73
+
  74
+>>> print w1.media['js']
  75
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  76
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  77
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  78
+
  79
+# Media objects can be combined. Any given media resource will appear only
  80
+# once. Duplicated media definitions are ignored.
  81
+>>> class MyWidget2(TextInput):
  82
+...     class Media:
  83
+...         css = {
  84
+...            'all': ('/path/to/css2','/path/to/css3')
  85
+...         }
  86
+...         js = ('/path/to/js1','/path/to/js4')
  87
+
  88
+>>> class MyWidget3(TextInput):
  89
+...     class Media:
  90
+...         css = {
  91
+...            'all': ('/path/to/css3','/path/to/css1')
  92
+...         }
  93
+...         js = ('/path/to/js1','/path/to/js4')
  94
+
  95
+>>> w2 = MyWidget2()
  96
+>>> w3 = MyWidget3()
  97
+>>> print w1.media + w2.media + w3.media
  98
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  99
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  100
+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
  101
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  102
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  103
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  104
+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
  105
+
  106
+# Check that media addition hasn't affected the original objects
  107
+>>> print w1.media
  108
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  109
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  110
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  111
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  112
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  113
+
  114
+###############################################################
  115
+# Property-based media definitions
  116
+###############################################################
  117
+
  118
+# Widget media can be defined as a property
  119
+>>> class MyWidget4(TextInput):
  120
+...     def _media(self):
  121
+...         return Media(css={'all': ('/some/path',)}, js = ('/some/js',))
  122
+...     media = property(_media)
  123
+
  124
+>>> w4 = MyWidget4()
  125
+>>> print w4.media
  126
+<link href="http://media.example.com/some/path" type="text/css" media="all" rel="stylesheet" />
  127
+<script type="text/javascript" src="http://media.example.com/some/js"></script>
  128
+
  129
+# Media properties can reference the media of their parents
  130
+>>> class MyWidget5(MyWidget4):
  131
+...     def _media(self):
  132
+...         return super(MyWidget5, self).media + Media(css={'all': ('/other/path',)}, js = ('/other/js',))
  133
+...     media = property(_media)
  134
+
  135
+>>> w5 = MyWidget5()
  136
+>>> print w5.media
  137
+<link href="http://media.example.com/some/path" type="text/css" media="all" rel="stylesheet" />
  138
+<link href="http://media.example.com/other/path" type="text/css" media="all" rel="stylesheet" />
  139
+<script type="text/javascript" src="http://media.example.com/some/js"></script>
  140
+<script type="text/javascript" src="http://media.example.com/other/js"></script>
  141
+
  142
+# Media properties can reference the media of their parents,
  143
+# even if the parent media was defined using a class
  144
+>>> class MyWidget6(MyWidget1):
  145
+...     def _media(self):
  146
+...         return super(MyWidget6, self).media + Media(css={'all': ('/other/path',)}, js = ('/other/js',))
  147
+...     media = property(_media)
  148
+
  149
+>>> w6 = MyWidget6()
  150
+>>> print w6.media
  151
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  152
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  153
+<link href="http://media.example.com/other/path" type="text/css" media="all" rel="stylesheet" />
  154
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  155
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  156
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  157
+<script type="text/javascript" src="http://media.example.com/other/js"></script>
  158
+
  159
+###############################################################
  160
+# Inheritance of media
  161
+###############################################################
  162
+
  163
+# If a widget extends another but provides no media definition, it inherits the parent widget's media
  164
+>>> class MyWidget7(MyWidget1):
  165
+...     pass
  166
+
  167
+>>> w7 = MyWidget7()
  168
+>>> print w7.media
  169
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  170
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  171
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  172
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  173
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  174
+
  175
+# If a widget extends another but defines media, it extends the parent widget's media by default
  176
+>>> class MyWidget8(MyWidget1):
  177
+...     class Media:
  178
+...         css = {
  179
+...            'all': ('/path/to/css3','/path/to/css1')
  180
+...         }
  181
+...         js = ('/path/to/js1','/path/to/js4')
  182
+
  183
+>>> w8 = MyWidget8()
  184
+>>> print w8.media
  185
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  186
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  187
+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
  188
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  189
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  190
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  191
+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
  192
+
  193
+# If a widget extends another but defines media, it extends the parents widget's media,
  194
+# even if the parent defined media using a property.
  195
+>>> class MyWidget9(MyWidget4):
  196
+...     class Media:
  197
+...         css = {
  198
+...             'all': ('/other/path',)
  199
+...         }
  200
+...         js = ('/other/js',)
  201
+
  202
+>>> w9 = MyWidget9()
  203
+>>> print w9.media
  204
+<link href="http://media.example.com/some/path" type="text/css" media="all" rel="stylesheet" />
  205
+<link href="http://media.example.com/other/path" type="text/css" media="all" rel="stylesheet" />
  206
+<script type="text/javascript" src="http://media.example.com/some/js"></script>
  207
+<script type="text/javascript" src="http://media.example.com/other/js"></script>
  208
+
  209
+# A widget can disable media inheritance by specifying 'extend=False'
  210
+>>> class MyWidget10(MyWidget1):
  211
+...     class Media:
  212
+...         extend = False
  213
+...         css = {
  214
+...            'all': ('/path/to/css3','/path/to/css1')
  215
+...         }
  216
+...         js = ('/path/to/js1','/path/to/js4')
  217
+
  218
+>>> w10 = MyWidget10()
  219
+>>> print w10.media
  220
+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
  221
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  222
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  223
+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
  224
+
  225
+# A widget can explicitly enable full media inheritance by specifying 'extend=True'
  226
+>>> class MyWidget11(MyWidget1):
  227
+...     class Media:
  228
+...         extend = True
  229
+...         css = {
  230
+...            'all': ('/path/to/css3','/path/to/css1')
  231
+...         }
  232
+...         js = ('/path/to/js1','/path/to/js4')
  233
+
  234
+>>> w11 = MyWidget11()
  235
+>>> print w11.media
  236
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  237
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  238
+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
  239
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  240
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  241
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  242
+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
  243
+
  244
+# A widget can enable inheritance of one media type by specifying extend as a tuple
  245
+>>> class MyWidget12(MyWidget1):
  246
+...     class Media:
  247
+...         extend = ('css',)
  248
+...         css = {
  249
+...            'all': ('/path/to/css3','/path/to/css1')
  250
+...         }
  251
+...         js = ('/path/to/js1','/path/to/js4')
  252
+
  253
+>>> w12 = MyWidget12()
  254
+>>> print w12.media
  255
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  256
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  257
+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
  258
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  259
+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
  260
+
  261
+###############################################################
  262
+# Multi-media handling for CSS
  263
+###############################################################
  264
+
  265
+# A widget can define CSS media for multiple output media types
  266
+>>> class MultimediaWidget(TextInput):
  267
+...     class Media:
  268
+...         css = {
  269
+...            'screen, print': ('/file1','/file2'),
  270
+...            'screen': ('/file3',),
  271
+...            'print': ('/file4',)
  272
+...         }
  273
+...         js = ('/path/to/js1','/path/to/js4')
  274
+
  275
+>>> multimedia = MultimediaWidget()
  276
+>>> print multimedia.media
  277
+<link href="http://media.example.com/file4" type="text/css" media="print" rel="stylesheet" />
  278
+<link href="http://media.example.com/file3" type="text/css" media="screen" rel="stylesheet" />
  279
+<link href="http://media.example.com/file1" type="text/css" media="screen, print" rel="stylesheet" />
  280
+<link href="http://media.example.com/file2" type="text/css" media="screen, print" rel="stylesheet" />
  281
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  282
+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
  283
+
  284
+###############################################################
  285
+# Multiwidget media handling
  286
+###############################################################
  287
+
  288
+# MultiWidgets have a default media definition that gets all the 
  289
+# media from the component widgets
  290
+>>> class MyMultiWidget(MultiWidget):
  291
+...     def __init__(self, attrs=None):
  292
+...         widgets = [MyWidget1, MyWidget2, MyWidget3]
  293
+...         super(MyMultiWidget, self).__init__(widgets, attrs)
  294
+            
  295
+>>> mymulti = MyMultiWidget()
  296
+>>> print mymulti.media   
  297
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  298
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  299
+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
  300
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  301
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  302
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  303
+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
  304
+
  305
+###############################################################
  306
+# Media processing for forms
  307
+###############################################################
  308
+
  309
+# You can ask a form for the media required by its widgets.
  310
+>>> class MyForm(Form):
  311
+...     field1 = CharField(max_length=20, widget=MyWidget1())
  312
+...     field2 = CharField(max_length=20, widget=MyWidget2())
  313
+>>> f1 = MyForm()
  314
+>>> print f1.media
  315
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  316
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  317
+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
  318
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  319
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  320
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  321
+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
  322
+
  323
+# Form media can be combined to produce a single media definition.
  324
+>>> class AnotherForm(Form):
  325
+...     field3 = CharField(max_length=20, widget=MyWidget3())
  326
+>>> f2 = AnotherForm()
  327
+>>> print f1.media + f2.media
  328
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  329
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  330
+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
  331
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  332
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  333
+<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>
  334
+<script type="text/javascript" src="http://media.example.com/path/to/js4"></script>
  335
+
  336
+# Forms can also define media, following the same rules as widgets.
  337
+>>> class FormWithMedia(Form):
  338
+...     field1 = CharField(max_length=20, widget=MyWidget1())
  339
+...     field2 = CharField(max_length=20, widget=MyWidget2())
  340
+...     class Media:
  341
+...         js = ('/some/form/javascript',)
  342
+...         css = {
  343
+...             'all': ('/some/form/css',)
  344
+...         }
  345
+>>> f3 = FormWithMedia()
  346
+>>> print f3.media
  347
+<link href="http://media.example.com/path/to/css1" type="text/css" media="all" rel="stylesheet" />
  348
+<link href="http://media.example.com/path/to/css2" type="text/css" media="all" rel="stylesheet" />
  349
+<link href="http://media.example.com/path/to/css3" type="text/css" media="all" rel="stylesheet" />
  350
+<link href="http://media.example.com/some/form/css" type="text/css" media="all" rel="stylesheet" />
  351
+<script type="text/javascript" src="http://media.example.com/path/to/js1"></script>
  352
+<script type="text/javascript" src="http://media.other.com/path/to/js2"></script>
  353
+<script type="text/javascript" src="htt