How to rewrite this Flask view function to follow the post/redirect/get pattern?
Asked Answered
L

3

8

I have a small log browser. It retrieves and displays a list of previously logged records depending on user's input. It does not update anything.

The code is very simple and is working fine. This is a simplified version:

@app.route('/log', methods=['GET', 'POST'])
def log():
    form = LogForm()
    if form.validate_on_submit():
        args = parse(form)
        return render_template('log.html', form=form, log=getlog(*args))
    return render_template('log.html', form=form)

However it does not follow the post/redirect/get pattern and I want to fix this.

Where should I store the posted data (i.e. the args) between post and get? What is the standard or recommended approach? Should I set a cookie? Should I use flask.session object, create a cache there? Could you please point me in the right direction? Most of the time I'm writing backends...


UPDATE:

I'm posting the resulting code.

@app.route('/log', methods=['POST'])
def log_post():
    form = LogForm()
    if form.validate_on_submit():
        session['logformdata'] = form.data
        return redirect(url_for('log'))
    # either flash errors here or display them in the template
    return render_template('log.html', form=form)

@app.route('/log', methods=['GET'])
def log():
    try:
        formdata = session.pop('logformdata')
    except KeyError:
        return render_template('log.html', form=LogForm())

    args = parse(formdata)
    log = getlog(args)
    return render_template('log.html', form=LogForm(data=formdata), log=log)
Leibniz answered 8/1, 2017 at 14:19 Comment(0)
C
8

So, ultimately the post/redirect/get pattern protects against submitting form data more than once. Since your POST here is not actually making any database changes the approach you're using seems fine. Typically in the pattern the POST makes a change to underlying data structure (e.g. UPDATE/INSERT/DELETE), then on the redirect you query the updated data (SELECT) so typically you don't need to "store" anything in between the redirect and get.

With all the being said my approach for this would be to use the Flask session object, which is a cookie that Flask manages for you. You could do something like this:

@app.route('/log', methods=['GET', 'POST'])
def log():
    form = LogForm()
    if form.validate_on_submit():
        args = parse(form)
        session['log'] = getlog(*args)
        return redirect(url_for('log'))
    saved = session.pop('log', None)
    return render_template('log.html', form=form, log=saved)

Also, to use session, you must have a secret_key set as part of you application configuration.

Flask Session API

UPDATE 1/9/16

Per ThiefMaster's comment, re-arranged the order of logic here to allow use of WTForms validation methods for invalid form submissions so invalid form submissions are not lost.

Cymatium answered 8/1, 2017 at 17:36 Comment(4)
Another reason for the pattern is that on reload browsers ask the user before resending the data. Even users understanding what is going on might find it annoying.Leibniz
I do appreciate the example. Working with session is easier than I thought previously. I have already updated my code based on your answer and it works as I wanted. Thank you. Answer accepted.Leibniz
I didn't know this pattern yet. Thank you for this Question and Answer. Found a good explanation of the pattern here: chase-seibert.github.io/blog/2012/10/19/…Recapture
This is wrong since you won't see any validation errors and also lose form input in case validation fails.Chace
C
1

The common way to do P/R/G in Flask is this:

@app.route('/log', methods=('GET', 'POST'))
def log():
    form = LogForm()
    if form.validate_on_submit():
        # process the form data
        # you can flash() a message here or add something to the session
        return redirect(url_for('log'))
    # this code is reached when the form was not submitted or failed to validate
    # if you add something to the session in case of successful submission, try
    # popping it here and pass it to the template
    return render_template('log.html', form=form)

By staying on the POSTed page in case the form failed to validate WTForms prefills the fields with the data entered by the user and you can show the errors of each field during form rendering (usually people write some Jinja macros to render a WTForm easily)

Chace answered 9/1, 2017 at 13:17 Comment(3)
Thanks--very good point. I updated my answer to reflect your comment. I don't use this feature of WTForms so this was not something I had thought of.Cymatium
Thank you for explaining the details. I did not literally copy the code from the accepted answer, just learned how to use the session object. It was a small but important missing piece. The error reporting was fine in my program. After some tests I have appended the version I'm satisfied with to the question.Leibniz
If in the POST request and the form fails to validate, the code will render template instead of redirect, and still not follow Post-Redirect-Get pattern.Stpeter
T
0

This method maintains form data as well as field errors on redirect. Use Flask-Session if your form has decimals or dates, as they'll get screwed up using regular Flask sessions

@app.route("/log", methods=["get", "post"])
def log():

    if request.method == "GET":
        form = session2form(LogForm) if "form" in session else LogForm()
        return render_template("log.html", form=form)
    else: # POST
        form = LogForm()
        if form.validate_on_submit():
            return redirect(url_for("...")) # your "success" endpoint
        # validation failed, so put form/errors in session and redirect to self:
        form2session(form)
        return redirect(url_for(request.endpoint))

Two helper functions to keep it DRY:

def session2form(form_cls):
    form_data, field_errors, form_errors = session["form"]

    form = form_cls(**form_data)
    form.form_errors = form_errors
    for field in form:
        field.errors = field_errors[field.name]
    session.pop("form", None)
    return form


def form2session(form):
    """can't store WTForm in session as it's not serializable,
    but can store form data and errors"""
    session["form"] = (
        form.data,
        {field.name: field.errors for field in form},
        form.form_errors,
    )
Talion answered 18/3, 2023 at 20:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.