How to make a field conditionally optional in WTForms?
Asked Answered
S

4

38

My form validation is working nearly complete, I just have 2 cases I don't know exactly how to solve: 1) The password field should be required of course but I also provide the possibility to log in with google or facebook account via OAuth and then name gets prefilled but I remove the password field completely from the form is there is a user (google) or a facebook user object:

<tr><td>
  <br />        {% if user or current_user %}    {% else %} 

  <div class="labelform">
     {% filter capitalize %}{% trans %}password{% endtrans %}{% endfilter %}:
  </div>
      </td><td>  <div class="adinput">{{ form.password|safe }}{% trans %}Choose a password{% endtrans %}</div>{% endif %}

  </td></tr>

So for these users who already are logged in and the password field has no meaning, I need some logic to make that field conditionally optional. I was thinking that I could have a variable for logged_in + a method in my form class such as this:

class AdForm(Form):
    logged_in = False
    my_choices = [('1', _('VEHICLES')), ('2', _('Cars')), ('3', _('Bicycles'))]
    name = TextField(_('Name'), [validators.Required(message=_('Name is required'))], widget=MyTextInput())
    title = TextField(_('title'), [validators.Required(message=_('Subject is required'))], widget=MyTextInput())
    text = TextAreaField(_('Text'),[validators.Required(message=_('Text is required'))], widget=MyTextArea())
    phonenumber = TextField(_('Phone number'))
    phoneview = BooleanField(_('Display phone number on site'))
    price = TextField(_('Price'),[validators.Regexp('\d', message=_('This is not an integer number, please see the example and try again')),validators.Optional()] )
    password = PasswordField(_('Password'),[validators.Optional()], widget=PasswordInput())
    email = TextField(_('Email'), [validators.Required(message=_('Email is required')), validators.Email(message=_('Your email is invalid'))], widget=MyTextInput())
    category = SelectField(choices = my_choices, default = '1')

    def validate_name(form, field):
        if len(field.data) > 50:
            raise ValidationError(_('Name must be less than 50 characters'))

    def validate_email(form, field):
        if len(field.data) > 60:
            raise ValidationError(_('Email must be less than 60 characters'))

    def validate_price(form, field):
        if len(field.data) > 8:
            raise ValidationError(_('Price must be less than 9 integers'))

    def validate_password(form, field):
        if not logged_in and not field:
            raise ValidationError(_('Password is required'))

Will the above validate_password work to achieve the desired effect? Is there another better way? Another way I could think is to have 2 different form class and in http post I instanciate the form class it should be:

def post(self):
    if not current_user:
      form = AdForm(self.request.params)
    if current_user:
      form = AdUserForm(self.request.params)

I also need conditional validation for the category field, when a certain category is selected then more choices appear and these should have validation only for a certain base-category eg. user selects "Car" and then via Ajax can choose registration data and mileage for the car and these fields are required given that the category Car was selected.

So it might be two questions but both cases relate to how I can make a field "conditionally optional" or "conditionally required".

My form looks like this

enter image description here

And for a logged in user I prefill the name and email address and the pasword field is simply not used, so the password field neither fits being "optional" nor "required", it would need something like "conditionally optional" or "conditionally required."

enter image description here

Thanks for any answer or comment

Swanson answered 11/12, 2011 at 10:8 Comment(0)
S
78

I'm not sure this quite fits your needs, but I've used a RequiredIf custom validator on fields before, which makes a field required if another field has a value in the form... for instance, in a datetime-and-timezone scenario, I can make the timezone field required to have a value if the user has entered a datetime.

Updated to use "InputRequired" instead of "Required"

class RequiredIf(InputRequired):
    # a validator which makes a field required if
    # another field is set and has a truthy value

    def __init__(self, other_field_name, *args, **kwargs):
        self.other_field_name = other_field_name
        super(RequiredIf, self).__init__(*args, **kwargs)

    def __call__(self, form, field):
        other_field = form._fields.get(self.other_field_name)
        if other_field is None:
            raise Exception('no field named "%s" in form' % self.other_field_name)
        if bool(other_field.data):
            super(RequiredIf, self).__call__(form, field)

The constructor takes the name of the other field that triggers making this field required, like:

class DateTimeForm(Form):
    datetime = TextField()
    timezone = SelectField(choices=..., validators=[RequiredIf('datetime')])

This could be a good starting point for implementing the sort of logic you need.

