In this part of the tutorial we’ll work on the user interface to make it, well, more user friendly.


flask by example part 7

Updates:

  • 03/22/2016: Upgraded to Python version 3.5.1.
  • 02/22/2015: Added Python 3 support.


Remember: Here’s what we’re building – A Flask app that calculates word-frequency pairs based on the text from a given URL.


  1. Part One: Set up a local development environment and then deploy both a staging and a production environment on Heroku.
  2. Part Two: Set up a PostgreSQL database along with SQLAlchemy and Alembic to handle migrations.
  3. Part Three: Add in the back-end logic to scrape and then process the word counts from a webpage using the requests, BeautifulSoup, and Natural Language Toolkit (NLTK) libraries.
  4. Part Four: Implement a Redis task queue to handle the text processing.
  5. Part Five: Set up Angular on the front-end to continuously poll the back-end to see if the request is done processing.
  6. Part Six: Push to the staging server on Heroku – setting up Redis and detailing how to run two processes (web and worker) on a single Dyno.
  7. Part Seven: Update the front-end to make it more user-friendly. (current)
  8. Part Eight: Create a custom Angular Directive to display a frequency distribution chart using JavaScript and D3.

Need the code? Grab it from the repo.

Let’s look at the current user interface…

Current User Interface

Start Redis in a terminal window:

1
$ redis-server

Then get your worker going in another window:

1
2
3
4
5
$ cd flask-by-example
$ python worker.py
17:11:39 RQ worker started, version 0.5.6
17:11:39
17:11:39 *** Listening on default...

Finally, in a third window, fire up the app:

1
2
$ cd flask-by-example
$ python manage.py runserver

Test the app out to make sure it still works. You should see something like:

current user interface

Let’s make some changes.

  1. We’ll start by disabling the submit button to prevent users from continually clicking while they are waiting for the submitted site to be counted.
  2. Next, while the application is counting the words, we’ll add a display throbber/loading spinner where the word count list will go to show the user that there is activity happening in the back-end.
  3. Finally, we’ll display an error if the domain is unable to be reached.

Changing the button

Change the button in the HTML to the following:

1
2
3
4
{% raw %}
  <button type="submit" class="btn btn-primary"
  ng-disabled="loading">{{ submitButtonText }}</button>
{% endraw %}

We added an ng-disabled directive and attached that to loading. This will disable the button when loading evaluates to true. Next, we added a variable to display to the user called submitButtonText. This way we’ll be able to change the text from "Submit" to "Loading..." so the user knows what’s going on. We then wrapped the button in {% raw %} and {% endraw %} so that Jinja knows to evaluate this as raw HTML. If we didn’t do this, Flask will try to evaluate the {{ submitButtonText }} as a Jinja variable and Angular won’t get a chance to evaluate it.

The accompanying JavaScript is fairly simple.

At the top of the WordcountController in main.js add the following code:

1
2
$scope.submitButtonText = 'Submit';
$scope.loading = false;

This set the initial value of loading to false so that the button will not be disabled. It also initialized the button’s text as "Submit".

Change the POST call to:

1
2
3
4
5
6
7
8
9
10
11
$http.post('/start', {'url': userInput}).
  success(function(results) {
    $log.log(results);
    getWordCount(results);
    $scope.wordcounts = null;
    $scope.loading = true;
    $scope.submitButtonText = 'Loading...';
  }).
  error(function(error) {
    $log.log(error);
  });

We added three lines, which set…

  1. wordcounts to null so that old values get cleared out.
  2. loading to true so that the loading button will be disabled via the ng-disabled directive we added to the HTML.
  3. submitButtonText to "Loading..." so that the user knows why the button is disabled.

Next update the poller function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var poller = function() {
  // fire another request
  $http.get('/results/'+jobID).
    success(function(data, status, headers, config) {
      if(status === 202) {
        $log.log(data, status);
      } else if (status === 200){
        $log.log(data);
        $scope.loading = false;
        $scope.submitButtonText = "Submit";
        $scope.wordcounts = data;
        $timeout.cancel(timeout);
        return false;
      }
      // continue to call the poller() function every 2 seconds
      // until the timeout is cancelled
      timeout = $timeout(poller, 2000);
    });
};

When the result is successful, we set loading back to false so that the button is enabled again and changed the button text back to "Submit" so the user knows they can submit a new URL.

Test it out!

Adding a spinner

Next, let’s add a spinner below the word count section so the user knows what’s going on. This is accomplished by adding an animated gif below the results div as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<div class="col-sm-5 col-sm-offset-1">
  <h2>Frequencies</h2>
  <br>
  <div id="results">
    <table class="table table-striped">
      <thead>
        <tr>
          <th>Word</th>
          <th>Count</th>
        </tr>
      </thead>
      <tbody>
        {% raw %}
          <tr ng-repeat="(key, val) in wordcounts">
            
            <td>{{key}}</td>
            <td>{{val}}</td>
            
          </tr>
        {% endraw %}
      </tbody>
    </table>
  </div>
  <img class="col-sm-3 col-sm-offset-4" src="{{ url_for('static',
  filename='spinner.gif') }}" ng-show="loading">
</div>

Be sure to grab spinner.gif from the repo.

You can see that ng-show is attached to loading just like the button is. This way when loading is set to true the spinner gif is shown. When loading is set to false – e.g., when the word count process finishes – the spinner disappears.

Dealing with errors

Finally, we want to deal with the case where the user submits a bad URL. Start by adding the following HTML below the form:

1
2
3
4
5
6
<div class="alert alert-danger" role="alert" ng-show='urlerror'>
  <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
  <span class="sr-only">Error:</span>
  <span>There was an error submitting your URL.<br>
  Please check to make sure it is valid before trying again.</span>
</div>

This used Bootstrap’s alert class to show a warning dialog if the user submits a bad URL. We used Angular’s ng-show directive to only display the dialog when urlerror is true.

Finally, in the WordcountController initialize $scope.urlerror to false so the warning doesn’t initially show up:

1
$scope.urlerror = false;

Catch the errors in the poller function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var poller = function() {
  // fire another request
  $http.get('/results/'+jobID).
    success(function(data, status, headers, config) {
      if(status === 202) {
        $log.log(data, status);
      } else if (status === 200){
        $log.log(data);
        $scope.loading = false;
        $scope.submitButtonText = "Submit";
        $scope.wordcounts = data;
        $timeout.cancel(timeout);
        return false;
      }
      // continue to call the poller() function every 2 seconds
      // until the timeout is cancelled
      timeout = $timeout(poller, 2000);
    }).
    error(function(error) {
      $log.log(error);
      $scope.loading = false;
      $scope.submitButtonText = "Submit";
      $scope.urlerror = true;
    });
};

This logged the error to the console, changed loading to false, set the submit button’s text back to "Submit" so that the user can try submitting again, and changed urlerror to true so that the warning shows up.

Lastly, in the success function for the POST call to '/start' set urlerror to false:

1
$scope.urlerror = false;

Now the warning dialog will disappear when the user tries to submit a new url.

With that, we’ve cleaned up the user interface a bit so that the user knows what is happening while we are running the word count functionality behind the scenes. Test this out!

Conclusion

What else could you add or change to better the user experience? Make the changes on your own or leave comments below. When done be sure to update both your staging and production environments.

See you next time!


This is a collaboration piece between Cam Linke, co-founder of Startup Edmonton, and the folks at Real Python

Comments