8 from sets
import Set
as set # Python 2.3 fallback
11 from itertools
import chain
12 from django
.conf
import settings
13 from django
.utils
.datastructures
import MultiValueDict
14 from django
.utils
.html
import escape
, conditional_escape
15 from django
.utils
.translation
import ugettext
16 from django
.utils
.encoding
import StrAndUnicode
, force_unicode
17 from django
.utils
.safestring
import mark_safe
18 from django
.utils
import datetime_safe
19 from util
import flatatt
20 from urlparse
import urljoin
23 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput',
24 'HiddenInput', 'MultipleHiddenInput',
25 'FileInput', 'DateTimeInput', 'Textarea', 'CheckboxInput',
26 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
27 'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
30 MEDIA_TYPES
= ('css','js')
32 class Media(StrAndUnicode
):
33 def __init__(self
, media
=None, **kwargs
):
35 media_attrs
= media
.__dict
__
42 for name
in MEDIA_TYPES
:
43 getattr(self
, 'add_' + name
)(media_attrs
.get(name
, None))
45 # Any leftover attributes must be invalid.
46 # if media_attrs != {}:
47 # raise TypeError, "'class Media' has invalid attribute(s): %s" % ','.join(media_attrs.keys())
49 def __unicode__(self
):
53 return u
'\n'.join(chain(*[getattr(self
, 'render_' + name
)() for name
in MEDIA_TYPES
]))
56 return [u
'<script type="text/javascript" src="%s"></script>' % self
.absolute_path(path
) for path
in self
._js
]
59 # To keep rendering order consistent, we can't just iterate over items().
60 # We need to sort the keys, and iterate over the sorted list.
61 media
= self
._css
.keys()
64 [u
'<link href="%s" type="text/css" media="%s" rel="stylesheet" />' % (self
.absolute_path(path
), medium
)
65 for path
in self
._css
[medium
]]
68 def absolute_path(self
, path
):
69 if path
.startswith(u
'http://') or path
.startswith(u
'https://') or path
.startswith(u
'/'):
71 return urljoin(settings
.MEDIA_URL
,path
)
73 def __getitem__(self
, name
):
74 "Returns a Media object that only contains media of the given type"
75 if name
in MEDIA_TYPES
:
76 return Media(**{name
: getattr(self
, '_' + name
)})
77 raise KeyError('Unknown media type "%s"' % name
)
79 def add_js(self
, data
):
81 self
._js
.extend([path
for path
in data
if path
not in self
._js
])
83 def add_css(self
, data
):
85 for medium
, paths
in data
.items():
86 self
._css
.setdefault(medium
, []).extend([path
for path
in paths
if path
not in self
._css
[medium
]])
88 def __add__(self
, other
):
90 for name
in MEDIA_TYPES
:
91 getattr(combined
, 'add_' + name
)(getattr(self
, '_' + name
, None))
92 getattr(combined
, 'add_' + name
)(getattr(other
, '_' + name
, None))
95 def media_property(cls
):
97 # Get the media property of the superclass, if it exists
98 if hasattr(super(cls
, self
), 'media'):
99 base
= super(cls
, self
).media
103 # Get the media definition for this class
104 definition
= getattr(cls
, 'Media', None)
106 extend
= getattr(definition
, 'extend', True)
112 for medium
in extend
:
114 return m
+ Media(definition
)
116 return Media(definition
)
119 return property(_media
)
121 class MediaDefiningClass(type):
122 "Metaclass for classes that can have media definitions"
123 def __new__(cls
, name
, bases
, attrs
):
124 new_class
= super(MediaDefiningClass
, cls
).__new
__(cls
, name
, bases
,
126 if 'media' not in attrs
:
127 new_class
.media
= media_property(new_class
)
130 class Widget(object):
131 __metaclass__
= MediaDefiningClass
132 is_hidden
= False # Determines whether this corresponds to an <input type="hidden">.
133 needs_multipart_form
= False # Determines does this widget need multipart-encrypted form
135 def __init__(self
, attrs
=None):
136 if attrs
is not None:
137 self
.attrs
= attrs
.copy()
141 def __deepcopy__(self
, memo
):
142 obj
= copy
.copy(self
)
143 obj
.attrs
= self
.attrs
.copy()
147 def render(self
, name
, value
, attrs
=None):
149 Returns this Widget rendered as HTML, as a Unicode string.
151 The 'value' given is not guaranteed to be valid input, so subclass
152 implementations should program defensively.
154 raise NotImplementedError
156 def build_attrs(self
, extra_attrs
=None, **kwargs
):
157 "Helper function for building an attribute dictionary."
158 attrs
= dict(self
.attrs
, **kwargs
)
160 attrs
.update(extra_attrs
)
163 def value_from_datadict(self
, data
, files
, name
):
165 Given a dictionary of data and this widget's name, returns the value
166 of this widget. Returns None if it's not provided.
168 return data
.get(name
, None)
170 def _has_changed(self
, initial
, data
):
172 Return True if data differs from initial.
174 # For purposes of seeing whether something has changed, None is
175 # the same as an empty string, if the data or inital value we get
176 # is None, replace it w/ u''.
184 initial_value
= initial
185 if force_unicode(initial_value
) != force_unicode(data_value
):
189 def id_for_label(self
, id_
):
191 Returns the HTML ID attribute of this Widget for use by a <label>,
192 given the ID of the field. Returns None if no ID is available.
194 This hook is necessary because some widgets have multiple HTML
195 elements and, thus, multiple IDs. In that case, this method should
196 return an ID value that corresponds to the first ID in the widget's
200 id_for_label
= classmethod(id_for_label
)
204 Base class for all <input> widgets (except type='checkbox' and
205 type='radio', which are special).
207 input_type
= None # Subclasses must define this.
209 def render(self
, name
, value
, attrs
=None):
210 if value
is None: value
= ''
211 final_attrs
= self
.build_attrs(attrs
, type=self
.input_type
, name
=name
)
213 # Only add the 'value' attribute if a value is non-empty.
214 final_attrs
['value'] = force_unicode(value
)
215 return mark_safe(u
'<input%s />' % flatatt(final_attrs
))
217 class TextInput(Input
):
220 class PasswordInput(Input
):
221 input_type
= 'password'
223 def __init__(self
, attrs
=None, render_value
=True):
224 super(PasswordInput
, self
).__init
__(attrs
)
225 self
.render_value
= render_value
227 def render(self
, name
, value
, attrs
=None):
228 if not self
.render_value
: value
=None
229 return super(PasswordInput
, self
).render(name
, value
, attrs
)
231 class HiddenInput(Input
):
232 input_type
= 'hidden'
235 class MultipleHiddenInput(HiddenInput
):
237 A widget that handles <input type="hidden"> for fields that have a list
240 def __init__(self
, attrs
=None, choices
=()):
241 super(MultipleHiddenInput
, self
).__init
__(attrs
)
242 # choices can be any iterable
243 self
.choices
= choices
245 def render(self
, name
, value
, attrs
=None, choices
=()):
246 if value
is None: value
= []
247 final_attrs
= self
.build_attrs(attrs
, type=self
.input_type
, name
=name
)
248 return mark_safe(u
'\n'.join([(u
'<input%s />' %
249 flatatt(dict(value
=force_unicode(v
), **final_attrs
)))
252 def value_from_datadict(self
, data
, files
, name
):
253 if isinstance(data
, MultiValueDict
):
254 return data
.getlist(name
)
255 return data
.get(name
, None)
257 class FileInput(Input
):
259 needs_multipart_form
= True
261 def render(self
, name
, value
, attrs
=None):
262 return super(FileInput
, self
).render(name
, None, attrs
=attrs
)
264 def value_from_datadict(self
, data
, files
, name
):
265 "File widgets take data from FILES, not POST"
266 return files
.get(name
, None)
268 def _has_changed(self
, initial
, data
):
273 class Textarea(Widget
):
274 def __init__(self
, attrs
=None):
275 # The 'rows' and 'cols' attributes are required for HTML correctness.
276 self
.attrs
= {'cols': '40', 'rows': '10'}
278 self
.attrs
.update(attrs
)
280 def render(self
, name
, value
, attrs
=None):
281 if value
is None: value
= ''
282 value
= force_unicode(value
)
283 final_attrs
= self
.build_attrs(attrs
, name
=name
)
284 return mark_safe(u
'<textarea%s>%s</textarea>' % (flatatt(final_attrs
),
285 conditional_escape(force_unicode(value
))))
287 class DateTimeInput(Input
):
289 format
= '%Y-%m-%d %H:%M:%S' # '2006-10-25 14:30:59'
291 def __init__(self
, attrs
=None, format
=None):
292 super(DateTimeInput
, self
).__init
__(attrs
)
296 def render(self
, name
, value
, attrs
=None):
299 elif hasattr(value
, 'strftime'):
300 value
= datetime_safe
.new_datetime(value
)
301 value
= value
.strftime(self
.format
)
302 return super(DateTimeInput
, self
).render(name
, value
, attrs
)
304 class CheckboxInput(Widget
):
305 def __init__(self
, attrs
=None, check_test
=bool):
306 super(CheckboxInput
, self
).__init
__(attrs
)
307 # check_test is a callable that takes a value and returns True
308 # if the checkbox should be checked for that value.
309 self
.check_test
= check_test
311 def render(self
, name
, value
, attrs
=None):
312 final_attrs
= self
.build_attrs(attrs
, type='checkbox', name
=name
)
314 result
= self
.check_test(value
)
315 except: # Silently catch exceptions
318 final_attrs
['checked'] = 'checked'
319 if value
not in ('', True, False, None):
320 # Only add the 'value' attribute if a value is non-empty.
321 final_attrs
['value'] = force_unicode(value
)
322 return mark_safe(u
'<input%s />' % flatatt(final_attrs
))
324 def value_from_datadict(self
, data
, files
, name
):
326 # A missing value means False because HTML form submission does not
327 # send results for unselected checkboxes.
329 return super(CheckboxInput
, self
).value_from_datadict(data
, files
, name
)
331 def _has_changed(self
, initial
, data
):
332 # Sometimes data or initial could be None or u'' which should be the
333 # same thing as False.
334 return bool(initial
) != bool(data
)
336 class Select(Widget
):
337 def __init__(self
, attrs
=None, choices
=()):
338 super(Select
, self
).__init
__(attrs
)
339 # choices can be any iterable, but we may need to render this widget
340 # multiple times. Thus, collapse it into a list so it can be consumed
342 self
.choices
= list(choices
)
344 def render(self
, name
, value
, attrs
=None, choices
=()):
345 if value
is None: value
= ''
346 final_attrs
= self
.build_attrs(attrs
, name
=name
)
347 output
= [u
'<select%s>' % flatatt(final_attrs
)]
348 options
= self
.render_options(choices
, [value
])
350 output
.append(options
)
351 output
.append('</select>')
352 return mark_safe(u
'\n'.join(output
))
354 def render_options(self
, choices
, selected_choices
):
355 def render_option(option_value
, option_label
):
356 option_value
= force_unicode(option_value
)
357 selected_html
= (option_value
in selected_choices
) and u
' selected="selected"' or ''
358 return u
'<option value="%s"%s>%s</option>' % (
359 escape(option_value
), selected_html
,
360 conditional_escape(force_unicode(option_label
)))
361 # Normalize to strings.
362 selected_choices
= set([force_unicode(v
) for v
in selected_choices
])
364 for option_value
, option_label
in chain(self
.choices
, choices
):
365 if isinstance(option_label
, (list, tuple)):
366 output
.append(u
'<optgroup label="%s">' % escape(force_unicode(option_value
)))
367 for option
in option_label
:
368 output
.append(render_option(*option
))
369 output
.append(u
'</optgroup>')
371 output
.append(render_option(option_value
, option_label
))
372 return u
'\n'.join(output
)
374 class NullBooleanSelect(Select
):
376 A Select Widget intended to be used with NullBooleanField.
378 def __init__(self
, attrs
=None):
379 choices
= ((u
'1', ugettext('Unknown')), (u
'2', ugettext('Yes')), (u
'3', ugettext('No')))
380 super(NullBooleanSelect
, self
).__init
__(attrs
, choices
)
382 def render(self
, name
, value
, attrs
=None, choices
=()):
384 value
= {True: u
'2', False: u
'3', u
'2': u
'2', u
'3': u
'3'}[value
]
387 return super(NullBooleanSelect
, self
).render(name
, value
, attrs
, choices
)
389 def value_from_datadict(self
, data
, files
, name
):
390 value
= data
.get(name
, None)
391 return {u
'2': True, u
'3': False, True: True, False: False}.get(value
, None)
393 def _has_changed(self
, initial
, data
):
394 # Sometimes data or initial could be None or u'' which should be the
395 # same thing as False.
396 return bool(initial
) != bool(data
)
398 class SelectMultiple(Select
):
399 def render(self
, name
, value
, attrs
=None, choices
=()):
400 if value
is None: value
= []
401 final_attrs
= self
.build_attrs(attrs
, name
=name
)
402 output
= [u
'<select multiple="multiple"%s>' % flatatt(final_attrs
)]
403 options
= self
.render_options(choices
, value
)
405 output
.append(options
)
406 output
.append('</select>')
407 return mark_safe(u
'\n'.join(output
))
409 def value_from_datadict(self
, data
, files
, name
):
410 if isinstance(data
, MultiValueDict
):
411 return data
.getlist(name
)
412 return data
.get(name
, None)
414 def _has_changed(self
, initial
, data
):
419 if len(initial
) != len(data
):
421 for value1
, value2
in zip(initial
, data
):
422 if force_unicode(value1
) != force_unicode(value2
):
426 class RadioInput(StrAndUnicode
):
428 An object used by RadioFieldRenderer that represents a single
429 <input type='radio'>.
432 def __init__(self
, name
, value
, attrs
, choice
, index
):
433 self
.name
, self
.value
= name
, value
435 self
.choice_value
= force_unicode(choice
[0])
436 self
.choice_label
= force_unicode(choice
[1])
439 def __unicode__(self
):
440 if 'id' in self
.attrs
:
441 label_for
= ' for="%s_%s"' % (self
.attrs
['id'], self
.index
)
444 choice_label
= conditional_escape(force_unicode(self
.choice_label
))
445 return mark_safe(u
'<label%s>%s %s</label>' % (label_for
, self
.tag(), choice_label
))
447 def is_checked(self
):
448 return self
.value
== self
.choice_value
451 if 'id' in self
.attrs
:
452 self
.attrs
['id'] = '%s_%s' % (self
.attrs
['id'], self
.index
)
453 final_attrs
= dict(self
.attrs
, type='radio', name
=self
.name
, value
=self
.choice_value
)
454 if self
.is_checked():
455 final_attrs
['checked'] = 'checked'
456 return mark_safe(u
'<input%s />' % flatatt(final_attrs
))
458 class RadioFieldRenderer(StrAndUnicode
):
460 An object used by RadioSelect to enable customization of radio widgets.
463 def __init__(self
, name
, value
, attrs
, choices
):
464 self
.name
, self
.value
, self
.attrs
= name
, value
, attrs
465 self
.choices
= choices
468 for i
, choice
in enumerate(self
.choices
):
469 yield RadioInput(self
.name
, self
.value
, self
.attrs
.copy(), choice
, i
)
471 def __getitem__(self
, idx
):
472 choice
= self
.choices
[idx
] # Let the IndexError propogate
473 return RadioInput(self
.name
, self
.value
, self
.attrs
.copy(), choice
, idx
)
475 def __unicode__(self
):
479 """Outputs a <ul> for this set of radio fields."""
480 return mark_safe(u
'<ul>\n%s\n</ul>' % u
'\n'.join([u
'<li>%s</li>'
481 % force_unicode(w
) for w
in self
]))
483 class RadioSelect(Select
):
484 renderer
= RadioFieldRenderer
486 def __init__(self
, *args
, **kwargs
):
487 # Override the default renderer if we were passed one.
488 renderer
= kwargs
.pop('renderer', None)
490 self
.renderer
= renderer
491 super(RadioSelect
, self
).__init
__(*args
, **kwargs
)
493 def get_renderer(self
, name
, value
, attrs
=None, choices
=()):
494 """Returns an instance of the renderer."""
495 if value
is None: value
= ''
496 str_value
= force_unicode(value
) # Normalize to string.
497 final_attrs
= self
.build_attrs(attrs
)
498 choices
= list(chain(self
.choices
, choices
))
499 return self
.renderer(name
, str_value
, final_attrs
, choices
)
501 def render(self
, name
, value
, attrs
=None, choices
=()):
502 return self
.get_renderer(name
, value
, attrs
, choices
).render()
504 def id_for_label(self
, id_
):
505 # RadioSelect is represented by multiple <input type="radio"> fields,
506 # each of which has a distinct ID. The IDs are made distinct by a "_X"
507 # suffix, where X is the zero-based index of the radio field. Thus,
508 # the label for a RadioSelect should reference the first one ('_0').
512 id_for_label
= classmethod(id_for_label
)
514 class CheckboxSelectMultiple(SelectMultiple
):
515 def render(self
, name
, value
, attrs
=None, choices
=()):
516 if value
is None: value
= []
517 has_id
= attrs
and 'id' in attrs
518 final_attrs
= self
.build_attrs(attrs
, name
=name
)
520 # Normalize to strings
521 str_values
= set([force_unicode(v
) for v
in value
])
522 for i
, (option_value
, option_label
) in enumerate(chain(self
.choices
, choices
)):
523 # If an ID attribute was given, add a numeric index as a suffix,
524 # so that the checkboxes don't all have the same ID attribute.
526 final_attrs
= dict(final_attrs
, id='%s_%s' % (attrs
['id'], i
))
527 label_for
= u
' for="%s"' % final_attrs
['id']
531 cb
= CheckboxInput(final_attrs
, check_test
=lambda value
: value
in str_values
)
532 option_value
= force_unicode(option_value
)
533 rendered_cb
= cb
.render(name
, option_value
)
534 option_label
= conditional_escape(force_unicode(option_label
))
535 output
.append(u
'<li><label%s>%s %s</label></li>' % (label_for
, rendered_cb
, option_label
))
536 output
.append(u
'</ul>')
537 return mark_safe(u
'\n'.join(output
))
539 def id_for_label(self
, id_
):
540 # See the comment for RadioSelect.id_for_label()
544 id_for_label
= classmethod(id_for_label
)
546 class MultiWidget(Widget
):
548 A widget that is composed of multiple widgets.
550 Its render() method is different than other widgets', because it has to
551 figure out how to split a single value for display in multiple widgets.
552 The ``value`` argument can be one of two things:
555 * A normal value (e.g., a string) that has been "compressed" from
558 In the second case -- i.e., if the value is NOT a list -- render() will
559 first "decompress" the value into a list before rendering it. It does so by
560 calling the decompress() method, which MultiWidget subclasses must
561 implement. This method takes a single "compressed" value and returns a
564 When render() does its HTML rendering, each value in the list is rendered
565 with the corresponding widget -- the first value is rendered in the first
566 widget, the second value is rendered in the second widget, etc.
568 Subclasses may implement format_output(), which takes the list of rendered
569 widgets and returns a string of HTML that formats them any way you'd like.
571 You'll probably want to use this class with MultiValueField.
573 def __init__(self
, widgets
, attrs
=None):
574 self
.widgets
= [isinstance(w
, type) and w() or w
for w
in widgets
]
575 super(MultiWidget
, self
).__init
__(attrs
)
577 def render(self
, name
, value
, attrs
=None):
578 # value is a list of values, each corresponding to a widget
580 if not isinstance(value
, list):
581 value
= self
.decompress(value
)
583 final_attrs
= self
.build_attrs(attrs
)
584 id_
= final_attrs
.get('id', None)
585 for i
, widget
in enumerate(self
.widgets
):
587 widget_value
= value
[i
]
591 final_attrs
= dict(final_attrs
, id='%s_%s' % (id_
, i
))
592 output
.append(widget
.render(name
+ '_%s' % i
, widget_value
, final_attrs
))
593 return mark_safe(self
.format_output(output
))
595 def id_for_label(self
, id_
):
596 # See the comment for RadioSelect.id_for_label()
600 id_for_label
= classmethod(id_for_label
)
602 def value_from_datadict(self
, data
, files
, name
):
603 return [widget
.value_from_datadict(data
, files
, name
+ '_%s' % i
) for i
, widget
in enumerate(self
.widgets
)]
605 def _has_changed(self
, initial
, data
):
607 initial
= [u
'' for x
in range(0, len(data
))]
609 initial
= self
.decompress(initial
)
610 for widget
, initial
, data
in zip(self
.widgets
, initial
, data
):
611 if widget
._has
_changed
(initial
, data
):
615 def format_output(self
, rendered_widgets
):
617 Given a list of rendered widgets (as strings), returns a Unicode string
618 representing the HTML for the whole lot.
620 This hook allows you to format the HTML design of the widgets, if
623 return u
''.join(rendered_widgets
)
625 def decompress(self
, value
):
627 Returns a list of decompressed values for the given compressed value.
628 The given value can be assumed to be valid, but not necessarily
631 raise NotImplementedError('Subclasses must implement this method.')
633 def _get_media(self
):
634 "Media for a multiwidget is the combination of all media of the subwidgets"
636 for w
in self
.widgets
:
637 media
= media
+ w
.media
639 media
= property(_get_media
)
641 class SplitDateTimeWidget(MultiWidget
):
643 A Widget that splits datetime input into two <input type="text"> boxes.
645 def __init__(self
, attrs
=None):
646 widgets
= (TextInput(attrs
=attrs
), TextInput(attrs
=attrs
))
647 super(SplitDateTimeWidget
, self
).__init
__(widgets
, attrs
)
649 def decompress(self
, value
):
651 return [value
.date(), value
.time().replace(microsecond
=0)]