Sigfried answered 11/12, 2011 at 13:56 Comment(4)
@Mehdi: meta.stackexchange.com/questions/12527/…Eldwon
Awesome³! Works really well!Janel
any idea what to do if the field that contains the condition is in a parent Form of the current Form? I have fields of a child-form inside a FormField of a parent-form that are conditional with respect to a field in the parent-form.Trometer
You need to add self.field_flags = {} at the end of __init__(). Otherwise, the field is rendered with a required attribute, which you don't want to have.Dinghy
I
9

I found this question helpful and based on the answer of @dcrosta I created another validator which is optional. The benefit is that you can combine it with other wtforms validators. Here is my optional validator which checks another field. Because I needed to check the value of the other field against some certain value I added a custom check for value:

class OptionalIfFieldEqualTo(wtf.validators.Optional):
    # a validator which makes a field optional if
    # another field has a desired value

    def __init__(self, other_field_name, value, *args, **kwargs):
        self.other_field_name = other_field_name
        self.value = value
        super(OptionalIfFieldEqualTo, self).__init__(*args, **kwargs)

    def __call__(self, form, field):
        other_field = form._fields.get(self.other_field_name)
        if other_field is None:
            raise Exception('no field named "%s" in form' % self.other_field_name)
        if other_field.data == self.value:
            super(OptionalIfFieldEqualTo, self).__call__(form, field)
Intercalate answered 20/8, 2014 at 10:22 Comment(0)
L
7

The answer from @dcrosta is great, but I think some things have changed in wtforms since this answer. Inheriting from DataRequired adds a required attribute to the form field, so the conditional validator never gets called. I made a minor change to the class from @dcrosta that works with wtforms 2.1. This only over-rides field_flags so that browser validation is not done.

from wtforms.validators import DataRequired


class RequiredIf(DataRequired):
    """Validator which makes a field required if another field is set and has a truthy value.

    Sources:
        - http://wtforms.simplecodes.com/docs/1.0.1/validators.html
        - https://mcmap.net/q/404215/-how-to-make-a-field-conditionally-optional-in-wtforms

    """
    field_flags = ('requiredif',)

    def __init__(self, other_field_name, message=None, *args, **kwargs):
        self.other_field_name = other_field_name
        self.message = message

    def __call__(self, form, field):
        other_field = form[self.other_field_name]
        if other_field is None:
            raise Exception('no field named "%s" in form' % self.other_field_name)
        if bool(other_field.data):
            super(RequiredIf, self).__call__(form, field)

A more ideal solution would manage to do the validation in the browser, like the current behavior of DataRequired.

Ladykiller answered 18/5, 2017 at 1:14 Comment(3)
It seems that this method broke in a recent update. All my conditional fields are rendered required. I don't know how to debug it properly. Any tips?Trometer
This works, but instead of making the field required, it causes an internal server error. Any workaround?Charissecharita
You can get rid of the required attribute by setting kwarg required=True inside your jinja2 template. For example: form.my_field(required=False). I used that in combination with a slight update to @drcosta answer (changed Required to InputRequired) and things are working as I would expect.Ashelman
A
2

The answer from @dennisobrien is useful, but I couldn't get it to work. I ended up with these modified versions, which do work for me (using WTForms==2.2.1):

from wtforms.validators import DataRequired, Optional


class RequiredIf:
    """
    Validator which makes a field required if another field is set and has a
    truthy value.
    Sources:
        - https://wtforms.readthedocs.io/en/2.3.x/validators/
        - https://mcmap.net/q/404215/-how-to-make-a-field-conditionally-optional-in-wtforms
    """

    field_flags = ("requiredif",)

    def __init__(self, other_field_name, message=None):
        self.other_field_name = other_field_name
        self.message = message

    def __call__(self, form, field):
        other_field = form[self.other_field_name]
        if other_field is None:
            raise Exception('no field named "%s" in form' % self.other_field_name)
        if bool(other_field.data):
            DataRequired(self.message).__call__(form, field)
        else:
            Optional(self.message).__call__(form, field)


class OptionalIf:
    """
    Validator which makes a field optional if another field is set and has a falsy value.
    See sources from RequiredIf
    """

    field_flags = ("optionalif",)

    def __init__(self, other_field_name, message=None):
        self.other_field_name = other_field_name
        self.message = message

    def __call__(self, form, field):
        other_field = form[self.other_field_name]
        if other_field is None:
            raise Exception('no field named "%s" in form' % self.other_field_name)
        if bool(other_field.data):
            Optional(self.message).__call__(form, field)
        else:
            DataRequired(self.message).__call__(form, field)
Aboard answered 3/3, 2023 at 6:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.