Search code examples
pythonflaskflask-wtforms

Flask WTForm: custom validation based on multiple form fields


I have been stuck on creating a validator for my Flask form, I have followed instructions from other questions/ chains, but none of them work.

Below is my form:

class RegisterForm(FlaskForm):
    artist = BooleanField("Are you an artist?", default=False)
    organiser = BooleanField("Do you organise events?", default=False)
    dj = BooleanField("Are you a DJ?", default=False)

    proof_photo = FileField(label="Upload proof photo (artists and/ or organisers, only)", description="Please upload a photo proof", validators=[FileAllowed(['jpg', 'png'], 'JPG or PNG only, please.'))
    proof = StringField("Evidence of being artist/ organiser", description="URL to your videos")
    submit = SubmitField("Sign me up!")

What I am trying to do is, if any of the three: artist, organiser or dj is ticked as True, the fields proof_photo and proof will become required.

I have tried below, but it's unfortunately not working, and I am not sure what I am doing wrong.

def authorisation_check(self):
    if not super(RegisterForm, self).validate():
        return False
    if (self.artist.data == True or self.organiser.data == True or self.dj.data == True) and  not (self.proof_photo.data and self.proof.data):
        msg = 'Please submit your proof'
        self.proof_photo.errors.append(msg)
        self.proof.errors.append(msg)
        return False
    return True

If anyone could help me out with this, that would be greatly appreciated :) Thanks


Solution

  • In the following example, I have written two custom validators that accept the names of the dependent fields. Depending on the input in the specified fields, the validation is executed and an error is thrown if necessary.

    from flask import (
        Flask, 
        render_template
    )
    from flask_wtf import FlaskForm
    from flask_wtf.file import FileField, FileAllowed, FileRequired
    from wtforms import BooleanField, StringField, SubmitField
    from wtforms.validators import DataRequired, StopValidation
    
    app = Flask(__name__)
    app.secret_key = 'your secret here'
    
    class DependentFileRequired(FileRequired):
        def __init__(self, fieldnames, message=None):
            super().__init__(message)
            self.fieldnames = fieldnames
            self.field_flags = {}
    
        def __call__(self, form, field):
            if any(form[name].data and \
                (not isinstance(form[name].data, str) or form[name].data.strip()) \
                for name in self.fieldnames
            ):
                super().__call__(form, field)
            else:
                raise StopValidation()
    
    class DependentDataRequired(DataRequired):
        def __init__(self, fieldnames, message=None):
            super().__init__(message)
            self.fieldnames = fieldnames
            self.field_flags = {}
    
        def __call__(self, form, field):
            if any(form[name].data and \
                (not isinstance(form[name].data, str) or form[name].data.strip()) \
                for name in self.fieldnames
            ):
                super().__call__(form, field)
            else:
                raise StopValidation()
    
    class RegisterForm(FlaskForm):
        artist = BooleanField("Are you an artist?", default=False)
        organiser = BooleanField("Do you organise events?", default=False)
        dj = BooleanField("Are you a DJ?", default=False)
    
        proof_photo = FileField(
            label="Upload proof photo (artists and/ or organisers, only)", 
            description="Please upload a photo proof", 
            validators=[
                DependentFileRequired(fieldnames=['artist', 'organiser', 'dj']), 
                FileAllowed(['jpg', 'png'], 'JPG or PNG only, please.')
        ])
        proof = StringField(
            "Evidence of being artist/ organiser", #
            description="URL to your videos", 
            validators=[
                DependentDataRequired(fieldnames=['artist', 'organiser', 'dj'])
            ]
        )
        submit = SubmitField("Sign me up!")
    
    
    @app.route('/register', methods=['GET', 'POST'])
    def register():
        form = RegisterForm()
        if form.validate_on_submit():
            print(form.data)
        return render_template('register.html', **locals())
    
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Register</title>
        <style>
            fieldset[disabled] {
                display: none;
            }
        </style>
    </head>
    <body>
        <form method="post" enctype="multipart/form-data">
            {{ form.hidden_tag() }}
            <div>
                {{ form.artist() }}
                {{ form.artist.label() }}
            </div>
            <div>
                {{ form.organiser() }}
                {{ form.organiser.label() }}
            </div>
            <div>
                {{ form.dj() }}
                {{ form.dj.label() }}
            </div>
            <fieldset name="proofset">
                <div>
                    {{ form.proof_photo.label() }}
                    {{ form.proof_photo() }}
                    {% if form.proof_photo.errors -%}
                    <ul>
                        {% for err in form.proof_photo.errors -%}
                        <li>{{ err }}</li>
                        {% endfor -%}
                    </ul>
                    {% endif -%}
                </div>
                <div>
                    {{ form.proof.label() }}
                    {{ form.proof() }}
                    {% if form.proof.errors -%}
                    <ul>
                        {% for err in form.proof.errors -%}
                        <li>{{ err }}</li>
                        {% endfor -%}
                    </ul>
                    {% endif -%}
                </div>
            </fieldset>
            {{ form.submit() }}
        </form>
        <script>
            (function() {
                const fieldids = ['#artist', '#organiser', '#dj'];
                const fields = Array.from(document.querySelectorAll(fieldids.join(',')));
                const fieldset = document.querySelector('fieldset[name="proofset"]');
                fieldset.disabled = !fields.some(field => field.checked);
                fields.forEach(field => {
                    field.addEventListener('change', () => {
                        fieldset.disabled = !fields.some(field => field.checked);
                    });
                });
    
            })();
        </script>
    </body>
    </html>