Fixed the admin_scripts tests to check for the right output with Python 2.4.
[django.git] / django / forms / widgets.py
blobcc065d71d8d6e86af91b81da91c27fe2e18ba4a8
1 """
2 HTML Widget classes
3 """
5 try:
6 set
7 except NameError:
8 from sets import Set as set # Python 2.3 fallback
10 import copy
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
22 __all__ = (
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):
34 if media:
35 media_attrs = media.__dict__
36 else:
37 media_attrs = kwargs
39 self._css = {}
40 self._js = []
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):
50 return self.render()
52 def render(self):
53 return u'\n'.join(chain(*[getattr(self, 'render_' + name)() for name in MEDIA_TYPES]))
55 def render_js(self):
56 return [u'<script type="text/javascript" src="%s"></script>' % self.absolute_path(path) for path in self._js]
58 def render_css(self):
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()
62 media.sort()
63 return chain(*[
64 [u'<link href="%s" type="text/css" media="%s" rel="stylesheet" />' % (self.absolute_path(path), medium)
65 for path in self._css[medium]]
66 for medium in media])
68 def absolute_path(self, path):
69 if path.startswith(u'http://') or path.startswith(u'https://') or path.startswith(u'/'):
70 return path
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):
80 if data:
81 self._js.extend([path for path in data if path not in self._js])
83 def add_css(self, data):
84 if 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):
89 combined = Media()
90 for name in MEDIA_TYPES:
91 getattr(combined, 'add_' + name)(getattr(self, '_' + name, None))
92 getattr(combined, 'add_' + name)(getattr(other, '_' + name, None))
93 return combined
95 def media_property(cls):
96 def _media(self):
97 # Get the media property of the superclass, if it exists
98 if hasattr(super(cls, self), 'media'):
99 base = super(cls, self).media
100 else:
101 base = Media()
103 # Get the media definition for this class
104 definition = getattr(cls, 'Media', None)
105 if definition:
106 extend = getattr(definition, 'extend', True)
107 if extend:
108 if extend == True:
109 m = base
110 else:
111 m = Media()
112 for medium in extend:
113 m = m + base[medium]
114 return m + Media(definition)
115 else:
116 return Media(definition)
117 else:
118 return base
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,
125 attrs)
126 if 'media' not in attrs:
127 new_class.media = media_property(new_class)
128 return 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()
138 else:
139 self.attrs = {}
141 def __deepcopy__(self, memo):
142 obj = copy.copy(self)
143 obj.attrs = self.attrs.copy()
144 memo[id(self)] = obj
145 return obj
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)
159 if extra_attrs:
160 attrs.update(extra_attrs)
161 return 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''.
177 if data is None:
178 data_value = u''
179 else:
180 data_value = data
181 if initial is None:
182 initial_value = u''
183 else:
184 initial_value = initial
185 if force_unicode(initial_value) != force_unicode(data_value):
186 return True
187 return False
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
197 tags.
199 return id_
200 id_for_label = classmethod(id_for_label)
202 class Input(Widget):
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)
212 if value != '':
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):
218 input_type = 'text'
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'
233 is_hidden = True
235 class MultipleHiddenInput(HiddenInput):
237 A widget that handles <input type="hidden"> for fields that have a list
238 of values.
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)))
250 for v in value]))
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):
258 input_type = 'file'
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):
269 if data is None:
270 return False
271 return True
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'}
277 if attrs:
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):
288 input_type = 'text'
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)
293 if format:
294 self.format = format
296 def render(self, name, value, attrs=None):
297 if value is None:
298 value = ''
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)
313 try:
314 result = self.check_test(value)
315 except: # Silently catch exceptions
316 result = False
317 if result:
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):
325 if name not in data:
326 # A missing value means False because HTML form submission does not
327 # send results for unselected checkboxes.
328 return False
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
341 # more than once.
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])
349 if options:
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])
363 output = []
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>')
370 else:
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=()):
383 try:
384 value = {True: u'2', False: u'3', u'2': u'2', u'3': u'3'}[value]
385 except KeyError:
386 value = u'1'
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)
404 if options:
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):
415 if initial is None:
416 initial = []
417 if data is None:
418 data = []
419 if len(initial) != len(data):
420 return True
421 for value1, value2 in zip(initial, data):
422 if force_unicode(value1) != force_unicode(value2):
423 return True
424 return False
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
434 self.attrs = attrs
435 self.choice_value = force_unicode(choice[0])
436 self.choice_label = force_unicode(choice[1])
437 self.index = index
439 def __unicode__(self):
440 if 'id' in self.attrs:
441 label_for = ' for="%s_%s"' % (self.attrs['id'], self.index)
442 else:
443 label_for = ''
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
450 def tag(self):
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
467 def __iter__(self):
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):
476 return self.render()
478 def render(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)
489 if renderer:
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').
509 if id_:
510 id_ += '_0'
511 return id_
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)
519 output = [u'<ul>']
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.
525 if has_id:
526 final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
527 label_for = u' for="%s"' % final_attrs['id']
528 else:
529 label_for = ''
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()
541 if id_:
542 id_ += '_0'
543 return id_
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:
554 * A list.
555 * A normal value (e.g., a string) that has been "compressed" from
556 a list of values.
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
562 list.
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
579 # in self.widgets.
580 if not isinstance(value, list):
581 value = self.decompress(value)
582 output = []
583 final_attrs = self.build_attrs(attrs)
584 id_ = final_attrs.get('id', None)
585 for i, widget in enumerate(self.widgets):
586 try:
587 widget_value = value[i]
588 except IndexError:
589 widget_value = None
590 if id_:
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()
597 if id_:
598 id_ += '_0'
599 return id_
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):
606 if initial is None:
607 initial = [u'' for x in range(0, len(data))]
608 else:
609 initial = self.decompress(initial)
610 for widget, initial, data in zip(self.widgets, initial, data):
611 if widget._has_changed(initial, data):
612 return True
613 return False
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
621 needed.
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
629 non-empty.
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"
635 media = Media()
636 for w in self.widgets:
637 media = media + w.media
638 return 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):
650 if value:
651 return [value.date(), value.time().replace(microsecond=0)]
652 return [None, None]