Build a to-do app with Flask
A basic understanding of Python and JavaScript are needed to follow this tutorial.
Flask is a really cool Python framework for building web applications. One of its major selling points is how simple it is to get started on building apps with it. In this tutorial, we will build a simple Flask CRUD application, and add realtime functionality to it using the superpowers Pusher grants us.
Our CRUD app is a simple realtime to-do list app that can find use in distributed teams, for example to manage their deliverables.
Here is what the final app will look like:
Prerequisites
To follow along properly, basic knowledge of Python, Flask and JavaScript (ES6 syntax) is needed. You will also need the following installed:
Virtualenv is great for creating isolated Python environments, so we can install dependencies (like Flask) in an isolated environment, and not pollute our global packages directory. To install virtualenv:
pip install virtualenv
Setup and Configuration
Installing Flask
Now, we can create our project folder, activate a virtual environment in it, and install Flask.
To activate a virtual environment:
mkdir realtime-todo
cd realtime-todo
virtualenv .venv
source .venv/bin/activate
To install Flask:
pip install flask
Setting up Pusher
We will be using Pusher to power our realtime updates. Head over to Pusher.com and register for a free account, if you don’t already have one. Then create an app on the dashboard, and copy out the app credentials (App ID, Key, Secret and Cluster). It is super straight-forward.
We also need to install the Pusher Python Library to communicate with Pusher on the backend:
pip install pusher
File and Folder Structure
We will keep things super simple and will only create a couple of files. Here’s the file/folder structure used:
├── realtime-todo
├── app.py
├── static
└── templates
The static
folder will contain the static files to be used as per Flask standards, and the templates
folder will contain the HTML templates for the app. App.py
is the main entrypoint for our app and will contain all our server-side code.
Building Our App Backend
Next, we will write code to show a simple view and create endpoints for adding, updating and deleting our to-do’s. We will not be persisting the data to a database, but will instead use Pusher events to broadcast data to all users subscribed to our channel.
Updating app.py
:
# ./app.py
from flask import Flask, render_template, request, jsonify
from pusher import Pusher
import json
# create flask app
app = Flask(__name__)
# configure pusher object
pusher = Pusher(
app_id='YOUR_APP_ID',
key='YOUR_APP_KEY',
secret='YOUR_APP_SECRET',
cluster='YOUR_APP_CLUSTER',
ssl=True
)
# index route, shows index.html view
@app.route('/')
def index():
return render_template('index.html')
# endpoint for storing todo item
@app.route('/add-todo', methods = ['POST'])
def addTodo():
data = json.loads(request.data) # load JSON data from request
pusher.trigger('todo', 'item-added', data) # trigger `item-added` event on `todo` channel
return jsonify(data)
# endpoint for deleting todo item
@app.route('/remove-todo/<item_id>')
def removeTodo(item_id):
data = {'id': item_id }
pusher.trigger('todo', 'item-removed', data)
return jsonify(data)
# endpoint for updating todo item
@app.route('/update-todo/<item_id>', methods = ['POST'])
def updateTodo(item_id):
data = {
'id': item_id,
'completed': json.loads(request.data).get('completed', 0)
}
pusher.trigger('todo', 'item-updated', data)
return jsonify(data)
# run Flask app in debug mode
app.run(debug=True)
In the code block above, after importing the needed modules and objects and initialising a Flask app, we initialise and configure Pusher. Remember to replace YOUR_APP_ID
and similar values with the actual values gotten from the Pusher dashboard for your app. With this pusher
object, we can then trigger events on whatever channels we define.
A clear example of this is seen in the addTodo()
procedure, where we trigger an item-added
event on the todo
channel with the trigger
method. The trigger method has the following syntax:
pusher.trigger('a_channel', 'an_event', {'some': 'data'})
You can find the docs for the Pusher Python library here, to get more information on configuring and using Pusher in Python.
In the code above, we also created an index route which is supposed to show our app view by rendering the index.html
template. In the next step, we will create this view and start communicating with our Python backend.
Creating Our App View
Now, we create our main app view in ./templates/index.html
. This is where the interface for our app will live.
First we will pull CSS for TodoMVC apps to take advantage of some pre-made to-do list app styles, and store the folder in the ./static
folder.
Next, we can write the basic markup for the view:
<!-- ./templates/index.html -->
<html>
<head>
<!-- link to the Todo MVC index.css file -->
<link rel="stylesheet" href="/static/todomvc-app-css/index.css">
<title>Realtime Todo List</title>
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>Todos</h1>
<input class="new-todo" placeholder="What needs to be done?"
autofocus="" onkeypress="addItem(event)">
</header>
<section class="main">
<ul class="todo-list"></ul>
</section>
<footer class="footer"></footer>
</section>
</body>
</html>
In the above markup, notice we added an addItem()
function to be called onkeypress
for the .new-todo
input. In the following steps we will define this function, as well as other JavaScript functions to handle the basic app functions and interact with our Python backend.
Creating, Removing and Updating To-do Items
Now, we can add the JavaScript code to interact with the to-do items. Whenever an item is to be added, removed or updated, we will make API calls to our backend to affect those changes. We will do this with the simple and intuitive Fetch API:
<!-- ./templates/index.html -->
<html>
<!-- // ... -->
<script>
// function that makes API call to add an item
function addItem(e) {
// if enter key is pressed on the form input, add new item
if (e.which == 13 || e.keyCode == 13) {
let item = document.querySelector('.new-todo');
fetch('/add-todo', {
method: 'post',
body: JSON.stringify({
id: `item-${Date.now()}`,
value: item.value,
completed: 0
})
})
.then(resp => {
// empty form input once a response is received
item.value = ""
});
}
}
// function that makes API call to remove an item
function removeItem(id) {
fetch(`/remove-todo/${id}`);
}
// function that makes API call to update an item
// toggles the state of the item between complete and
// incomplete states
function toggleComplete(elem) {
let id = elem.dataset.id,
completed = (elem.dataset.completed == "1" ? "0" : "1");
fetch(`/update-todo/${id}`, {
method: 'post',
body: JSON.stringify({ completed })
});
}
// helper function to append new ToDo item to current ToDo list
function appendToList(data) {
let html = `
<li id="${data.id}">
<div class="view">
<input class="toggle" type="checkbox" onclick="toggleComplete(this)"
data-completed="${data.completed}" data-id="${data.id}">
<label>${data.value}</label>
<button class="destroy" onclick="removeItem('${data.id}')"></button>
</div>
</li>`;
let list = document.querySelector(".todo-list")
list.innerHTML += html;
};
</script>
</body>
</html>
Note: The JavaScript Fetch API is great for making AJAX requests, although it requires a polyfill for older browsers. A great alternative is axios.
In the above block of code, we define 4 functions to help us interact with the items on our to-do list. The addItem()
function makes a POST API call to add a new item to the to-do list, with the value from our input field, we also try to mock a unique ID for each item by assigning them a value of item-${Date.now()}
(ideally this would be implemented by our data store, but it is beyond the scope of this tutorial). Lastly, we assign an initial state of 0
to the completed
property for each item, this is to show that the item is just added, and has not yet been completed.
The removeItem()
function makes a request to delete an item, while the toggleComplete()
function makes a request to update the completed
property of an item. An appendToList()
helper function is also defined to update our to-do list with new items, this helper function will be used in the next step when we start listening for events.
Listening For Events
In this step we will listen for events from Pusher, and update our app view based on the data received. Updating index.html
:
<!-- ./templates/index.html -->
<html>
<!-- .// -->
<script src="https://js.pusher.com/4.1/pusher.min.js"></script>
<script>
// Enable pusher logging for debugging - don't include this in production
Pusher.logToConsole = true;
// configure pusher
const pusher = new Pusher('YOUR_APP_KEY', {
cluster: 'eu', // gotten from Pusher app dashboard
encrypted: true // optional
});
// subscribe to `todo` public channel, on which we'd be broadcasting events
const channel = pusher.subscribe('todo');
// listen for item-added events, and update todo list once event triggered
channel.bind('item-added', data => {
appendToList(data);
});
// listen for item-removed events
channel.bind('item-removed', data => {
let item = document.querySelector(`#${data.id}`);
item.parentNode.removeChild(item);
});
// listen for item-updated events
channel.bind('item-updated', data => {
let elem = document.querySelector(`#${data.id} .toggle`);
let item = document.querySelector(`#${data.id}`);
item.classList.toggle("completed");
elem.dataset.completed = data.completed;
elem.checked = data.completed == 1;
});
// ...
</script>
</body>
</html>
The first thing we have to do here is to include the pusher-js library to help us communicate with the Pusher service. Next, we initialise the Pusher service by passing in our App Key, and some other options — for a full list of configuration options, you can check the docs here.
After successfully initialising Pusher and assigning it to the pusher
object we can then subscribe to the channel from which we want to receive events, in our case that’s the public todo
channel:
const channel = pusher.subscribe('todo');
Note: Pusher provides various types on channels, including Public, Private and Presence channels. Read about them here.
Finally, we bind
the various events we’re listening for on the channel. The bind()
method has the following syntax - channel.bind(event_name, callback_function)
Optionally, we can add a loader to the page, which would show whenever a request is made.
The final index.html
file would look like this, and our app should be ready now!
To run our app:
python app.py
And here is what the demo looks like:
Conclusion
In this tutorial, we have learned how to build a Python Flask project from scratch and add realtime functionality to it using Pusher and Vanilla JavaScript. The entire code for this tutorial is hosted on GitHub.
There are many other use cases for adding realtime functionality to Python applications. Do you have any more improvements, suggestions or use cases? Let us know in the comments!
13 October 2017
by Olayinka Omole