choices.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import itertools
  2. from wtforms import widgets
  3. from wtforms.fields.core import Field
  4. from wtforms.validators import ValidationError
  5. __all__ = (
  6. "SelectField",
  7. "SelectMultipleField",
  8. "RadioField",
  9. )
  10. class SelectFieldBase(Field):
  11. option_widget = widgets.Option()
  12. """
  13. Base class for fields which can be iterated to produce options.
  14. This isn't a field, but an abstract base class for fields which want to
  15. provide this functionality.
  16. """
  17. def __init__(self, label=None, validators=None, option_widget=None, **kwargs):
  18. super().__init__(label, validators, **kwargs)
  19. if option_widget is not None:
  20. self.option_widget = option_widget
  21. def iter_choices(self):
  22. """
  23. Provides data for choice widget rendering. Must return a sequence or
  24. iterable of (value, label, selected) tuples.
  25. """
  26. raise NotImplementedError()
  27. def has_groups(self):
  28. return False
  29. def iter_groups(self):
  30. raise NotImplementedError()
  31. def __iter__(self):
  32. opts = dict(
  33. widget=self.option_widget,
  34. validators=self.validators,
  35. name=self.name,
  36. render_kw=self.render_kw,
  37. _form=None,
  38. _meta=self.meta,
  39. )
  40. for i, (value, label, checked, render_kw) in enumerate(self.iter_choices()):
  41. opt = self._Option(
  42. label=label, id="%s-%d" % (self.id, i), **opts, **render_kw
  43. )
  44. opt.process(None, value)
  45. opt.checked = checked
  46. yield opt
  47. class _Option(Field):
  48. checked = False
  49. def _value(self):
  50. return str(self.data)
  51. class SelectField(SelectFieldBase):
  52. widget = widgets.Select()
  53. def __init__(
  54. self,
  55. label=None,
  56. validators=None,
  57. coerce=str,
  58. choices=None,
  59. validate_choice=True,
  60. **kwargs,
  61. ):
  62. super().__init__(label, validators, **kwargs)
  63. self.coerce = coerce
  64. if callable(choices):
  65. choices = choices()
  66. if choices is not None:
  67. self.choices = choices if isinstance(choices, dict) else list(choices)
  68. else:
  69. self.choices = None
  70. self.validate_choice = validate_choice
  71. def iter_choices(self):
  72. if not self.choices:
  73. choices = []
  74. elif isinstance(self.choices, dict):
  75. choices = list(itertools.chain.from_iterable(self.choices.values()))
  76. else:
  77. choices = self.choices
  78. return self._choices_generator(choices)
  79. def has_groups(self):
  80. return isinstance(self.choices, dict)
  81. def iter_groups(self):
  82. if isinstance(self.choices, dict):
  83. for label, choices in self.choices.items():
  84. yield (label, self._choices_generator(choices))
  85. def _choices_generator(self, choices):
  86. if not choices:
  87. _choices = []
  88. elif isinstance(choices[0], (list, tuple)):
  89. _choices = choices
  90. else:
  91. _choices = zip(choices, choices)
  92. for value, label, *other_args in _choices:
  93. render_kw = other_args[0] if len(other_args) else {}
  94. yield (value, label, self.coerce(value) == self.data, render_kw)
  95. def process_data(self, value):
  96. try:
  97. # If value is None, don't coerce to a value
  98. self.data = self.coerce(value) if value is not None else None
  99. except (ValueError, TypeError):
  100. self.data = None
  101. def process_formdata(self, valuelist):
  102. if not valuelist:
  103. return
  104. try:
  105. self.data = self.coerce(valuelist[0])
  106. except ValueError as exc:
  107. raise ValueError(self.gettext("Invalid Choice: could not coerce.")) from exc
  108. def pre_validate(self, form):
  109. if self.choices is None:
  110. raise TypeError(self.gettext("Choices cannot be None."))
  111. if not self.validate_choice:
  112. return
  113. for _, _, match, _ in self.iter_choices():
  114. if match:
  115. break
  116. else:
  117. raise ValidationError(self.gettext("Not a valid choice."))
  118. class SelectMultipleField(SelectField):
  119. """
  120. No different from a normal select field, except this one can take (and
  121. validate) multiple choices. You'll need to specify the HTML `size`
  122. attribute to the select field when rendering.
  123. """
  124. widget = widgets.Select(multiple=True)
  125. def _choices_generator(self, choices):
  126. if choices:
  127. if isinstance(choices[0], (list, tuple)):
  128. _choices = choices
  129. else:
  130. _choices = zip(choices, choices)
  131. else:
  132. _choices = []
  133. for value, label, *args in _choices:
  134. selected = self.data is not None and self.coerce(value) in self.data
  135. render_kw = args[0] if len(args) else {}
  136. yield (value, label, selected, render_kw)
  137. def process_data(self, value):
  138. try:
  139. self.data = list(self.coerce(v) for v in value)
  140. except (ValueError, TypeError):
  141. self.data = None
  142. def process_formdata(self, valuelist):
  143. try:
  144. self.data = list(self.coerce(x) for x in valuelist)
  145. except ValueError as exc:
  146. raise ValueError(
  147. self.gettext(
  148. "Invalid choice(s): one or more data inputs could not be coerced."
  149. )
  150. ) from exc
  151. def pre_validate(self, form):
  152. if self.choices is None:
  153. raise TypeError(self.gettext("Choices cannot be None."))
  154. if not self.validate_choice or not self.data:
  155. return
  156. acceptable = {c[0] for c in self.iter_choices()}
  157. if any(d not in acceptable for d in self.data):
  158. unacceptable = [str(d) for d in set(self.data) - acceptable]
  159. raise ValidationError(
  160. self.ngettext(
  161. "'%(value)s' is not a valid choice for this field.",
  162. "'%(value)s' are not valid choices for this field.",
  163. len(unacceptable),
  164. )
  165. % dict(value="', '".join(unacceptable))
  166. )
  167. class RadioField(SelectField):
  168. """
  169. Like a SelectField, except displays a list of radio buttons.
  170. Iterating the field will produce subfields (each containing a label as
  171. well) in order to allow custom rendering of the individual radio fields.
  172. """
  173. widget = widgets.ListWidget(prefix_label=False)
  174. option_widget = widgets.RadioInput()