Build a live polling web app with Python

Introduction

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:

python-realtime-poll-demo

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:

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.

Setting up Pusher Channels

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.

python-realtime-poll-keys

We also need to install the Pusher Channels 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:

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.

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:

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.

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:

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&nbsp;&nbsp;</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&nbsp;&nbsp;</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&nbsp;&nbsp;</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&nbsp;&nbsp;</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.

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:

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:

python-realtime-poll-demo

Conclusion

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.