Django

Code

root/django/branches/newforms-admin/django/newforms/widgets.py

Revision 7948, 24.0 kB (checked in by brosner, 5 months ago)

newforms-admin: Merged from trunk up to [7947].

  • 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
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
21
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',
28 )
29
30 MEDIA_TYPES = ('css','js')
31
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
38            
39         self._css = {}
40         self._js = []
41        
42         for name in MEDIA_TYPES:
43             getattr(self, 'add_' + name)(media_attrs.get(name, None))
44
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())
48        
49     def __unicode__(self):
50         return self.render()
51        
52     def render(self):
53         return u'\n'.join(chain(*[getattr(self, 'render_' + name)() for name in MEDIA_TYPES]))
54        
55     def render_js(self):
56         return [u'<script type="text/javascript" src="%s"></script>' % self.absolute_path(path) for path in self._js]
57        
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])
67        
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)
72
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)
78
79     def add_js(self, data):
80         if data:   
81             self._js.extend([path for path in data if path not in self._js])
82            
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]])
87
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
94
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()
102        
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)
120    
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
129        
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
134
135     def __init__(self, attrs=None):
136         if attrs is not None:
137             self.attrs = attrs.copy()
138         else:
139             self.attrs = {}
140
141     def __deepcopy__(self, memo):
142         obj = copy.copy(self)
143         obj.attrs = self.attrs.copy()
144         memo[id(self)] = obj
145         return obj
146
147     def render(self, name, value, attrs=None):
148         """
149         Returns this Widget rendered as HTML, as a Unicode string.
150
151         The 'value' given is not guaranteed to be valid input, so subclass
152         implementations should program defensively.
153         """
154         raise NotImplementedError
155
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
162
163     def value_from_datadict(self, data, files, name):
164         """
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.
167         """
168         return data.get(name, None)
169
170     def _has_changed(self, initial, data):
171         """
172         Return True if data differs from initial.
173         """
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
188
189     def id_for_label(self, id_):
190         """
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.
193
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.
198         """
199         return id_
200     id_for_label = classmethod(id_for_label)
201
202 class Input(Widget):
203     """
204     Base class for all <input> widgets (except type='checkbox' and
205     type='radio', which are special).
206     """
207     input_type = None # Subclasses must define this.
208
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))
216
217 class TextInput(Input):
218     input_type = 'text'
219
220 class PasswordInput(Input):
221     input_type = 'password'
222
223     def __init__(self, attrs=None, render_value=True):
224         super(PasswordInput, self).__init__(attrs)
225         self.render_value = render_value
226
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)
230
231 class HiddenInput(Input):
232     input_type = 'hidden'
233     is_hidden = True
234
235 class MultipleHiddenInput(HiddenInput):
236     """
237     A widget that handles <input type="hidden"> for fields that have a list
238     of values.
239     """
240     def __init__(self, attrs=None, choices=()):
241         super(MultipleHiddenInput, self).__init__(attrs)
242         # choices can be any iterable
243         self.choices = choices
244
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]))
251
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)
256
257 class FileInput(Input):
258     input_type = 'file'
259     needs_multipart_form = True
260
261     def render(self, name, value, attrs=None):
262         return super(FileInput, self).render(name, None, attrs=attrs)
263
264     def value_from_datadict(self, data, files, name):
265         "File widgets take data from FILES, not POST"
266         return files.get(name, None)
267    
268     def _has_changed(self, initial, data):
269         if data is None:
270             return False
271         return True
272
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)
279
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))))
286
287 class DateTimeInput(Input):
288     input_type = 'text'
289     format = '%Y-%m-%d %H:%M:%S'     # '2006-10-25 14:30:59'
290
291     def __init__(self, attrs=None, format=None):
292         super(DateTimeInput, self).__init__(attrs)
293         if format:
294             self.format = format
295
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)
303
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
310
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))
323
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)
330
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)
335
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)
343
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         # Normalize to string.
349         str_value = force_unicode(value)
350         for option_value, option_label in chain(self.choices, choices):
351             option_value = force_unicode(option_value)
352             selected_html = (option_value == str_value) and u' selected="selected"' or ''
353             output.append(u'<option value="%s"%s>%s</option>' % (
354                     escape(option_value), selected_html,
355                     conditional_escape(force_unicode(option_label))))
356         output.append(u'</select>')
357         return mark_safe(u'\n'.join(output))
358
359 class NullBooleanSelect(Select):
360     """
361     A Select Widget intended to be used with NullBooleanField.
362     """
363     def __init__(self, attrs=None):
364         choices = ((u'1', ugettext('Unknown')), (u'2', ugettext('Yes')), (u'3', ugettext('No')))
365         super(NullBooleanSelect, self).__init__(attrs, choices)
366
367     def render(self, name, value, attrs=None, choices=()):
368         try:
369             value = {True: u'2', False: u'3', u'2': u'2', u'3': u'3'}[value]
370         except KeyError:
371             value = u'1'
372         return super(NullBooleanSelect, self).render(name, value, attrs, choices)
373
374     def value_from_datadict(self, data, files, name):
375         value = data.get(name, None)
376         return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
377
378     def _has_changed(self, initial, data):
379         # Sometimes data or initial could be None or u'' which should be the
380         # same thing as False.
381         return bool(initial) != bool(data)
382
383 class SelectMultiple(Widget):
384     def __init__(self, attrs=None, choices=()):
385         super(SelectMultiple, self).__init__(attrs)
386         # choices can be any iterable
387         self.choices = choices
388
389     def render(self, name, value, attrs=None, choices=()):
390         if value is None: value = []
391         final_attrs = self.build_attrs(attrs, name=name)
392         output = [u'<select multiple="multiple"%s>' % flatatt(final_attrs)]
393         str_values = set([force_unicode(v) for v in value]) # Normalize to strings.
394         for option_value, option_label in chain(self.choices, choices):
395             option_value = force_unicode(option_value)
396             selected_html = (option_value in str_values) and ' selected="selected"' or ''
397             output.append(u'<option value="%s"%s>%s</option>' % (
398                     escape(option_value), selected_html,
399                     conditional_escape(force_unicode(option_label))))
400         output.append(u'</select>')
401         return mark_safe(u'\n'.join(output))
402
403     def value_from_datadict(self, data, files, name):
404         if isinstance(data, MultiValueDict):
405             return data.getlist(name)
406         return data.get(name, None)
407    
408     def _has_changed(self, initial, data):
409         if initial is None:
410             initial = []
411         if data is None:
412             data = []
413         if len(initial) != len(data):
414             return True
415         for value1, value2 in zip(initial, data):
416             if force_unicode(value1) != force_unicode(value2):
417                 return True
418         return False
419
420 class RadioInput(StrAndUnicode):
421     """
422     An object used by RadioFieldRenderer that represents a single
423     <input type='radio'>.
424     """
425
426     def __init__(self, name, value, attrs, choice, index):
427         self.name, self.value = name, value
428         self.attrs = attrs
429         self.choice_value = force_unicode(choice[0])
430         self.choice_label = force_unicode(choice[1])
431         self.index = index
432
433     def __unicode__(self):
434         if 'id' in self.attrs:
435             label_for = ' for="%s_%s"' % (self.attrs['id'], self.index)
436         else:
437             label_for = ''
438         choice_label = conditional_escape(force_unicode(self.choice_label))
439         return mark_safe(u'<label%s>%s %s</label>' % (label_for, self.tag(), choice_label))
440
441     def is_checked(self):
442         return self.value == self.choice_value
443
444     def tag(self):
445         if 'id' in self.attrs:
446             self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index)
447         final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value)
448         if self.is_checked():
449             final_attrs['checked'] = 'checked'
450         return mark_safe(u'<input%s />' % flatatt(final_attrs))
451
452 class RadioFieldRenderer(StrAndUnicode):
453     """
454     An object used by RadioSelect to enable customization of radio widgets.
455     """
456
457     def __init__(self, name, value, attrs, choices):
458         self.name, self.value, self.attrs = name, value, attrs
459         self.choices = choices
460
461     def __iter__(self):
462         for i, choice in enumerate(self.choices):
463             yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i)
464
465     def __getitem__(self, idx):
466         choice = self.choices[idx] # Let the IndexError propogate
467         return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx)
468
469     def __unicode__(self):
470         return self.render()
471
472     def render(self):
473         """Outputs a <ul> for this set of radio fields."""
474         return mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>'
475                 % force_unicode(w) for w in self]))
476
477 class RadioSelect(Select):
478     renderer = RadioFieldRenderer
479
480     def __init__(self, *args, **kwargs):
481         # Override the default renderer if we were passed one.
482         renderer = kwargs.pop('renderer', None)
483         if renderer:
484             self.renderer = renderer
485         super(RadioSelect, self).__init__(*