Search code examples
pythongoogle-app-enginevalidationwtforms

How to make a field conditionally optional in WTForms?


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


Solution

  • 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.