使用Flask/WTForms实现多部分表单

12

我有一个多部分表单需要生成 - 这类似于购物车的工作流程,您需要填写一个由多个“部分”(例如详细信息、账单、付款等)组成的表单,在一次显示中逐步完成。

关键细节:

  • 我有3个表单部分
  • 必须按顺序完成各部分,第2部分依赖于第1部分的信息
  • 除非用户完成整个流程(第1、2和3部分),否则表单数据无用。

我考虑过的方法:

  • 使用一个路由def,并在request.args中存储一个值,告诉我当前处于哪个“部分”,然后根据部分render_template不同的表单模板。这感觉很投机...
  • 为每个部分设置不同的路由和视图。这感觉不对,而且我必须阻止人们通过URL直接转到第2步。
  • 将所有部分都放在一个表单中,使用JavaScript隐藏某些部分和移动部分之间的方式来切换各个部分。

在Flask / WTForms中完成此操作的最佳方法是什么?我发布的上述方法都不正确,并且我确信这是一个相当常见的需求。


个人认为这是利用Jinja模板的好情况,但我不是专家,不能声称它是最好的甚至是正确的。如果您在第一部分获取所需数据,则显示第二部分,如果您获取第二部分中的数据等等。这种仅使用Flask-Wtforms的方法需要一个提交按钮,除非你想添加JavaScript事件。我很想知道哪种方法适合您。 - wgwz
@skywalker 通过Jinja如何实现这个?模板只会被调用一次然后渲染,所以递归渲染也需要一个按钮触发,不是吗? - mal-wan
你正在使用哪些表单?这可能取决于你的表单是什么,但你可以在Python端检测表单数据,并将其传递到Jinja模板中,然后有条件地显示表单的某些部分。是的,这种方法需要提交按钮。除非例如你正在使用选择多个字段,在这种情况下你可以使用JavaScript检测状态变化。 - wgwz
2个回答

3
最优雅的解决方案无疑需要一些JavaScript,正如您在上一个想法中提到的那样。您可以使用JS隐藏表单的不同部分,并在客户端执行必要的检查和/或数据操作,仅当正确且完整时将其提交到flask路由。
我已经使用了您提到的第一种方法。这是它的样子:
@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

你说的没错,这样做可能有些“hacky(不太正式)”。然而,如果路由除了处理表单以外没有太多其他任务,那么这种方法是可行的。我的路由方式是收集数据,然后询问用户一系列与数据相关的问题。根据你所描述的情况,需要在每个步骤中收集数据,我真的建议采用JavaScript解决方案。


谢谢!只是想确保没有“更好的方法”。由于我需要一些服务器端验证,所以我选择了第一种选项(在每个部分之后访问服务器),但可能会在有时间重构时将其迁移到JS,并使用JS调用服务器进行验证。 - mal-wan

1

我会尝试用一般步骤简化,以便您可以尽可能轻松地将其应用于例如购物,并使代码更易读。

代码结构:

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

以下是每个文件的代码:

  • 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 %}

输出:

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接