Search code examples
pythonformsflaskflask-wtforms

How to use multiple forms on the same page with Python/Flask?


I am trying to use two Flask forms on the same page. The first form gathers infos to list availables databases, which are passed on as choices to the "SelectField" of the second form.

In my views.py, I define my Forms

class AvailableBDDForm(FlaskForm):
    host = StringField('host', 
    validators=\[DataRequired()\])
    port = StringField('port', 
    validators=\[DataRequired()\])
    user = StringField('user', 
    validators=\[DataRequired()\])
    password = StringField('password', validators=\[DataRequired()\])
    submitAvailableBDD = SubmitField('METTRE A JOUR LA LISTE')
    next = HiddenField()

class ControlerBDDForm(FlaskForm):
    database = SelectField('database', validators=\[DataRequired()\])
    submit = SubmitField('SE CONNECTER')
    next = HiddenField()

    def update_choices(self, databases):
        self.database.choices = databases

As well as my route


@app.route("/analye-bdd/", methods=("GET","POST",))
def analyse_bdd():
    availableForm = AvailableBDDForm()
    controlerForm = ControlerBDDForm()

    selected_options = []
    scroll=""
    
    bdd = BDD()
    available_databases = bdd.lister_bdd()
    
    try:
        if availableForm.validate_on_submit():
            print("first if")
            bdd.update_parameters(availableForm.host.data, availableForm.port.data, availableForm.user.data, availableForm.password.data)
            available_databases = bdd.lister_bdd()
            print(available_databases)
            controlerForm.update_choices(available_databases)
    
        if controlerForm.validate_on_submit():
            print("helloooo")
        
    
    except Exception as e:
        print(f"An error occurred: {e}")
    
    return render_template(
        "analyse-bdd.html",
        title="Analyze",
        AvailableBDDForm=availableForm,
        ControlerBDDForm=controlerForm,
        selected_options=selected_options, 
        scroll_to=scroll,
        host = bdd.host,
        port=bdd.port,
        database=bdd.database,
        user=bdd.user,
        password=bdd.password
    )

And here is what I put in my HTML page:


        <form method="POST" action="{{ url_for('analyse_bdd')}}">
            <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
            {{ AvailableBDDForm.host(size=30, value=host, class="texteInput")}}
            {{ AvailableBDDForm.port(size=10, value=port) }}
            {{ AvailableBDDForm.user(size=30, value=user) }}
            {{ AvailableBDDForm.password(size=30, value=password) }}
            {{ AvailableBDDForm.next }}
            <br>
            {{ AvailableBDDForm.submitAvailableBDD(class_="btn btn-primary") }}
        </form>
    
        <form method="POST" action="{{ url_for('analyse_bdd')}}">
            <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> 
    
            <label>Database</label>
            
                {{ ControlerBDDForm.database }}
                {{ ControlerBDDForm.submit(class_="btn btn-primary") }}
        </form>

When I click on the first submit button (linked to AvailableBDDForm), the terminal shows:

 first if ['db1', 'db2']

So it works. Alas, when I click on the second submit button (linked to ControlerBDDForm):

An error occurred: Choices cannot be None.

I get neither of my prints (first if or hellooooo), but I do get this error.

I really don't understand what's going on, plus the logic inside the try is all messed up, like I also previously tried


@app.route("/analye-bdd/", methods=("GET","POST",))
def analyse_bdd():
    availableForm = AvailableBDDForm()
    controlerForm = ControlerBDDForm()

    selected_options = []
    scroll=""
    
    bdd = BDD()
    available_databases = bdd.lister_bdd()
    
    try:

        availableFormBool = availableForm.validate_on_submit()
        controlerFormBool = controlerForm.validate_on_submit()
        print(availableFormBool, controlerFormBool)

        if availableForm.validate_on_submit():
            print("first if")
            bdd.update_parameters(availableForm.host.data, availableForm.port.data, availableForm.user.data, availableForm.password.data)
            available_databases = bdd.lister_bdd()
            print(available_databases)
            controlerForm.update_choices(available_databases)
    
            if controlerForm.validate_on_submit():
                print("helloooo")
        
    
    except Exception as e:
        print(f"An error occurred: {e}")
    
    return render_template(
        "analyse-bdd.html",
        title="Analyze",
        AvailableBDDForm=availableForm,
        ControlerBDDForm=controlerForm,
        selected_options=selected_options, 
        scroll_to=scroll,
        host = bdd.host,
        port=bdd.port,
        database=bdd.database,
        user=bdd.user,
        password=bdd.password
    )

