Flask Load New Page After Streaming Data

Question:

I have simple Flask App that takes a CSV upload, makes some changes, and streams the results back to the user’s download folder as CSV.

HTML Form

<form action = {{uploader_page}} method = "POST" enctype = "multipart/form-data">
    <label>CSV file</label><br>
    <input type = "file" name = "input_file" required></input><br><br>
    <!-- some other inputs -->
    <div id = "submit_btn_container">
        <input id="submit_btn" onclick = "this.form.submit(); this.disabled = true; this.value = 'Processing';" type = "submit"></input>
    </div>
</form>

PYTHON

from flask import Flask, request, Response, redirect, flash, render_template
from io import BytesIO
import pandas as pd

@app.route('/uploader', methods = ['POST'])
def uploadFile():
    uploaded_file = request.files['input_file']
    data_df = pd.read_csv(BytesIO(uploaded_file.read()))
    # do stuff
    
    # stream the pandas df as a csv to the user download folder
    return Response(data_df.to_csv(index = False),
                            mimetype = "text/csv",
                            headers = {"Content-Disposition": "attachment; filename=result.csv"})

This works great and I see the file in my downloads folder.

However, I’d like to display "Download Complete" page after it finishes.

How can I do this? Normally I use return redirect("some_url") to change pages.

Asked By: sushi

||

Answers:

Here is some changes.

set window.open('') in input onclick event.

HTML Form

<form action ="/uploader" method = "POST" enctype = "multipart/form-data">
        <label>CSV file</label><br>
        <input type = "file" name = "input_file" required></input><br><br>
        <!-- some other inputs -->
        <div id = "submit_btn_container">
            <input id="submit_btn" onclick = "this.form.submit(); this.disabled = true; this.value = 'Processing'; window.open('your_url');" type = "submit"></input>
        </div>
</form>
Answered By: jak bin

Consider using send_file() or send_from_directory() for sending files.

Getting 2 responses from 1 request is not possible, but you can split the problem into chunks with the help of some JS, following this simple diagram (not very UML precise, but that’s it):

this diagram refers to a previous and simpler version of the code, which was later updated after the OP asked for flash() to be called

enter image description here

  1. POST to /uploader through a function called from the form by onsubmit, so that besides saving the file you can also have some logic there, like checking the response status

  2. process the file (I did a mockup of your processing through upper())

  3. if the server responds with 201 ("Created") then you can save the file and print "Download Complete" (I used window.document.body.innerHTML because it’s only one tag and we can replace all the previous DOM; it shouldn’t be used to change complex HTML)

  4. else, if the server responds with other status codes (like 500), POST to /something-went-wrong to get the new – possibly flashed – HTML to be rendered. The POST step is not shown in the diagram.

To test the error page, make some syntax error in the processing inside upload_file(), like data_df = pd.read_csv(BytesIO(uploaded_file.aread()))

In the something-went-wrong response I addeed a CSP header to mitigate a possible malicious attack, because we can’t trust the user enough.

Here’s the code:

main.py

from flask import (Flask,
                   request,
                   redirect,
                   render_template,
                   send_file,
                   url_for,
                   Response, jsonify, flash, make_response)
from flask_wtf.csrf import CSRFProtect

from io import BytesIO
import pandas as pd

app = Flask(__name__)
app.secret_key = "your_secret"

csrf = CSRFProtect(app)


@app.route('/')
def index():
    return render_template("index.html")


@app.route("/uploader", methods=['POST'])
def upload_file():
    try:
        uploaded_file = request.files['input_file']
        data_df = pd.read_csv(BytesIO(uploaded_file.read()))

        # do stuff
        data_df["foo"] = data_df["foo"].str.upper()

        # Stream buffer:
        io_buffer = BytesIO()
        data_df.to_csv(io_buffer)
        io_buffer.seek(0)

    except Exception as ex:
        print(ex)  # and log if needed
        # Here I return the exception string just as an example! Not good in real usage.
        return jsonify({"message": str(ex)}), 500
    else:
        return send_file(io_buffer,
                         download_name="result.csv",
                         as_attachment=True), 201


@app.route("/something-went-wrong", methods=["POST"])
def something_went_wrong():
    flash(request.get_json()["message"])
    response = make_response(render_template("something-went-wrong.html"), 200)
    response.headers['Content-Security-Policy'] = "default-src 'self'"
    return response

The form with the JS handler:

<form id="myForm" enctype="multipart/form-data" onsubmit="return submitHandler()">
    <input type="hidden" name="csrfToken" value="{{ csrf_token() }}"/>
    <label>CSV file</label><br>
    <input type="file" id="inputFile" name="input_file" required/><br><br>
    <!-- some other inputs -->
    <div id="submitBtnContainer">
        <input id="submitBtn" type="submit"/>
    </div>
</form>

<script>
    function submitHandler() {
        const csrf_token = "{{ csrf_token() }}";

        let formData = new FormData();
        const file = document.getElementById('inputFile').files[0];
        formData.append("input_file", file);

        fetch("/uploader", {
            method: "POST",
            body: formData,
            headers: {
                "X-CSRFToken": csrf_token,
            },
        })
        .then(response => {
            if (response.status != 201) {
                response.json().then(data => {
                    fetch("/something-went-wrong", {
                        method: "POST",
                        body: JSON.stringify({"message": data["message"]}),
                        headers: {
                            "Content-Type": "application/json",
                            "X-CSRFToken": csrf_token,
                        },
                    })
                    .then(response => response.text())
                    .then(text => {
                        window.document.body.innerHTML = text;
                    })
                });
            }
            else {
                return response.blob().then(blob => {
                    const file = new Blob([blob], { type: 'text/csv' });
                    const fileURL = URL.createObjectURL(file);
                    let fileLink = document.createElement('a');
                    fileLink.href = fileURL;
                    fileLink.download = "result.csv";
                    fileLink.click();
                    window.document.body.innerHTML = "<h1>Download Complete</h1>";
                });
            }
        })
        return false;
    }
</script>

For completeness, my dummy csv "file.csv":

foo
bar
Answered By: Jonathan Ciapetti

You need two functions, one to deal with the processing such as uploadFile(), and another in the same app route to return render template.

When the uploadFile() function is completed: completed = True

Then, code another function which tests the global variable if completed: to return render template.

See: How can I use the same route for multiple functions in Flask

Finally, return a variable to the page with Jinja2 and use Javascript to identify if that variable exists to load your ‘download completed’ page by Javascript.

Python:

    from flask import Flask, request, Response, redirect, flash, render_template
    from io import BytesIO
    import pandas as pd

    completed = False
    
    @app.route('/uploader', methods = ['POST'])
    def uploadFile():
        uploaded_file = request.files['input_file']
        data_df = pd.read_csv(BytesIO(uploaded_file.read()))
        # do stuff
        # When stuff is done
        global completed
        completed = True
        # stream the pandas df as a csv to the user download folder
        return Response(data_df.to_csv(index = False),
                                mimetype = "text/csv",
                                headers = {"Content-Disposition": "attachment; filename=result.csv"})
                                

How to load a new page: https://www.geeksforgeeks.org/how-can-a-page-be-forced-to-load-another-page-in-javascript/

Javascript conditionals: https://www.w3docs.com/learn-javascript/conditional-operators-if.html

Using Jinja2 to render a variable: https://jinja.palletsprojects.com/en/3.0.x/templates/

Also, you should really wrap your uploadFile() function with try and except to catch upload errors.

Answered By: Olney1