pallets-eco / wtforms

A flexible forms validation and rendering library for Python.
https://wtforms.readthedocs.io
BSD 3-Clause "New" or "Revised" License
1.51k stars 395 forks source link

Optional doesn't support multivalued fields #835

Open markhobson opened 8 months ago

markhobson commented 8 months ago

The Optional validator is used to short-circuit validation to allow empty field values. This works for single-valued fields but not for multivalued fields. For example, when DateField is used with separate day, month and year controls.

Actual Behavior

>>> import wtforms
>>> from werkzeug.datastructures import MultiDict
>>> class F(wtforms.Form):
...     foo = wtforms.DateField(format="%d %m %Y", validators=[wtforms.validators.Optional()])
>>> f = F(formdata=MultiDict([("foo", ""), ("foo", "3"), ("foo", "2007")]))
>>> print(f.validate())
True

Expected Behavior

>>> import wtforms
>>> from werkzeug.datastructures import MultiDict
>>> class F(wtforms.Form):
...     foo = wtforms.DateField(format="%d %m %Y", validators=[wtforms.validators.Optional()])
>>> f = F(formdata=MultiDict([("foo", ""), ("foo", "3"), ("foo", "2007")]))
>>> print(f.validate())
False
>>> print(f.foo.errors)
['Not a valid date value.']

This issue is due to Optional only considering the first raw data value.

A custom validator can be used to work around this issue:

class MultivalueOptional(Optional):
    """
    A validator that allows empty input and supports multivalued fields.
    """

    def __call__(self, form: BaseForm, field: Field) -> None:
        if not field.raw_data or all(self._is_empty(value) for value in field.raw_data):
            field.errors = []
            raise StopValidation()

    def _is_empty(self, value: Any) -> bool:
        return isinstance(value, str) and not self.string_check(value)

Although this functionality could easily be accommodated by the library's Optional validator.

Environment

markhobson commented 8 months ago

A similar issue exists for InputRequired. Here's a workaround for that:

class MultivalueInputRequired(InputRequired):
    """
    A validator that ensures input was provided and supports multivalued fields.
    """

    def __call__(self, form: BaseForm, field: Field) -> None:
        if field.raw_data and all(field.raw_data):
            return

        if self.message is None:
            message = field.gettext("This field is required.")
        else:
            message = self.message

        field.errors = []
        raise StopValidation(message)
Daverball commented 5 months ago

This is a general design flaw with WTForms. There's many other ways to break Optional/InputRequired/DataRequired.

The best way to clean this up, would be to change the API in 4.0 and expect individual Field classes to deal with the implementation details of what it means for that field to be optional/required.