To see which form was submitted when I clicked, but I just didn't get anything on the terminal, neither the result of

print(availableFormBool, controlerFormBool)

or the "first if" print, nothing...

Also, a similar post exists on stackoverflow (flask multiple forms on the same page) but my forms are already different in id, name and value, so it doesn't solve my problem.

Thanks in advance

EDIT : Original problem solved but I have another question

There is a list of checkboxes (input) on my HTML page, and I can access their content in my view with the following code :

`for field_name in bdd.analyze_options:
                if request.form.get(field_name):
                    selected_options.append(request.form.get(field_name))

            print("selected options", selected_options)

            analyze_results = bdd.affichage_analyse(selected_options)
            print(f"analyze results {analyze_results}")``

I placed it here :

@app.route("/analyze/", methods=("GET","POST",))
def analyze():
    bdd = BDD()
    form = AvailableBDDForm(request.form, data=session.get('avail_bdd', {}))
    selected_options = []
    analyze_results = {}
    scroll_to = ""

    if form.validate_on_submit():
        data = {k:v for k,v in form.data.items() if k in ('host', 'port', 'user', 'password',)}
        if session.get('avail_bdd', {}) != data:
            session['avail_bdd'] = data
            return redirect(url_for('analyze'))

    if 'avail_bdd' in session: 

        bdd.update_parameters(**session['avail_bdd'])
        form1 = ControlerBDDForm(request.form, data=session.get('ctrl_bdd', {}))
        form1.update_choices(bdd.lister_bdd())
        if form1.validate_on_submit():
            session['ctrl_bdd'] = {k:v for k,v in form1.data.items() if k in ('database',)}
            print(form1.database.data)

            scroll_to = "for-scroll-results"

            for field_name in bdd.analyze_options:
                if request.form.get(field_name):
                    selected_options.append(request.form.get(field_name))

            print("selected options", selected_options)

            analyze_results = bdd.affichage_analyse(selected_options)
            print(f"analyze results {analyze_results}")
            
            return redirect(url_for('analyze'))

    return render_template('analyze.html', **locals(), 
                           title="Analyze BDD")

I don't know why, selected_options only has content when I click on the submit button of AvailableBDDForm. But I would want this part of the code that concerns the checkboxes only when ControlerBDDForm is validated... I tried a variety of locations and alternatives to this code, but to no avail.


Solution

  • My example uses the session to temporarily save the form entries.

    If the valid data has changed when the first form was submitted compared to the data saved in the session, it is updated in the session. If this is not the case, the data from the session is used to fill out the second form. After the second form has been submitted, its entries can be output.

    To request checkboxes, you can add a SelectMultipleField to the respective form in which the widgets are adjusted. The options are added in the same way as with a SelectField. However, due to a bug, the data of the fields must be set manually so that they retain their status.

    from flask import session
    from wtforms.fields import SelectMultipleField
    from wtforms.widgets import ListWidget, CheckboxInput
    
    # ...
    
    class AvailableBDDForm(FlaskForm):
        host = StringField('Host', 
            validators=[DataRequired()])
        port = StringField('Port', 
            validators=[DataRequired()])
        user = StringField('User', 
            validators=[DataRequired()])
        password = StringField('Password', 
            validators=[DataRequired()])
        submit = SubmitField('METTRE A JOUR LA LISTE')
        next = HiddenField()
    
    class ControlerBDDForm(FlaskForm):
        database = SelectField('Database', validators=[DataRequired()])
        options = SelectMultipleField('Analyze Options', 
            option_widget=CheckboxInput(), 
            widget=ListWidget(prefix_label=False))
        submit = SubmitField('SE CONNECTER')
        next = HiddenField()
    
        def update_choices(self, databases):
            self.database.choices = databases
    
        def update_options(self, options, data=[]):
            self.options.choices = options
            if not self.is_submitted():
                self.options.data = data
    
    @app.route('/analyze', methods=['GET', 'POST'])
    def analyze():
        bdd = BDD()
        form = AvailableBDDForm(request.form, data=session.get('avail_bdd', {}))
    
        if form.validate_on_submit():
            data = {k:v for k,v in form.data.items() if k in ('host', 'port', 'user', 'password',)}
            if session.get('avail_bdd', {}) != data:
                session['avail_bdd'] = data
                return redirect(url_for('analyze'))
    
        if 'avail_bdd' in session: 
            bdd.update_parameters(**session['avail_bdd'])
            data = session.get('ctrl_bdd', {})
            form1 = ControlerBDDForm(request.form, data=data)
            form1.update_choices(bdd.lister_bdd()) 
            form1.update_options(bdd.analyze_options, data.get('options', []))
            if form1.validate_on_submit():
                session['ctrl_bdd'] = {k:v for k,v in form1.data.items() if k in ('database','options',)}
                print(form1.database.data)
                print(form1.options.data)
                return redirect(url_for('analyze'))
    
        return render_template('analyze.html', **locals())
    
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Analyze</title>
    </head>
    <body>
        <form method="POST">
            {{ form.hidden_tag() }}
            <div>
                {{ form.host.label() }}
                {{ form.host() }}
            </div>
            <div>
                {{ form.port.label() }}
                {{ form.port() }}
            </div>
            <div>
                {{ form.user.label() }}
                {{ form.user() }}
            </div>
            <div>
                {{ form.password.label() }}
                {{ form.password() }}
            </div>
            {{ form.submit() }}
        </form>
    
        {% if form1 -%}
        <form method="POST">
            {{ form1.hidden_tag() }}
            <div>
                {{ form1.database.label() }}
                {{ form1.database() }}
            </div>
            <div>
                {{ form1.options.label() }}
                {{ form1.options() }}
            </div>
            {{ form1.submit() }}
        </form>
        {% endif -%}
    
    </body>
    </html>
    

    If you want to use more than two forms, I recommend using a URL parameter that is incremented depending on the form submitted.

    @app.route('/analyze', methods=['GET', 'POST'])
    def analyze():
        bdd = BDD()
        form = AvailableBDDForm(request.form, data=session.get('avail_bdd', {}))
    
        step = request.args.get('step', 1, type=int)
     
        match step:
            case 1: 
                if form.validate_on_submit():
                    data = {k:v for k,v in form.data.items() if k in ('host', 'port', 'user', 'password',)}
                    if session.get('avail_bdd', {}) != data:
                        session['avail_bdd'] = data
                    return redirect(url_for('analyze', step=step+1))
            case 2:
                if 'avail_bdd' in session: 
                    bdd.update_parameters(**session['avail_bdd'])
                    data = session.get('ctrl_bdd', {})
                    form1 = ControlerBDDForm(request.form, data=data)
                    form1.update_choices(bdd.lister_bdd()) 
                    form1.update_options(bdd.analyze_options, data.get('options', []))
                    if form1.validate_on_submit():
                        session['ctrl_bdd'] = {k:v for k,v in form1.data.items() if k in ('database',)}
                        print(form1.database.data)
                        print(form1.options.data)
    
                        # possibly here forward
                        # return redirect(url_for('analyze_1', step=step+1))
    
            # ...
    
            case _:
                pass
    
        return render_template('analyze.html', **locals())
    
    <form method="POST" action="{{ url_for('analyze', step=1) }}">
        {# ... #}
    </form>
    
    {% if form1 -%}
    <form method="POST" action="{{ url_for('analyze', step=2) }}">
        {# ... #}
    </form>
    {% endif -%}