1 | """
|
---|
2 | HTML Widget classes
|
---|
3 | """
|
---|
4 |
|
---|
5 | from __future__ import absolute_import
|
---|
6 |
|
---|
7 | import copy
|
---|
8 | import datetime
|
---|
9 | from itertools import chain
|
---|
10 | from urlparse import urljoin
|
---|
11 |
|
---|
12 | from django.conf import settings
|
---|
13 | from django.forms.util import flatatt, to_current_timezone
|
---|
14 | from django.utils.datastructures import MultiValueDict, MergeDict
|
---|
15 | from django.utils.html import escape, conditional_escape
|
---|
16 | from django.utils.translation import ugettext, ugettext_lazy
|
---|
17 | from django.utils.encoding import StrAndUnicode, force_unicode
|
---|
18 | from django.utils.safestring import mark_safe
|
---|
19 | from django.utils import datetime_safe, formats
|
---|
20 |
|
---|
21 | __all__ = (
|
---|
22 | 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput',
|
---|
23 | 'HiddenInput', 'MultipleHiddenInput', 'ClearableFileInput',
|
---|
24 | 'FileInput', 'DateInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput',
|
---|
25 | 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
|
---|
26 | 'CheckboxSelectMultiple', 'MultiWidget',
|
---|
27 | '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 mark_safe(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, prefix=None):
|
---|
69 | if path.startswith(u'http://') or path.startswith(u'https://') or path.startswith(u'/'):
|
---|
70 | return path
|
---|
71 | if prefix is None:
|
---|
72 | if settings.STATIC_URL is None:
|
---|
73 | # backwards compatibility
|
---|
74 | prefix = settings.MEDIA_URL
|
---|
75 | else:
|
---|
76 | prefix = settings.STATIC_URL
|
---|
77 | return urljoin(prefix, path)
|
---|
78 |
|
---|
79 | def __getitem__(self, name):
|
---|
80 | "Returns a Media object that only contains media of the given type"
|
---|
81 | if name in MEDIA_TYPES:
|
---|
82 | return Media(**{str(name): getattr(self, '_' + name)})
|
---|
83 | raise KeyError('Unknown media type "%s"' % name)
|
---|
84 |
|
---|
85 | def add_js(self, data):
|
---|
86 | if data:
|
---|
87 | for path in data:
|
---|
88 | if path not in self._js:
|
---|
89 | self._js.append(path)
|
---|
90 |
|
---|
91 | def add_css(self, data):
|
---|
92 | if data:
|
---|
93 | for medium, paths in data.items():
|
---|
94 | for path in paths:
|
---|
95 | if not self._css.get(medium) or path not in self._css[medium]:
|
---|
96 | self._css.setdefault(medium, []).append(path)
|
---|
97 |
|
---|
98 | def __add__(self, other):
|
---|
99 | combined = Media()
|
---|
100 | for name in MEDIA_TYPES:
|
---|
101 | getattr(combined, 'add_' + name)(getattr(self, '_' + name, None))
|
---|
102 | getattr(combined, 'add_' + name)(getattr(other, '_' + name, None))
|
---|
103 | return combined
|
---|
104 |
|
---|
105 | def media_property(cls):
|
---|
106 | def _media(self):
|
---|
107 | # Get the media property of the superclass, if it exists
|
---|
108 | if hasattr(super(cls, self), 'media'):
|
---|
109 | base = super(cls, self).media
|
---|
110 | else:
|
---|
111 | base = Media()
|
---|
112 |
|
---|
113 | # Get the media definition for this class
|
---|
114 | definition = getattr(cls, 'Media', None)
|
---|
115 | if definition:
|
---|
116 | extend = getattr(definition, 'extend', True)
|
---|
117 | if extend:
|
---|
118 | if extend == True:
|
---|
119 | m = base
|
---|
120 | else:
|
---|
121 | m = Media()
|
---|
122 | for medium in extend:
|
---|
123 | m = m + base[medium]
|
---|
124 | return m + Media(definition)
|
---|
125 | else:
|
---|
126 | return Media(definition)
|
---|
127 | else:
|
---|
128 | return base
|
---|
129 | return property(_media)
|
---|
130 |
|
---|
131 | class MediaDefiningClass(type):
|
---|
132 | "Metaclass for classes that can have media definitions"
|
---|
133 | def __new__(cls, name, bases, attrs):
|
---|
134 | new_class = super(MediaDefiningClass, cls).__new__(cls, name, bases,
|
---|
135 | attrs)
|
---|
136 | if 'media' not in attrs:
|
---|
137 | new_class.media = media_property(new_class)
|
---|
138 | return new_class
|
---|
139 |
|
---|
140 | class SubWidget(StrAndUnicode):
|
---|
141 | """
|
---|
142 | Some widgets are made of multiple HTML elements -- namely, RadioSelect.
|
---|
143 | This is a class that represents the "inner" HTML element of a widget.
|
---|
144 | """
|
---|
145 | def __init__(self, parent_widget, name, value, attrs, choices):
|
---|
146 | self.parent_widget = parent_widget
|
---|
147 | self.name, self.value = name, value
|
---|
148 | self.attrs, self.choices = attrs, choices
|
---|
149 |
|
---|
150 | def __unicode__(self):
|
---|
151 | args = [self.name, self.value, self.attrs]
|
---|
152 | if self.choices:
|
---|
153 | args.append(self.choices)
|
---|
154 | return self.parent_widget.render(*args)
|
---|
155 |
|
---|
156 | class Widget(object):
|
---|
157 | __metaclass__ = MediaDefiningClass
|
---|
158 | is_hidden = False # Determines whether this corresponds to an <input type="hidden">.
|
---|
159 | needs_multipart_form = False # Determines does this widget need multipart form
|
---|
160 | is_localized = False
|
---|
161 | is_required = False
|
---|
162 |
|
---|
163 | def __init__(self, attrs=None):
|
---|
164 | if attrs is not None:
|
---|
165 | self.attrs = attrs.copy()
|
---|
166 | else:
|
---|
167 | self.attrs = {}
|
---|
168 |
|
---|
169 | def __deepcopy__(self, memo):
|
---|
170 | obj = copy.copy(self)
|
---|
171 | obj.attrs = self.attrs.copy()
|
---|
172 | memo[id(self)] = obj
|
---|
173 | return obj
|
---|
174 |
|
---|
175 | def subwidgets(self, name, value, attrs=None, choices=()):
|
---|
176 | """
|
---|
177 | Yields all "subwidgets" of this widget. Used only by RadioSelect to
|
---|
178 | allow template access to individual <input type="radio"> buttons.
|
---|
179 |
|
---|
180 | Arguments are the same as for render().
|
---|
181 | """
|
---|
182 | yield SubWidget(self, name, value, attrs, choices)
|
---|
183 |
|
---|
184 | def render(self, name, value, attrs=None):
|
---|
185 | """
|
---|
186 | Returns this Widget rendered as HTML, as a Unicode string.
|
---|
187 |
|
---|
188 | The 'value' given is not guaranteed to be valid input, so subclass
|
---|
189 | implementations should program defensively.
|
---|
190 | """
|
---|
191 | raise NotImplementedError
|
---|
192 |
|
---|
193 | def build_attrs(self, extra_attrs=None, **kwargs):
|
---|
194 | "Helper function for building an attribute dictionary."
|
---|
195 | attrs = dict(self.attrs, **kwargs)
|
---|
196 | if extra_attrs:
|
---|
197 | attrs.update(extra_attrs)
|
---|
198 | return attrs
|
---|
199 |
|
---|
200 | def value_from_datadict(self, data, files, name):
|
---|
201 | """
|
---|
202 | Given a dictionary of data and this widget's name, returns the value
|
---|
203 | of this widget. Returns None if it's not provided.
|
---|
204 | """
|
---|
205 | return data.get(name, None)
|
---|
206 |
|
---|
207 | def _has_changed(self, initial, data):
|
---|
208 | """
|
---|
209 | Return True if data differs from initial.
|
---|
210 | """
|
---|
211 | # For purposes of seeing whether something has changed, None is
|
---|
212 | # the same as an empty string, if the data or inital value we get
|
---|
213 | # is None, replace it w/ u''.
|
---|
214 | if data is None:
|
---|
215 | data_value = u''
|
---|
216 | else:
|
---|
217 | data_value = data
|
---|
218 | if initial is None:
|
---|
219 | initial_value = u''
|
---|
220 | else:
|
---|
221 | initial_value = initial
|
---|
222 | if force_unicode(initial_value) != force_unicode(data_value):
|
---|
223 | return True
|
---|
224 | return False
|
---|
225 |
|
---|
226 | def id_for_label(self, id_):
|
---|
227 | """
|
---|
228 | Returns the HTML ID attribute of this Widget for use by a <label>,
|
---|
229 | given the ID of the field. Returns None if no ID is available.
|
---|
230 |
|
---|
231 | This hook is necessary because some widgets have multiple HTML
|
---|
232 | elements and, thus, multiple IDs. In that case, this method should
|
---|
233 | return an ID value that corresponds to the first ID in the widget's
|
---|
234 | tags.
|
---|
235 | """
|
---|
236 | return id_
|
---|
237 |
|
---|
238 | class Input(Widget):
|
---|
239 | """
|
---|
240 | Base class for all <input> widgets (except type='checkbox' and
|
---|
241 | type='radio', which are special).
|
---|
242 | """
|
---|
243 | input_type = None # Subclasses must define this.
|
---|
244 |
|
---|
245 | def _format_value(self, value):
|
---|
246 | if self.is_localized:
|
---|
247 | return formats.localize_input(value)
|
---|
248 | return value
|
---|
249 |
|
---|
250 | def render(self, name, value, attrs=None):
|
---|
251 | if value is None:
|
---|
252 | value = ''
|
---|
253 | final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
|
---|
254 | if value != '':
|
---|
255 | # Only add the 'value' attribute if a value is non-empty.
|
---|
256 | final_attrs['value'] = force_unicode(self._format_value(value))
|
---|
257 | return mark_safe(u'<input%s />' % flatatt(final_attrs))
|
---|
258 |
|
---|
259 | class TextInput(Input):
|
---|
260 | input_type = 'text'
|
---|
261 |
|
---|
262 | class PasswordInput(Input):
|
---|
263 | input_type = 'password'
|
---|
264 |
|
---|
265 | def __init__(self, attrs=None, render_value=False):
|
---|
266 | super(PasswordInput, self).__init__(attrs)
|
---|
267 | self.render_value = render_value
|
---|
268 |
|
---|
269 | def render(self, name, value, attrs=None):
|
---|
270 | if not self.render_value: value=None
|
---|
271 | return super(PasswordInput, self).render(name, value, attrs)
|
---|
272 |
|
---|
273 | class HiddenInput(Input):
|
---|
274 | input_type = 'hidden'
|
---|
275 | is_hidden = True
|
---|
276 |
|
---|
277 | class MultipleHiddenInput(HiddenInput):
|
---|
278 | """
|
---|
279 | A widget that handles <input type="hidden"> for fields that have a list
|
---|
280 | of values.
|
---|
281 | """
|
---|
282 | def __init__(self, attrs=None, choices=()):
|
---|
283 | super(MultipleHiddenInput, self).__init__(attrs)
|
---|
284 | # choices can be any iterable
|
---|
285 | self.choices = choices
|
---|
286 |
|
---|
287 | def render(self, name, value, attrs=None, choices=()):
|
---|
288 | if value is None: value = []
|
---|
289 | final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
|
---|
290 | id_ = final_attrs.get('id', None)
|
---|
291 | inputs = []
|
---|
292 | for i, v in enumerate(value):
|
---|
293 | input_attrs = dict(value=force_unicode(v), **final_attrs)
|
---|
294 | if id_:
|
---|
295 | # An ID attribute was given. Add a numeric index as a suffix
|
---|
296 | # so that the inputs don't all have the same ID attribute.
|
---|
297 | input_attrs['id'] = '%s_%s' % (id_, i)
|
---|
298 | inputs.append(u'<input%s />' % flatatt(input_attrs))
|
---|
299 | return mark_safe(u'\n'.join(inputs))
|
---|
300 |
|
---|
301 | def value_from_datadict(self, data, files, name):
|
---|
302 | if isinstance(data, (MultiValueDict, MergeDict)):
|
---|
303 | return data.getlist(name)
|
---|
304 | return data.get(name, None)
|
---|
305 |
|
---|
306 | class FileInput(Input):
|
---|
307 | input_type = 'file'
|
---|
308 | needs_multipart_form = True
|
---|
309 |
|
---|
310 | def render(self, name, value, attrs=None):
|
---|
311 | return super(FileInput, self).render(name, None, attrs=attrs)
|
---|
312 |
|
---|
313 | def value_from_datadict(self, data, files, name):
|
---|
314 | "File widgets take data from FILES, not POST"
|
---|
315 | return files.get(name, None)
|
---|
316 |
|
---|
317 | def _has_changed(self, initial, data):
|
---|
318 | if data is None:
|
---|
319 | return False
|
---|
320 | return True
|
---|
321 |
|
---|
322 | FILE_INPUT_CONTRADICTION = object()
|
---|
323 |
|
---|
324 | class ClearableFileInput(FileInput):
|
---|
325 | initial_text = ugettext_lazy('Currently')
|
---|
326 | input_text = ugettext_lazy('Change')
|
---|
327 | clear_checkbox_label = ugettext_lazy('Clear')
|
---|
328 |
|
---|
329 | template_with_initial = u'%(initial_text)s: %(initial)s %(clear_template)s<br />%(input_text)s: %(input)s'
|
---|
330 |
|
---|
331 | template_with_clear = u'%(clear)s <label for="%(clear_checkbox_id)s">%(clear_checkbox_label)s</label>'
|
---|
332 |
|
---|
333 | def clear_checkbox_name(self, name):
|
---|
334 | """
|
---|
335 | Given the name of the file input, return the name of the clear checkbox
|
---|
336 | input.
|
---|
337 | """
|
---|
338 | return name + '-clear'
|
---|
339 |
|
---|
340 | def clear_checkbox_id(self, name):
|
---|
341 | """
|
---|
342 | Given the name of the clear checkbox input, return the HTML id for it.
|
---|
343 | """
|
---|
344 | return name + '_id'
|
---|
345 |
|
---|
346 | def render(self, name, value, attrs=None):
|
---|
347 | substitutions = {
|
---|
348 | 'initial_text': self.initial_text,
|
---|
349 | 'input_text': self.input_text,
|
---|
350 | 'clear_template': '',
|
---|
351 | 'clear_checkbox_label': self.clear_checkbox_label,
|
---|
352 | }
|
---|
353 | template = u'%(input)s'
|
---|
354 | substitutions['input'] = super(ClearableFileInput, self).render(name, value, attrs)
|
---|
355 |
|
---|
356 | if value and hasattr(value, "url"):
|
---|
357 | template = self.template_with_initial
|
---|
358 | substitutions['initial'] = (u'<a href="%s">%s</a>'
|
---|
359 | % (escape(value.url),
|
---|
360 | escape(force_unicode(value))))
|
---|
361 | if not self.is_required:
|
---|
362 | checkbox_name = self.clear_checkbox_name(name)
|
---|
363 | checkbox_id = self.clear_checkbox_id(checkbox_name)
|
---|
364 | substitutions['clear_checkbox_name'] = conditional_escape(checkbox_name)
|
---|
365 | substitutions['clear_checkbox_id'] = conditional_escape(checkbox_id)
|
---|
366 | substitutions['clear'] = CheckboxInput().render(checkbox_name, False, attrs={'id': checkbox_id})
|
---|
367 | substitutions['clear_template'] = self.template_with_clear % substitutions
|
---|
368 |
|
---|
369 | return mark_safe(template % substitutions)
|
---|
370 |
|
---|
371 | def value_from_datadict(self, data, files, name):
|
---|
372 | upload = super(ClearableFileInput, self).value_from_datadict(data, files, name)
|
---|
373 | if not self.is_required and CheckboxInput().value_from_datadict(
|
---|
374 | data, files, self.clear_checkbox_name(name)):
|
---|
375 | if upload:
|
---|
376 | # If the user contradicts themselves (uploads a new file AND
|
---|
377 | # checks the "clear" checkbox), we return a unique marker
|
---|
378 | # object that FileField will turn into a ValidationError.
|
---|
379 | return FILE_INPUT_CONTRADICTION
|
---|
380 | # False signals to clear any existing value, as opposed to just None
|
---|
381 | return False
|
---|
382 | return upload
|
---|
383 |
|
---|
384 | class Textarea(Widget):
|
---|
385 | def __init__(self, attrs=None):
|
---|
386 | # The 'rows' and 'cols' attributes are required for HTML correctness.
|
---|
387 | default_attrs = {'cols': '40', 'rows': '10'}
|
---|
388 | if attrs:
|
---|
389 | default_attrs.update(attrs)
|
---|
390 | super(Textarea, self).__init__(default_attrs)
|
---|
391 |
|
---|
392 | def render(self, name, value, attrs=None):
|
---|
393 | if value is None: value = ''
|
---|
394 | final_attrs = self.build_attrs(attrs, name=name)
|
---|
395 | return mark_safe(u'<textarea%s>%s</textarea>' % (flatatt(final_attrs),
|
---|
396 | conditional_escape(force_unicode(value))))
|
---|
397 |
|
---|
398 | class DateInput(Input):
|
---|
399 | input_type = 'text'
|
---|
400 |
|
---|
401 | def __init__(self, attrs=None, format=None):
|
---|
402 | super(DateInput, self).__init__(attrs)
|
---|
403 | if format:
|
---|
404 | self.format = format
|
---|
405 | self.manual_format = True
|
---|
406 | else:
|
---|
407 | self.format = formats.get_format('DATE_INPUT_FORMATS')[0]
|
---|
408 | self.manual_format = False
|
---|
409 |
|
---|
410 | def _format_value(self, value):
|
---|
411 | if self.is_localized and not self.manual_format:
|
---|
412 | return formats.localize_input(value)
|
---|
413 | elif hasattr(value, 'strftime'):
|
---|
414 | value = datetime_safe.new_date(value)
|
---|
415 | return value.strftime(self.format)
|
---|
416 | return value
|
---|
417 |
|
---|
418 | def _has_changed(self, initial, data):
|
---|
419 | # If our field has show_hidden_initial=True, initial will be a string
|
---|
420 | # formatted by HiddenInput using formats.localize_input, which is not
|
---|
421 | # necessarily the format used for this widget. Attempt to convert it.
|
---|
422 | try:
|
---|
423 | input_format = formats.get_format('DATE_INPUT_FORMATS')[0]
|
---|
424 | initial = datetime.datetime.strptime(initial, input_format).date()
|
---|
425 | except (TypeError, ValueError):
|
---|
426 | pass
|
---|
427 | return super(DateInput, self)._has_changed(self._format_value(initial), data)
|
---|
428 |
|
---|
429 | class DateTimeInput(Input):
|
---|
430 | input_type = 'text'
|
---|
431 |
|
---|
432 | def __init__(self, attrs=None, format=None):
|
---|
433 | super(DateTimeInput, self).__init__(attrs)
|
---|
434 | if format:
|
---|
435 | self.format = format
|
---|
436 | self.manual_format = True
|
---|
437 | else:
|
---|
438 | self.format = formats.get_format('DATETIME_INPUT_FORMATS')[0]
|
---|
439 | self.manual_format = False
|
---|
440 |
|
---|
441 | def _format_value(self, value):
|
---|
442 | if self.is_localized and not self.manual_format:
|
---|
443 | return formats.localize_input(value)
|
---|
444 | elif hasattr(value, 'strftime'):
|
---|
445 | value = datetime_safe.new_datetime(value)
|
---|
446 | return value.strftime(self.format)
|
---|
447 | return value
|
---|
448 |
|
---|
449 | def _has_changed(self, initial, data):
|
---|
450 | # If our field has show_hidden_initial=True, initial will be a string
|
---|
451 | # formatted by HiddenInput using formats.localize_input, which is not
|
---|
452 | # necessarily the format used for this widget. Attempt to convert it.
|
---|
453 | try:
|
---|
454 | input_format = formats.get_format('DATETIME_INPUT_FORMATS')[0]
|
---|
455 | initial = datetime.datetime.strptime(initial, input_format)
|
---|
456 | except (TypeError, ValueError):
|
---|
457 | pass
|
---|
458 | return super(DateTimeInput, self)._has_changed(self._format_value(initial), data)
|
---|
459 |
|
---|
460 | class TimeInput(Input):
|
---|
461 | input_type = 'text'
|
---|
462 |
|
---|
463 | def __init__(self, attrs=None, format=None):
|
---|
464 | super(TimeInput, self).__init__(attrs)
|
---|
465 | if format:
|
---|
466 | self.format = format
|
---|
467 | self.manual_format = True
|
---|
468 | else:
|
---|
469 | self.format = formats.get_format('TIME_INPUT_FORMATS')[0]
|
---|
470 | self.manual_format = False
|
---|
471 |
|
---|
472 | def _format_value(self, value):
|
---|
473 | if self.is_localized and not self.manual_format:
|
---|
474 | return formats.localize_input(value)
|
---|
475 | elif hasattr(value, 'strftime'):
|
---|
476 | return value.strftime(self.format)
|
---|
477 | return value
|
---|
478 |
|
---|
479 | def _has_changed(self, initial, data):
|
---|
480 | # If our field has show_hidden_initial=True, initial will be a string
|
---|
481 | # formatted by HiddenInput using formats.localize_input, which is not
|
---|
482 | # necessarily the format used for this widget. Attempt to convert it.
|
---|
483 | try:
|
---|
484 | input_format = formats.get_format('TIME_INPUT_FORMATS')[0]
|
---|
485 | initial = datetime.datetime.strptime(initial, input_format).time()
|
---|
486 | except (TypeError, ValueError):
|
---|
487 | pass
|
---|
488 | return super(TimeInput, self)._has_changed(self._format_value(initial), data)
|
---|
489 |
|
---|
490 |
|
---|
491 | # Defined at module level so that CheckboxInput is picklable (#17976)
|
---|
492 | def boolean_check(v):
|
---|
493 | return not (v is False or v is None or v == '')
|
---|
494 |
|
---|
495 |
|
---|
496 | class CheckboxInput(Widget):
|
---|
497 | def __init__(self, attrs=None, check_test=None):
|
---|
498 | super(CheckboxInput, self).__init__(attrs)
|
---|
499 | # check_test is a callable that takes a value and returns True
|
---|
500 | # if the checkbox should be checked for that value.
|
---|
501 | self.check_test = boolean_check if check_test is None else check_test
|
---|
502 |
|
---|
503 | def render(self, name, value, attrs=None):
|
---|
504 | final_attrs = self.build_attrs(attrs, type='checkbox', name=name)
|
---|
505 | try:
|
---|
506 | result = self.check_test(value)
|
---|
507 | except: # Silently catch exceptions
|
---|
508 | result = False
|
---|
509 | if result:
|
---|
510 | final_attrs['checked'] = 'checked'
|
---|
511 | if not (value == True or value == False or value == None or value == ''):
|
---|
512 | # Only add the 'value' attribute if a value is non-empty.
|
---|
513 | final_attrs['value'] = force_unicode(value)
|
---|
514 | return mark_safe(u'<input%s />' % flatatt(final_attrs))
|
---|
515 |
|
---|
516 | def value_from_datadict(self, data, files, name):
|
---|
517 | if name not in data:
|
---|
518 | # A missing value means False because HTML form submission does not
|
---|
519 | # send results for unselected checkboxes.
|
---|
520 | return False
|
---|
521 | value = data.get(name)
|
---|
522 | # Translate true and false strings to boolean values.
|
---|
523 | values = {'true': True, 'false': False}
|
---|
524 | if isinstance(value, basestring):
|
---|
525 | value = values.get(value.lower(), value)
|
---|
526 | return value
|
---|
527 |
|
---|
528 | def _has_changed(self, initial, data):
|
---|
529 | # Sometimes data or initial could be None or u'' which should be the
|
---|
530 | # same thing as False.
|
---|
531 | return bool(initial) != bool(data)
|
---|
532 |
|
---|
533 | class Select(Widget):
|
---|
534 | allow_multiple_selected = False
|
---|
535 |
|
---|
536 | def __init__(self, attrs=None, choices=()):
|
---|
537 | super(Select, self).__init__(attrs)
|
---|
538 | # choices can be any iterable, but we may need to render this widget
|
---|
539 | # multiple times. Thus, collapse it into a list so it can be consumed
|
---|
540 | # more than once.
|
---|
541 | self.choices = list(choices)
|
---|
542 |
|
---|
543 | def render(self, name, value, attrs=None, choices=()):
|
---|
544 | if value is None: value = ''
|
---|
545 | final_attrs = self.build_attrs(attrs, name=name)
|
---|
546 | output = [u'<select%s>' % flatatt(final_attrs)]
|
---|
547 | options = self.render_options(choices, [value])
|
---|
548 | if options:
|
---|
549 | output.append(options)
|
---|
550 | output.append(u'</select>')
|
---|
551 | return mark_safe(u'\n'.join(output))
|
---|
552 |
|
---|
553 | def render_option(self, selected_choices, option_value, option_label):
|
---|
554 | option_value = force_unicode(option_value)
|
---|
555 | if option_value in selected_choices:
|
---|
556 | selected_html = u' selected="selected"'
|
---|
557 | if not self.allow_multiple_selected:
|
---|
558 | # Only allow for a single selection.
|
---|
559 | selected_choices.remove(option_value)
|
---|
560 | else:
|
---|
561 | selected_html = ''
|
---|
562 | return u'<option value="%s"%s>%s</option>' % (
|
---|
563 | escape(option_value), selected_html,
|
---|
564 | conditional_escape(force_unicode(option_label)))
|
---|
565 |
|
---|
566 | def render_options(self, choices, selected_choices):
|
---|
567 | # Normalize to strings.
|
---|
568 | selected_choices = set(force_unicode(v) for v in selected_choices)
|
---|
569 | output = []
|
---|
570 | for option_value, option_label in chain(self.choices, choices):
|
---|
571 | if isinstance(option_label, (list, tuple)):
|
---|
572 | output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value)))
|
---|
573 | for option in option_label:
|
---|
574 | output.append(self.render_option(selected_choices, *option))
|
---|
575 | output.append(u'</optgroup>')
|
---|
576 | else:
|
---|
577 | output.append(self.render_option(selected_choices, option_value, option_label))
|
---|
578 | return u'\n'.join(output)
|
---|
579 |
|
---|
580 | class NullBooleanSelect(Select):
|
---|
581 | """
|
---|
582 | A Select Widget intended to be used with NullBooleanField.
|
---|
583 | """
|
---|
584 | def __init__(self, attrs=None):
|
---|
585 | choices = ((u'1', ugettext_lazy('Unknown')),
|
---|
586 | (u'2', ugettext_lazy('Yes')),
|
---|
587 | (u'3', ugettext_lazy('No')))
|
---|
588 | super(NullBooleanSelect, self).__init__(attrs, choices)
|
---|
589 |
|
---|
590 | def render(self, name, value, attrs=None, choices=()):
|
---|
591 | try:
|
---|
592 | value = {True: u'2', False: u'3', u'2': u'2', u'3': u'3'}[value]
|
---|
593 | except KeyError:
|
---|
594 | value = u'1'
|
---|
595 | return super(NullBooleanSelect, self).render(name, value, attrs, choices)
|
---|
596 |
|
---|
597 | def value_from_datadict(self, data, files, name):
|
---|
598 | value = data.get(name, None)
|
---|
599 | return {u'2': True,
|
---|
600 | True: True,
|
---|
601 | 'True': True,
|
---|
602 | u'3': False,
|
---|
603 | 'False': False,
|
---|
604 | False: False}.get(value, None)
|
---|
605 |
|
---|
606 | def _has_changed(self, initial, data):
|
---|
607 | # For a NullBooleanSelect, None (unknown) and False (No)
|
---|
608 | # are not the same
|
---|
609 | if initial is not None:
|
---|
610 | initial = bool(initial)
|
---|
611 | if data is not None:
|
---|
612 | data = bool(data)
|
---|
613 | return initial != data
|
---|
614 |
|
---|
615 | class SelectMultiple(Select):
|
---|
616 | allow_multiple_selected = True
|
---|
617 |
|
---|
618 | def render(self, name, value, attrs=None, choices=()):
|
---|
619 | if value is None: value = []
|
---|
620 | final_attrs = self.build_attrs(attrs, name=name)
|
---|
621 | output = [u'<select multiple="multiple"%s>' % flatatt(final_attrs)]
|
---|
622 | options = self.render_options(choices, value)
|
---|
623 | if options:
|
---|
624 | output.append(options)
|
---|
625 | output.append('</select>')
|
---|
626 | return mark_safe(u'\n'.join(output))
|
---|
627 |
|
---|
628 | def value_from_datadict(self, data, files, name):
|
---|
629 | if isinstance(data, (MultiValueDict, MergeDict)):
|
---|
630 | return data.getlist(name)
|
---|
631 | return data.get(name, None)
|
---|
632 |
|
---|
633 | def _has_changed(self, initial, data):
|
---|
634 | if initial is None:
|
---|
635 | initial = []
|
---|
636 | if data is None:
|
---|
637 | data = []
|
---|
638 | if len(initial) != len(data):
|
---|
639 | return True
|
---|
640 | initial_set = set([force_unicode(value) for value in initial])
|
---|
641 | data_set = set([force_unicode(value) for value in data])
|
---|
642 | return data_set != initial_set
|
---|
643 |
|
---|
644 | class RadioInput(SubWidget):
|
---|
645 | """
|
---|
646 | An object used by RadioFieldRenderer that represents a single
|
---|
647 | <input type='radio'>.
|
---|
648 | """
|
---|
649 |
|
---|
650 | def __init__(self, name, value, attrs, choice, index):
|
---|
651 | self.name, self.value = name, value
|
---|
652 | self.attrs = attrs
|
---|
653 | self.choice_value = force_unicode(choice[0])
|
---|
654 | self.choice_label = force_unicode(choice[1])
|
---|
655 | self.index = index
|
---|
656 |
|
---|
657 | def __unicode__(self):
|
---|
658 | return self.render()
|
---|
659 |
|
---|
660 | def render(self, name=None, value=None, attrs=None, choices=()):
|
---|
661 | name = name or self.name
|
---|
662 | value = value or self.value
|
---|
663 | attrs = attrs or self.attrs
|
---|
664 | if 'id' in self.attrs:
|
---|
665 | label_for = ' for="%s_%s"' % (self.attrs['id'], self.index)
|
---|
666 | else:
|
---|
667 | label_for = ''
|
---|
668 | choice_label = conditional_escape(force_unicode(self.choice_label))
|
---|
669 | return mark_safe(u'<label%s>%s %s</label>' % (label_for, self.tag(), choice_label))
|
---|
670 |
|
---|
671 | def is_checked(self):
|
---|
672 | return self.value == self.choice_value
|
---|
673 |
|
---|
674 | def tag(self):
|
---|
675 | if 'id' in self.attrs:
|
---|
676 | self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index)
|
---|
677 | final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value)
|
---|
678 | if self.is_checked():
|
---|
679 | final_attrs['checked'] = 'checked'
|
---|
680 | return mark_safe(u'<input%s />' % flatatt(final_attrs))
|
---|
681 |
|
---|
682 | class RadioFieldRenderer(StrAndUnicode):
|
---|
683 | """
|
---|
684 | An object used by RadioSelect to enable customization of radio widgets.
|
---|
685 | """
|
---|
686 |
|
---|
687 | def __init__(self, name, value, attrs, choices):
|
---|
688 | self.name, self.value, self.attrs = name, value, attrs
|
---|
689 | self.choices = choices
|
---|
690 |
|
---|
691 | def __iter__(self):
|
---|
692 | for i, choice in enumerate(self.choices):
|
---|
693 | yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i)
|
---|
694 |
|
---|
695 | def __getitem__(self, idx):
|
---|
696 | choice = self.choices[idx] # Let the IndexError propogate
|
---|
697 | return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx)
|
---|
698 |
|
---|
699 | def __unicode__(self):
|
---|
700 | return self.render()
|
---|
701 |
|
---|
702 | def render(self):
|
---|
703 | """Outputs a <ul> for this set of radio fields."""
|
---|
704 | return mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>'
|
---|
705 | % force_unicode(w) for w in self]))
|
---|
706 |
|
---|
707 | class RadioSelect(Select):
|
---|
708 | renderer = RadioFieldRenderer
|
---|
709 |
|
---|
710 | def __init__(self, *args, **kwargs):
|
---|
711 | # Override the default renderer if we were passed one.
|
---|
712 | renderer = kwargs.pop('renderer', None)
|
---|
713 | if renderer:
|
---|
714 | self.renderer = renderer
|
---|
715 | super(RadioSelect, self).__init__(*args, **kwargs)
|
---|
716 |
|
---|
717 | def subwidgets(self, name, value, attrs=None, choices=()):
|
---|
718 | for widget in self.get_renderer(name, value, attrs, choices):
|
---|
719 | yield widget
|
---|
720 |
|
---|
721 | def get_renderer(self, name, value, attrs=None, choices=()):
|
---|
722 | """Returns an instance of the renderer."""
|
---|
723 | if value is None: value = ''
|
---|
724 | str_value = force_unicode(value) # Normalize to string.
|
---|
725 | final_attrs = self.build_attrs(attrs)
|
---|
726 | choices = list(chain(self.choices, choices))
|
---|
727 | return self.renderer(name, str_value, final_attrs, choices)
|
---|
728 |
|
---|
729 | def render(self, name, value, attrs=None, choices=()):
|
---|
730 | return self.get_renderer(name, value, attrs, choices).render()
|
---|
731 |
|
---|
732 | def id_for_label(self, id_):
|
---|
733 | # RadioSelect is represented by multiple <input type="radio"> fields,
|
---|
734 | # each of which has a distinct ID. The IDs are made distinct by a "_X"
|
---|
735 | # suffix, where X is the zero-based index of the radio field. Thus,
|
---|
736 | # the label for a RadioSelect should reference the first one ('_0').
|
---|
737 | if id_:
|
---|
738 | id_ += '_0'
|
---|
739 | return id_
|
---|
740 |
|
---|
741 | class CheckboxSelectMultiple(SelectMultiple):
|
---|
742 | def render(self, name, value, attrs=None, choices=()):
|
---|
743 | if value is None: value = []
|
---|
744 | has_id = attrs and 'id' in attrs
|
---|
745 | final_attrs = self.build_attrs(attrs, name=name)
|
---|
746 | output = [u'<ul>']
|
---|
747 | # Normalize to strings
|
---|
748 | str_values = set([force_unicode(v) for v in value])
|
---|
749 | for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
|
---|
750 | # If an ID attribute was given, add a numeric index as a suffix,
|
---|
751 | # so that the checkboxes don't all have the same ID attribute.
|
---|
752 | if has_id:
|
---|
753 | final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
|
---|
754 | label_for = u' for="%s"' % final_attrs['id']
|
---|
755 | else:
|
---|
756 | label_for = ''
|
---|
757 |
|
---|
758 | cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
|
---|
759 | option_value = force_unicode(option_value)
|
---|
760 | rendered_cb = cb.render(name, option_value)
|
---|
761 | option_label = conditional_escape(force_unicode(option_label))
|
---|
762 | output.append(u'<li><label%s>%s %s</label></li>' % (label_for, rendered_cb, option_label))
|
---|
763 | output.append(u'</ul>')
|
---|
764 | return mark_safe(u'\n'.join(output))
|
---|
765 |
|
---|
766 | def id_for_label(self, id_):
|
---|
767 | # See the comment for RadioSelect.id_for_label()
|
---|
768 | if id_:
|
---|
769 | id_ += '_0'
|
---|
770 | return id_
|
---|
771 |
|
---|
772 | class MultiWidget(Widget):
|
---|
773 | """
|
---|
774 | A widget that is composed of multiple widgets.
|
---|
775 |
|
---|
776 | Its render() method is different than other widgets', because it has to
|
---|
777 | figure out how to split a single value for display in multiple widgets.
|
---|
778 | The ``value`` argument can be one of two things:
|
---|
779 |
|
---|
780 | * A list.
|
---|
781 | * A normal value (e.g., a string) that has been "compressed" from
|
---|
782 | a list of values.
|
---|
783 |
|
---|
784 | In the second case -- i.e., if the value is NOT a list -- render() will
|
---|
785 | first "decompress" the value into a list before rendering it. It does so by
|
---|
786 | calling the decompress() method, which MultiWidget subclasses must
|
---|
787 | implement. This method takes a single "compressed" value and returns a
|
---|
788 | list.
|
---|
789 |
|
---|
790 | When render() does its HTML rendering, each value in the list is rendered
|
---|
791 | with the corresponding widget -- the first value is rendered in the first
|
---|
792 | widget, the second value is rendered in the second widget, etc.
|
---|
793 |
|
---|
794 | Subclasses may implement format_output(), which takes the list of rendered
|
---|
795 | widgets and returns a string of HTML that formats them any way you'd like.
|
---|
796 |
|
---|
797 | You'll probably want to use this class with MultiValueField.
|
---|
798 | """
|
---|
799 | def __init__(self, widgets, attrs=None):
|
---|
800 | self.widgets = [isinstance(w, type) and w() or w for w in widgets]
|
---|
801 | super(MultiWidget, self).__init__(attrs)
|
---|
802 |
|
---|
803 | def render(self, name, value, attrs=None):
|
---|
804 | if self.is_localized:
|
---|
805 | for widget in self.widgets:
|
---|
806 | widget.is_localized = self.is_localized
|
---|
807 | # value is a list of values, each corresponding to a widget
|
---|
808 | # in self.widgets.
|
---|
809 | if not isinstance(value, list):
|
---|
810 | value = self.decompress(value)
|
---|
811 | output = []
|
---|
812 | final_attrs = self.build_attrs(attrs)
|
---|
813 | id_ = final_attrs.get('id', None)
|
---|
814 | for i, widget in enumerate(self.widgets):
|
---|
815 | try:
|
---|
816 | widget_value = value[i]
|
---|
817 | except IndexError:
|
---|
818 | widget_value = None
|
---|
819 | if id_:
|
---|
820 | final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
|
---|
821 | output.append(widget.render(name + '_%s' % i, widget_value, final_attrs))
|
---|
822 | return mark_safe(self.format_output(output))
|
---|
823 |
|
---|
824 | def id_for_label(self, id_):
|
---|
825 | # See the comment for RadioSelect.id_for_label()
|
---|
826 | if id_:
|
---|
827 | id_ += '_0'
|
---|
828 | return id_
|
---|
829 |
|
---|
830 | def value_from_datadict(self, data, files, name):
|
---|
831 | return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
|
---|
832 |
|
---|
833 | def _has_changed(self, initial, data):
|
---|
834 | if initial is None:
|
---|
835 | initial = [u'' for x in range(0, len(data))]
|
---|
836 | else:
|
---|
837 | if not isinstance(initial, list):
|
---|
838 | initial = self.decompress(initial)
|
---|
839 | for widget, initial, data in zip(self.widgets, initial, data):
|
---|
840 | if widget._has_changed(initial, data):
|
---|
841 | return True
|
---|
842 | return False
|
---|
843 |
|
---|
844 | def format_output(self, rendered_widgets):
|
---|
845 | """
|
---|
846 | Given a list of rendered widgets (as strings), returns a Unicode string
|
---|
847 | representing the HTML for the whole lot.
|
---|
848 |
|
---|
849 | This hook allows you to format the HTML design of the widgets, if
|
---|
850 | needed.
|
---|
851 | """
|
---|
852 | return u''.join(rendered_widgets)
|
---|
853 |
|
---|
854 | def decompress(self, value):
|
---|
855 | """
|
---|
856 | Returns a list of decompressed values for the given compressed value.
|
---|
857 | The given value can be assumed to be valid, but not necessarily
|
---|
858 | non-empty.
|
---|
859 | """
|
---|
860 | raise NotImplementedError('Subclasses must implement this method.')
|
---|
861 |
|
---|
862 | def _get_media(self):
|
---|
863 | "Media for a multiwidget is the combination of all media of the subwidgets"
|
---|
864 | media = Media()
|
---|
865 | for w in self.widgets:
|
---|
866 | media = media + w.media
|
---|
867 | return media
|
---|
868 | media = property(_get_media)
|
---|
869 |
|
---|
870 | def __deepcopy__(self, memo):
|
---|
871 | obj = super(MultiWidget, self).__deepcopy__(memo)
|
---|
872 | obj.widgets = copy.deepcopy(self.widgets)
|
---|
873 | return obj
|
---|
874 |
|
---|
875 | class SplitDateTimeWidget(MultiWidget):
|
---|
876 | """
|
---|
877 | A Widget that splits datetime input into two <input type="text"> boxes.
|
---|
878 | """
|
---|
879 |
|
---|
880 | def __init__(self, attrs=None, date_format=None, time_format=None):
|
---|
881 | widgets = (DateInput(attrs=attrs, format=date_format),
|
---|
882 | TimeInput(attrs=attrs, format=time_format))
|
---|
883 | super(SplitDateTimeWidget, self).__init__(widgets, attrs)
|
---|
884 |
|
---|
885 | def decompress(self, value):
|
---|
886 | if value:
|
---|
887 | value = to_current_timezone(value)
|
---|
888 | return [value.date(), value.time().replace(microsecond=0)]
|
---|
889 | return [None, None]
|
---|
890 |
|
---|
891 | class SplitHiddenDateTimeWidget(SplitDateTimeWidget):
|
---|
892 | """
|
---|
893 | A Widget that splits datetime input into two <input type="hidden"> inputs.
|
---|
894 | """
|
---|
895 | is_hidden = True
|
---|
896 |
|
---|
897 | def __init__(self, attrs=None, date_format=None, time_format=None):
|
---|
898 | super(SplitHiddenDateTimeWidget, self).__init__(attrs, date_format, time_format)
|
---|
899 | for widget in self.widgets:
|
---|
900 | widget.input_type = 'hidden'
|
---|
901 | widget.is_hidden = True
|
---|