mirror of
https://github.com/django/django.git
synced 2025-04-01 03:56:42 +00:00
See documentation in templates.txt and templates_python.txt for how everything works. Backwards incompatible if you're inserting raw HTML output via template variables. Based on an original design from Simon Willison and with debugging help from Michael Radziej. git-svn-id: http://code.djangoproject.com/svn/django/trunk@6671 bcc190cf-cafb-0310-a4f2-bffc1f526a37
465 lines
17 KiB
Python
465 lines
17 KiB
Python
"""
|
|
HTML Widget classes
|
|
"""
|
|
|
|
try:
|
|
set
|
|
except NameError:
|
|
from sets import Set as set # Python 2.3 fallback
|
|
|
|
import copy
|
|
from itertools import chain
|
|
|
|
from django.utils.datastructures import MultiValueDict
|
|
from django.utils.html import escape
|
|
from django.utils.translation import ugettext
|
|
from django.utils.encoding import StrAndUnicode, force_unicode
|
|
from django.utils.safestring import mark_safe
|
|
from util import flatatt
|
|
|
|
__all__ = (
|
|
'Widget', 'TextInput', 'PasswordInput',
|
|
'HiddenInput', 'MultipleHiddenInput',
|
|
'FileInput', 'DateTimeInput', 'Textarea', 'CheckboxInput',
|
|
'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
|
|
'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
|
|
)
|
|
|
|
class Widget(object):
|
|
is_hidden = False # Determines whether this corresponds to an <input type="hidden">.
|
|
needs_multipart_form = False # Determines does this widget need multipart-encrypted form
|
|
|
|
def __init__(self, attrs=None):
|
|
if attrs is not None:
|
|
self.attrs = attrs.copy()
|
|
else:
|
|
self.attrs = {}
|
|
|
|
def __deepcopy__(self, memo):
|
|
obj = copy.copy(self)
|
|
obj.attrs = self.attrs.copy()
|
|
memo[id(self)] = obj
|
|
return obj
|
|
|
|
def render(self, name, value, attrs=None):
|
|
"""
|
|
Returns this Widget rendered as HTML, as a Unicode string.
|
|
|
|
The 'value' given is not guaranteed to be valid input, so subclass
|
|
implementations should program defensively.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def build_attrs(self, extra_attrs=None, **kwargs):
|
|
"Helper function for building an attribute dictionary."
|
|
attrs = dict(self.attrs, **kwargs)
|
|
if extra_attrs:
|
|
attrs.update(extra_attrs)
|
|
return attrs
|
|
|
|
def value_from_datadict(self, data, files, name):
|
|
"""
|
|
Given a dictionary of data and this widget's name, returns the value
|
|
of this widget. Returns None if it's not provided.
|
|
"""
|
|
return data.get(name, None)
|
|
|
|
def id_for_label(self, id_):
|
|
"""
|
|
Returns the HTML ID attribute of this Widget for use by a <label>,
|
|
given the ID of the field. Returns None if no ID is available.
|
|
|
|
This hook is necessary because some widgets have multiple HTML
|
|
elements and, thus, multiple IDs. In that case, this method should
|
|
return an ID value that corresponds to the first ID in the widget's
|
|
tags.
|
|
"""
|
|
return id_
|
|
id_for_label = classmethod(id_for_label)
|
|
|
|
class Input(Widget):
|
|
"""
|
|
Base class for all <input> widgets (except type='checkbox' and
|
|
type='radio', which are special).
|
|
"""
|
|
input_type = None # Subclasses must define this.
|
|
|
|
def render(self, name, value, attrs=None):
|
|
if value is None: value = ''
|
|
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
|
|
if value != '':
|
|
# Only add the 'value' attribute if a value is non-empty.
|
|
final_attrs['value'] = force_unicode(value)
|
|
return mark_safe(u'<input%s />' % flatatt(final_attrs))
|
|
|
|
class TextInput(Input):
|
|
input_type = 'text'
|
|
|
|
class PasswordInput(Input):
|
|
input_type = 'password'
|
|
|
|
def __init__(self, attrs=None, render_value=True):
|
|
super(PasswordInput, self).__init__(attrs)
|
|
self.render_value = render_value
|
|
|
|
def render(self, name, value, attrs=None):
|
|
if not self.render_value: value=None
|
|
return super(PasswordInput, self).render(name, value, attrs)
|
|
|
|
class HiddenInput(Input):
|
|
input_type = 'hidden'
|
|
is_hidden = True
|
|
|
|
class MultipleHiddenInput(HiddenInput):
|
|
"""
|
|
A widget that handles <input type="hidden"> for fields that have a list
|
|
of values.
|
|
"""
|
|
def __init__(self, attrs=None, choices=()):
|
|
super(MultipleHiddenInput, self).__init__(attrs)
|
|
# choices can be any iterable
|
|
self.choices = choices
|
|
|
|
def render(self, name, value, attrs=None, choices=()):
|
|
if value is None: value = []
|
|
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
|
|
return mark_safe(u'\n'.join([(u'<input%s />' %
|
|
flatatt(dict(value=force_unicode(v), **final_attrs)))
|
|
for v in value]))
|
|
|
|
def value_from_datadict(self, data, files, name):
|
|
if isinstance(data, MultiValueDict):
|
|
return data.getlist(name)
|
|
return data.get(name, None)
|
|
|
|
class FileInput(Input):
|
|
input_type = 'file'
|
|
needs_multipart_form = True
|
|
|
|
def render(self, name, value, attrs=None):
|
|
return super(FileInput, self).render(name, None, attrs=attrs)
|
|
|
|
def value_from_datadict(self, data, files, name):
|
|
"File widgets take data from FILES, not POST"
|
|
return files.get(name, None)
|
|
|
|
class Textarea(Widget):
|
|
def __init__(self, attrs=None):
|
|
# The 'rows' and 'cols' attributes are required for HTML correctness.
|
|
self.attrs = {'cols': '40', 'rows': '10'}
|
|
if attrs:
|
|
self.attrs.update(attrs)
|
|
|
|
def render(self, name, value, attrs=None):
|
|
if value is None: value = ''
|
|
value = force_unicode(value)
|
|
final_attrs = self.build_attrs(attrs, name=name)
|
|
return mark_safe(u'<textarea%s>%s</textarea>' % (flatatt(final_attrs),
|
|
escape(value)))
|
|
|
|
class DateTimeInput(Input):
|
|
input_type = 'text'
|
|
format = '%Y-%m-%d %H:%M:%S' # '2006-10-25 14:30:59'
|
|
|
|
def __init__(self, attrs=None, format=None):
|
|
super(DateTimeInput, self).__init__(attrs)
|
|
if format:
|
|
self.format = format
|
|
|
|
def render(self, name, value, attrs=None):
|
|
if value is None:
|
|
value = ''
|
|
elif hasattr(value, 'strftime'):
|
|
value = value.strftime(self.format)
|
|
return super(DateTimeInput, self).render(name, value, attrs)
|
|
|
|
class CheckboxInput(Widget):
|
|
def __init__(self, attrs=None, check_test=bool):
|
|
super(CheckboxInput, self).__init__(attrs)
|
|
# check_test is a callable that takes a value and returns True
|
|
# if the checkbox should be checked for that value.
|
|
self.check_test = check_test
|
|
|
|
def render(self, name, value, attrs=None):
|
|
final_attrs = self.build_attrs(attrs, type='checkbox', name=name)
|
|
try:
|
|
result = self.check_test(value)
|
|
except: # Silently catch exceptions
|
|
result = False
|
|
if result:
|
|
final_attrs['checked'] = 'checked'
|
|
if value not in ('', True, False, None):
|
|
# Only add the 'value' attribute if a value is non-empty.
|
|
final_attrs['value'] = force_unicode(value)
|
|
return mark_safe(u'<input%s />' % flatatt(final_attrs))
|
|
|
|
def value_from_datadict(self, data, files, name):
|
|
if name not in data:
|
|
# A missing value means False because HTML form submission does not
|
|
# send results for unselected checkboxes.
|
|
return False
|
|
return super(CheckboxInput, self).value_from_datadict(data, files, name)
|
|
|
|
class Select(Widget):
|
|
def __init__(self, attrs=None, choices=()):
|
|
super(Select, self).__init__(attrs)
|
|
# choices can be any iterable, but we may need to render this widget
|
|
# multiple times. Thus, collapse it into a list so it can be consumed
|
|
# more than once.
|
|
self.choices = list(choices)
|
|
|
|
def render(self, name, value, attrs=None, choices=()):
|
|
if value is None: value = ''
|
|
final_attrs = self.build_attrs(attrs, name=name)
|
|
output = [u'<select%s>' % flatatt(final_attrs)]
|
|
# Normalize to string.
|
|
str_value = force_unicode(value)
|
|
for option_value, option_label in chain(self.choices, choices):
|
|
option_value = force_unicode(option_value)
|
|
selected_html = (option_value == str_value) and u' selected="selected"' or ''
|
|
output.append(u'<option value="%s"%s>%s</option>' % (escape(option_value), selected_html, escape(force_unicode(option_label))))
|
|
output.append(u'</select>')
|
|
return mark_safe(u'\n'.join(output))
|
|
|
|
class NullBooleanSelect(Select):
|
|
"""
|
|
A Select Widget intended to be used with NullBooleanField.
|
|
"""
|
|
def __init__(self, attrs=None):
|
|
choices = ((u'1', ugettext('Unknown')), (u'2', ugettext('Yes')), (u'3', ugettext('No')))
|
|
super(NullBooleanSelect, self).__init__(attrs, choices)
|
|
|
|
def render(self, name, value, attrs=None, choices=()):
|
|
try:
|
|
value = {True: u'2', False: u'3', u'2': u'2', u'3': u'3'}[value]
|
|
except KeyError:
|
|
value = u'1'
|
|
return super(NullBooleanSelect, self).render(name, value, attrs, choices)
|
|
|
|
def value_from_datadict(self, data, files, name):
|
|
value = data.get(name, None)
|
|
return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
|
|
|
|
class SelectMultiple(Widget):
|
|
def __init__(self, attrs=None, choices=()):
|
|
super(SelectMultiple, self).__init__(attrs)
|
|
# choices can be any iterable
|
|
self.choices = choices
|
|
|
|
def render(self, name, value, attrs=None, choices=()):
|
|
if value is None: value = []
|
|
final_attrs = self.build_attrs(attrs, name=name)
|
|
output = [u'<select multiple="multiple"%s>' % flatatt(final_attrs)]
|
|
str_values = set([force_unicode(v) for v in value]) # Normalize to strings.
|
|
for option_value, option_label in chain(self.choices, choices):
|
|
option_value = force_unicode(option_value)
|
|
selected_html = (option_value in str_values) and ' selected="selected"' or ''
|
|
output.append(u'<option value="%s"%s>%s</option>' % (escape(option_value), selected_html, escape(force_unicode(option_label))))
|
|
output.append(u'</select>')
|
|
return mark_safe(u'\n'.join(output))
|
|
|
|
def value_from_datadict(self, data, files, name):
|
|
if isinstance(data, MultiValueDict):
|
|
return data.getlist(name)
|
|
return data.get(name, None)
|
|
|
|
class RadioInput(StrAndUnicode):
|
|
"""
|
|
An object used by RadioFieldRenderer that represents a single
|
|
<input type='radio'>.
|
|
"""
|
|
|
|
def __init__(self, name, value, attrs, choice, index):
|
|
self.name, self.value = name, value
|
|
self.attrs = attrs
|
|
self.choice_value = force_unicode(choice[0])
|
|
self.choice_label = force_unicode(choice[1])
|
|
self.index = index
|
|
|
|
def __unicode__(self):
|
|
return mark_safe(u'<label>%s %s</label>' % (self.tag(),
|
|
self.choice_label))
|
|
|
|
def is_checked(self):
|
|
return self.value == self.choice_value
|
|
|
|
def tag(self):
|
|
if 'id' in self.attrs:
|
|
self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index)
|
|
final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value)
|
|
if self.is_checked():
|
|
final_attrs['checked'] = 'checked'
|
|
return mark_safe(u'<input%s />' % flatatt(final_attrs))
|
|
|
|
class RadioFieldRenderer(StrAndUnicode):
|
|
"""
|
|
An object used by RadioSelect to enable customization of radio widgets.
|
|
"""
|
|
|
|
def __init__(self, name, value, attrs, choices):
|
|
self.name, self.value, self.attrs = name, value, attrs
|
|
self.choices = choices
|
|
|
|
def __iter__(self):
|
|
for i, choice in enumerate(self.choices):
|
|
yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i)
|
|
|
|
def __getitem__(self, idx):
|
|
choice = self.choices[idx] # Let the IndexError propogate
|
|
return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx)
|
|
|
|
def __unicode__(self):
|
|
return self.render()
|
|
|
|
def render(self):
|
|
"""Outputs a <ul> for this set of radio fields."""
|
|
return mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>'
|
|
% force_unicode(w) for w in self]))
|
|
|
|
class RadioSelect(Select):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.renderer = kwargs.pop('renderer', None)
|
|
if not self.renderer:
|
|
self.renderer = RadioFieldRenderer
|
|
super(RadioSelect, self).__init__(*args, **kwargs)
|
|
|
|
def get_renderer(self, name, value, attrs=None, choices=()):
|
|
"""Returns an instance of the renderer."""
|
|
if value is None: value = ''
|
|
str_value = force_unicode(value) # Normalize to string.
|
|
final_attrs = self.build_attrs(attrs)
|
|
choices = list(chain(self.choices, choices))
|
|
return self.renderer(name, str_value, final_attrs, choices)
|
|
|
|
def render(self, name, value, attrs=None, choices=()):
|
|
return self.get_renderer(name, value, attrs, choices).render()
|
|
|
|
def id_for_label(self, id_):
|
|
# RadioSelect is represented by multiple <input type="radio"> fields,
|
|
# each of which has a distinct ID. The IDs are made distinct by a "_X"
|
|
# suffix, where X is the zero-based index of the radio field. Thus,
|
|
# the label for a RadioSelect should reference the first one ('_0').
|
|
if id_:
|
|
id_ += '_0'
|
|
return id_
|
|
id_for_label = classmethod(id_for_label)
|
|
|
|
class CheckboxSelectMultiple(SelectMultiple):
|
|
def render(self, name, value, attrs=None, choices=()):
|
|
if value is None: value = []
|
|
has_id = attrs and 'id' in attrs
|
|
final_attrs = self.build_attrs(attrs, name=name)
|
|
output = [u'<ul>']
|
|
# Normalize to strings
|
|
str_values = set([force_unicode(v) for v in value])
|
|
for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
|
|
# If an ID attribute was given, add a numeric index as a suffix,
|
|
# so that the checkboxes don't all have the same ID attribute.
|
|
if has_id:
|
|
final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
|
|
cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
|
|
option_value = force_unicode(option_value)
|
|
rendered_cb = cb.render(name, option_value)
|
|
output.append(u'<li><label>%s %s</label></li>' % (rendered_cb, escape(force_unicode(option_label))))
|
|
output.append(u'</ul>')
|
|
return mark_safe(u'\n'.join(output))
|
|
|
|
def id_for_label(self, id_):
|
|
# See the comment for RadioSelect.id_for_label()
|
|
if id_:
|
|
id_ += '_0'
|
|
return id_
|
|
id_for_label = classmethod(id_for_label)
|
|
|
|
class MultiWidget(Widget):
|
|
"""
|
|
A widget that is composed of multiple widgets.
|
|
|
|
Its render() method is different than other widgets', because it has to
|
|
figure out how to split a single value for display in multiple widgets.
|
|
The ``value`` argument can be one of two things:
|
|
|
|
* A list.
|
|
* A normal value (e.g., a string) that has been "compressed" from
|
|
a list of values.
|
|
|
|
In the second case -- i.e., if the value is NOT a list -- render() will
|
|
first "decompress" the value into a list before rendering it. It does so by
|
|
calling the decompress() method, which MultiWidget subclasses must
|
|
implement. This method takes a single "compressed" value and returns a
|
|
list.
|
|
|
|
When render() does its HTML rendering, each value in the list is rendered
|
|
with the corresponding widget -- the first value is rendered in the first
|
|
widget, the second value is rendered in the second widget, etc.
|
|
|
|
Subclasses may implement format_output(), which takes the list of rendered
|
|
widgets and returns a string of HTML that formats them any way you'd like.
|
|
|
|
You'll probably want to use this class with MultiValueField.
|
|
"""
|
|
def __init__(self, widgets, attrs=None):
|
|
self.widgets = [isinstance(w, type) and w() or w for w in widgets]
|
|
super(MultiWidget, self).__init__(attrs)
|
|
|
|
def render(self, name, value, attrs=None):
|
|
# value is a list of values, each corresponding to a widget
|
|
# in self.widgets.
|
|
if not isinstance(value, list):
|
|
value = self.decompress(value)
|
|
output = []
|
|
final_attrs = self.build_attrs(attrs)
|
|
id_ = final_attrs.get('id', None)
|
|
for i, widget in enumerate(self.widgets):
|
|
try:
|
|
widget_value = value[i]
|
|
except IndexError:
|
|
widget_value = None
|
|
if id_:
|
|
final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
|
|
output.append(widget.render(name + '_%s' % i, widget_value, final_attrs))
|
|
return self.format_output(output)
|
|
|
|
def id_for_label(self, id_):
|
|
# See the comment for RadioSelect.id_for_label()
|
|
if id_:
|
|
id_ += '_0'
|
|
return id_
|
|
id_for_label = classmethod(id_for_label)
|
|
|
|
def value_from_datadict(self, data, files, name):
|
|
return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
|
|
|
|
def format_output(self, rendered_widgets):
|
|
"""
|
|
Given a list of rendered widgets (as strings), returns a Unicode string
|
|
representing the HTML for the whole lot.
|
|
|
|
This hook allows you to format the HTML design of the widgets, if
|
|
needed.
|
|
"""
|
|
return u''.join(rendered_widgets)
|
|
|
|
def decompress(self, value):
|
|
"""
|
|
Returns a list of decompressed values for the given compressed value.
|
|
The given value can be assumed to be valid, but not necessarily
|
|
non-empty.
|
|
"""
|
|
raise NotImplementedError('Subclasses must implement this method.')
|
|
|
|
class SplitDateTimeWidget(MultiWidget):
|
|
"""
|
|
A Widget that splits datetime input into two <input type="text"> boxes.
|
|
"""
|
|
def __init__(self, attrs=None):
|
|
widgets = (TextInput(attrs=attrs), TextInput(attrs=attrs))
|
|
super(SplitDateTimeWidget, self).__init__(widgets, attrs)
|
|
|
|
def decompress(self, value):
|
|
if value:
|
|
return [value.date(), value.time().replace(microsecond=0)]
|
|
return [None, None]
|
|
|