Build a live polling web app with Python
You will need Python 3+ installed on your machine. A basic knowledge of Python and JavaScript will be helpful.
In this article, we will build a voting poll application that keeps track of each vote in a poll and broadcasts recent updates to all subscribed clients. We will be broadcasting the updates in realtime using Pusher and this will ensure that every connected user knows when a new vote has been made. There will also be an admin section that displays a chart. This chart will update in realtime and show voting statistics.
Here is what the final application will look like:
The top-left screen shows a browser window that loads the voting application and sends realtime updates. To show that these updates are propagated across every other connected client, we can see that the bottom-left screen also updates as a vote is cast on the former screen. Finally, the admin screen by the right displays the chart and delivers realtime voting statistics. There is also a database that stores the status of votes of each poll member.
We will build the backend server for this application using a Python framework called Flask. We will use this framework to develop a simple backend API that can respond to the requests we will be sending from our JavaScript frontend.
Prerequisites
To follow along with this tutorial, a basic knowledge of Python, Flask, and JavaScript (ES6 syntax) is required. You will also need the following installed:
Virtualenv is great for creating isolated Python environments, so we can install dependencies in an isolated environment, and not pollute our global packages directory.
Let’s install virtualenv
with this command:
$ pip install virtualenv
Setting up the app environment
Let’s create our project folder, and activate a virtual environment in it. Run the commands below:
$ mkdir python-poll-pusher
$ cd python-poll-pusher
$ virtualenv .venv
$ source .venv/bin/activate # Linux based systems
$ \path\to\env\Scripts\activate # Windows users
Now that we have the virtual environment setup, we can install Flask within it with this command:
$ pip install flask
Let’s install two more packages that will ensure that the application works correctly:
$ pip install -U flask-cors
$ pip install simplejson
Before we do anything else, we need to install the Pusher library as we will need that for realtime updates.
Setting up Pusher
The first step will be to get a Pusher Channels application. We will need the application credentials for our realtime features to work.
Go to the Pusher website and create an account. After creating an account, you should create a new application. Follow the application creation wizard and then you should be given your application credentials, we will use this later in the article.
We also need to install the Pusher Python Library to send events to Pusher. Install this using the command below:
$ pip install pusher
File and folder structure
We don’t need to create so many files and folders for this application since it’s a simple one. Here’s the file/folder structure:
├── python-poll-pusher
├── app.py
├── dbsetup.py
├── static
└── templates
The static
folder will contain the static files to be used as is defined by Flask standards. The templates
folder will contain the HTML templates. In our application, app.py
is the main entry point and will contain our server-side code. To keep things modular, we will write all the code that we need to interact with the database in dbsetup.py
.
Create the app.py
and dbsetup.py
files, and then the static
and templates
folders.
Building the backend
In the dbsetup.py
file, we will write all the code that is needed for creating a database and interacting with it. Open the dbsetup.py
file and paste the following:
import sqlite3, json
from sqlite3 import Error
def create_connection(database):
try:
conn = sqlite3.connect(database, isolation_level=None, check_same_thread = False)
conn.row_factory = lambda c, r: dict(zip([col[0] for col in c.description], r))
return conn
except Error as e:
print(e)
def create_table(c):
sql = """
CREATE TABLE IF NOT EXISTS items (
id integer PRIMARY KEY,
name varchar(225) NOT NULL,
votes integer NOT NULL Default 0
);
"""
c.execute(sql)
def create_item(c, item):
sql = ''' INSERT INTO items(name)
VALUES (?) '''
c.execute(sql, item)
def update_item(c, item):
sql = ''' UPDATE items
SET votes = votes+1
WHERE name = ? '''
c.execute(sql, item)
def select_all_items(c, name):
sql = ''' SELECT * FROM items '''
c.execute(sql)
rows = c.fetchall()
rows.append({'name' : name})
return json.dumps(rows)
def main():
database = "./pythonsqlite.db"
conn = create_connection(database)
create_table(conn)
create_item(conn, ["Go"])
create_item(conn, ["Python"])
create_item(conn, ["PHP"])
create_item(conn, ["Ruby"])
print("Connection established!")
if __name__ == '__main__':
main()
Next, run the dbsetup.py
file so that it creates a new SQLite database for us. We can run it with this command:
$ python dbsetup.py
We should see this text logged to the terminal — ‘Connection established!’ — and there should be a new file — pythonsqlite.db
— added to the project’s root directory.
Next, let’s open up the app.py
file and start writing the backend code that will handle incoming requests. Here, we are going to register three routes: the first two will handle the GET
requests that return the home and admin pages respectively. The last route will handle the POST
requests that attempt to update the status of a particular vote member, both on the user’s page and on the admin’s page.
In this file, we will instantiate a fresh instance of Pusher and use it to broadcast data through a channel that we will shortly define within the application. We will also import some of the database handling methods we defined in dbsetup.py
so that we can use them here.
Open the app.py
file and paste the following code:
from flask import Flask, render_template, request, jsonify, make_response
from dbsetup import create_connection, select_all_items, update_item
from flask_cors import CORS, cross_origin
from pusher import Pusher
import simplejson
app = Flask(__name__)
cors = CORS(app)
app.config['CORS_HEADERS'] = 'Content-Type'
# configure pusher object
pusher = Pusher(
app_id='PUSHER_APP_ID',
key='PUSHER_APP_KEY',
secret='PUSHER_APP_SECRET',
cluster='PUSHER_APP_CLUSTER',
ssl=True)
database = "./pythonsqlite.db"
conn = create_connection(database)
c = conn.cursor()
def main():
global conn, c
@app.route('/')
def index():
return render_template('index.html')
@app.route('/admin')
def admin():
return render_template('admin.html')
@app.route('/vote', methods=['POST'])
def vote():
data = simplejson.loads(request.data)
update_item(c, [data['member']])
output = select_all_items(c, [data['member']])
pusher.trigger(u'poll', u'vote', output)
return request.data
if __name__ == '__main__':
main()
app.run(debug=True)
First, we imported the required modules and objects, then we initialized a Flask app. Next, we ensured that the backend server can receive requests from a client on another computer, then we initialized and configured Pusher. We also registered some routes and defined the functions that will handle them.
Replace the
PUSHER_APP_*
keys with the values on your Pusher dashboard.
With the pusher
object instantiated, we can trigger events on whatever channels we define.
In the /vote
route, we trigger a ‘vote’ event on the ‘poll’ channel. The trigger method has the following syntax:
pusher.trigger("a_channel", "an_event", {key: "data to pass with event"})
You can find the docs for the Pusher Python library here, to get more information on configuring and using Pusher in Python.
The first two routes we defined will return our application’s view by rendering the index.html
and admin.html
templates. However, we are yet to create these files so there is nothing to render, let’s create the app view in the next step and start using the frontend to communicate with our Python backend API.
Setting up the app view
We need to create two files in the templates
directory. These files will be named index.html
and admin.html
, this is where the view for our code will live. The index
page will render the view that displays the voting page for users to interact with while the admin
page will display the chart that will update in realtime when a new vote is cast.
In the ./templates/index.html
file, you can paste this code:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Python Poll</title>
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css">
<style type="text/css">
.poll-member h1 {
cursor: pointer
}
.percentageBarParent{
height: 22px;
width: 100%;
border: 1px solid black;
}
.percentageBar {
height: 20px;
width: 0%;
}
</style>
</head>
<body>
<div class="main">
<div class="container">
<h1>What's your preferred language?</h1>
<div class="col-md-12">
<div class="row">
<div class="col-md-6">
<div class="poll-member Go">
<h1>Go </h1>
<div class="percentageBarParent">
<div class="percentageBar" id="Go"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="poll-member ">
<h1>Python </h1>
<div class="percentageBarParent">
<div class="percentageBar" id="Python"></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="poll-member PHP">
<h1>PHP </h1>
<div class="percentageBarParent">
<div class="percentageBar" id="PHP"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="poll-member Ruby">
<h1>Ruby </h1>
<div class="percentageBarParent">
<div class="percentageBar" id="Ruby"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript" src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
<script src="https://js.pusher.com/4.0/pusher.min.js"></script>
<script type="text/javascript" src="{{ url_for('static', filename='app.js') }}" defer></script>
</body>
</html>
Next, let’s copy and paste in this code into the ./templates/admin.html
file:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Python Poll Admin</title>
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css">
</head>
<body>
<div class="main">
<div class="container">
<h1>Chart</h1>
<div id="chartContainer" style="height: 300px; width: 100%;"></div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
<script src="https://js.pusher.com/4.0/pusher.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvasjs/1.7.0/canvasjs.js"></script>
<script src="{{ url_for('static', filename='admin.js') }}" defer></script>
</body>
</html>
That’s all for the index.html
and admin.html
files, we have described how they should be rendered on the DOM but our application lacks all forms of interactivity.
In the next section, we will write the scripts that will send the POST
requests to our Python backend server.
Communicating with the backend
Create two more files in the static
folder, one for the index
page and the other for the admin
page. These files are the scripts that will define how our application interacts with click events, communicates with the backend server for realtime updates and display a progress bar.
Let’s create the following files in the static
folder:
- app.js
- admin.js
In the ./static/app.js
file, we can paste the following:
var pollMembers = document.querySelectorAll('.poll-member')
var members = ['Go', 'Python', 'PHP', 'Ruby']
// Sets up click events for all the cards on the DOM
pollMembers.forEach((pollMember, index) => {
pollMember.addEventListener('click', (event) => {
handlePoll(members[index])
}, true)
})
// Sends a POST request to the server using axios
var handlePoll = function(member) {
axios.post('http://localhost:5000/vote', {member}).then((r) => console.log(r))
}
// Configure Pusher instance
const pusher = new Pusher('PUSHER_APP_KEY', {
cluster: 'PUSHER_APP_CLUSTER',
encrypted: true
});
// Subscribe to poll trigger
var channel = pusher.subscribe('poll');
// Listen to vote event
channel.bind('vote', function(data) {
for (i = 0; i < (data.length - 1); i++) {
var total = data[0].votes + data[1].votes + data[2].votes + data[3].votes
document.getElementById(data[i].name).style.width = calculatePercentage(total, data[i].votes)
document.getElementById(data[i].name).style.background = "#388e3c"
}
});
let calculatePercentage = function(total, amount) {
return (amount / total) * 100 + "%"
}
Replace the
PUSHER_APP_*
keys with the keys on your Pusher dashboard.
First, we registered click events on all the members of the poll, then we configure Axios to send a POST request whenever a user votes for a member of the poll. Next, we configured a Pusher instance to communicate with the Pusher service. Next, we register a listener for the events Pusher sends. Finally, we bind the events we are listening to on the channel we created.
Next, open the admin.js
file and paste in this code:
var dataPoints = [
{ label: "Go", y: 0 },
{ label: "Python", y: 0 },
{ label: "PHP", y: 0 },
{ label: "Ruby", y: 0 },
]
var chartContainer = document.querySelector('#chartContainer');
if (chartContainer) {
var chart = new CanvasJS.Chart("chartContainer", {
animationEnabled: true,
theme: "theme2",
data: [
{
type: "column",
dataPoints: dataPoints
}
]
});
chart.render();
}
Pusher.logToConsole = true;
// Configure Pusher instance
const pusher = new Pusher('PUSHER_APP_KEY', {
cluster: 'PUSHER_APP_CLUSTER',
encrypted: true
});
// Subscribe to poll trigger
var channel = pusher.subscribe('poll');
// Listen to vote event
channel.bind('vote', function(data) {
dataPoints = dataPoints.map(dataPoint => {
if(dataPoint.label == data[4].name[0]) {
dataPoint.y += 10;
}
return dataPoint
});
// Re-render chart
chart.render()
});
Replace the
PUSHER_APP_*
keys with the keys on your Pusher dashboard.
Just as we did in the previous code, here we also receive Pusher events and use the received data to update the chart on the admin’s page.
Our application is good to go! Now we can run the app using this command:
$ flask run
Now if we visit 127.0.0.1:5000 and 127.0.0.1:5000/admin we should see our app:
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 available on GitHub.
26 June 2018
by Neo Ighodaro