Multi-part form using Flask / WTForms
Asked Answered
H

2

13

I have a multi-part form to generate - think similar workflow to a Shopping Cart where you have multiple "sections" (eg details, billing, payment etc) for the one form that display one at a time.

Key Details:

  • I have 3 sections to the form
  • The sections must be completed in sequence and Section 2 relies on information from Section 1
  • The form data is useless (eg will be thrown away) unless the person completes the FULL process (Section 1, 2 and 3).

Ways I've considered approaching this:

  • Using the one Route def and storing a value in request.args that tells me which "Section" I am at then render_template a different Form template depending on the section. This feels hacky...
  • Having a different Route and View for each section. This feels wrong and I'd have to stop people from going directly to Step 2 via URL
  • Putting all Sections on the one form, using Javascript to hide portions of the one form and moving between sections that way

What's the best method to accomplish this in Flask/WTForms? None of the methods I've posted above seem right and I have no doubt this is a fairly common requirement.

Hinson answered 18/11, 2015 at 0:6 Comment(3)
Personally, I think that this is a good situation to make use of jinja templating, but I am no expert so I can't claim that it is the best or even proper. If you get the data required in section 1, display section 2, if you get data in section 2 , etc.. This method with just Flask-Wtforms requires a submit button. Unless you want to add in Javascript events. I'm curious to find out what works for you.Outofdoor
@skywalker How would this be accomplished via Jinja? Doesn't the template only get hit once and then rendered? So recursive renders would require a button hit too wouldn't they?Hinson
What are the form/s you are using? It might depend on what your forms are, but you could detect data from the forms on the Python side, and pass that into the Jinja template, which could then conditionally display certain segments of the form/s. Yes, this method would require the submit button. Unless for example, you are using select multiple fields where you could detect a change in state with JavaScript.Outofdoor
O
3

The most elegant solution will no doubt require some javascript as you mentioned in your last idea. You can use JS to hide the different parts of your form and perform the necessary checks and/or data manipulations on the client side and ONLY when that is correct and complete submit it to your flask route.

I have used the first method you mentioned. Here is what it looked like:

@simple_blueprint.route('/give', methods=['GET', 'POST'])
@simple_blueprint.route('/give/step/<int:step>', methods=['GET', 'POST'])
@login_required
def give(step=0):
    form = GiveForm()
    ...
    return blah blah

You are right that this feels "hacky". However it can work if the route doesn't have to do much else besides handling the form. The way my route worked was to collect data and then ask users a bunch of questions about the data. The way you are explaining your situation, with the need to collect data on each step, I would really recommend the javascript solution.

Oracular answered 18/11, 2015 at 0:53 Comment(1)
Thanks! Just wanted to make sure there wasn't a "better way to do it". Since I want some server-side validation I've gone with the first option (hitting the server after each part) but will probably migrate it to JS with JS calls to the server to validate when I have the time to refactor.Hinson
F
5

I will try to simplify with general steps so that you can apply it to say shopping as easily as possible and to make the code more readable.

Code structure:

.
├── app.py
└── templates
    ├── finish.html
    └── step.html

Below I will provide the code for each of the files:

  • app.py
from flask import Flask, render_template, redirect, url_for, request, session
from flask_bootstrap import Bootstrap
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import InputRequired
from flask_wtf import FlaskForm

app = Flask(__name__)
app.secret_key = 'secret'
bootstrap = Bootstrap(app)


class StepOneForm(FlaskForm):
    title = 'Step One'
    name = StringField('Name', validators=[InputRequired()])
    submit = SubmitField('Next')


class StepTwoForm(FlaskForm):
    title = 'Step Two'
    email = StringField('Email', validators=[InputRequired()])
    submit = SubmitField('Next')


class StepThreeForm(FlaskForm):
    title = 'Step Three'
    address = TextAreaField('Address', validators=[InputRequired()])
    submit = SubmitField('Next')


class StepFourForm(FlaskForm):
    title = 'Step Four'
    phone = StringField('Phone', validators=[InputRequired()])
    submit = SubmitField('Finish')


