Lukas
Lukas

Reputation: 451

How to download a file with plotly-dash on a multi-page app?

I already know about the following approach (link here):

server = Flask(__name__)
app = dash.Dash(server=server)


@server.route("/download/<path:path>")
def download(path):
    """Serve a file from the upload directory."""
    return send_from_directory(UPLOAD_DIRECTORY, path, as_attachment=True)

But the problem is, when I use a multi-page-approach like suggested from Plotly (link here (below "Structuring a Multi-Page App" - index.py)):

    app.layout = html.Div([
    dcc.Location(id='url', refresh=False),
    html.Div(id='page-content')
])


@app.callback(Output('page-content', 'children'),
              [Input('url', 'pathname')])
def display_page(pathname):
    if pathname == '/apps/app1':
        return app1.layout
    elif pathname == '/apps/app2':
        return app2.layout
    else:
        return '404'

I cannot use server.route because it will be caught by the callback shown above.

What is the best way to still make files downloadable?

Upvotes: 2

Views: 4053

Answers (2)

hussam
hussam

Reputation: 859

I figured it out
Note that the directory stage is where my temporary download files are stored
First this is what my directory tree looks like:

   root
    |
    |---apps
    |     └── main.py
    |---assets
    |---layouts
    |     |--layout.py
    |     └── error.py
    |---stage 
    |---app.py
    |---functions.py   
    └── index.py

This is what my index.py looks like

import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State

from app import app
from apps import main


app.layout = html.Div(
    [dcc.Location(id="url", refresh=False), html.Div(id="page-content")]
)

@app.callback(Output("page-content", "children"), [Input("url", "pathname")])
def display_page(pathname):
    if pathname == "/main":
        return main.layout
    else:
        return main.error_page


if __name__ == "__main__":
    app.run_server(debug=True, host="llp-lnx-dt-10200", port=15021)

in functions.py I will dynamically produce a dash-table + the html.A(... with the link to download, the function is

def display_final_results(table):

    import dash_html_components as html
    import dash_core_components as dcc
    import dash_table
    import pandas as pd

    return html.Div(
        [
            html.H5("""File processed and stuff worked"""),
            dash_table.DataTable(
                id="result_table",
                data=table.iloc[:20, :].to_dict("records"),
                columns=[{"name": i, "id": i} for i in list(table)],
            ),
            html.Hr(),
            dcc.Store(id="result_vault", data=table.to_dict()),
            html.A(id="download_link", children=html.Button(children="Download")),
        ]
    )

in main.py I call on the function def_final_results(table) passing in the table that I want to display in the dash-table and also the link for download.

Here's what the callback in main.py looks like followed by the app.server.route()

@app.callback(
    Output("download_link", "href"),
    [Input("result_vault","data"),
     Input("h5_filename", "children")]
)
def return_download_link(data, upload_filename):
    
    shutil.rmtree("stage")
    os.mkdir("stage")

    target = pd.DataFrame(data)
    download_filename = upload_filename.split(":")[1].strip() + f"""_{filestamp()}.xlsx"""
    uri = f"""stage/{download_filename}"""
    target.to_excel(
        uri, engine="xlsxwriter", index=False, header=True, sheet_name="results"
    )

    return uri

@app.server.route("/stage/<path:path>")
def serve_static(path):
    root_dir = os.getcwd()
    return flask.send_from_directory(os.path.join(root_dir, "stage"), filename=path)

in main.py the table target is saved into the directory /stage and the uri object which is the path to the file /stage/filename+filestamp is sent to object with id download_link as the href attribute, this is the html.A(... in the file functions.py. I returned the href since the download attribute did not work for me.

The big mistake that I made was that my index.py dcc.Location url used to be:

if pathname == "apps/main":
    return main.layout

So every time the routing would go to https://llp-lnx-dt-10200:15021/apps/stage/filename rather than https://llp-lnx-dt-10200:15021/stage/filename.
By removing the apps from the url the problem was promptly solves.

Upvotes: 0

Lukas
Lukas

Reputation: 451

Ok, I have solved it now.

In the documentation it says:

The dcc.Location component represents the location or address bar in your web browser.

So I used an html.A element with the download option. As stated here, download

Prompts the user to save the linked URL instead of navigating to it.

This means when the user clicks on the link, it doesn't change the address bar. Hence, the callback from the display_page(pathname) isn't called and the link is directed to the download(path)-method via the @server.route statement.

Upvotes: 2

Related Questions