Search code examples
pythonflaskflask-wtformswtformsfieldlist

Nested WTForms FieldList results in HTML in fields


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>

Nested Field List rendering HTML


Solution

  • 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)