How can I create a form from a list of models using WTForms?

Tags: , , , ,



I have a list of Prediction models. I want to bind them to a form and allow the use to post back. How can I structure my form so the post associates a Home/Away score with a Prediction model’s id field for each item I bind to the form?

view

@app.route('/predictor/',methods=['GET','POST'])
@login_required
def predictions():    
    user_id = g.user.id
    prediction= # retrieve prediction
    if request.method == 'POST':
        if form.validate() == False:
            flash('A score is missing, please fill in all predictions')
            render_template('predictor.html', prediction=prediction, form=form)
        else:
            for pred in prediction:
                # store my prediction
            flash('Prediction added')
            return redirect(url_for("predictions"))    
    # display current predictions
    elif request.method == 'GET':
        return render_template('predictor.html', prediction=prediction, form=form)

form

class PredictionForm(WTForm):
    id = fields.IntegerField(validators=[validators.required()], widget=HiddenInput())
    home_score = fields.TextField(validators=[validators.required()])
    away_score = fields.TextField(validators=[validators.required()])

template

  <form action="" method="post">
    {{form.hidden_tag()}}
    <table>
        {% for pred in prediction %}
        <tr>
            <td>{{pred.id}}</td>
            <td>{{form.home_score(size=1)}}</td>
            <td>{{form.away_score(size=1)}}</td>               
        </tr>
        {% endfor %}
    </table>
    <p><input type="submit" value="Submit Predictions"></p>
   </form>

I am unable to get my data to bind correctly on POST. The required validators continually fail because the post data is missing all the Required fields.

Answer

You need a subform that will bind to the items in a list of predictions:

The form you have described will only allow you to submit a single prediction. There seems to be a discrepancy because you bind an iterable of predictions and it would appear that you want a home and away prediction for each. In fact as it stands it will never post back an id field. This will always cause you to fail form validation. I think what you want is a list of subforms. Like so:

# Flask's form inherits from wtforms.ext.SecureForm by default
# this is the WTForm base form. 
from wtforms import Form as WTForm

# Never render this form publicly because it won't have a csrf_token
class PredictionForm(WTForm):
    id = fields.IntegerField(validators=[validators.required()], widget=HiddenInput())
    home_score = fields.TextField(validators=[validators.required()])
    away_score = fields.TextField(validators=[validators.required()])

class PredictionListForm(Form):
    predictions = FieldList(FormField(PredictionForm))

Your view will need to return something along the lines of:

predictions = # get your iterable of predictions from the database
from werkzeug.datastructures import MultiDict
data = {'predictions': predictions}
form = PredictionListForm(data=MultiDict(data))
    
return render_template('predictor.html', form=form)

Your form will need to change to something more like this:

<form action='my-action' method='post'>
    {{ form.hidden_tag() }}
    {{ form.predictions() }}
</form>

Now this will print a <ul> with an <li> per item because thats what FieldList does. I’ll leave it up to you to style it and get it into a tabular form. It might be a bit tricky but its not impossible.

On POST a you will get a formdata dictionary with a home and away score for each prediction’s id. You can then bind these predictions back into your SQLAlchemy model.

[{'id': 1, 'home': 7, 'away': 2}, {'id': 2, 'home': 3, 'away': 12}]


Source: stackoverflow