Index: django/contrib/admin/options.py
===================================================================
--- django/contrib/admin/options.py (revision 5651)
+++ django/contrib/admin/options.py (working copy)
@@ -2,6 +2,7 @@
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 @@
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 @@
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 @@
# 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 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 @@
save_as = False
save_on_top = False
ordering = None
- js = None
prepopulated_fields = {}
filter_vertical = ()
filter_horizontal = ()
@@ -194,38 +209,20 @@
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 %}
Index: django/contrib/admin/templates/admin/change_form.html
===================================================================
--- django/contrib/admin/templates/admin/change_form.html (revision 5651)
+++ django/contrib/admin/templates/admin/change_form.html (working copy)
@@ -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 %}
Index: django/newforms/formsets.py
===================================================================
--- django/newforms/formsets.py (revision 5651)
+++ django/newforms/formsets.py (working copy)
@@ -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'
@@ -149,6 +149,15 @@
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}
Index: django/newforms/forms.py
===================================================================
--- django/newforms/forms.py (revision 5651)
+++ django/newforms/forms.py (working copy)
@@ -9,7 +9,7 @@
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 @@
"""
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,8 +51,12 @@
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
# class is different than Form. See the comments by the Form class for more
@@ -234,6 +239,16 @@
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
Index: django/newforms/widgets.py
===================================================================
--- django/newforms/widgets.py (revision 5651)
+++ django/newforms/widgets.py (working copy)
@@ -8,6 +8,7 @@
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 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):
@@ -377,6 +477,14 @@
"""
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.
@@ -389,3 +497,4 @@
if value:
return [value.date(), value.time()]
return [None, None]
+
\ No newline at end of file
Index: tests/regressiontests/forms/media.py
===================================================================
--- tests/regressiontests/forms/media.py (revision 0)
+++ tests/regressiontests/forms/media.py (revision 0)
@@ -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
Index: tests/regressiontests/forms/tests.py
===================================================================
--- tests/regressiontests/forms/tests.py (revision 5651)
+++ tests/regressiontests/forms/tests.py (working copy)
@@ -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 *
@@ -3698,6 +3699,7 @@
'localflavor': localflavor_tests,
'regressions': regression_tests,
'formset_tests': formset_tests,
+ 'media_tests': media_tests,
}
if __name__ == "__main__":
Index: docs/newforms.txt
===================================================================
--- docs/newforms.txt (revision 5651)
+++ docs/newforms.txt (working copy)
@@ -919,7 +919,7 @@
~~~~~~~~~~
The ``widget`` argument lets you specify a ``Widget`` class to use when
-rendering this ``Field``. See "Widgets" below for more information.
+rendering this ``Field``. See "Widgets"_ below for more information.
``help_text``
~~~~~~~~~~~~~
@@ -1325,6 +1325,124 @@
senders = MultiEmailField()
cc_myself = forms.BooleanField()
+Widgets
+=======
+
+A widget is Django's representation of a HTML form widget. The widget
+handles the rendering of the HTML, and the extraction of data from a GET/POST
+dictionary that corresponds to the widget.
+
+Django provides a representation of all the basic HTML widgets, plus some
+commonly used groups of widgets:
+
+ ============================ ===========================================
+ Widget HTML Equivalent
+ ============================ ===========================================
+ ``TextInput`` ``...``
+ ``CheckboxInput`` ``