使用 flask、wtforms 和 jinja2 设置动态子表单列表的问题

Problem with setting up dynamic list of subforms with flask, wtforms and jinja2

我对 Python 世界相当陌生(计划进行切换并离开 CurlyBracesCamelCaseWorld)并且我正在开发简单的应用程序 go get exp in the whole initial stack (database, server ,处理 html 页面和资产等)。

到目前为止一切顺利,开发的速度让我惊叹不已,资源的数量也在不断增加。

但是我遇到了一个问题,就是过不去。

我现在尝试做的事情:

在我的应用程序场景中,用户将能够指定要回答的问题列表 - 因此会有一个包含通用详细信息 + 动态列表的表单,我可以使用它在提交时将数据保存到数据库中。

我这里有一整套问题

1) 主要问题是提交表单时无法读取.validate()中的子表单数据

2) 另一件事是我不能强制标签显示我想动态设置的自定义值

3) 我需要阅读更多有关在子表单中处理 csfr 以及如何解决该问题的文章

4) 最后一个 - 我如何验证子表单 - 必填字段、长度等

1 和 2 是我现在最关心的问题,我感觉这两个问题的根本原因相同 我的直觉告诉我损坏的元素 id 是有意义的('content' 对于每个子表单字符串字段,而不是索引 'entries-0-content' - 我在提交时看到)

我找不到完整的示例如何做到这一点,我正在努力连接我收集的片段。我准备了简单的代码,python & jinja2 模板,准备运行,来演示问题。我很乐意 post 找到完整的工作代码,因为我很想找到它..

所以,服务器 ->

from flask import Flask, redirect, url_for, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, FieldList, FormField, SubmitField, HiddenField, Label
from wtforms.validators import DataRequired

app = Flask(__name__, template_folder='flaskblog/templates')
app.config['SECRET_KEY'] = 'SECRET_KEY-SECRET_KEY-SECRET_KEY'


# subforms
class SubForm(FlaskForm):
    # how to handle hidden id that I can use to properly commit that on submit?
    # entry_type_id = HiddenField()

    # validators for subforms don't work, but that's something I'll try to address later
    content = StringField(validators=[DataRequired()])

    # I use custom __init__ set custom label for the field - or rather I try, as it doesn't work..
    def __init__(self, custom_label=None, *args, **kwargs):
        # not sure if safe - even just for the subform! #
        # Without that, I get 'TypeError: argument of type 'CSRFTokenField' is not iterable' on main_form.validate_on_submit()
        kwargs['csrf_enabled'] = False
        FlaskForm.__init__(self, *args, **kwargs)

        if custom_label is not None:
            self.content.label = Label(self.content.id, custom_label)
            print(f'INIT // id: [{self.content.id}] // content.data: [{self.content.data}] // label: [{self.content.label.text}]')


# main forms
class MainForm(FlaskForm):
    title = StringField('title')
    entries = FieldList(FormField(SubForm))
    submit = SubmitField('Post')


@app.route("/test", methods=['GET', 'POST'])
def test_route():
    # the main form
    main_form = MainForm(title='title')

    # sub forms, created before validate_on_submit()
    sub_form_1 = SubForm(content='Default answer 1', custom_label='Question 1')
    sub_form_2 = SubForm(content='Default answer 2', custom_label='Question 2')

    main_form.entries.append_entry(sub_form_1)
    main_form.entries.append_entry(sub_form_2)

    if main_form.validate_on_submit():
        for entry in main_form.entries.entries:
            print(f'LOOP // id: [{entry.content.id}] // content.data: [{entry.content.data}] // label: [{entry.content.label.text}]')

        return redirect(url_for('test_route'))

    print(f'INSTANCE_1 // id: [{sub_form_1.content.id}] // content.data: [{sub_form_1.content.data}] // label: [{sub_form_1.content.label.text}]')
    print(f'INSTANCE_2 // id: [{sub_form_2.content.id}] // content.data: [{sub_form_2.content.data}] // label: [{sub_form_2.content.label.text}]')

    return render_template('test_form.html', title='Test Form', main_form=main_form, legend='Test Form')


if __name__ == '__main__':
    app.run(debug=True)

