I'm seeing the same strange behavior posted to Filling WTForms FormField FieldList with data results in HTML in fields where my raw fields are rendering with HTML rather than their default values. In that other example there's basically a one layer deep stacking of a FieldList over a single FormField. In my case I'm creating a 2D structure of a FieldList over a FieldList over a FormField. I can't figure out where I'm off here.
app.py
import os
from flask import Flask, redirect, render_template, request, send_file, url_for
from flask_wtf import FlaskForm
from flask_wtf.csrf import CSRFProtect
from wtforms import FieldList, FormField, RadioField, TextAreaField, validators
app = Flask(__name__)
csrf = CSRFProtect(app)
SECRET_KEY = os.urandom(32)
app.config['SECRET_KEY'] = SECRET_KEY
#region FORMS
class TestCaseItem(FlaskForm) :
pass_fail_radio = RadioField( '' , choices=[('Pass','Pass'), ('Fail','Fail')] , default='Pass' , validators=[validators.DataRequired()] )
failure_message = TextAreaField(default='')
class TestCaseForm(FlaskForm) :
test_items = FieldList( FormField( TestCaseItem ))
class ManualTestForm(FlaskForm):
test_cases = FieldList( FormField(TestCaseForm))
#endregion
@app.route("/" , methods = ['POST', 'GET'])
def index():
form = ManualTestForm()
test_cases = ["test case {}".format(i) for i in range(5)]
devices = ["device {}".format(i) for i in range(3)]
# Expand the field list for each test case
for tc in test_cases :
tcf = TestCaseForm()
# expand its field list for each test device
for device in devices :
tci = TestCaseItem()
tci.failure_message = 'abc'
tcf.test_items.append_entry( tci )
form.test_cases.append_entry( tcf )
return render_template('test_template.html', form=form, test_cases=test_cases, devices=devices )
if __name__ == "__main__" :
app.run(debug=True, port=5001) # http://127.0.0.1:5001
templates/test_template.html
<html>
<head>
</head>
<body>
<h1>Manual Test Submission</h1>
<h2>Test Suite</h2>
<form method="post">
{{ form.csrf_token }}
<!--TEST CASES-->
<table>
<tr>
<th>Test Case ID</th>
{% for test_item in form.test_cases[0].test_items %}
{% set device = devices[loop.index0] %}
<th>TC Status: {{device}}</th>
<th>TC Input: {{device}}</th>{% endfor %}
</tr>
{% for test_case in form.test_cases %}
{{test_case.hidden_tag()}}
<tr>
<td>{{ test_cases[ loop.index0 ]}}</td>
{% for test_item in test_case.test_items %}
<td>{{ test_item.pass_fail_radio }}</td>
<td>{{ test_item.failure_message }}</td>{% endfor %}
</tr>{% endfor %}
</table>
</form>
</body>
</html>
Set all the field values the usual way in your view, but in your template use field .data
attributes for the fields belonging to nested forms:
{% for test_case in form.test_cases %}
{{ test_case.hidden_tag() }}
{{ test_cases[loop.index0] }}
{% for test_item in test_case.test_items %}
{{ test_item.pass_fail_radio.data }}
{{ test_item.failure_message.data }}
{% endfor %}
{% endfor %}
For what it's worth, there's another gotcha when dealing with nested FieldLists: if you construct the form in the logical way, the field id
and name
attributes won't be fully namespaced due to the way .append_entry()
works. As a result, the expected values won't be POSTed and validation also breaks.
Broken:
form = RecipientsForm()
for proprietor in proprietors:
proprietor_form = ProprietorForm()
# Set proprietor name in hidden input field.
proprietor_form.prop_name = proprietor['name']
# populate and append addresses to proprietor form.
for address in proprietor['addresses']:
address_form = AddressForm()
address_form.address = address['address']
address_form.address_type = address['type']
proprietor_form.addresses.append_entry(address_form)
form.proprietors.append_entry(proprietor_form)
Works:
form = RecipientsForm()
proprietors = proprietor_api_call()
# Populate and append proprietors to Recipients form.
for idx, proprietor in enumerate(proprietors):
proprietor_form = ProprietorForm()
proprietor_form.prop_name = proprietor['name']
form.proprietors.append_entry(proprietor_form)
# Populate and append addresses to Proprietor form.
for address in proprietor['addresses']:
address_form = AddressForm()
address_form.address = address['address']
address_form.address_type = address['type']
form.proprietors[idx].addresses.append_entry(address_form)