Django

Code

root/django/trunk/django/forms/widgets.py

Revision 9145, 24.7 kB (checked in by mtredinnick, 3 months ago)

Fixed #9294 -- Removed a (harmless) double conversion to unicode in one form
widget. Patch from nkilday.

  • Property svn:eol-style set to native
Line 
1 """
2 HTML Widget classes
3 """
4
5 try:
6     set
7 except NameError:
8     from sets import Set as set   # Python 2.3 fallback
9
10 import copy
11 from itertools import chain
12 from django.conf import settings
13 from django.utils.datastructures import MultiValueDict, MergeDict
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 datetime import time
20 from util import flatatt
21 from urlparse import urljoin
22
23 __all__ = (
24     'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput',
25     'HiddenInput', 'MultipleHiddenInput',
26     'FileInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput',
27     'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
28     'CheckboxSelectMultiple', 'MultiWidget',
29     'SplitDateTimeWidget',
30 )
31
32 MEDIA_TYPES = ('css','js')
33
34 class Media(StrAndUnicode):
35     def __init__(self, media=None, **kwargs):
36         if media:
37             media_attrs = media.__dict__
38         else:
39             media_attrs = kwargs
40
41         self._css = {}
42         self._js = []
43
44         for name in MEDIA_TYPES:
45             getattr(self, 'add_' + name)(media_attrs.get(name, None))
46
47         # Any leftover attributes must be invalid.
48         # if media_attrs != {}:
49         #     raise TypeError, "'class Media' has invalid attribute(s): %s" % ','.join(media_attrs.keys())
50
51     def __unicode__(self):
52         return self.render()
53
54     def render(self):
55         return mark_safe(u'\n'.join(chain(*[getattr(self, 'render_' + name)() for name in MEDIA_TYPES])))
56
57     def render_js(self):
58         return [u'<script type="text/javascript" src="%s"></script>' % self.absolute_path(path) for path in self._js]
59
60     def render_css(self):
61         # To keep rendering order consistent, we can't just iterate over items().
62         # We need to sort the keys, and iterate over the sorted list.
63         media = self._css.keys()
64         media.sort()
65         return chain(*[
66             [u'<link href="%s" type="text/css" media="%s" rel="stylesheet" />' % (self.absolute_path(path), medium)
67                     for path in self._css[medium]]
68                 for medium in media])
69
70     def absolute_path(self, path):
71         if path.startswith(u'http://') or path.startswith(u'https://') or path.startswith(u'/'):
72             return path
73         return urljoin(settings.MEDIA_URL,path)
74
75     def __getitem__(self, name):
76         "Returns a Media object that only contains media of the given type"
77         if name in MEDIA_TYPES:
78             return Media(**{name: getattr(self, '_' + name)})
79         raise KeyError('Unknown media type "%s"' % name)
80
81     def add_js(self, data):
82         if data:
83             self._js.extend([path for path in data if path not in self._js])
84
85     def add_css(self, data):
86         if data:
87             for medium, paths in data.items():
88                 self._css.setdefault(medium, []).extend([path for path in paths if path not in self._css[medium]])
89
90     def __add__(self, other):
91         combined = Media()
92         for name in MEDIA_TYPES:
93             getattr(combined, 'add_' + name)(getattr(self, '_' + name, None))
94             getattr(combined, 'add_' + name)(getattr(other, '_' + name, None))
95         return combined
96
97 def media_property(cls):
98     def _media(self):
99         # Get the media property of the superclass, if it exists
100         if hasattr(super(cls, self), 'media'):
101             base = super(cls, self).media
102         else:
103             base = Media()
104
105         # Get the media definition for this class
106         definition = getattr(cls, 'Media', None)
107         if definition:
108             extend = getattr(definition, 'extend', True)
109             if extend:
110                 if extend == True:
111                     m = base
112                 else:
113                     m = Media()
114                     for medium in extend:
115                         m = m + base[medium]
116                 return m + Media(definition)
117             else:
118                 return Media(definition)
119         else:
120             return base
121     return property(_media)
122
123 class MediaDefiningClass(type):
124     "Metaclass for classes that can have media definitions"
125     def __new__(cls, name, bases, attrs):
126         new_class = super(MediaDefiningClass, cls).__new__(cls, name, bases,
127                                                            attrs)
128         if 'media' not in attrs:
129             new_class.media = media_property(new_class)
130         return new_class
131
132 class Widget(object):
133     __metaclass__ = MediaDefiningClass
134     is_hidden = False          # Determines whether this corresponds to an <input type="hidden">.
135     needs_multipart_form = False # Determines does this widget need multipart-encrypted form
136
137     def __init__(self, attrs=None):
138         if attrs is not None:
139             self.attrs = attrs.copy()
140         else:
141             self.attrs = {}
142
143     def __deepcopy__(self, memo):
144         obj = copy.copy(self)
145         obj.attrs = self.attrs.copy()
146         memo[id(self)] = obj
147         return obj
148
149     def render(self, name, value, attrs=None):
150         """
151         Returns this Widget rendered as HTML, as a Unicode string.
152
153         The 'value' given is not guaranteed to be valid input, so subclass
154         implementations should program defensively.
155         """
156         raise NotImplementedError
157
158     def build_attrs(self, extra_attrs=None, **kwargs):
159         "Helper function for building an attribute dictionary."
160         attrs = dict(self.attrs, **kwargs)
161         if extra_attrs:
162             attrs.update(extra_attrs)
163         return attrs
164
165     def value_from_datadict(self, data, files, name):
166         """
167         Given a dictionary of data and this widget's name, returns the value
168         of this widget. Returns None if it's not provided.
169         """
170         return data.get(name, None)
171
172     def _has_changed(self, initial, data):
173         """
174         Return True if data differs from initial.
175         """
176         # For purposes of seeing whether something has changed, None is
177         # the same as an empty string, if the data or inital value we get
178         # is None, replace it w/ u''.
179         if data is None:
180             data_value = u''
181         else:
182             data_value = data
183         if initial is None:
184             initial_value = u''
185         else:
186             initial_value = initial
187         if force_unicode(initial_value) != force_unicode(data_value):
188             return True
189         return False
190
191     def id_for_label(self, id_):
192         """
193         Returns the HTML ID attribute of this Widget for use by a <label>,
194         given the ID of the field. Returns None if no ID is available.
195
196         This hook is necessary because some widgets have multiple HTML
197         elements and, thus, multiple IDs. In that case, this method should
198         return an ID value that corresponds to the first ID in the widget's
199         tags.
200         """
201         return id_
202     id_for_label = classmethod(id_for_label)
203
204 class Input(Widget):
205     """
206     Base class for all <input> widgets (except type='checkbox' and
207     type='radio', which are special).
208     """
209     input_type = None # Subclasses must define this.
210
211     def render(self, name, value, attrs=None):
212         if value is None: value = ''
213         final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
214         if value != '':
215             # Only add the 'value' attribute if a value is non-empty.
216             final_attrs['value'] = force_unicode(value)
217         return mark_safe(u'<input%s />' % flatatt(final_attrs))
218
219 class TextInput(Input):
220     input_type = 'text'
221
222 class PasswordInput(Input):
223     input_type = 'password'
224
225     def __init__(self, attrs=None, render_value=True):
226         super(PasswordInput, self).__init__(attrs)
227         self.render_value = render_value
228
229     def render(self, name, value, attrs=None):
230         if not self.render_value: value=None
231         return super(PasswordInput, self).render(name, value, attrs)
232
233 class HiddenInput(Input):
234     input_type = 'hidden'
235     is_hidden = True
236
237 class MultipleHiddenInput(HiddenInput):
238     """
239     A widget that handles <input type="hidden"> for fields that have a list
240     of values.
241     """
242     def __init__(self, attrs=None, choices=()):
243         super(MultipleHiddenInput, self).__init__(attrs)
244         # choices can be any iterable
245         self.choices = choices
246
247     def render(self, name, value, attrs=None, choices=()):
248         if value is None: value = []
249         final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
250         return mark_safe(u'\n'.join([(u'<input%s />' %
251             flatatt(dict(value=force_unicode(v), **final_attrs)))
252             for v in value]))
253
254     def value_from_datadict(self, data, files, name):
255         if isinstance(data, (MultiValueDict, MergeDict)):
256             return data.getlist(name)
257         return data.get(name, None)
258
259 class FileInput(Input):
260     input_type = 'file'
261     needs_multipart_form = True
262
263     def render(self, name, value, attrs=None):
264         return super(FileInput, self).render(name, None, attrs=attrs)
265
266     def value_from_datadict(self, data, files, name):
267         "File widgets take data from FILES, not POST"
268         return files.get(name, None)
269
270     def _has_changed(self, initial, data):
271         if data is None:
272             return False
273         return True
274
275 class Textarea(Widget):
276     def __init__(self, attrs=None):
277         # The 'rows' and 'cols' attributes are required for HTML correctness.
278         self.attrs = {'cols': '40', 'rows': '10'}
279         if attrs:
280             self.attrs.update(attrs)
281
282     def render(self, name, value, attrs=None):
283         if value is None: value = ''
284         final_attrs = self.build_attrs(attrs, name=name)
285         return mark_safe(u'<textarea%s>%s</textarea>' % (flatatt(final_attrs),
286                 conditional_escape(force_unicode(value))))
287
288 class DateTimeInput(Input):
289     input_type = 'text'
290     format = '%Y-%m-%d %H:%M:%S'     # '2006-10-25 14:30:59'
291
292     def __init__(self, attrs=None, format=None):
293         super(DateTimeInput, self).__init__(attrs)
294         if format:
295             self.format = format
296
297     def render(self, name, value, attrs=None):
298         if value is None:
299             value = ''
300         elif hasattr(value, 'strftime'):
301             value = datetime_safe.new_datetime(value)
302             value = value.strftime(self.format)
303         return super(DateTimeInput, self).render(name, value, attrs)
304
305 class TimeInput(Input):
306     input_type = 'text'
307
308     def render(self, name, value, attrs=None):
309         if value is None:
310             value = ''
311         elif isinstance(value, time):
312             value = value.replace(microsecond=0)
313         return super(TimeInput, self).render(name, value, attrs)
314
315 class CheckboxInput(Widget):
316     def __init__(self, attrs=None, check_test=bool):
317         super(CheckboxInput, self).__init__(attrs)
318         # check_test is a callable that takes a value and returns True
319         # if the checkbox should be checked for that value.
320         self.check_test = check_test
321
322     def render(self, name, value, attrs=None):
323         final_attrs = self.build_attrs(attrs, type='checkbox', name=name)
324         try:
325             result = self.check_test(value)
326         except: # Silently catch exceptions
327             result = False
328         if result:
329             final_attrs['checked'] = 'checked'
330         if value not in ('', True, False, None):
331             # Only add the 'value' attribute if a value is non-empty.
332             final_attrs['value'] = force_unicode(value)
333         return mark_safe(u'<input%s />' % flatatt(final_attrs))
334
335     def value_from_datadict(self, data, files, name):
336         if name not in data:
337             # A missing value means False because HTML form submission does not
338             # send results for unselected checkboxes.
339             return False
340         return super(CheckboxInput, self).value_from_datadict(data, files, name)
341
342     def _has_changed(self, initial, data):
343         # Sometimes data or initial could be None or u'' which should be the
344         # same thing as False.
345         return bool(initial) != bool(data)
346
347 class Select(Widget):
348     def __init__(self, attrs=None, choices=()):
349         super(Select, self).__init__(attrs)
350         # choices can be any iterable, but we may need to render this widget
351         # multiple times. Thus, collapse it into a list so it can be consumed
352         # more than once.
353         self.choices = list(choices)
354
355     def render(self, name, value, attrs=None, choices=()):
356         if value is None: value = ''
357         final_attrs = self.build_attrs(attrs, name=name)
358         output = [u'<select%s>' % flatatt(final_attrs)]
359         options = self.render_options(choices, [value])
360         if options:
361             output.append(options)
362         output.append('</select>')
363         return mark_safe(u'\n'.join(output))
364
365     def render_options(self, choices, selected_choices):
366         def render_option(option_value, option_label):
367             option_value = force_unicode(option_value)
368             selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
369             return u'<option value="%s"%s>%s</option>' % (
370                 escape(option_value), selected_html,
371                 conditional_escape(force_unicode(option_label)))
372         # Normalize to strings.
373         selected_choices = set([force_unicode(v) for v in selected_choices])
374         output = []
375         for option_value, option_label in chain(self.choices, choices):
376             if isinstance(option_label, (list, tuple)):
377                 output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value)))
378                 for option in option_label:
379                     output.append(render_option(*option))
380                 output.append(u'</optgroup>')
381             else:
382                 output.append(render_option(option_value, option_label))
383         return u'\n'.join(output)
384
385 class NullBooleanSelect(Select):
386     """
387     A Select Widget intended to be used with NullBooleanField.
388     """
389     def __init__(self, attrs=None):
390         choices = ((u'1', ugettext('Unknown')), (u'2', ugettext('Yes')), (u'3', ugettext('No')))
391         super(NullBooleanSelect, self).__init__(attrs, choices)
392
393     def render(self, name, value, attrs=None, choices=()):
394         try:
395             value = {True: u'2', False: u'3', u'2': u'2', u'3': u'3'}[value]
396         except KeyError:
397             value = u'1'
398         return super(NullBooleanSelect, self).render(name, value, attrs, choices)
399
400     def value_from_datadict(self, data, files, name):
401         value = data.get(name, None)
402         return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
403
404     def _has_changed(self, initial, data):
405         # Sometimes data or initial could be None or u'' which should be the
406         # same thing as False.
407         return bool(initial) != bool(data)
408
409 class SelectMultiple(Select):
410     def render(self, name, value, attrs=None, choices=()):
411         if value is None: value = []
412         final_attrs = self.build_attrs(attrs, name=name)
413         output = [u'<select multiple="multiple"%s>' % flatatt(final_attrs)]
414         options = self.render_options(choices, value)
415         if options:
416             output.append(options)
417         output.append('</select>')
418         return mark_safe(u'\n'.join(output))
419
420     def value_from_datadict(self, data, files, name):
421         if isinstance(data, (MultiValueDict, MergeDict)):
422             return data.getlist(name)
423         return data.get(name, None)
424
425     def _has_changed(self, initial, data):
426         if initial is None:
427             initial = []
428         if data is None:
429             data = []
430         if len(initial) != len(data):
431             return True
432         for value1, value2 in zip(initial, data):
433             if force_unicode(value1) != force_unicode(value2):
434                 return True
435         return False
436
437 class RadioInput(StrAndUnicode):
438     """
439     An object used by RadioFieldRenderer that represents a single
440     <input type='radio'>.
441     """
442
443     def __init__(self, name, value, attrs, choice, index):
444         self.name, self.value = name, value
445         self.attrs = attrs
446         self.choice_value = force_unicode(choice[0])
447         self.choice_label = force_unicode(choice[1])
448         self.index = index
449
450     def __unicode__(self):
451         if 'id' in self.attrs:
452             label_for = ' for="%s_%s"' % (self.attrs['id'], self.index)
453         else:
454             label_for = ''
455         choice_label = conditional_escape(force_unicode(self.choice_label))
456         return mark_safe(u'<label%s>%s %s</label>' % (label_for, self.tag(), choice_label))
457
458     def is_checked(self):
459         return self.value == self.choice_value
460
461     def tag(self):
462         if 'id' in self.attrs:
463             self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index)
464         final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value)
465         if self.is_checked():
466             final_attrs['checked'] = 'checked'
467         return mark_safe(u'<input%s />' % flatatt(final_attrs))
468
469 class RadioFieldRenderer(StrAndUnicode):
470     """
471     An object used by RadioSelect to enable customization of radio widgets.
472     """
473
474     def __init__(self, name, value, attrs, choices):
475         self.name, self.value, self.attrs = name, value, attrs
476         self.choices = choices
477
478     def __iter__(self):
479         for i, choice in enumerate(self.choices):
480             yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i)
481
482     def __getitem__(self, idx):
483         choice = self.choices[idx] # Let the IndexError propogate
484         return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx)
485
486     def __unicode__(self):
487         return self.render()
488
489     def render