123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888 |
- """
- HTML Widget classes
- """
- from __future__ import unicode_literals
- import copy
- from itertools import chain
- import warnings
- from django.conf import settings
- from django.forms.utils import flatatt, to_current_timezone
- from django.utils.datastructures import MultiValueDict, MergeDict
- from django.utils.deprecation import RemovedInDjango18Warning
- from django.utils.encoding import force_text, python_2_unicode_compatible
- from django.utils.html import conditional_escape, format_html
- from django.utils.translation import ugettext_lazy
- from django.utils.safestring import mark_safe
- from django.utils import formats, six
- from django.utils.six.moves.urllib.parse import urljoin
- __all__ = (
- 'Media', 'MediaDefiningClass', 'Widget', 'TextInput',
- 'EmailInput', 'URLInput', 'NumberInput', 'PasswordInput',
- 'HiddenInput', 'MultipleHiddenInput', 'ClearableFileInput',
- 'FileInput', 'DateInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput',
- 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
- 'CheckboxSelectMultiple', 'MultiWidget',
- 'SplitDateTimeWidget', 'SplitHiddenDateTimeWidget',
- )
- MEDIA_TYPES = ('css', 'js')
- @python_2_unicode_compatible
- class Media(object):
- def __init__(self, media=None, **kwargs):
- if media:
- media_attrs = media.__dict__
- else:
- media_attrs = kwargs
- self._css = {}
- self._js = []
- for name in MEDIA_TYPES:
- getattr(self, 'add_' + name)(media_attrs.get(name, None))
- # Any leftover attributes must be invalid.
- # if media_attrs != {}:
- # raise TypeError("'class Media' has invalid attribute(s): %s" % ','.join(media_attrs.keys()))
- def __str__(self):
- return self.render()
- def render(self):
- return mark_safe('\n'.join(chain(*[getattr(self, 'render_' + name)() for name in MEDIA_TYPES])))
- def render_js(self):
- return [format_html('<script type="text/javascript" src="{0}"></script>', self.absolute_path(path)) for path in self._js]
- def render_css(self):
- # To keep rendering order consistent, we can't just iterate over items().
- # We need to sort the keys, and iterate over the sorted list.
- media = sorted(self._css.keys())
- return chain(*[
- [format_html('<link href="{0}" type="text/css" media="{1}" rel="stylesheet" />', self.absolute_path(path), medium)
- for path in self._css[medium]]
- for medium in media])
- def absolute_path(self, path, prefix=None):
- if path.startswith(('http://', 'https://', '/')):
- return path
- if prefix is None:
- if settings.STATIC_URL is None:
- # backwards compatibility
- prefix = settings.MEDIA_URL
- else:
- prefix = settings.STATIC_URL
- return urljoin(prefix, path)
- def __getitem__(self, name):
- "Returns a Media object that only contains media of the given type"
- if name in MEDIA_TYPES:
- return Media(**{str(name): getattr(self, '_' + name)})
- raise KeyError('Unknown media type "%s"' % name)
- def add_js(self, data):
- if data:
- for path in data:
- if path not in self._js:
- self._js.append(path)
- def add_css(self, data):
- if data:
- for medium, paths in data.items():
- for path in paths:
- if not self._css.get(medium) or path not in self._css[medium]:
- self._css.setdefault(medium, []).append(path)
- def __add__(self, other):
- combined = Media()
- for name in MEDIA_TYPES:
- getattr(combined, 'add_' + name)(getattr(self, '_' + name, None))
- getattr(combined, 'add_' + name)(getattr(other, '_' + name, None))
- return combined
- def media_property(cls):
- def _media(self):
- # Get the media property of the superclass, if it exists
- sup_cls = super(cls, self)
- try:
- base = sup_cls.media
- except AttributeError:
- base = Media()
- # Get the media definition for this class
- definition = getattr(cls, 'Media', None)
- if definition:
- extend = getattr(definition, 'extend', True)
- if extend:
- if extend is True:
- m = base
- else:
- m = Media()
- for medium in extend:
- m = m + base[medium]
- return m + Media(definition)
- else:
- return Media(definition)
- else:
- return base
- return property(_media)
- class MediaDefiningClass(type):
- """
- Metaclass for classes that can have media definitions.
- """
- def __new__(mcs, name, bases, attrs):
- new_class = (super(MediaDefiningClass, mcs)
- .__new__(mcs, name, bases, attrs))
- if 'media' not in attrs:
- new_class.media = media_property(new_class)
- return new_class
- @python_2_unicode_compatible
- class SubWidget(object):
- """
- Some widgets are made of multiple HTML elements -- namely, RadioSelect.
- This is a class that represents the "inner" HTML element of a widget.
- """
- def __init__(self, parent_widget, name, value, attrs, choices):
- self.parent_widget = parent_widget
- self.name, self.value = name, value
- self.attrs, self.choices = attrs, choices
- def __str__(self):
- args = [self.name, self.value, self.attrs]
- if self.choices:
- args.append(self.choices)
- return self.parent_widget.render(*args)
- class Widget(six.with_metaclass(MediaDefiningClass)):
- needs_multipart_form = False # Determines does this widget need multipart form
- is_localized = False
- is_required = False
- def __init__(self, attrs=None):
- if attrs is not None:
- self.attrs = attrs.copy()
- else:
- self.attrs = {}
- def __deepcopy__(self, memo):
- obj = copy.copy(self)
- obj.attrs = self.attrs.copy()
- memo[id(self)] = obj
- return obj
- @property
- def is_hidden(self):
- return self.input_type == 'hidden' if hasattr(self, 'input_type') else False
- @is_hidden.setter
- def is_hidden(self, *args):
- warnings.warn(
- "`is_hidden` property is now read-only (and checks `input_type`). "
- "Please update your code.",
- RemovedInDjango18Warning, stacklevel=2
- )
- def subwidgets(self, name, value, attrs=None, choices=()):
- """
- Yields all "subwidgets" of this widget. Used only by RadioSelect to
- allow template access to individual <input type="radio"> buttons.
- Arguments are the same as for render().
- """
- yield SubWidget(self, name, value, attrs, choices)
- def render(self, name, value, attrs=None):
- """
- Returns this Widget rendered as HTML, as a Unicode string.
- The 'value' given is not guaranteed to be valid input, so subclass
- implementations should program defensively.
- """
- raise NotImplementedError('subclasses of Widget must provide a render() method')
- def build_attrs(self, extra_attrs=None, **kwargs):
- "Helper function for building an attribute dictionary."
- attrs = dict(self.attrs, **kwargs)
- if extra_attrs:
- attrs.update(extra_attrs)
- return attrs
- def value_from_datadict(self, data, files, name):
- """
- Given a dictionary of data and this widget's name, returns the value
- of this widget. Returns None if it's not provided.
- """
- return data.get(name, None)
- def id_for_label(self, id_):
- """
- Returns the HTML ID attribute of this Widget for use by a <label>,
- given the ID of the field. Returns None if no ID is available.
- This hook is necessary because some widgets have multiple HTML
- elements and, thus, multiple IDs. In that case, this method should
- return an ID value that corresponds to the first ID in the widget's
- tags.
- """
- return id_
- class Input(Widget):
- """
- Base class for all <input> widgets (except type='checkbox' and
- type='radio', which are special).
- """
- input_type = None # Subclasses must define this.
- def _format_value(self, value):
- if self.is_localized:
- return formats.localize_input(value)
- return value
- def render(self, name, value, attrs=None):
- if value is None:
- value = ''
- final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
- if value != '':
- # Only add the 'value' attribute if a value is non-empty.
- final_attrs['value'] = force_text(self._format_value(value))
- return format_html('<input{0} />', flatatt(final_attrs))
- class TextInput(Input):
- input_type = 'text'
- def __init__(self, attrs=None):
- if attrs is not None:
- self.input_type = attrs.pop('type', self.input_type)
- super(TextInput, self).__init__(attrs)
- class NumberInput(TextInput):
- input_type = 'number'
- class EmailInput(TextInput):
- input_type = 'email'
- class URLInput(TextInput):
- input_type = 'url'
- class PasswordInput(TextInput):
- input_type = 'password'
- def __init__(self, attrs=None, render_value=False):
- super(PasswordInput, self).__init__(attrs)
- self.render_value = render_value
- def render(self, name, value, attrs=None):
- if not self.render_value:
- value = None
- return super(PasswordInput, self).render(name, value, attrs)
- class HiddenInput(Input):
- input_type = 'hidden'
- class MultipleHiddenInput(HiddenInput):
- """
- A widget that handles <input type="hidden"> for fields that have a list
- of values.
- """
- def __init__(self, attrs=None, choices=()):
- super(MultipleHiddenInput, self).__init__(attrs)
- # choices can be any iterable
- self.choices = choices
- def render(self, name, value, attrs=None, choices=()):
- if value is None:
- value = []
- final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
- id_ = final_attrs.get('id', None)
- inputs = []
- for i, v in enumerate(value):
- input_attrs = dict(value=force_text(v), **final_attrs)
- if id_:
- # An ID attribute was given. Add a numeric index as a suffix
- # so that the inputs don't all have the same ID attribute.
- input_attrs['id'] = '%s_%s' % (id_, i)
- inputs.append(format_html('<input{0} />', flatatt(input_attrs)))
- return mark_safe('\n'.join(inputs))
- def value_from_datadict(self, data, files, name):
- if isinstance(data, (MultiValueDict, MergeDict)):
- return data.getlist(name)
- return data.get(name, None)
- class FileInput(Input):
- input_type = 'file'
- needs_multipart_form = True
- def render(self, name, value, attrs=None):
- return super(FileInput, self).render(name, None, attrs=attrs)
- def value_from_datadict(self, data, files, name):
- "File widgets take data from FILES, not POST"
- return files.get(name, None)
- FILE_INPUT_CONTRADICTION = object()
- class ClearableFileInput(FileInput):
- initial_text = ugettext_lazy('Currently')
- input_text = ugettext_lazy('Change')
- clear_checkbox_label = ugettext_lazy('Clear')
- template_with_initial = '%(initial_text)s: %(initial)s %(clear_template)s<br />%(input_text)s: %(input)s'
- template_with_clear = '%(clear)s <label for="%(clear_checkbox_id)s">%(clear_checkbox_label)s</label>'
- url_markup_template = '<a href="{0}">{1}</a>'
- def clear_checkbox_name(self, name):
- """
- Given the name of the file input, return the name of the clear checkbox
- input.
- """
- return name + '-clear'
- def clear_checkbox_id(self, name):
- """
- Given the name of the clear checkbox input, return the HTML id for it.
- """
- return name + '_id'
- def render(self, name, value, attrs=None):
- substitutions = {
- 'initial_text': self.initial_text,
- 'input_text': self.input_text,
- 'clear_template': '',
- 'clear_checkbox_label': self.clear_checkbox_label,
- }
- template = '%(input)s'
- substitutions['input'] = super(ClearableFileInput, self).render(name, value, attrs)
- if value and hasattr(value, "url"):
- template = self.template_with_initial
- substitutions['initial'] = format_html(self.url_markup_template,
- value.url,
- force_text(value))
- if not self.is_required:
- checkbox_name = self.clear_checkbox_name(name)
- checkbox_id = self.clear_checkbox_id(checkbox_name)
- substitutions['clear_checkbox_name'] = conditional_escape(checkbox_name)
- substitutions['clear_checkbox_id'] = conditional_escape(checkbox_id)
- substitutions['clear'] = CheckboxInput().render(checkbox_name, False, attrs={'id': checkbox_id})
- substitutions['clear_template'] = self.template_with_clear % substitutions
- return mark_safe(template % substitutions)
- def value_from_datadict(self, data, files, name):
- upload = super(ClearableFileInput, self).value_from_datadict(data, files, name)
- if not self.is_required and CheckboxInput().value_from_datadict(
- data, files, self.clear_checkbox_name(name)):
- if upload:
- # If the user contradicts themselves (uploads a new file AND
- # checks the "clear" checkbox), we return a unique marker
- # object that FileField will turn into a ValidationError.
- return FILE_INPUT_CONTRADICTION
- # False signals to clear any existing value, as opposed to just None
- return False
- return upload
- class Textarea(Widget):
- def __init__(self, attrs=None):
- # Use slightly better defaults than HTML's 20x2 box
- default_attrs = {'cols': '40', 'rows': '10'}
- if attrs:
- default_attrs.update(attrs)
- super(Textarea, self).__init__(default_attrs)
- def render(self, name, value, attrs=None):
- if value is None:
- value = ''
- final_attrs = self.build_attrs(attrs, name=name)
- return format_html('<textarea{0}>\r\n{1}</textarea>',
- flatatt(final_attrs),
- force_text(value))
- class DateTimeBaseInput(TextInput):
- format_key = ''
- supports_microseconds = False
- def __init__(self, attrs=None, format=None):
- super(DateTimeBaseInput, self).__init__(attrs)
- self.format = format if format else None
- def _format_value(self, value):
- return formats.localize_input(value,
- self.format or formats.get_format(self.format_key)[0])
- class DateInput(DateTimeBaseInput):
- format_key = 'DATE_INPUT_FORMATS'
- class DateTimeInput(DateTimeBaseInput):
- format_key = 'DATETIME_INPUT_FORMATS'
- class TimeInput(DateTimeBaseInput):
- format_key = 'TIME_INPUT_FORMATS'
- # Defined at module level so that CheckboxInput is picklable (#17976)
- def boolean_check(v):
- return not (v is False or v is None or v == '')
- class CheckboxInput(Widget):
- def __init__(self, attrs=None, check_test=None):
- super(CheckboxInput, self).__init__(attrs)
- # check_test is a callable that takes a value and returns True
- # if the checkbox should be checked for that value.
- self.check_test = boolean_check if check_test is None else check_test
- def render(self, name, value, attrs=None):
- final_attrs = self.build_attrs(attrs, type='checkbox', name=name)
- if self.check_test(value):
- final_attrs['checked'] = 'checked'
- if not (value is True or value is False or value is None or value == ''):
- # Only add the 'value' attribute if a value is non-empty.
- final_attrs['value'] = force_text(value)
- return format_html('<input{0} />', flatatt(final_attrs))
- def value_from_datadict(self, data, files, name):
- if name not in data:
- # A missing value means False because HTML form submission does not
- # send results for unselected checkboxes.
- return False
- value = data.get(name)
- # Translate true and false strings to boolean values.
- values = {'true': True, 'false': False}
- if isinstance(value, six.string_types):
- value = values.get(value.lower(), value)
- return bool(value)
- class Select(Widget):
- allow_multiple_selected = False
- def __init__(self, attrs=None, choices=()):
- super(Select, self).__init__(attrs)
- # choices can be any iterable, but we may need to render this widget
- # multiple times. Thus, collapse it into a list so it can be consumed
- # more than once.
- self.choices = list(choices)
- def render(self, name, value, attrs=None, choices=()):
- if value is None:
- value = ''
- final_attrs = self.build_attrs(attrs, name=name)
- output = [format_html('<select{0}>', flatatt(final_attrs))]
- options = self.render_options(choices, [value])
- if options:
- output.append(options)
- output.append('</select>')
- return mark_safe('\n'.join(output))
- def render_option(self, selected_choices, option_value, option_label):
- if option_value is None:
- option_value = ''
- option_value = force_text(option_value)
- if option_value in selected_choices:
- selected_html = mark_safe(' selected="selected"')
- if not self.allow_multiple_selected:
- # Only allow for a single selection.
- selected_choices.remove(option_value)
- else:
- selected_html = ''
- return format_html('<option value="{0}"{1}>{2}</option>',
- option_value,
- selected_html,
- force_text(option_label))
- def render_options(self, choices, selected_choices):
- # Normalize to strings.
- selected_choices = set(force_text(v) for v in selected_choices)
- output = []
- for option_value, option_label in chain(self.choices, choices):
- if isinstance(option_label, (list, tuple)):
- output.append(format_html('<optgroup label="{0}">', force_text(option_value)))
- for option in option_label:
- output.append(self.render_option(selected_choices, *option))
- output.append('</optgroup>')
- else:
- output.append(self.render_option(selected_choices, option_value, option_label))
- return '\n'.join(output)
- class NullBooleanSelect(Select):
- """
- A Select Widget intended to be used with NullBooleanField.
- """
- def __init__(self, attrs=None):
- choices = (('1', ugettext_lazy('Unknown')),
- ('2', ugettext_lazy('Yes')),
- ('3', ugettext_lazy('No')))
- super(NullBooleanSelect, self).__init__(attrs, choices)
- def render(self, name, value, attrs=None, choices=()):
- try:
- value = {True: '2', False: '3', '2': '2', '3': '3'}[value]
- except KeyError:
- value = '1'
- return super(NullBooleanSelect, self).render(name, value, attrs, choices)
- def value_from_datadict(self, data, files, name):
- value = data.get(name, None)
- return {'2': True,
- True: True,
- 'True': True,
- '3': False,
- 'False': False,
- False: False}.get(value, None)
- class SelectMultiple(Select):
- allow_multiple_selected = True
- def render(self, name, value, attrs=None, choices=()):
- if value is None:
- value = []
- final_attrs = self.build_attrs(attrs, name=name)
- output = [format_html('<select multiple="multiple"{0}>', flatatt(final_attrs))]
- options = self.render_options(choices, value)
- if options:
- output.append(options)
- output.append('</select>')
- return mark_safe('\n'.join(output))
- def value_from_datadict(self, data, files, name):
- if isinstance(data, (MultiValueDict, MergeDict)):
- return data.getlist(name)
- return data.get(name, None)
- @python_2_unicode_compatible
- class ChoiceInput(SubWidget):
- """
- An object used by ChoiceFieldRenderer that represents a single
- <input type='$input_type'>.
- """
- input_type = None # Subclasses must define this
- def __init__(self, name, value, attrs, choice, index):
- self.name = name
- self.value = value
- self.attrs = attrs
- self.choice_value = force_text(choice[0])
- self.choice_label = force_text(choice[1])
- self.index = index
- if 'id' in self.attrs:
- self.attrs['id'] += "_%d" % self.index
- def __str__(self):
- return self.render()
- def render(self, name=None, value=None, attrs=None, choices=()):
- if self.id_for_label:
- label_for = format_html(' for="{0}"', self.id_for_label)
- else:
- label_for = ''
- return format_html('<label{0}>{1} {2}</label>', label_for, self.tag(), self.choice_label)
- def is_checked(self):
- return self.value == self.choice_value
- def tag(self):
- final_attrs = dict(self.attrs, type=self.input_type, name=self.name, value=self.choice_value)
- if self.is_checked():
- final_attrs['checked'] = 'checked'
- return format_html('<input{0} />', flatatt(final_attrs))
- @property
- def id_for_label(self):
- return self.attrs.get('id', '')
- class RadioChoiceInput(ChoiceInput):
- input_type = 'radio'
- def __init__(self, *args, **kwargs):
- super(RadioChoiceInput, self).__init__(*args, **kwargs)
- self.value = force_text(self.value)
- class RadioInput(RadioChoiceInput):
- def __init__(self, *args, **kwargs):
- msg = "RadioInput has been deprecated. Use RadioChoiceInput instead."
- warnings.warn(msg, RemovedInDjango18Warning, stacklevel=2)
- super(RadioInput, self).__init__(*args, **kwargs)
- class CheckboxChoiceInput(ChoiceInput):
- input_type = 'checkbox'
- def __init__(self, *args, **kwargs):
- super(CheckboxChoiceInput, self).__init__(*args, **kwargs)
- self.value = set(force_text(v) for v in self.value)
- def is_checked(self):
- return self.choice_value in self.value
- @python_2_unicode_compatible
- class ChoiceFieldRenderer(object):
- """
- An object used by RadioSelect to enable customization of radio widgets.
- """
- choice_input_class = None
- def __init__(self, name, value, attrs, choices):
- self.name = name
- self.value = value
- self.attrs = attrs
- self.choices = choices
- def __getitem__(self, idx):
- choice = self.choices[idx] # Let the IndexError propagate
- return self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, idx)
- def __str__(self):
- return self.render()
- def render(self):
- """
- Outputs a <ul> for this set of choice fields.
- If an id was given to the field, it is applied to the <ul> (each
- item in the list will get an id of `$id_$i`).
- """
- id_ = self.attrs.get('id', None)
- start_tag = format_html('<ul id="{0}">', id_) if id_ else '<ul>'
- output = [start_tag]
- for i, choice in enumerate(self.choices):
- choice_value, choice_label = choice
- if isinstance(choice_label, (tuple, list)):
- attrs_plus = self.attrs.copy()
- if id_:
- attrs_plus['id'] += '_{0}'.format(i)
- sub_ul_renderer = ChoiceFieldRenderer(name=self.name,
- value=self.value,
- attrs=attrs_plus,
- choices=choice_label)
- sub_ul_renderer.choice_input_class = self.choice_input_class
- output.append(format_html('<li>{0}{1}</li>', choice_value,
- sub_ul_renderer.render()))
- else:
- w = self.choice_input_class(self.name, self.value,
- self.attrs.copy(), choice, i)
- output.append(format_html('<li>{0}</li>', force_text(w)))
- output.append('</ul>')
- return mark_safe('\n'.join(output))
- class RadioFieldRenderer(ChoiceFieldRenderer):
- choice_input_class = RadioChoiceInput
- class CheckboxFieldRenderer(ChoiceFieldRenderer):
- choice_input_class = CheckboxChoiceInput
- class RendererMixin(object):
- renderer = None # subclasses must define this
- _empty_value = None
- def __init__(self, *args, **kwargs):
- # Override the default renderer if we were passed one.
- renderer = kwargs.pop('renderer', None)
- if renderer:
- self.renderer = renderer
- super(RendererMixin, self).__init__(*args, **kwargs)
- def subwidgets(self, name, value, attrs=None, choices=()):
- for widget in self.get_renderer(name, value, attrs, choices):
- yield widget
- def get_renderer(self, name, value, attrs=None, choices=()):
- """Returns an instance of the renderer."""
- if value is None:
- value = self._empty_value
- final_attrs = self.build_attrs(attrs)
- choices = list(chain(self.choices, choices))
- return self.renderer(name, value, final_attrs, choices)
- def render(self, name, value, attrs=None, choices=()):
- return self.get_renderer(name, value, attrs, choices).render()
- def id_for_label(self, id_):
- # Widgets using this RendererMixin are made of a collection of
- # subwidgets, each with their own <label>, and distinct ID.
- # The IDs are made distinct by y "_X" suffix, where X is the zero-based
- # index of the choice field. Thus, the label for the main widget should
- # reference the first subwidget, hence the "_0" suffix.
- if id_:
- id_ += '_0'
- return id_
- class RadioSelect(RendererMixin, Select):
- renderer = RadioFieldRenderer
- _empty_value = ''
- class CheckboxSelectMultiple(RendererMixin, SelectMultiple):
- renderer = CheckboxFieldRenderer
- _empty_value = []
- class MultiWidget(Widget):
- """
- A widget that is composed of multiple widgets.
- Its render() method is different than other widgets', because it has to
- figure out how to split a single value for display in multiple widgets.
- The ``value`` argument can be one of two things:
- * A list.
- * A normal value (e.g., a string) that has been "compressed" from
- a list of values.
- In the second case -- i.e., if the value is NOT a list -- render() will
- first "decompress" the value into a list before rendering it. It does so by
- calling the decompress() method, which MultiWidget subclasses must
- implement. This method takes a single "compressed" value and returns a
- list.
- When render() does its HTML rendering, each value in the list is rendered
- with the corresponding widget -- the first value is rendered in the first
- widget, the second value is rendered in the second widget, etc.
- Subclasses may implement format_output(), which takes the list of rendered
- widgets and returns a string of HTML that formats them any way you'd like.
- You'll probably want to use this class with MultiValueField.
- """
- def __init__(self, widgets, attrs=None):
- self.widgets = [w() if isinstance(w, type) else w for w in widgets]
- super(MultiWidget, self).__init__(attrs)
- @property
- def is_hidden(self):
- return all(w.is_hidden for w in self.widgets)
- def render(self, name, value, attrs=None):
- if self.is_localized:
- for widget in self.widgets:
- widget.is_localized = self.is_localized
- # value is a list of values, each corresponding to a widget
- # in self.widgets.
- if not isinstance(value, list):
- value = self.decompress(value)
- output = []
- final_attrs = self.build_attrs(attrs)
- id_ = final_attrs.get('id', None)
- for i, widget in enumerate(self.widgets):
- try:
- widget_value = value[i]
- except IndexError:
- widget_value = None
- if id_:
- final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
- output.append(widget.render(name + '_%s' % i, widget_value, final_attrs))
- return mark_safe(self.format_output(output))
- def id_for_label(self, id_):
- # See the comment for RadioSelect.id_for_label()
- if id_:
- id_ += '_0'
- return id_
- def value_from_datadict(self, data, files, name):
- return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
- def format_output(self, rendered_widgets):
- """
- Given a list of rendered widgets (as strings), returns a Unicode string
- representing the HTML for the whole lot.
- This hook allows you to format the HTML design of the widgets, if
- needed.
- """
- return ''.join(rendered_widgets)
- def decompress(self, value):
- """
- Returns a list of decompressed values for the given compressed value.
- The given value can be assumed to be valid, but not necessarily
- non-empty.
- """
- raise NotImplementedError('Subclasses must implement this method.')
- def _get_media(self):
- "Media for a multiwidget is the combination of all media of the subwidgets"
- media = Media()
- for w in self.widgets:
- media = media + w.media
- return media
- media = property(_get_media)
- def __deepcopy__(self, memo):
- obj = super(MultiWidget, self).__deepcopy__(memo)
- obj.widgets = copy.deepcopy(self.widgets)
- return obj
- @property
- def needs_multipart_form(self):
- return any(w.needs_multipart_form for w in self.widgets)
- class SplitDateTimeWidget(MultiWidget):
- """
- A Widget that splits datetime input into two <input type="text"> boxes.
- """
- supports_microseconds = False
- def __init__(self, attrs=None, date_format=None, time_format=None):
- widgets = (DateInput(attrs=attrs, format=date_format),
- TimeInput(attrs=attrs, format=time_format))
- super(SplitDateTimeWidget, self).__init__(widgets, attrs)
- def decompress(self, value):
- if value:
- value = to_current_timezone(value)
- return [value.date(), value.time().replace(microsecond=0)]
- return [None, None]
- class SplitHiddenDateTimeWidget(SplitDateTimeWidget):
- """
- A Widget that splits datetime input into two <input type="hidden"> inputs.
- """
- def __init__(self, attrs=None, date_format=None, time_format=None):
- super(SplitHiddenDateTimeWidget, self).__init__(attrs, date_format, time_format)
- for widget in self.widgets:
- widget.input_type = 'hidden'
|