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 Channels 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.
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
Let’s create our project folder, and activate a virtual environment in it. Run the commands below:
1$ mkdir python-poll-pusher 2 $ cd python-poll-pusher 3 $ virtualenv .venv 4 $ source .venv/bin/activate # Linux based systems 5 $ \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:
1$ pip install -U flask-cors 2 $ pip install simplejson
Before we do anything else, we need to install the Pusher library as we will need that for realtime updates.
The first step will be to get a Pusher Channels application. We will need the application credentials for our realtime features to work.
To get started with Pusher Channels, sign up for a free Pusher account. Then to go the dashboard and create a Channels app instance. There, you will receive the app credentials.
We also need to install the Pusher Channels Python library to send events to Pusher. Install this using the command below:
$ pip install pusher
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:
1├── python-poll-pusher 2 ├── app.py 3 ├── dbsetup.py 4 ├── static 5 └── 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.
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:
1import sqlite3, json 2 from sqlite3 import Error 3 4 def create_connection(database): 5 try: 6 conn = sqlite3.connect(database, isolation_level=None, check_same_thread = False) 7 conn.row_factory = lambda c, r: dict(zip([col[0] for col in c.description], r)) 8 9 return conn 10 except Error as e: 11 print(e) 12 13 def create_table(c): 14 sql = """ 15 CREATE TABLE IF NOT EXISTS items ( 16 id integer PRIMARY KEY, 17 name varchar(225) NOT NULL, 18 votes integer NOT NULL Default 0 19 ); 20 """ 21 c.execute(sql) 22 23 def create_item(c, item): 24 sql = ''' INSERT INTO items(name) 25 VALUES (?) ''' 26 c.execute(sql, item) 27 28 def update_item(c, item): 29 sql = ''' UPDATE items 30 SET votes = votes+1 31 WHERE name = ? ''' 32 c.execute(sql, item) 33 34 def select_all_items(c, name): 35 sql = ''' SELECT * FROM items ''' 36 c.execute(sql) 37 38 rows = c.fetchall() 39 rows.append({'name' : name}) 40 return json.dumps(rows) 41 42 def main(): 43 database = "./pythonsqlite.db" 44 conn = create_connection(database) 45 create_table(conn) 46 create_item(conn, ["Go"]) 47 create_item(conn, ["Python"]) 48 create_item(conn, ["PHP"]) 49 create_item(conn, ["Ruby"]) 50 print("Connection established!") 51 52 if __name__ == '__main__': 53 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 Channels 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:
1from flask import Flask, render_template, request, jsonify, make_response 2 from dbsetup import create_connection, select_all_items, update_item 3 from flask_cors import CORS, cross_origin 4 from pusher import Pusher 5 import simplejson 6 7 app = Flask(__name__) 8 cors = CORS(app) 9 app.config['CORS_HEADERS'] = 'Content-Type' 10 11 # configure pusher object 12 pusher = Pusher( 13 app_id='PUSHER_APP_ID', 14 key='PUSHER_APP_KEY', 15 secret='PUSHER_APP_SECRET', 16 cluster='PUSHER_APP_CLUSTER', 17 ssl=True) 18 19 database = "./pythonsqlite.db" 20 conn = create_connection(database) 21 c = conn.cursor() 22 23 def main(): 24 global conn, c 25 26 @app.route('/') 27 def index(): 28 return render_template('index.html') 29 30 @app.route('/admin') 31 def admin(): 32 return render_template('admin.html') 33 34 @app.route('/vote', methods=['POST']) 35 def vote(): 36 data = simplejson.loads(request.data) 37 update_item(c, [data['member']]) 38 output = select_all_items(c, [data['member']]) 39 pusher.trigger(u'poll', u'vote', output) 40 return request.data 41 42 if __name__ == '__main__': 43 main() 44 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 Channels. We also registered some routes and defined the functions that will handle them.
NOTE: 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 Channels Python library here, to get more information on configuring and using Pusher Channels 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.
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:
1<!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>Python Poll</title> 6 <link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"> 7 <style type="text/css"> 8 .poll-member h1 { 9 cursor: pointer 10 } 11 .percentageBarParent{ 12 height: 22px; 13 width: 100%; 14 border: 1px solid black; 15 } 16 .percentageBar { 17 height: 20px; 18 width: 0%; 19 } 20 </style> 21 </head> 22 <body> 23 <div class="main"> 24 <div class="container"> 25 <h1>What's your preferred language?</h1> 26 <div class="col-md-12"> 27 <div class="row"> 28 <div class="col-md-6"> 29 <div class="poll-member Go"> 30 <h1>Go </h1> 31 <div class="percentageBarParent"> 32 <div class="percentageBar" id="Go"></div> 33 </div> 34 </div> 35 </div> 36 <div class="col-md-6"> 37 <div class="poll-member "> 38 <h1>Python </h1> 39 <div class="percentageBarParent"> 40 <div class="percentageBar" id="Python"></div> 41 </div> 42 </div> 43 </div> 44 </div> 45 <div class="row"> 46 <div class="col-md-6"> 47 <div class="poll-member PHP"> 48 <h1>PHP </h1> 49 <div class="percentageBarParent"> 50 <div class="percentageBar" id="PHP"></div> 51 </div> 52 </div> 53 </div> 54 <div class="col-md-6"> 55 <div class="poll-member Ruby"> 56 <h1>Ruby </h1> 57 <div class="percentageBarParent"> 58 <div class="percentageBar" id="Ruby"></div> 59 </div> 60 </div> 61 </div> 62 </div> 63 </div> 64 </div> 65 </div> 66 67 <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.js"></script> 68 <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> 69 <script type="text/javascript" src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script> 70 <script src="https://js.pusher.com/4.0/pusher.min.js"></script> 71 <script type="text/javascript" src="{{ url_for('static', filename='app.js') }}" defer></script> 72 </body> 73 </html>
Next, let’s copy and paste in this code into the ./templates/admin.html
file:
1<!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>Python Poll Admin</title> 6 <link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"> 7 </head> 8 <body> 9 10 <div class="main"> 11 <div class="container"> 12 <h1>Chart</h1> 13 <div id="chartContainer" style="height: 300px; width: 100%;"></div> 14 </div> 15 </div> 16 17 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> 18 <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script> 19 <script src="https://js.pusher.com/4.0/pusher.min.js"></script> 20 <script src="https://cdnjs.cloudflare.com/ajax/libs/canvasjs/1.7.0/canvasjs.js"></script> 21 <script src="{{ url_for('static', filename='admin.js') }}" defer></script> 22 </body> 23 </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.
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:
In the ./static/app.js
file, we can paste the following:
1var pollMembers = document.querySelectorAll('.poll-member') 2 3 var members = ['Go', 'Python', 'PHP', 'Ruby'] 4 5 // Sets up click events for all the cards on the DOM 6 pollMembers.forEach((pollMember, index) => { 7 pollMember.addEventListener('click', (event) => { 8 handlePoll(members[index]) 9 }, true) 10 }) 11 12 // Sends a POST request to the server using axios 13 var handlePoll = function(member) { 14 axios.post('http://localhost:5000/vote', {member}).then((r) => console.log(r)) 15 } 16 17 // Configure Pusher instance 18 const pusher = new Pusher('PUSHER_APP_KEY', { 19 cluster: 'PUSHER_APP_CLUSTER', 20 encrypted: true 21 }); 22 23 // Subscribe to poll trigger 24 var channel = pusher.subscribe('poll'); 25 26 // Listen to vote event 27 channel.bind('vote', function(data) { 28 for (i = 0; i < (data.length - 1); i++) { 29 var total = data[0].votes + data[1].votes + data[2].votes + data[3].votes 30 document.getElementById(data[i].name).style.width = calculatePercentage(total, data[i].votes) 31 document.getElementById(data[i].name).style.background = "#388e3c" 32 } 33 }); 34 35 let calculatePercentage = function(total, amount) { 36 return (amount / total) * 100 + "%" 37 }
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 Channels 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:
1var dataPoints = [ 2 { label: "Go", y: 0 }, 3 { label: "Python", y: 0 }, 4 { label: "PHP", y: 0 }, 5 { label: "Ruby", y: 0 }, 6 ] 7 8 var chartContainer = document.querySelector('#chartContainer'); 9 10 if (chartContainer) { 11 var chart = new CanvasJS.Chart("chartContainer", { 12 animationEnabled: true, 13 theme: "theme2", 14 data: [ 15 { 16 type: "column", 17 dataPoints: dataPoints 18 } 19 ] 20 }); 21 22 chart.render(); 23 } 24 25 Pusher.logToConsole = true; 26 27 // Configure Pusher instance 28 const pusher = new Pusher('PUSHER_APP_KEY', { 29 cluster: 'PUSHER_APP_CLUSTER', 30 encrypted: true 31 }); 32 33 // Subscribe to poll trigger 34 var channel = pusher.subscribe('poll'); 35 36 // Listen to vote event 37 channel.bind('vote', function(data) { 38 dataPoints = dataPoints.map(dataPoint => { 39 if(dataPoint.label == data[4].name[0]) { 40 dataPoint.y += 10; 41 } 42 43 return dataPoint 44 }); 45 46 // Re-render chart 47 chart.render() 48 });
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:
In this tutorial, we have learned how to build a Python Flask project from scratch and add realtime functionality to it using Pusher Channels and vanilla JavaScript. The entire code for this tutorial is available on GitHub.