和 html 模板 ->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title> Confused -.-' </title>
</head>
<body>
<div class="content-section">

    <form action="" method="post">
        {{ main_form.hidden_tag() }}
        {{ main_form.title.label(class="form-control-label") }}: {{ main_form.title(class="form-control form-control-lg") }}

        {% for entry_line in main_form.entries %}
            <div class="form-group">
                {{ entry_line.content.label(class="form-control-label") }}
                {{ entry_line.content.data(class="form-control form-control-lg") }}
            </div>
        {% endfor %}

        {# For the main form I use main_form.title(), main_form.submit(), etc - without .data(). #}
        {# If I try to do main_form.title.data() I get the ex that I can't call on 'str' #}

        {# But, for entry_lines, if I don't add .data() and just use entry_line.content()  #}
        {# I can see the input field, but it's prepopulated with HTML for that input instead of the value (that I see in that html) #}

        <div class="form-group">
            {{ main_form.submit(class="btn btn-outline-info") }}
        </div>
    </form>

</div>
</body>
</html>


GET 调试:

INIT // id: [content] // content.data: [Default answer 1] // label: [Question 1]
INIT // id: [content] // content.data: [Default answer 2] // label: [Question 2]
INSTANCE_1 // id: [content] // content.data: [Default answer 1] // label: [Question 1]
INSTANCE_2 // id: [content] // content.data: [Default answer 2] // label: [Question 2]

在 POST 上调试:

INIT // id: [content] // content.data: [my ans 1] // label: [Question 1]
INIT // id: [content] // content.data: [my ans 1] // label: [Question 2]
LOOP // id: [entries-0-content] // content.data: [<input id="content" name="content" type="text" value="my ans 1">] // label: [Content]
LOOP // id: [entries-1-content] // content.data: [<input id="content" name="content" type="text" value="my ans 1">] // label: [Content]
INIT // id: [content] // content.data: [Default answer 1] // label: [Question 1]
INIT // id: [content] // content.data: [Default answer 2] // label: [Question 2]
INSTANCE_1 // id: [content] // content.data: [Default answer 1] // label: [Question 1]
INSTANCE_2 // id: [content] // content.data: [Default answer 2] // label: [Question 2]

ID 显然存在一些问题(2x 内容与 entries-0-content)& 我两次得到第一个结果.. (value="my ans 1")

我希望能够根据问题列表生成完整的表单列表(我在这里只使用静态 2),为每个子表单设置自定义标签,然后在服务器中获取数据,所以我可以在那里完成剩下的工作。

在那之后我可以自己与验证和 csfr 作斗争,但是拥有有效的脚手架似乎是有效的第一步。我花了很多时间四处寻找,但我觉得我现在 运行 正在兜圈子。

而且 ofc - 如果您认为我关于如何实现我想要实现的目标的假设是错误的并且我应该这样做 - 请告诉我。我想写正确的东西,而不仅仅是有用的东西。

Pastebin 链接,如果你喜欢

编辑 - 工作代码

感谢@Nick K9!

from flask import Flask, redirect, url_for, render_template, request
from flask_wtf import FlaskForm
from wtforms import StringField, FieldList, FormField, SubmitField, HiddenField, Label
from wtforms.validators import DataRequired

app = Flask(__name__, template_folder='flaskblog/templates')
app.config['SECRET_KEY'] = 'SECRET_KEY-SECRET_KEY-SECRET_KEY'

subform_datasource = {
    0: {'question': 'Question 1', 'answare': 'Answare 1'},
    1: {'question': 'Question 2', 'answare': 'Answare 2'}
}


# subforms
class SubForm(FlaskForm):
    entry_type_id = HiddenField()
    content = StringField(validators=[DataRequired()])


# main forms
class MainForm(FlaskForm):
    title = StringField('title')
    entries = FieldList(FormField(SubForm))
    submit = SubmitField('Post')


@app.route("/test", methods=['GET', 'POST'])
def test_route():
    main_form = MainForm()
    if main_form.validate_on_submit():
        for entry in main_form.entries.entries:
            entry_message = (
                f'POST // wtform id: [{entry.content.id}] '
                f' //  entry_type_id id: [{entry.entry_type_id.data}]'
                f' //  content.data: [{entry.content.data}]'
                f' //  label: [{entry.content.label.text}]'
            )
            print(str(entry_message))

        return redirect(url_for('test_route'))
    elif request.method == 'GET':
        # You can indeed set the default values, but you need to pass the dict, not the SubForm instance!
        for key, subform in subform_datasource.items():
            main_form.entries.append_entry({'content': subform['answare'], 'entry_type_id': key})

    # Moved out from the constructor - on subform failed validation labels reset to the default value 'Content'
    # I guess that matching what was send to the form does not cast back the labels but creates the fresh instances with just the value
    # What, of course, makes sense - it's an edge case, no point in affecting performance for everyone
    for entry in main_form.entries.entries:
        entry.content.label.text = subform_datasource[entry.entry_type_id.data]['question']

    return render_template('test_form.html', title='Test Form', main_form=main_form, legend='Test Form')


if __name__ == '__main__':
    app.run(debug=True)

2 个对我也有帮助的链接

你的想法是对的,但是你所做的有一些问题:

  1. 您不应显式创建 SubForm 实例。将字典传递给 append_entry() 以填充子表单中的字段。 (编辑: 删除了有关将表单实例传递给 append_entry() 的错误信息。它需要是一个字典对象。)
  2. 您应该在 validate_on_submit()append_entry() 之后调用 ,而不是之前。当 POST 请求返回您的表单时,它已经创建了足够的子表单。您在构建页面时就这样做了。您只需要阅读所有表单内容并在重定向之前提取 out/save 您的数据。
  3. 您提到了缺失数据和未调用的验证。我有一种预感,目前您正在调用验证方法之前覆盖表单数据。所以这个问题可能会自行解决。
  4. 您提到了 CSRF。您需要在子表单 for 循环中包含 {{ entry_line.hidden_tag() }}。这应该是让 CSRF 使用子表单所需的全部内容。

试试看你的表单是否开始工作。