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