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