Django

Code

root/django/branches/gis/django/forms/widgets.py

Revision 7979, 24.1 kB (checked in by jbronn, 4 months ago)

gis: Merged revisions 7921,7926-7928,7938-7941,7945-7947,7949-7950,7952,7955-7956,7961,7964-7968,7970-7978 via svnmerge from trunk.

This includes the newforms-admin branch, and thus is backwards-incompatible. The geographic admin is _not_ in this changeset, and is forthcoming.

  • 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         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))
353
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)
373
374 class NullBooleanSelect(Select):
375     """
376     A Select Widget intended to be used with NullBooleanField.
377     """
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)
381
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)
388
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)
392
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)
397
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))
408
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)
413    
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
425
426 class RadioInput(StrAndUnicode):
427     """
428     An object used by RadioFieldRenderer that represents a single
429     <input type='radio'>.
430     """
431
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
438
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))
446
447     def is_checked(self):
448         return self.value == self.choice_value
449
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))
457
458 class RadioFieldRenderer(StrAndUnicode):
459     """
460     An object used by RadioSelect to enable customization of radio widgets.
461     """
462
463     def __init__(self, name, value, attrs, choices):
464         self.name, self.value, self.attrs = name, value, attrs
465         self.choices = choices
466
467     def __iter__(self):
468         for i, choice in enumerate(self.choices):
469             yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i)
470
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)
474
475     def __unicode__(self):
476         return self.render()
477
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]))
482
483 class RadioSelect(Select):
484     renderer = RadioFieldRenderer
485
486     def __init__(self, *args, **kwargs):
487<