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()