codekingpro/portable-devtools
114k
1import itertools2 3from wtforms import widgets4from wtforms.fields.core import Field5from wtforms.validators import ValidationError6 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 to21 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_widget29 30 def iter_choices(self):31 """32 Provides data for choice widget rendering. Must return a sequence or33 iterable of (value, label, selected, render_kw) tuples.34 """35 raise NotImplementedError()36 37 def has_groups(self):38 return False39 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 = choice55 else:56 value, label, checked = choice57 render_kw = {}58 59 opt = self._Option(60 label=label, id="%s-%d" % (self.id, i), **opts, **render_kw61 )62 opt.process(None, value)63 opt.checked = checked64 yield opt65 66 class _Option(Field):67 checked = False68 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 = coerce87 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 = None93 self.validate_choice = validate_choice94 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.choices102 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 = choices119 120 else:121 _choices = zip(choices, choices)122 123 for value, label, *other_args in _choices:124 selected = self.coerce(value) == self.data125 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 value131 self.data = self.coerce(value) if value is not None else None132 except (ValueError, TypeError):133 self.data = None134 135 def process_formdata(self, valuelist):136 if not valuelist:137 return138 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 exc143 144 def pre_validate(self, form):145 if not self.validate_choice:146 return147 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 break154 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 (and161 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 = choices173 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.data179 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 = None187 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 exc197 198 def pre_validate(self, form):199 if not self.validate_choice or not self.data:200 return201 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 acceptable209 ]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 as225 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()230 