1

I found myself searching for this question and came to a solution that was aided in part by other stackoverflow threads.

The context:

I have a flask app that processes large datasets (most of this is done through Dash and Plotly, which I highly recommend).

I have one page where the user can request a downloads of particular parts of the dataset. The files are generated dynamically and can sometimes be large.

The user experience problem:

When users click button/link requesting the file download, the page can sit for up to 10 seconds while flask prepares the dynamically generated file, and then ultimately responds to the request with send_file and the download is initiated. This period of waiting is confusing for the user, and the experience would be aided with a spinner to indicate that the request was received.

The technical problem:

The issue is that when you point the browser window to the new file url, you lose control of how the webpage is displayed... in other words it's easy to start the spinner - stopping it is the crux. This topic has been discussed in many other threads here, but one solution stood out and integrated well with the flask send_file routine. I tried a number of other solutions before settling on the one I will post under "answers".

Related question (s)

This implementation may be helpful for someone else because it's specific to flask, but the question is similar to this thread: Detect when browser receives file download

The solution, however, mostly derives from this thread: Download a file by jQuery.Ajax

One benefit is that it retains the dynamic naming conventions for the file that you define in flask by appending a header to the response. Overall, though, its nothing groundbreaking, just a working solution to a more specific question. It's flask, though, so there are surely a million other ways to get it done.

Community
  • 1
  • 1
abogaard
  • 339
  • 3
  • 8

1 Answers1

3

As I stated above, this solution is a combination of pieces from many other threads, but most solidly inspired by: Download a file by jQuery.Ajax

What it does

Allows the user to interact with a dynamic or static file download links/buttons and gives feedback when the flask server starts to generate the file, and serves the file. In other words, it displays a spinner once a request for a file is started, and hides the spinner once the download is initiated. At this point the download still has to run, but most users are clear about how the browser gives feedback on this. The user never leaves the request page, which is useful in the case that the requests are generated dynamically by forms (the forms don't change state).

The flask side

@downloads.route('/downloads/report_1/<year>/<month>/<person>')
def report_1(year, month, person):

    #create an output stream
    output = BytesIO()
    writer = pd.ExcelWriter(output, engine='xlsxwriter') # for example. another great package
    
    #prepare your file using arguments

    #the writer has done its job
    writer.close()

    #go back to the beginning of the stream
    output.seek(0)

    response = make_response(send_file(output, attachment_filename=fname, as_attachment=True))
    response.headers['my_filename'] = fname # I had problems with how the native "filename" key
    return response

Javascript

<script type="text/javascript">

        function downloadReport(url) {
          showspinner();

          fetch(url) // for instance: /downloads/report_1/2020/2/Robert
          .then(
            function(response) {
              if (response.status !== 200) {
                console.log('Looks like there was a problem. Status Code: '+response.status);
                return;
              }

              var fname = response.headers.get('my_filename'); // this was necessary because the native filename key was oddly concatinated with another

              // Examine the response
              response.blob().then(function(blob) {
                const url = window.URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.style.display = 'none';
                a.href = url;
                // the filename you want
                a.download = fname;
                document.body.appendChild(a);
                a.click();
                window.URL.revokeObjectURL(url);
                hidespinner();
              });
            }
          )
          .catch(function(err) {
            console.log('Fetch Error :-S', err);
          });

        }

        function showspinner(){
            document.getElementsByClassName("loading_spinner")[0].style.display = "block";
        }

        function hidespinner(){
            document.getElementsByClassName("loading_spinner")[0].style.display = "none";
        }

</script>

The web page

On my page, I have form fields populated by flask whose values are used to generate the url above when a button is clicked (downloadReport(url) is triggered by an onclick attribute on the button).

CSS Spinner

For completeness, I'll share the spinner I am using (which can be found on multiple sites across the internet as lds-spinner):

.loading_spinner {
/*   display: inline-block; */
/*   transform: translateZ(1px); */
  position: absolute;
  left:50%;
  top:30%;
  display:none;
}
.loading_spinner > div {
  display: inline-block;
  width: 64px;
  height: 64px;
  margin: 8px;
  border-radius: 50%;
  background: #25aae1;
  animation: lds-circle 2.4s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
@keyframes loading_spinner {
  0%, 100% {
    animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5);
  }
  0% {
    transform: rotateY(0deg);
  }
  50% {
    transform: rotateY(1800deg);
    animation-timing-function: cubic-bezier(0, 0.5, 0.5, 1);
  }
  100% {
    transform: rotateY(3600deg);
  }
}

with the following on the webpage

<div class="loading_spinner"><div></div></div>
Community
  • 1
  • 1
abogaard
  • 339
  • 3
  • 8