简体   繁体   English

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

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

I'm fairly new to the Python world (planing to make a switch and leave the CurlyBracesCamelCaseWorld) and I'm working on the simple app go get exp in the whole initial stack (database, server, handling html pages & assets, etc). 我对Python世界还很陌生(打算进行切换并离开CurlyBracesCamelCaseWorld),并且我正在开发简单的应用go在整个初始堆栈(数据库,服务器,处理html页面和资产等)中获取exp 。

So far so good, the pace of the development never ceases to amaze me and the amount of resources is hearth worming. 到目前为止,发展的步伐从未令我惊叹,资源的数量却泛滥成灾。

But I've encounter the problem that I just can't get past by. 但是我遇到了我无法克服的问题。

What I try to do now: 我现在尝试做的是:

  • Have a main wtform with a static set of fields 有一个主wtform带有一组静态字段
  • Add dynamically generated list of sub-forms - prepopulated with values and with custom labels 添加动态生成的子表单列表-预先填充值和自定义标签

In my apps scenario user will be able to specify the list of questions to answer - so there will be a form with common details + the dynamic list, that I can use to save the data to the database on submit. 在我的应用程序场景中,用户将能够指定要回答的问题的列表-因此将有一个表单,其中包含公用详细信息+动态列表,我可以使用该表单在提交时将数据保存到数据库中。

I have a whole list of issues here 我这里有一个完整的问题清单

1) The main problem is that I can't read the subforms data in the .validate() when I submit the form 1)主要问题是我提交表单时无法读取.validate()中的子表单数据

2) The other thing is that I can't force the labels to show the custom values That I want to set dynamically 2)另一件事是我不能强制标签显示要动态设置的自定义值

3) I need to do more reading on handling csfr in subforms and how to work around that as well 3)我需要更多阅读有关在子窗体中处理csfr的信息以及如何解决该问题的方法

4) And the last one - how do I validate the subforms - for required fields, length, etc 4)最后一个-如何验证子表单-用于必填字段,长度等

1 & 2 is my main concern now and I have a feeling that the issues have the same root-cause My gut feeling tells me that the broken element id is meaningful ('content' for each subform string field, instead of indexed 'entries-0-content' - that I see on submit) 1&2是我现在主要关心的问题,我感到这些问题具有相同的根本原因。我的直觉告诉我,破碎的元素ID是有意义的(每个子表单字符串字段的“内容”,而不是索引的“条目- 0内容”-我在提交时看到)

I wasn't able to find the full example how to do that and I'm struggling to connect the pieces I collected. 我找不到完整的示例该如何做,而我正在努力连接我收集的作品。 I prepared the simple code, both python & jinja2 template, ready to run, to demonstrate the problems. 我准备了简单的代码,包括python和jinja2模板,可以运行,以演示问题。 I'll gladly post the full working code after I figure it out as I'd kill to find just that.. 在找出完整的工作代码后,我很乐意发布完整的工作代码,因为我很想找到它。

So, the server -> 因此,服务器->

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)

And the html template -> 和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>


Debug on GET: 在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]

Debug on POST: 在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]

There is clearly some issue with ids (2x content vs entries-0-content) & I get the first result twice.. (value="my ans 1") id显然存在一些问题(2x内容vs条目0-内容),我两次都得到第一个结果。(值=“ my ans 1”)

I'd love to be able to generate the full form list based on the list of questions (I just use a static 2 here), set the custom labels per subform and then get the data in the server, so I can do the rest of the job inn there. 我希望能够根据问题列表生成完整的表单列表(我在这里仅使用静态2),为每个子表单设置自定义标签,然后在服务器中获取数据,因此我可以完成其余工作那里的工作旅馆。

I can fight with validation and csfr myself after that, but having the working scaffolding seems like a valid first step. 在那之后,我可以自己进行验证和使用csfr,但是拥有可工作的脚手架似乎是有效的第一步。 I've spend a lot of time poking around but I feel like I'm running in circle now. 我已经花了很多时间在闲逛,但是我觉得自己现在正在转圈。

And ofc - if you think that my assumptions on how to achieve what I want to achieve are wrong and I should use to do that - let me know. 而且,ofc-如果您认为我对如何实现自己想要实现的目标的假设是错误的,并且我应该习惯这样做,请告诉我。 I want to write the right thing, not just something that works. 我想写正确的东西,而不仅仅是行之有效的东西。

Pastebin links, if you prefer Pastebin链接,如果您愿意

EDIT - working code 编辑-工作代码

Thanks to @Nick K9! 感谢@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 links that also helped me 2个也对我有帮助的链接

You have the right idea, but there are a few issues with what you've done: 您有正确的想法,但是您所做的工作存在一些问题:

  1. You shouldn't explicitly create the SubForm instances. 您不应该显式创建SubForm实例。 Pass a dictionary to append_entry() to populate the fields in the subform. 将字典传递给append_entry()以填充子表单中的字段。 ( Edit: Took out incorrect information about passing form instances to append_entry() . It needs to be a dictionary object.) 编辑:获取了有关将表单实例传递给append_entry()错误信息。它必须是一个字典对象。)
  2. You should call append_entry() after the validate_on_submit() block, not before. 你应该叫append_entry() validate_on_submit()之前没有块。 When the POST request comes back with your form, it will already have sufficient subforms created. 当POST请求与表单一起返回时,它将已经创建了足够的子表单。 You did that when the page was built. 您在构建页面时就这样做了。 You just need to read all the form contents and pull out/save your data before the redirect. 您只需要阅读所有表单内容并在重定向之前提取/保存数据。
  3. You mentioned missing data and uncalled validation. 您提到缺少数据和未进行验证。 I have a hunch that at present you're overwriting the form data before the validation method is called. 我有一种预感,目前您正在调用验证方法之前覆盖表单数据。 So this problem may resolve itself. 因此,此问题可能会自行解决。
  4. You mentioned CSRF. 您提到了CSRF。 You need to include {{ entry_line.hidden_tag() }} inside the subform for loop. 您需要包括{{ entry_line.hidden_tag() }}子窗体里面for循环。 That should be all you need to get CSRF working with the subform. 这就是使CSRF与子窗体一起工作所需要的。

Try that and see if your form starts working. 尝试一下,看看您的表单是否开始工作。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM