diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 77a65ee990..6373f6028c 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -2,6 +2,7 @@ from django import oldforms, template from django import newforms as forms from django.newforms.formsets import all_valid from django.newforms.models import inline_formset +from django.newforms.widgets import Media, MediaDefiningClass from django.contrib.admin import widgets from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.db import models @@ -48,6 +49,13 @@ class AdminForm(object): def first_field(self): for bf in self.form: return bf + + def _media(self): + media = self.form.media + for fs in self.fieldsets: + media = media + fs.media + return media + media = property(_media) class Fieldset(object): def __init__(self, name=None, fields=(), classes=(), description=None): @@ -55,6 +63,13 @@ class Fieldset(object): self.classes = u' '.join(classes) self.description = description + def _media(self): + from django.conf import settings + if 'collapse' in self.classes: + return Media(js=['%sjs/admin/CollapsedFieldsets.js' % settings.ADMIN_MEDIA_PREFIX]) + return Media() + media = property(_media) + class BoundFieldset(object): def __init__(self, form, fieldset): self.form, self.fieldset = form, fieldset @@ -123,12 +138,12 @@ class BaseModelAdmin(object): # For DateFields, add a custom CSS class. if isinstance(db_field, models.DateField): - kwargs['widget'] = forms.TextInput(attrs={'class': 'vDateField', 'size': '10'}) + kwargs['widget'] = widgets.AdminDateWidget return db_field.formfield(**kwargs) # For TimeFields, add a custom CSS class. if isinstance(db_field, models.TimeField): - kwargs['widget'] = forms.TextInput(attrs={'class': 'vTimeField', 'size': '8'}) + kwargs['widget'] = widgets.AdminTimeWidget return db_field.formfield(**kwargs) # For ForeignKey or ManyToManyFields, use a special widget. @@ -148,7 +163,8 @@ class BaseModelAdmin(object): class ModelAdmin(BaseModelAdmin): "Encapsulates all admin options and functionality for a given model." - + __metaclass__ = MediaDefiningClass + list_display = ('__str__',) list_display_links = () list_filter = () @@ -159,7 +175,6 @@ class ModelAdmin(BaseModelAdmin): save_as = False save_on_top = False ordering = None - js = None prepopulated_fields = {} filter_vertical = () filter_horizontal = () @@ -194,38 +209,20 @@ class ModelAdmin(BaseModelAdmin): else: return self.change_view(request, unquote(url)) - def javascript(self, request, fieldsets): - """ - Returns a list of URLs to include via -{% for js in javascript_imports %}{% include_admin_script js %}{% endfor %} {% endblock %} {% block stylesheet %}{% admin_media_prefix %}css/forms.css{% endblock %} {% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %} diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index 75c645665d..d0cce130db 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -3,8 +3,7 @@ {% block extrahead %}{{ block.super }} -{% for js in javascript_imports %} -{% endfor %} +{{ media }} {% endblock %} {% block stylesheet %}{% admin_media_prefix %}css/forms.css{% endblock %} diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 7718110a8b..f5d0c458c9 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -5,6 +5,7 @@ Form Widget classes specific to the Django admin site. from django import newforms as forms from django.utils.text import capfirst from django.utils.translation import ugettext as _ +from django.conf import settings class FilteredSelectMultiple(forms.SelectMultiple): """ @@ -28,13 +29,28 @@ class FilteredSelectMultiple(forms.SelectMultiple): (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), settings.ADMIN_MEDIA_PREFIX)) return u''.join(output) +class AdminDateWidget(forms.TextInput): + class Media: + js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", + settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js") + + def __init__(self, attrs={}): + super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'}) + +class AdminTimeWidget(forms.TextInput): + class Media: + js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", + settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js") + + def __init__(self, attrs={}): + super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'}) + class AdminSplitDateTime(forms.SplitDateTimeWidget): """ A SplitDateTime Widget that has some admin-specific styling. """ def __init__(self, attrs=None): - widgets = [forms.TextInput(attrs={'class': 'vDateField', 'size': '10'}), - forms.TextInput(attrs={'class': 'vTimeField', 'size': '8'})] + widgets = [AdminDateWidget, AdminTimeWidget] # Note that we're calling MultiWidget, not SplitDateTimeWidget, because # we want to define widgets. forms.MultiWidget.__init__(self, widgets, attrs) diff --git a/django/newforms/forms.py b/django/newforms/forms.py index bf1d9a88cf..0e803d2613 100644 --- a/django/newforms/forms.py +++ b/django/newforms/forms.py @@ -9,7 +9,7 @@ from django.utils.html import escape from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode from fields import Field -from widgets import TextInput, Textarea +from widgets import Media, media_property, TextInput, Textarea from util import flatatt, ErrorDict, ErrorList, ValidationError __all__ = ('BaseForm', 'Form') @@ -37,6 +37,7 @@ class DeclarativeFieldsMetaclass(type): """ Metaclass that converts Field attributes to a dictionary called 'base_fields', taking into account parent class 'base_fields' as well. + Also integrates any additional media definitions """ def __new__(cls, name, bases, attrs): fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] @@ -50,7 +51,11 @@ class DeclarativeFieldsMetaclass(type): fields = base.base_fields.items() + fields attrs['base_fields'] = SortedDictFromList(fields) - return type.__new__(cls, name, bases, attrs) + + new_class = type.__new__(cls, name, bases, attrs) + if 'media' not in attrs: + new_class.media = media_property(new_class) + return new_class class BaseForm(StrAndUnicode): # This is the main implementation of all the Form logic. Note that this @@ -235,6 +240,16 @@ class BaseForm(StrAndUnicode): self.is_bound = False self.__errors = None + def _get_media(self): + """ + Provide a description of all media required to render the widgets on this form + """ + media = Media() + for field in self.fields.values(): + media = media + field.widget.media + return media + media = property(_get_media) + class Form(BaseForm): "A collection of Fields, plus their associated data." # This is a separate class from BaseForm in order to abstract the way diff --git a/django/newforms/formsets.py b/django/newforms/formsets.py index b6e142ed95..7637a093d9 100644 --- a/django/newforms/formsets.py +++ b/django/newforms/formsets.py @@ -1,6 +1,6 @@ from forms import Form, ValidationError from fields import IntegerField, BooleanField -from widgets import HiddenInput +from widgets import HiddenInput, Media # special field names FORM_COUNT_FIELD_NAME = 'COUNT' @@ -154,6 +154,15 @@ class BaseFormSet(object): self.full_clean() return self._is_valid + def _get_media(self): + # All the forms on a FormSet are the same, so you only need to + # interrogate the first form for media. + if self.forms: + return self.forms[0].media + else: + return Media() + media = property(_get_media) + def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False): """Return a FormSet for the given form class.""" attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable} diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py index f985124389..e17e22fa8f 100644 --- a/django/newforms/widgets.py +++ b/django/newforms/widgets.py @@ -8,6 +8,7 @@ except NameError: from sets import Set as set # Python 2.3 fallback from itertools import chain +from django.conf import settings from django.utils.datastructures import MultiValueDict from django.utils.html import escape from django.utils.translation import ugettext @@ -15,14 +16,113 @@ from django.utils.encoding import StrAndUnicode, force_unicode from util import flatatt __all__ = ( - 'Widget', 'TextInput', 'PasswordInput', + 'Media', 'Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 'MultipleHiddenInput', 'FileInput', 'Textarea', 'CheckboxInput', 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget', ) +MEDIA_TYPES = ('css','js') + +class Media(StrAndUnicode): + def __init__(self, media=None, **kwargs): + if media: + media_attrs = media.__dict__ + else: + media_attrs = kwargs + + self._css = {} + self._js = [] + + for name in MEDIA_TYPES: + getattr(self, 'add_' + name)(media_attrs.get(name, None)) + + # Any leftover attributes must be invalid. + # if media_attrs != {}: + # raise TypeError, "'class Media' has invalid attribute(s): %s" % ','.join(media_attrs.keys()) + + def __unicode__(self): + return self.render() + + def render(self): + return u'\n'.join(chain(*[getattr(self, 'render_' + name)() for name in MEDIA_TYPES])) + + def render_js(self): + return [u'' % self.absolute_path(path) for path in self._js] + + def render_css(self): + # To keep rendering order consistent, we can't just iterate over items(). + # We need to sort the keys, and iterate over the sorted list. + media = self._css.keys() + media.sort() + return chain(*[ + [u'' % (self.absolute_path(path), medium) + for path in self._css[medium]] + for medium in media]) + + def absolute_path(self, path): + return (path.startswith(u'http://') or path.startswith(u'https://')) and path or u''.join([settings.MEDIA_URL,path]) + + def __getitem__(self, name): + "Returns a Media object that only contains media of the given type" + if name in MEDIA_TYPES: + return Media(**{name: getattr(self, '_' + name)}) + raise KeyError('Unknown media type "%s"' % name) + + def add_js(self, data): + if data: + self._js.extend([path for path in data if path not in self._js]) + + def add_css(self, data): + if data: + for medium, paths in data.items(): + self._css.setdefault(medium, []).extend([path for path in paths if path not in self._css[medium]]) + + def __add__(self, other): + combined = Media() + for name in MEDIA_TYPES: + getattr(combined, 'add_' + name)(getattr(self, '_' + name, None)) + getattr(combined, 'add_' + name)(getattr(other, '_' + name, None)) + return combined + +def media_property(cls): + def _media(self): + # Get the media property of the superclass, if it exists + if hasattr(super(cls, self), 'media'): + base = super(cls, self).media + else: + base = Media() + + # Get the media definition for this class + definition = getattr(cls, 'Media', None) + if definition: + extend = getattr(definition, 'extend', True) + if extend: + if extend == True: + m = base + else: + m = Media() + for medium in extend: + m = m + base[medium] + m = m + Media(definition) + return m + Media(definition) + else: + return Media(definition) + else: + return base + return property(_media) + +class MediaDefiningClass(type): + "Metaclass for classes that can have media definitions" + def __new__(cls, name, bases, attrs): + new_class = type.__new__(cls, name, bases, attrs) + if 'media' not in attrs: + new_class.media = media_property(new_class) + return new_class + class Widget(object): + __metaclass__ = MediaDefiningClass is_hidden = False # Determines whether this corresponds to an . def __init__(self, attrs=None): @@ -405,6 +505,14 @@ class MultiWidget(Widget): """ raise NotImplementedError('Subclasses must implement this method.') + def _get_media(self): + "Media for a multiwidget is the combination of all media of the subwidgets" + media = Media() + for w in self.widgets: + media = media + w.media + return media + media = property(_get_media) + class SplitDateTimeWidget(MultiWidget): """ A Widget that splits datetime input into two boxes. @@ -417,3 +525,4 @@ class SplitDateTimeWidget(MultiWidget): if value: return [value.date(), value.time()] return [None, None] + \ No newline at end of file diff --git a/docs/newforms.txt b/docs/newforms.txt index 803dc6458a..db4e6596e4 100644 --- a/docs/newforms.txt +++ b/docs/newforms.txt @@ -76,6 +76,9 @@ The library deals with these concepts: * **Form** -- A collection of fields that knows how to validate itself and display itself as HTML. + * **Media** -- A definition of the CSS and JavaScript resources that are + required to render a form. + The library is decoupled from the other Django components, such as the database layer, views and templates. It relies only on Django settings, a couple of ``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, you shouldn't use these shortcuts. Creating a ``Form`` class the "long" way isn't that difficult, after all. +Media +===== + +Rendering an attractive and easy-to-use web form requires more than just +HTML - it also requires CSS stylesheets, and if you want to use fancy +"Web2.0" widgets, you may also need to include some JavaScript on each +page. The exact combination of CSS and JavaScript that is required for +any given page will depend upon the widgets that are in use on that page. + +This is where Django media definitions come in. Django allows you to +associate different media files with the forms and widgets that require +that media. For example, if you want to use a calendar to render DateFields, +you can define a custom Calendar widget. This widget can then be associated +with the CSS and Javascript that is required to render the calendar. When +the Calendar widget is used on a form, Django is able to identify the CSS and +JavaScript files that are required, and provide the list of file names +in a form suitable for easy inclusion on your web page. + +.. admonition:: Media and Django Admin + + The Django Admin application defines a number of customized widgets + for calendars, filtered selections, and so on. These widgets define + media requirements, and the Django Admin uses the custom widgets + in place of the Django defaults. The Admin templates will only include + those media files that are required to render the widgets on any + given page. + + If you like the widgets that the Django Admin application uses, + feel free to use them in your own application! They're all stored + in ``django.contrib.admin.widgets``. + +.. admonition:: Which JavaScript toolkit? + + Many JavaScript toolkits exist, and many of them include widgets (such + as calendar widgets) that can be used to enhance your application. + Django has deliberately avoided blessing any one JavaScript toolkit. + Each toolkit has its own relative strengths and weaknesses - use + whichever toolkit suits your requirements. Django is able to integrate + with any JavaScript toolkit. + +Media as a static definition +---------------------------- + +The easiest way to define media is as a static definition. Using this method, +the media declaration is an inner class. The properties of the inner class +define the media requirements. + +Here's a simple example:: + + class CalendarWidget(forms.TextInput): + class Media: + css = { + 'all': ('pretty.css',) + } + js = ('animations.js', 'actions.js') + +This code defines a ``CalendarWidget``, which will be based on ``TextInput``. +Every time the CalendarWidget is used on a form, that form will be directed +to include the CSS file ``pretty.css``, and the JavaScript files +``animations.js`` and ``actions.js``. + +This static media definition is converted at runtime into a widget property +named ``media``. The media for a CalendarWidget instance can be retrieved +through this property:: + + >>> w = CalendarWidget() + >>> print w.media + + + + +Here's a list of all possible ``Media`` options. There are no required options. + +``css`` +~~~~~~~ + +A dictionary describing the CSS files required for various forms of output +media. + +The values in the dictionary should be a tuple/list of file names. See +`the section on media paths`_ for details of how to specify paths to media +files. + +.. _the section on media paths: `Paths in media definitions`_ + +The keys in the dictionary are the output media types. These are the same +types accepted by CSS files in media declarations: 'all', 'aural', 'braille', +'embossed', 'handheld', 'print', 'projection', 'screen', 'tty' and 'tv'. If +you need to have different stylesheets for different media types, provide +a list of CSS files for each output medium. The following example would +provide two CSS options -- one for the screen, and one for print:: + + class Media: + css = { + 'screen': ('pretty.css',), + 'print': ('newspaper.css',) + } + +If a group of CSS files are appropriate for multiple output media types, +the dictionary key can be a comma separated list of output media types. +In the following example, TV's and projectors will have the same media +requirements:: + + class Media: + css = { + 'screen': ('pretty.css',), + 'tv,projector': ('lo_res.css',), + 'print': ('newspaper.css',) + } + +If this last CSS definition were to be rendered, it would become the following HTML:: + + + + + +``js`` +~~~~~~ + +A tuple describing the required javascript files. See +`the section on media paths`_ for details of how to specify paths to media +files. + +``extend`` +~~~~~~~~~~ + +A boolean defining inheritance behavior for media declarations. + +By default, any object using a static media definition will inherit all the +media associated with the parent widget. This occurs regardless of how the +parent defines its media requirements. For example, if we were to extend our +basic Calendar widget from the example above:: + + class FancyCalendarWidget(CalendarWidget): + class Media: + css = { + 'all': ('fancy.css',) + } + js = ('whizbang.js',) + + >>> w = FancyCalendarWidget() + >>> print w.media + + + + + + +The FancyCalendar widget inherits all the media from it's parent widget. If +you don't want media to be inherited in this way, add an ``extend=False`` +declaration to the media declaration:: + + class FancyCalendar(Calendar): + class Media: + extend = False + css = { + 'all': ('fancy.css',) + } + js = ('whizbang.js',) + + >>> w = FancyCalendarWidget() + >>> print w.media + + + +If you require even more control over media inheritance, define your media +using a `dynamic property`_. Dynamic properties give you complete control over +which media files are inherited, and which are not. + +.. _dynamic property: `Media as a dynamic property`_ + +Media as a dynamic property +--------------------------- + +If you need to perform some more sophisticated manipulation of media +requirements, you can define the media property directly. This is done +by defining a model property that returns an instance of ``forms.Media``. +The constructor for ``forms.Media`` accepts ``css`` and ``js`` keyword +arguments in the same format as that used in a static media definition. + +For example, the static media definition for our Calendar Widget could +also be defined in a dynamic fashion:: + + class CalendarWidget(forms.TextInput): + def _media(self): + return forms.Media(css={'all': ('pretty.css',)}, + js=('animations.js', 'actions.js')) + media = property(_media) + +See the section on `Media objects`_ for more details on how to construct +return values for dynamic media properties. + +Paths in media definitions +-------------------------- + +Paths used to specify media can be either relative or absolute. If a path +starts with 'http://' or 'https://', it will be interpreted as an absolute +path, and left as-is. All other paths will be prepended with the value of +``settings.MEDIA_URL``. For example, if the MEDIA_URL for your site was +``http://media.example.com/``:: + + class CalendarWidget(forms.TextInput): + class Media: + js = ('animations.js', 'http://othersite.com/actions.js') + + >>> w = CalendarWidget() + >>> print w.media + + + +Media objects +------------- + +When you interrogate the media attribute of a widget or form, the value that +is returned is a ``forms.Media`` object. As we have already seen, the string +representation of a Media object is the HTML required to include media +in the ```` block of your HTML page. + +However, Media objects have some other interesting properties. + +Media subsets +~~~~~~~~~~~~~ + +If you only want media of a particular type, you can use the subscript operator +to filter out a medium of interest. For example:: + + >>> w = CalendarWidget() + >>> print w.media + + + + + >>> print w.media['css'] + + +When you use the subscript operator, the value that is returned is a new +Media object -- but one that only contains the media of interest. + +Combining media objects +~~~~~~~~~~~~~~~~~~~~~~~ + +Media objects can also be added together. When two media objects are added, +the resulting Media object contains the union of the media from both files:: + + class CalendarWidget(forms.TextInput): + class Media: + css = { + 'all': ('pretty.css',) + } + js = ('animations.js', 'actions.js') + + class OtherWidget(forms.TextInput): + class Media: + js = ('whizbang.js',) + + >>> w1 = CalendarWidget() + >>> w2 = OtherWidget() + >>> print w1+w2 + + + + + +Media on Forms +-------------- + +Widgets aren't the only objects that can have media definitions -- forms +can also define media. The rules for media definitions on forms are the +same as the rules for widgets: declarations can be static or dynamic; +path and inheritance rules for those declarations are exactly the same. + +Regardless of whether you define a media declaration, *all* Form objects +have a media property. The default value for this property is the result +of adding the media definitions for all widgets that are part of the form:: + + class ContactForm(forms.Form): + date = DateField(widget=CalendarWidget) + name = CharField(max_length=40, widget=OtherWidget) + + >>> f = ContactForm() + >>> f.media + + + + + +If you want to associate additional media with a form -- for example, CSS for form +layout -- simply add a media declaration to the form:: + + class ContactForm(forms.Form): + date = DateField(widget=CalendarWidget) + name = CharField(max_length=40, widget=OtherWidget) + + class Media: + css = { + 'all': ('layout.css',) + } + + >>> f = ContactForm() + >>> f.media + + + + + + More coming soon ================ diff --git a/tests/regressiontests/forms/media.py b/tests/regressiontests/forms/media.py new file mode 100644 index 0000000000..f9b0fbc492 --- /dev/null +++ b/tests/regressiontests/forms/media.py @@ -0,0 +1,357 @@ +# -*- coding: utf-8 -*- +# Tests for the media handling on widgets and forms + +media_tests = r""" +>>> from django.newforms import TextInput, Media, TextInput, CharField, Form, MultiWidget +>>> from django.conf import settings +>>> settings.MEDIA_URL = 'http://media.example.com' + +# Check construction of media objects +>>> 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')) +>>> print m + + + + + + +>>> class Foo: +... 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') +>>> m3 = Media(Foo) +>>> print m3 + + + + + + +>>> m3 = Media(Foo) +>>> print m3 + + + + + + +# A widget can exist without a media definition +>>> class MyWidget(TextInput): +... pass + +>>> w = MyWidget() +>>> print w.media + + +############################################################### +# DSL Class-based media definitions +############################################################### + +# A widget can define media if it needs to. +# Any absolute path will be preserved; relative paths are combined +# with the value of settings.MEDIA_URL +>>> class MyWidget1(TextInput): +... class 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') + +>>> w1 = MyWidget1() +>>> print w1.media + + + + + + +# Media objects can be interrogated by media type +>>> print w1.media['css'] + + + +>>> print w1.media['js'] + + + + +# Media objects can be combined. Any given media resource will appear only +# once. Duplicated media definitions are ignored. +>>> class MyWidget2(TextInput): +... class Media: +... css = { +... 'all': ('/path/to/css2','/path/to/css3') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> class MyWidget3(TextInput): +... class Media: +... css = { +... 'all': ('/path/to/css3','/path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w2 = MyWidget2() +>>> w3 = MyWidget3() +>>> print w1.media + w2.media + w3.media + + + + + + + + +# Check that media addition hasn't affected the original objects +>>> print w1.media + + + + + + +############################################################### +# Property-based media definitions +############################################################### + +# Widget media can be defined as a property +>>> class MyWidget4(TextInput): +... def _media(self): +... return Media(css={'all': ('/some/path',)}, js = ('/some/js',)) +... media = property(_media) + +>>> w4 = MyWidget4() +>>> print w4.media + + + +# Media properties can reference the media of their parents +>>> class MyWidget5(MyWidget4): +... def _media(self): +... return super(MyWidget5, self).media + Media(css={'all': ('/other/path',)}, js = ('/other/js',)) +... media = property(_media) + +>>> w5 = MyWidget5() +>>> print w5.media + + + + + +# Media properties can reference the media of their parents, +# even if the parent media was defined using a class +>>> class MyWidget6(MyWidget1): +... def _media(self): +... return super(MyWidget6, self).media + Media(css={'all': ('/other/path',)}, js = ('/other/js',)) +... media = property(_media) + +>>> w6 = MyWidget6() +>>> print w6.media + + + + + + + + +############################################################### +# Inheritance of media +############################################################### + +# If a widget extends another but provides no media definition, it inherits the parent widget's media +>>> class MyWidget7(MyWidget1): +... pass + +>>> w7 = MyWidget7() +>>> print w7.media + + + + + + +# If a widget extends another but defines media, it extends the parent widget's media by default +>>> class MyWidget8(MyWidget1): +... class Media: +... css = { +... 'all': ('/path/to/css3','/path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w8 = MyWidget8() +>>> print w8.media + + + + + + + + +# If a widget extends another but defines media, it extends the parents widget's media, +# even if the parent defined media using a property. +>>> class MyWidget9(MyWidget4): +... class Media: +... css = { +... 'all': ('/other/path',) +... } +... js = ('/other/js',) + +>>> w9 = MyWidget9() +>>> print w9.media + + + + + +# A widget can disable media inheritance by specifying 'extend=False' +>>> class MyWidget10(MyWidget1): +... class Media: +... extend = False +... css = { +... 'all': ('/path/to/css3','/path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w10 = MyWidget10() +>>> print w10.media + + + + + +# A widget can explicitly enable full media inheritance by specifying 'extend=True' +>>> class MyWidget11(MyWidget1): +... class Media: +... extend = True +... css = { +... 'all': ('/path/to/css3','/path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w11 = MyWidget11() +>>> print w11.media + + + + + + + + +# A widget can enable inheritance of one media type by specifying extend as a tuple +>>> class MyWidget12(MyWidget1): +... class Media: +... extend = ('css',) +... css = { +... 'all': ('/path/to/css3','/path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w12 = MyWidget12() +>>> print w12.media + + + + + + +############################################################### +# Multi-media handling for CSS +############################################################### + +# A widget can define CSS media for multiple output media types +>>> class MultimediaWidget(TextInput): +... class Media: +... css = { +... 'screen, print': ('/file1','/file2'), +... 'screen': ('/file3',), +... 'print': ('/file4',) +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> multimedia = MultimediaWidget() +>>> print multimedia.media + + + + + + + +############################################################### +# Multiwidget media handling +############################################################### + +# MultiWidgets have a default media definition that gets all the +# media from the component widgets +>>> class MyMultiWidget(MultiWidget): +... def __init__(self, attrs=None): +... widgets = [MyWidget1, MyWidget2, MyWidget3] +... super(MyMultiWidget, self).__init__(widgets, attrs) + +>>> mymulti = MyMultiWidget() +>>> print mymulti.media + + + + + + + + +############################################################### +# Media processing for forms +############################################################### + +# You can ask a form for the media required by its widgets. +>>> class MyForm(Form): +... field1 = CharField(max_length=20, widget=MyWidget1()) +... field2 = CharField(max_length=20, widget=MyWidget2()) +>>> f1 = MyForm() +>>> print f1.media + + + + + + + + +# Form media can be combined to produce a single media definition. +>>> class AnotherForm(Form): +... field3 = CharField(max_length=20, widget=MyWidget3()) +>>> f2 = AnotherForm() +>>> print f1.media + f2.media + + + + + + + + +# Forms can also define media, following the same rules as widgets. +>>> class FormWithMedia(Form): +... field1 = CharField(max_length=20, widget=MyWidget1()) +... field2 = CharField(max_length=20, widget=MyWidget2()) +... class Media: +... js = ('/some/form/javascript',) +... css = { +... 'all': ('/some/form/css',) +... } +>>> f3 = FormWithMedia() +>>> print f3.media + + + + + + + + + + +""" \ No newline at end of file diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index 6eea519cd2..d2da38f1c8 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -2,6 +2,7 @@ from localflavor import localflavor_tests from regressions import regression_tests from formsets import formset_tests +from media import media_tests form_tests = r""" >>> from django.newforms import * @@ -3804,6 +3805,7 @@ __test__ = { 'localflavor': localflavor_tests, 'regressions': regression_tests, 'formset_tests': formset_tests, + 'media_tests': media_tests, } if __name__ == "__main__":