Introduction
In the past, if we needed to develop a web platform that kept track of user behaviors and displayed changes accordingly, such as on the admin dashboard, we would have to reload the dashboard on a regular basis – generally instinctively — to check for new updates.
Nowadays, however, we can create a completely interactive online application that receives real-time changes. We'll create an interactive website with a dashboard that shows real-time updates on user actions in this lesson. For the purposes of this article, we'll use the Flask Python framework to create the backend server. To send HTTP queries and connect with the backend API, we'll utilize JavaScript on the frontend.
Requirements
A basic understanding of Python, Flask, and JavaScript (ES6 syntax) is necessary to follow along with this lesson. You'll also need to have the following software installed:
- Python (>= v3.x).
- virtualenv.
- Flask.
Virtualenv is excellent for building isolated Python environments, allowing us to install dependencies without polluting our global packages directory.
install virtualenv
with this command:
$ pip install virtualenv
Configuring the app's environment
Let's start by making a project folder and activating a virtual environment inside of it:
$ mkdir pusher-python-realtime-dashboard
$ cd pusher-python-realtime-dashboard
$ virtualenv .venv
$ source .venv/bin/activate # Linux based systems
$ \path\to\env\Scripts\activate # Windows users
Now that we've built up the virtual environment, we can run the following command to install Flask:
$ pip install flask
We'll require the Pusher library for real-time updates, therefore we'll need to install that.
Setup Pusher
Obtaining a Pusher Channels application will be the first step. For our real-time features to operate, we'll need the application credentials.
Create an account on the Pusher website. You should create a new application after creating an account. You should be given your application credentials after completing the application creation procedure; we'll need these later in the tutorial.
To transmit events to Pusher, we'll also need to install the Pusher Python Library. Install it with the following command:
$ pip install pusher
Structure of files and folders
Because this is a basic program, we don't need to generate a lot of files and directories. The following is the file/folder structure:
├── pusher-python-realtime-dashboard
├── app.py
├── static
└── templates
The static
folder will hold the static files that must be utilized according to Flask specifications. The HTML templates will be stored in the templates
folder. Our application's main entry point is app.py
, which contains our server-side code.
We'll start by creating the app.py
file, followed by the static
and templates
directories.
Let's build the Backend
Let's open the app.py
file and begin developing the backend code that will handle the HTTP requests that come in. We'll register five routes and their associated handler functions in this file. The /
and /dashboard
routes, respectively, will display the website and admin dashboard pages. These pages will be created as soon as possible.
Three additional routes will be defined: /orders
, /message
, and /customer
. These are going to be API endpoints. These endpoints will be in charge of processing POST
requests coming in from our frontend and receiving user data.
We'll also start a new Pusher instance and utilize it to broadcast data over three channels, one for each of the three user actions:
- Make a purchase.
- Send an email
- Create a new customer account.
Paste the following code into the app.py
file:
from flask import Flask, render_template, request
from pusher import Pusher
app = Flask(__name__)
# configure pusher object
pusher = Pusher(
app_id='PUSHER_APP_ID',
key='PUSHER_APP_KEY',
secret='PUSHER_APP_SECRET',
cluster='PUSHER_APP_CLUSTER',
ssl=True)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/dashboard')
def dashboard():
return render_template('dashboard.html')
@app.route('/orders', methods=['POST'])
def order():
data = request.form
pusher.trigger(u'order', u'place', {
u'units': data['units']
})
return "units logged"
@app.route('/message', methods=['POST'])
def message():
data = request.form
pusher.trigger(u'message', u'send', {
u'name': data['name'],
u'message': data['message']
})
return "message sent"
@app.route('/customer', methods=['POST'])
def customer():
data = request.form
pusher.trigger(u'customer', u'add', {
u'name': data['name'],
u'position': data['position'],
u'office': data['office'],
u'age': data['age'],
u'salary': data['salary'],
})
return "customer added"
if __name__ == '__main__':
app.run(debug=True)
We imported the required modules and objects, then initialized a Flask app, in the code above. After that, we registered the routes and their associated handler functions, as well as initialized and configured Pusher.
Replace the PUSHERAPP* keys with the values on your Pusher dashboard.
We may use the pusher
object to trigger events on any of the channels we specify.
We initiate events on three channels in the handler routines of the /orders
, /message
, and /customer
routes. The following is the syntax for the trigger method:
pusher.trigger("a_channel", "an_event", {key: "data to pass with event"})
More information on setting and using Pusher in Python may be found in the Pusher Python library documentation.
The index.html
and dashboard.html
templates will be rendered by the /
and /dashboard
routes, therefore we'll need to build these files and write the code to establish the frontend layout. The app view will be created next, and the frontend will be used to interface with the Python backend.
App View Setup
In the templates
directory, we'll need to make two files. The view for our code will be stored in these files, which will be titled index.html
and dashboard.html
. The index.html
page will be shown as the homepage when we visit our application's root URL. The dashboard.html file will be rendered on the browser when we visit the [/dashboard](http://127.0.0.1:5000/dashboard)
location.
You may paste the following code into the ./templates/index.html
file:
<!DOCTYPE html>
<html>
<head>
<title>Pusher Python Realtime Dashboard</title>
</head>
<body>
<form method="post" action="/orders">
<h3>Place a new order</h3>
<input type="number" name="units" placeholder="units"><br>
<input type="submit" name="Submit">
</form>
<form method="post" action="/message">
<h3>Send a new message</h3>
<input type="text" name="name" placeholder="name here"><br>
<textarea name="message" placeholder="message here"></textarea><br>
<input type="submit" name="Submit">
</form>
<form method="post" action="/customer">
<h3>Add new customer</h3>
<input type="text" name="name" placeholder="name here"><br>
<input type="text" name="position" placeholder="position here"><br>
<input type="text" name="office" placeholder="office here"><br>
<input type="number" name="age" placeholder="age here"><br>
<input type="text" name="salary" placeholder="salary here"><br>
<input type="submit" name="Submit">
</form>
</body>
</html>
We generated three forms with the POST
method and described their behaviors in the markup above. User data is transferred to the Python backend server that we built in the previous step whenever one of these forms is submitted.
We'll get some CSS
and JS from startbootstrap.com before writing the code for dashboard-single.html
and dashboard
files. Click Download after going to the URL. Unzip the file and place the css and js directories in our project's static directory. Let's move on to the frontend of our application now.
Paste the following into the ./templates/dashboard.html
file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>SB Admin - Start Bootstrap Template</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="https://cdn.datatables.net/1.10.16/css/dataTables.bootstrap4.min.css" rel="stylesheet">
<link href="{{ url_for('static', filename='css/sb-admin.css') }}" rel="stylesheet">
</head>
<body class="fixed-nav sticky-footer bg-dark" id="page-top">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top" id="mainNav">
<a class="navbar-brand" href="index.html">Start Bootstrap</a>
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav navbar-sidenav" id="exampleAccordion">
<li class="nav-item" data-toggle="tooltip" data-placement="right" title="Dashboard">
<a class="nav-link" href="/dashboard">
<i class="fa fa-fw fa-dashboard"></i>
<span class="nav-link-text">Dashboard</span>
</a>
</li>
</ul>
</div>
</nav>
<div class="content-wrapper">
<div class="container-fluid">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="#">Dashboard</a>
</li>
<li class="breadcrumb-item active">My Dashboard</li>
</ol>
<div class="row">
<div class="col-xl-3 col-sm-6 mb-3">
<div class="card text-white bg-primary o-hidden h-100">
<div class="card-body">
<div class="card-body-icon">
<i class="fa fa-fw fa-comments"></i>
</div>
<div class="mr-5"><span id="message-count">26</span> New Messages!</div>
</div>
<a class="card-footer text-white clearfix small z-1" href="#">
<span class="float-left">View Details</span>
<span class="float-right">
<i class="fa fa-angle-right"></i>
</span>
</a>
</div>
</div>
<div class="col-xl-3 col-sm-6 mb-3">
<div class="card text-white bg-warning o-hidden h-100">
<div class="card-body">
<div class="card-body-icon">
<i class="fa fa-fw fa-list"></i>
</div>
<div class="mr-5">11 New Tasks!</div>
</div>
<a class="card-footer text-white clearfix small z-1" href="#">
<span class="float-left">View Details</span>
<span class="float-right">
<i class="fa fa-angle-right"></i>
</span>
</a>
</div>
</div>
<div class="col-xl-3 col-sm-6 mb-3">
<div class="card text-white bg-success o-hidden h-100">
<div class="card-body">
<div class="card-body-icon">
<i class="fa fa-fw fa-shopping-cart"></i>
</div>
<div class="mr-5"><span id="order-count">123</span> New Orders!</div>
</div>
<a class="card-footer text-white clearfix small z-1" href="#">
<span class="float-left">View Details</span>
<span class="float-right">
<i class="fa fa-angle-right"></i>
</span>
</a>
</div>
</div>
<div class="col-xl-3 col-sm-6 mb-3">
<div class="card text-white bg-danger o-hidden h-100">
<div class="card-body">
<div class="card-body-icon">
<i class="fa fa-fw fa-support"></i>
</div>
<div class="mr-5">13 New Tickets!</div>
</div>
<a class="card-footer text-white clearfix small z-1" href="#">
<span class="float-left">View Details</span>
<span class="float-right">
<i class="fa fa-angle-right"></i>
</span>
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card mb-3">
<div class="card-header">
<i class="fa fa-bar-chart"></i> Revenue Chart</div>
<div class="card-body">
<div class="row">
<div class="col-sm-8 my-auto">
<canvas id="myBarChart" width="100" height="50"></canvas>
</div>
<div class="col-sm-4 text-center my-auto">
<div class="h4 mb-0 text-primary">$34,693</div>
<div class="small text-muted">YTD Revenue</div>
<hr>
<div class="h4 mb-0 text-warning">$18,474</div>
<div class="small text-muted">YTD Expenses</div>
<hr>
<div class="h4 mb-0 text-success">$16,219</div>
<div class="small text-muted">YTD Margin</div>
</div>
</div>
</div>
<div class="card-footer small text-muted">Updated yesterday at 11:59 PM</div>
</div>
</div>
<div class="col-lg-4">
<!-- Example Notifications Card-->
<div class="card mb-3">
<div class="card-header">
<i class="fa fa-bell-o"></i> Message Feed</div>
<div class="list-group list-group-flush small">
<div id="message-box">
<a class="list-group-item list-group-item-action" href="#">
<div class="media">
<img class="d-flex mr-3 rounded-circle" src="http://placehold.it/45x45" alt="">
<div class="media-body">
<strong>Jeffery Wellings</strong>added a new photo to the album
<strong>Beach</strong>.
<div class="text-muted smaller">Today at 4:31 PM - 1hr ago</div>
</div>
</div>
</a>
<a class="list-group-item list-group-item-action" href="#">
<div class="media">
<img class="d-flex mr-3 rounded-circle" src="http://placehold.it/45x45" alt="">
<div class="media-body">
<i class="fa fa-code-fork"></i>
<strong>Monica Dennis</strong>forked the
<strong>startbootstrap-sb-admin</strong>repository on
<strong>GitHub</strong>.
<div class="text-muted smaller">Today at 3:54 PM - 2hrs ago</div>
</div>
</div>
</a>
</div>
<a class="list-group-item list-group-item-action" href="#">View all activity...</a>
</div>
<div class="card-footer small text-muted">Updated yesterday at 11:59 PM</div>
</div>
</div>
</div>
<!-- Example DataTables Card-->
<div class="card mb-3">
<div class="card-header">
<i class="fa fa-table"></i> Customer Order Record</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Age</th>
<th>Start date</th>
<th>Salary</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Age</th>
<th>Start date</th>
<th>Salary</th>
</tr>
</tfoot>
<tbody id="customer-table">
<tr>
<td>Cedric Kelly</td>
<td>Senior Javascript Developer</td>
<td>Edinburgh</td>
<td>22</td>
<td>2012/03/29</td>
<td>$433,060</td>
</tr>
<tr>
<td>Airi Satou</td>
<td>Accountant</td>
<td>Tokyo</td>
<td>33</td>
<td>2008/11/28</td>
<td>$162,700</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card-footer small text-muted">Updated yesterday at 11:59 PM</div>
</div>
</div>
<footer class="sticky-footer">
<div class="container">
<div class="text-center">
<small>Copyright © Your Website 2018</small>
</div>
</div>
</footer>
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<!-- Page level plugin JavaScript-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
<script src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.10.16/js/dataTables.bootstrap4.min.js"></script>
<script src="https://js.pusher.com/4.0/pusher.min.js"></script>
<script src="{{ url_for('static', filename='js/customer.js') }}"></script>
<script src="{{ url_for('static', filename='js/order.js') }}"></script>
<script src="{{ url_for('static', filename='js/message.js') }}"></script>
</div>
</body>
</html>
We imported the JQuery and JavaScript Pusher libraries and wrote the markup to specify the layout for the home and dashboard pages in the code above. We'll develop the JavaScript files that will handle the real-time changes in the next stage.
Communicate with the Back-end In the static directory, create a new folder named js and fill it with three new files:
order.js
— in this file, we will subscribe to theorder
channel and update the admin dashboard in realtime whenever a new order is placed.message.js
— in this file, we will subscribe to themessage
channel and update the admin dashboard in realtime whenever a new message is sent.customer.js
— in this file, we will subscribe to thecustomer
channel and update the admin dashboard in realtime whenever a new customer is registered.
Paste the following code in the ./static/js/order.js
file:
Chart.defaults.global.defaultFontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
Chart.defaults.global.defaultFontColor = '#292b2c';
var ctx = document.getElementById("myBarChart");
var myLineChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ["February", "March", "April", "May", "June", "July"],
datasets: [{
label: "Revenue",
backgroundColor: "rgba(2,117,216,1)",
borderColor: "rgba(2,117,216,1)",
data: [5312, 6251, 7841, 9821, 14984, 0],
}],
},
options: {
scales: {
xAxes: [{
time: {
unit: 'month'
},
gridLines: {
display: false
},
ticks: {
maxTicksLimit: 6
}
}],
},
legend: {
display: false
}
}
});
// Configure Pusher instance
const pusher = new Pusher('PUSHER_APP_KEY', {
cluster: 'PUSHER_APP_CLUSTER',
encrypted: true
});
// Subscribe to poll trigger
var orderChannel = pusher.subscribe('order');
// Listen to 'order placed' event
var order = document.getElementById('order-count')
orderChannel.bind('place', function(data) {
myLineChart.data.datasets.forEach((dataset) => {
dataset.data.fill(parseInt(data.units),-1);
});
myLineChart.update();
order.innerText = parseInt(order.innerText)+1
});
Replace the PUSHERAPP* keys with the keys on your Pusher dashboard.
We used the ID myBarChart
to target the bar chart on the dashboard page, and then we created its data object in the code above. Then, to interface with the Pusher service, we set up a Pusher instance. On the place
event, we register a listener and listen to the events that Pusher sends.
Then, in the ./static/js/message.js
file, paste the following code:
$(document).ready(function () {
$('.navbar-sidenav [data-toggle="tooltip"]').tooltip({
template: '<div class="tooltip navbar-sidenav-tooltip" role="tooltip" style="pointer-events: none;"><div class="arrow"></div><div class="tooltip-inner"></div></div>'
})
$('[data-toggle="tooltip"]').tooltip()
var messageChannel = pusher.subscribe('message');
messageChannel.bind('send', function(data) {
var message = document.getElementById('message-count')
var date = new Date();
var toAppend = document.createElement('a')
toAppend.classList.add('list-group-item', 'list-group-item-action')
toAppend.href = '#'
document.getElementById('message-box').appendChild(toAppend)
toAppend.innerHTML ='<div class="media">'+
'<img class="d-flex mr-3 rounded-circle" src="http://placehold.it/45x45" alt="">'+
'<div class="media-body">'+
`<strong>${data.name}</strong> posted a new message `+
`<em>${data.message}</em>.`+
`<div class="text-muted smaller">Today at ${date.getHours()} : ${date.getMinutes()}</div>`+
'</div>'+
'</div>'
message.innerText = parseInt(message.innerText)+1
});
});
We bind to the sent
event and listen for changes from Pusher, just as we did previously, and once there is an update, we display it on the admin dashboard.
Finally, insert the following code into the ./static/js/customer.js
file:
$(document).ready(function(){
var dataTable = $("#dataTable").DataTable()
var customerChannel = pusher.subscribe('customer');
customerChannel.bind('add', function(data) {
var date = new Date();
dataTable.row.add([
data.name,
data.position,
data.office,
data.age,
`${date.getFullYear()}/${date.getMonth()}/${date.getDay()}`,
data.salary
]).draw( false );
});
});
We subscribe to the customer
channel and bind to the add
event in the preceding code so that we can update the dashboard in real time whenever a new customer is added.
We've completed our construction! This command can be used to execute the application:
$ flask run
We should now see our app if we go to 127.0.0.1:5000
and 127.0.0.1:5000/dashboard
:
Conclusion
We learned how to create a Python Flask project from the ground up and integrate realtime features using Pusher and JavaScript in this lesson. Thanks.