Reputation: 4476
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:
Ways I've considered approaching this:
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...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.
Upvotes: 14
Views: 5838
Reputation: 8673
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 %}
Upvotes: 5
Reputation: 1618
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.
Upvotes: 3