@app.route('/')
def index():
    return redirect(url_for('step', step=1))


@app.route('/step/<int:step>', methods=['GET', 'POST'])
def step(step):
    forms = {
        1: StepOneForm(),
        2: StepTwoForm(),
        3: StepThreeForm(),
        4: StepFourForm(),
    }

    form = forms.get(step, 1)

    if request.method == 'POST':
        if form.validate_on_submit():
            # Save form data to session
            session['step{}'.format(step)] = form.data
            if step < len(forms):
                # Redirect to next step
                return redirect(url_for('step', step=step+1))
            else:
                # Redirect to finish
                return redirect(url_for('finish'))

    # If form data for this step is already in the session, populate the form with it
    if 'step{}'.format(step) in session:
        form.process(data=session['step{}'.format(step)])

    content = {
        'progress': int(step / len(forms) * 100),
        'step': step, 
        'form': form,
    }
    return render_template('step.html', **content)


@app.route('/finish')
def finish():
    data = {}
    for key in session.keys():
        if key.startswith('step'):
            data.update(session[key])
    session.clear()
    return render_template('finish.html', data=data)


if __name__ == '__main__':
    app.run(debug=True)
  • finish.html
{% extends 'bootstrap/base.html' %}

{% block content %}
<div class="container">
  <div class="row">
    <div class="col-md-8 offset-md-2">
      <h1>Finish</h1>
      <p>Thank you for your submission!</p>
      <table class="table">
        {% for key, value in data.items() %}
          {% if key not in ['csrf_token', 'submit', 'previous']%}
            <tr>
              <th>{{ key }}</th>
              <td>{{ value }}</td>
            </tr>
          {% endif %}
        {% endfor %}
      </table>
    </div>
  </div>
</div>
{% endblock %}
  • step.html
{% extends 'bootstrap/base.html' %}
{% import "bootstrap/wtf.html" as wtf %}

{% block content %}
<div class="container">
    <div class="row">
        <div class="col-md-8 offset-md-2">
            <div class="progress mb-4">
                <div class="progress-bar" role="progressbar" style="width: {{ progress }}%" aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="100">{{ form.title }}: {{ progress }}%</div>
            </div>
            <br>
            {% with messages = get_flashed_messages() %}
            {% if messages %}
            <ul class=flashes>
              {% for message in messages %}
              <li>{{ message }}</li>
              {% endfor %}
            </ul>
            {% endif %}
            {% endwith %}
            <br>
            <h3>{{ form.title.upper() }}</h3>
            <hr>
            {{ wtf.quick_form(form) }}
            <br>
            {% if step > 1 %}
            <a href="{{ url_for('step', step=step-1) }}" class="btn btn-default">Previous</a>
            {% endif %}
        </div>
    </div>
</div>
{% endblock %}

OUTPUT:

enter image description here enter image description here enter image description here enter image description here enter image description here

Falcate answered 23/3, 2023 at 1:9 Comment(0)
O
3

The most elegant solution will no doubt require some javascript as you mentioned in your last idea. You can use JS to hide the different parts of your form and perform the necessary checks and/or data manipulations on the client side and ONLY when that is correct and complete submit it to your flask route.

I have used the first method you mentioned. Here is what it looked like:

@simple_blueprint.route('/give', methods=['GET', 'POST'])
@simple_blueprint.route('/give/step/<int:step>', methods=['GET', 'POST'])
@login_required
def give(step=0):
    form = GiveForm()
    ...
    return blah blah

You are right that this feels "hacky". However it can work if the route doesn't have to do much else besides handling the form. The way my route worked was to collect data and then ask users a bunch of questions about the data. The way you are explaining your situation, with the need to collect data on each step, I would really recommend the javascript solution.

Oracular answered 18/11, 2015 at 0:53 Comment(1)
Thanks! Just wanted to make sure there wasn't a "better way to do it". Since I want some server-side validation I've gone with the first option (hitting the server after each part) but will probably migrate it to JS with JS calls to the server to validate when I have the time to refactor.Hinson

© 2022 - 2024 — McMap. All rights reserved.