This is the second part of our sleep tracker web application tutorial, covering the creation of the sleep tracker interface.
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 database on MongoDB Atlas 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 visualise 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:
- A form with fields for the date and hours slept, so users can provide sleep data for different days.
- A way to view a graph of this data.
- A logout button, that logs the user out.
To do 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 in 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 come in handy soon – we'll use this line to tell whether or not 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 as 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, andNone
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 toTrue
. Later, we'll expandmain.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 Atlas
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 or not 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 with the help of 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 behaviour 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. Customise 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 of 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
:
- Create a file named
Procfile
in your project directory (do not add a file extension). - Open the
Procfile
, enterweb: gunicorn app:app
, and save the file. This tells Code Capsules to use the Gunicorn WSGI server to runapp.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 of the files on GitHub, we can deploy the sleep tracer to Code Capsules:
- Log in to Code Capsules, and create a Team and Space as necessary.
- Link Code Capsules to the GitHub repository created previously.
- Enter your Code Capsules Space.
- Create a new Capsule, selecting the "Backend" capsule type.
- Select the GitHub repository containing the sleep tracker – leave "Repo subpath" empty and click "Next".
- 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:
- Navigate to your Capsule.
- Click the "Config" tab.
- 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 want 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.
Top comments (0)