Dev a Persistent Sleep Tracker Part 2: Tracking and Graphing

Recap

In the first part of this series, we built a web application with user registration and login using Python’s Flask web framework and a hosted NoSQL MongoDB database for data persistence. We will now build on the code we wrote in that tutorial, so you must have completed it.

In this second part of the series, we’ll implement the logic that allows users to enter their sleep data and see that data on an interactive graph, generated using Plotly. Registered users will be able to log the number of hours slept on different days and visualize this data as a graph.

Creating the Sleep Tracker Front-end

At the end of our last tutorial, we saw a blank page when we logged in and were redirected to the application’s /main page. This happened because our main.html file was empty, so let’s add some content to it. We’ll need the following:

  1. A form with fields for the date and hours slept, so users can provide sleep data for different days.
  2. A way to view a graph of this data.
  3. A logout button, that logs the user out.

To achieve all of this, we will first need to build a main.html containing both static HTML and dynamic Jinja template components that will change depending on which user is logged in and what sleep data they’ve provided. We will also implement some front-end JavaScript code to make our sleep data graph interactive.

Let’s add the sleep data logging form first. Open the main.html file in the templates directory. Add the following:

{% extends "base.html" %}

{% block content %}
<h2>Hey, {{ user['username'] }}! Let's track your sleep.</h2>
<form action="" method="POST">
  <ul>
    <li> 
      <label for="time">Time Slept (hours):</label>
      <input type="text" id="time" name="time">
    </li>
    <li>
      <label for="date">Date:</label>
      <input type="date" id="date" name="date" value="">
    </li>
    <li class="button">
      <input type="submit" name="submit" value="Submit">
      <input type="submit" name="graph" value="View Graph">
      <input type="submit" name="logout" value="Logout">
    </li>
  </ul>
</form>
{% endblock %}

This works similarly to the login.html file we created in the previous tutorial, with base.html acting as the page skeleton and our unique content being entered between the {% block content %} and {% endblock %} lines. We also ensure that our form uses the POST method, so we can differentiate between a user visiting the page and clicking one of the three form buttons. To determine which button a user has clicked in a given POST request, we’ll use the button’s HTML name attribute (submit, graph or logout) in Flask.

Notice the Jinja snippet {{ user['username'] }}. This will display data sent from our Flask back-end code on the page – in this case, the user’s name.

The <input type="date" id="date" name="date" value=""> line creates an interactive calendar, so users can click on dates rather than typing them out.

Adding Sleep Data Submission and Logout

In the app.py file, add the following line below the app = Flask(__name__) line.

app.config['plotting'] = False

Here, we add a new entry in our Flask application’s configuration settings. This will be helpful soon – we’ll use this line to tell whether a user has clicked the “View Graph” button in our main.html file. If a user has clicked the button, we’ll set this line to “True” and a graph with the user’s sleep data will display.

Now, we can implement the functionality for our “Submit”, “View Graph” and “Logout” buttons. Add the following code below the main() function.

@app.route('/main', methods = ['POST'])
def submit_sleep():
    if request.form.get('submit'): # if submitting new sleep data
        time_entered = float(request.form.get('time'))
        date_entered = request.form.get('date')
        message = add_sleep(time_entered, date_entered, db.users.find_one({'username': current_user.get_id()}))
        if message:
          return message

    if request.form.get('logout'):
        logout_user()
        app.config['plotting'] = False
        return 'You logged out!'

    elif request.form.get('graph'):
        app.config['plotting'] = True

    return redirect(url_for('main'))

This function operates similarly to our login_or_register function:

  • If a user clicks “Submit”, the data they entered will be stored in their MongoDB entry via the add_sleep function (that we’ll create next). This function will return a string with an error message if it encounters an error, and None if it succeeds.
  • If a user clicks “Logout”, the user will be logged out.
  • If a user clicks “View Graph”, the app.config['plotting'] entry is set to True. Later, we’ll expand main.html to display the graph.

Let’s wrap up our button functionality by creating the add_sleep function that is called when a user clicks “Submit”. Add the following code above the submit_logout_plot function to create the add_sleep function:

def add_sleep(time, date, user):
    if not re.match("[0-9]{4}-[0-1][0-9]-[0-3][0-9]", date):
        return "Invalid date supplied."

    if time < 0.0 or time > 24.0:
        return "Sleep time must be between 0 and 24 hours."

    if 'date' in user:
        user['date'].append(date)
        user['time'].append(time)
    else: # adding sleep data for the first time
        user['date'] = [date]
        user['time'] = [time]

    # Update MongoDB 
    db.users.update_one({ 'username': user['username'] },
        { '$set': { 'date': user['date'],
          'time': user['time'] }})

Our add_sleep function takes three variables:

  • time: The number of hours slept that the user entered.
  • date: The calendar date the user selected.
  • user: The user’s MongoDB entry.

First, we validate the user’s input to ensure that a correctly formatted date has been provided and that the time given is not a negative number or larger than 24. If either value does not pass validation, we return a relevant error message from the function without writing to the database. Otherwise, we continue.

If a user has never entered any sleep data, we add a new 'date' and 'time' entry to the user’s MongoDB entry with the date and time entered. Otherwise, we take the data they entered and add it to their existing sleep data. Finally, we update the user’s MongoDB entry with db.users.update_one.

We’ve implemented functionality for all three buttons. Now we need to modify the main() function to pass the current user’s data to main.html. This will allow us to display their username on the page, and to graph their sleep data. Find the main() function and modify it like so:

@app.route('/main')
def main():
    if current_user.get_id() is None:
        return redirect(url_for('login')) # redirect to login page if not logged in

    user_data = db.users.find_one({ 'username': current_user.get_id() })
    return render_template("main.html", user=user_data, plot=app.config['plotting'])

First, we leverage Flask-Login’s anonymous users functionality to check if the current user is not logged in and, if so, we redirect them to the login page. If the current user is logged it, we retrieve their MongoDB entry and assign it to user_data. Then we pass this variable to the render_template function as user. This is how the line below will access and display the user’s name.

<h2>Hey, {{ user['username'] }}! Let's track your sleep.</h2>

As user_data contains the entire MongoDB user entry, our template will be able to access the current user’s sleep data from the user variable as well.

We’ve also passed the template the value of app.config['plotting'] in plot. This is how our application will know whether to display a graph on the /main page.

All that’s left now is to add the sleep data graph in our main.html file. After that, we can deploy our application to Code Capsules.

Adding the Plotly Graph

As mentioned at the beginning of the article, we’ll add the ability to graph sleep data thanks to Plotly. Plotly provides an external JavaScript library that we can use to create interactive graphs for the web.

In the main.html file, find the </form> line. Right below this line, add the following:

{% if plot %}
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<div id="graph">
    <script>
      var x = {{ user['date'] | safe }};
      var y = {{ user['time'] | safe }};

    var trace1 = {
    x: x,
    y: y,
    type: 'bar'
  };

  var data = [trace1];

  var layout = {

    title: {
      text:'My Sleep',
      font: {
        family:'Courier New, monospace',
        size: 24
      },
      xref: 'paper',
      x: 0.05,
    },
    xaxis: {
      title: {
        text: 'Date',
        font: {
          family:'Courier New, monospace',
          size: 18,
          color: '#7f7f7f'
        }
      },
    },
    yaxis: {
      title: {
        text: 'Time Slept (hrs)',
        font: {
          family:'Courier New, monospace',
          size: 18,
          color: '#7f7f7f'
        }
      }
    }
  };
  Plotly.newPlot('graph', data, layout);
    </script>
</div>
{% endif %}

Let’s break this down, starting with the line {% if plot %}. This line references the plot variable we created in our app.py file in the main function. If someone has clicked “View Graph”, we set plot to true. If plot is true, the HTML between {% if plot %} and {% endif %} will be included in the page served to the user; otherwise it will be left out.

The line <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> imports the Plotly graphing library. This is similar to an import statement in Python.

Under this line, we see a lot of code enclosed in <script>...</script> tags. This is all JavaScript code. In this code, we create two variables, x and y, which contain arrays of the dates and number of hours slept that a user has logged.

Note that we have set these variables as safe, which means that Jinja will not attempt to escape or encode any of the characters within them when it renders the HTML. This is dangerous to do with user input, which is why we validated both the date and time values in our Python code. If we had not validated them, a malicious user might be able to supply JavaScript code in the date or time input fields and alter the behavior of this page.

The code inside var trace1 = {...} tells Plotly which data to use for the x and y axes, and the type of graph we’ll make – a bar graph.

All the code in var layout = {...} effects things like x and y-axis labelling, font type, and size of font. Customize this to your liking!

Finally, The line Plotly.newPlot('graph', data, layout) creates the actual graph and displays it to a user.

Try running the application by opening a terminal in the sleep-tracker directory, activating the virtual environment and entering flask run. You should be able to register a new user account, log in, enter sleep data, and view your graph.

Preparing for Deployment

With our sleep tracker functionally complete, we need to make one last modification to our app.py file and add some files in the sleep-tracker directory before we can push our code to GitHub and deploy it on Code Capsules.

Creating environment variables

Before we push our code to GitHub, we need to remove our app.config['SECRET_KEY'] and the MongoDB user credentials. If we were to push our code now, anyone could use our secret key to forge user sessions on our application or our MongoDB credentials to alter our database. Luckily, there is an easy fix.

First, save your secret key and MongoDB credentials somewhere safe, outside this project’s directory so that you don’t lose them. Then, at the top of app.py, add the line:

import os

Then, replace the line:

app.config['SECRET_KEY'] = 'your-secret-key-here' 

with this:

app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') 

And replace this line:

client = pymongo.MongoClient('mongodb+srv://YOURUSERNAME:<password>@cluster0.e2fw3.mongodb.net/<dbname>?retryWrites=true&w=majorhostity')

with this:

client = pymongo.MongoClient('MONGO_CONNECTION_STRING')

os.getenv('SECRET_KEY') and os.getenv('MONGO_CONNECTION_STRING') will look for environment variables with the names “SECRET_KEY” and “MONGO_CONNECTION_STRING”. When we host the sleep tracker application on Code Capsules, we’ll set these environment variables to the values we removed from the code.

Creating a Procfile and requirements.txt

Code Capsules requires a couple of files to deploy our application: Procfile and requirements.txt. The first one tells Code Capsules how to run our application, and the second one tells it which libraries it needs to install.

To create the Procfile:

  1. Create a file named Procfile in your project directory (do not add a file extension).
  2. Open the Procfile, enter web: gunicorn app:app, and save the file. This tells Code Capsules to use the Gunicorn WSGI server to run app.py.

In the same terminal, activate the virtual environment and enter pip3 freeze > requirements.txt to create requirements.txt and populate it with all the libraries we’ve used to create this application.

Now we can push our code to GitHub. Create a GitHub repository and send every file and directory to GitHub, except for virtual env’s env directory.

Deploying the Sleep Tracker to Code Capsules

With all the files on GitHub, we can deploy the sleep tracer to Code Capsules:

  1. Log in to Code Capsules, and create a Team and Space as necessary.
  2. Link Code Capsules to the GitHub repository created previously.
  3. Enter your Code Capsules Space.
  4. Create a new Capsule, selecting the “Backend” capsule type.
  5. Select the GitHub repository containing the sleep tracker – leave “Repo subpath” empty and click “Next”.
  6. Leave the “Run Command” blank and click “Create Capsule”.

Now we just need to set those environment variables we mentioned previously.

Creating environment variables in Code Capsules

Let’s create set the environment variables, so our sleep tracker will work properly:

  1. Navigate to your Capsule.
  2. Click the “Config” tab.
  3. Add two environment variables, one named “SECRET_KEY” and another “MONGO_CONNECTION_STRING”. Enter the secret key and connection string values you saved earlier.

When done, make sure to click “Update”.

Now the sleep tracker is ready to try out! The application is complete.

What Next?

There are many ways to expand or improve this application. Some ideas include:

  • Improve the application’s styling – it’s fairly simple right now. If you are keen to learn more about CSS styling, this tutorial written by Mozilla is a great place to start.
  • Add a way for users to keep track of other data (calories, daily notes, exercise).
  • Display better-looking error messages, preferably somewhere in the current page.