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

WTForms needs an `Exists` validator #846

Closed jb2170 closed 4 months ago

jb2170 commented 4 months ago

tldr: a validator for a non-optional (required) field that is allowed to be the empty string

An Exists validator class would check merely for the presence of the field name in the form, to make sure that a user has not forgotten the field exists, or has submitted data under the wrong field name.

This is useful say for a field such as a picture description, which may be allowed to be an empty string, but we want to make sure that the user did indeed send an empty string, and that a non-empty string has not gone walkabout under the wrong field name of desc, or the user forgot that a description field existed in the first place, which they would've liked to have filled in.

This is particularly useful eg in building a JSON API which doesn't display a HTML form directly to the user, only validating the received form on the server side with WTForms.

Observe that Exists would fill in a gap in the truth table below, with validators increasing in order of permissibility.

URL encoded form data DataRequired InputRequired Exists Optional remarks
'decsc=' F F F T Optional is too permissive
'description=' F F T T InputRequired is too strict
'description=%20' F T T T
'description=foo' T T T T

Implementation

No-frills implementation:

from wtforms.validators import StopValidation

class Exists:
    def __call__(self, form, field):
        if field.data is None:
            raise StopValidation('This field must exist.')

Observe that we use is None, not just a loose false-y check. This the is essential difference from InputRequired.

Full implementation:

from wtforms.validators import StopValidation

class Exists:
    def __init__(self, message=None):
        self.message = message

    def __call__(self, form, field):
        if field.data is None:
            if self.message is None:
                message = field.gettext('This field must exist.')
            else:
                message = self.message

            field.errors[:] = []
            raise StopValidation(message)
jb2170 commented 4 months ago

Can be achieved with validators = [..., NoneOf([None]), ...]

It's been a while since I've done Flask :smile:

But nonetheless charting out that table of permissibility helped me see the order of validation strictness :+1: