A great way to track what users are doing in your application is to visualise their activities in a feed. This would be especially useful when creating a dashboard for your application.
In this tutorial, I will show you how to build a quick and easy realtime activity feed using Python (Flask), JavaScript and Pusher Channels. We will build a realtime blog, and a feed page which will show user activity from the blog.
Here is what the final app will look like:
To follow along properly, basic knowledge of Python, Flask and JavaScript (ES6 syntax) is needed. You will also need to install Python and virtualenv locally.
Virtualenv is a tool that helps us create isolated Python environments. This makes it possible for us to install dependencies (like Flask) in an isolated environment, and not pollute our global packages directory. To install virtualenv:
pip install virtualenv
As stated earlier, we will be developing using Flask, a web framework for Python. In this step, we will activate a virtual Python environment and install Flask for use in our project.
To activate a virtual environment:
1mkdir realtime-feed 2 cd realtime-feed 3 virtualenv .venv 4 source .venv/bin/activate
To install Flask:
pip install flask
Pusher is a service that makes it easy for us to supercharge our web and mobile applications with realtime updates. We will be using the Channels API primarily to power our realtime blog and activity feed. Head over to Pusher.com and register for a free account, if you don’t already have one.
Next, create a Channels app on the dashboard and copy out the app credentials (App ID, Key, Secret and Cluster), as we would be needing these in our app.
Now we can install the Pusher Python library to help our backend communicate with the Pusher service:
pip install pusher
Here is the folder structure for the app. We will only limit it to things necessary so as to avoid bloat:
1├── realtime-feed 2 ├── app.py 3 └── templates 4 ├── index.html 5 └── feed.html
The templates folder contains our HTML files, while app.py
will house all our server-side code. One of the great things about Flask is how it allows you to set up small web projects with minimal code and very few files.
Next, we will write some code to display our pages and handle requests from our app. We will use Pusher to handle the management of data sent to our backend. We will broadcast events, with corresponding data on a channel, and listen for these events in our app.
Let us start by importing the needed modules and configuring the Pusher object:
1# ./app.py 2 from flask import Flask, render_template, request, jsonify 3 from pusher import Pusher 4 import uuid 5 6 # create flask app 7 app = Flask(__name__) 8 9 # configure pusher object 10 pusher = Pusher( 11 app_id='YOUR_APP_ID', 12 key='YOUR_APP_KEY', 13 secret='YOUR_APP_SECRET', 14 cluster='YOUR_APP_CLUSTER', 15 ssl=True 16 )
In the code above, we initialise the Pusher object with the credentials gotten from the Pusher dashboard. Remember to replace YOUR_APP_ID
and similar values with the actual values for your own app.
Next we define the different routes in our app for handling requests. Updating app.py
:
1# ./app.py 2 3 # index route, shows index.html view 4 @app.route('/') 5 def index(): 6 return render_template('index.html') 7 8 # feed route, shows feed.html view 9 @app.route('/feed') 10 def feed(): 11 return render_template('feed.html')
The first 2 routes defined serve our two app views. The index
(or home) page which shows the blog, and the feed
page which shows the activity feed.
Note: The
render_template()
function renders a template from the template folder.
Now we can define API endpoints for interacting with the blog posts:
1# ./app.py 2 3 # store post 4 @app.route('/post', methods=['POST']) 5 def addPost(): 6 data = { 7 'id': "post-{}".format(uuid.uuid4().hex), 8 'title': request.form.get('title'), 9 'content': request.form.get('content'), 10 'status': 'active', 11 'event_name': 'created' 12 } 13 pusher.trigger("blog", "post-added", data) 14 return jsonify(data) 15 16 # deactivate or delete post 17 @app.route('/post/<id>', methods=['PUT','DELETE']) 18 def updatePost(id): 19 data = { 'id': id } 20 if request.method == 'DELETE': 21 data['event_name'] = 'deleted' 22 pusher.trigger("blog", "post-deleted", data) 23 else: 24 data['event_name'] = 'deactivated' 25 pusher.trigger("blog", "post-deactivated", data) 26 return jsonify(data)
The endpoints defined above broadcast events for various actions (storing posts, deactivating posts, deleting posts) via Pusher.
We use the configured pusher
object for broadcasting events on specific channels. To broadcast an event, we use the trigger()
method with the following syntax:
pusher.trigger('a_channel', 'an_event', {'some': 'data'})
Note: You can find the docs for the Pusher Python library here.
Pusher also grants us the ability to trigger events on various types of channels including Public, Private and Presence channels. Read about them here.
Finally, to start the app in debug mode:
1# ./app.py 2 3 # run Flask app in debug mode 4 app.run(debug=True)
You can find the full app.py
file here. In the next step, we will build the views for our app.
This will serve as the homepage, and is where our users will interact with blog posts (creating, deactivating and deleting them). In the index.html
file:
1<!-- ./templates/index.html --> 2 <html> 3 <head> 4 <title>Home!</title> 5 <!-- import Bulma CSS --> 6 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.0/css/bulma.min.css"> 7 <!-- custom styles --> 8 <style> 9 #post-list .card { 10 margin-bottom: 10px; 11 } 12 #post-list .card.deactivated { 13 opacity: 0.5; 14 cursor: not-allowed; 15 } 16 </style> 17 </head> 18 <body> 19 <section class="section"> 20 <div class="container"> 21 <h1 class="title">Realtime Blog</h1> 22 <p class="subtitle">Realtime blog built with <strong><a href="https://pusher.com" target="_blank">Pusher</a></strong>!</p> 23 24 <div class="columns"> 25 <div class="column"> 26 <form id="post-form"> 27 <div class="field"> 28 <label class="label">Title</label> 29 <div class="control"> 30 <input name="title" class="input" type="text" placeholder="Hello world"> 31 </div> 32 </div> 33 34 <div class="field"> 35 <label class="label">Content</label> 36 <div class="control"> 37 <textarea class="textarea" name="content" rows="10" cols="10"></textarea> 38 </div> 39 </div> 40 41 <div class="field"> 42 <button class="button is-primary">Submit</button> 43 </div> 44 </form> 45 </div> 46 47 <div class="column"> 48 <div id="post-list"></div> 49 </div> 50 51 </div> 52 53 </div> 54 </section> 55 </body> 56 </html>
The above code contains the basic markup for the homepage. We imported Bulma (a cool CSS framework) to take advantage of some pre-made styles.
Next, we will define some JavaScript functions to handle our app functions and communicate with our backend:
1<!-- ./templates/index.html --> 2 <!-- // ... --> 3 <script> 4 const form = document.querySelector('#post-form'); 5 6 // makes POST request to store blog post on form submit 7 form.onsubmit = e => { 8 e.preventDefault(); 9 fetch("/post", { 10 method: 'POST', 11 body: new FormData(form) 12 }) 13 .then(r => { 14 form.reset(); 15 }); 16 } 17 18 // makes DELETE request to delete a post 19 function deletePost(id) { 20 fetch(`/post/${id}`, { 21 method: 'DELETE' 22 }); 23 } 24 25 // makes PUT request to deactivate a post 26 function deactivatePost(id) { 27 fetch(`/post/${id}`, { 28 method: 'PUT' 29 }); 30 } 31 32 // appends new posts to the list of blog posts on the page 33 function appendToList(data) { 34 const html = ` 35 <div class="card" id="${data.id}"> 36 <header class="card-header"> 37 <p class="card-header-title">${data.title}</p> 38 </header> 39 <div class="card-content"> 40 <div class="content"> 41 <p>${data.content}</p> 42 </div> 43 </div> 44 <footer class="card-footer"> 45 <a href="#" onclick="deactivatePost('${data.id}')" class="card-footer-item">Deactivate</a> 46 <a href="#" onclick="deletePost('${data.id}')" class="card-footer-item">Delete</a> 47 </footer> 48 </div>`; 49 let list = document.querySelector("#post-list") 50 list.innerHTML += html; 51 }; 52 </script> 53 </body> 54 </html>
We make use of the JavaScript Fetch API to make AJAX requests to our backend. While this is great because the API is simple to use, note that it requires a polyfill for older browsers. A great alternative is axios.
Now that we have established communication with our backend, we can listen for events from Pusher, using the Pusher JavaScript client library:
1<!-- ./templates/index.html --> 2 <!-- // ... --> 3 <script src="https://js.pusher.com/4.1/pusher.min.js"></script> 4 <script> 5 // configure pusher 6 const pusher = new Pusher('YOUR_APP_KEY', { 7 cluster: 'YOUR_APP_CLUSTER', // gotten from Pusher app dashboard 8 encrypted: true // optional 9 }); 10 // subscribe to `blog` public channel 11 const channel = pusher.subscribe('blog'); 12 13 channel.bind('post-added', data => { 14 appendToList(data); 15 }); 16 17 channel.bind('post-deleted', data => { 18 const post = document.querySelector(`#${data.id}`); 19 post.parentNode.removeChild(post); 20 }); 21 22 channel.bind('post-deactivated', data => { 23 const post = document.querySelector(`#${data.id}`); 24 post.classList.add('deactivated'); 25 }); 26 27 // ... 28 29 </script> 30 </body> 31 </html>
In the code block above, we import the Pusher JavaScript client library, subscribe to the channel (blog
) on which we’re publishing events from our backend, and listen for those events.
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)
. We’re listening for 3 events on the blog view - post-added
, post-deleted
and post``-deactivated
.
Now that we have finished building the blog page, we can proceed to create the feed page and listen for the same set of events.
Finally we will build a simple page to display the events being triggered from our blog.
In the feed.html
file:
1<!-- ./templates/feed.html --> 2 <html> 3 <head> 4 <title>Activity Feed</title> 5 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> 6 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.0/css/bulma.min.css"> 7 </head> 8 <body> 9 <section class="section"> 10 <div class="container"> 11 <h1 class="title">Blog Realtime Activity Feed!</h1> 12 <div id="events"></div> 13 </div> 14 </section> 15 16 <!-- import Pusher-js library --> 17 <script src="https://js.pusher.com/4.1/pusher.min.js"></script> 18 19 <script> 20 // connect to Pusher 21 const pusher = new Pusher('YOUR_APP_KEY', { 22 cluster: 'YOUR_APP_CLUSTER', // gotten from Pusher app dashboard 23 encrypted: true // optional 24 }); 25 // subscribe to blog channel 26 const channel = pusher.subscribe('blog'); 27 28 // listen for relevant events 29 channel.bind('post-added', eventHandler); 30 channel.bind('post-deleted', eventHandler); 31 channel.bind('post-deactivated', eventHandler); 32 33 // handler function to show feed of events 34 function eventHandler (data) { 35 const html = ` 36 <div class="box"> 37 <article class="media"> 38 <div class="media-content"> 39 <div class="content"> 40 <p> 41 <strong>Post ${data.event_name}</strong> 42 <small> 43 <i class="fa fa-${ data.event_name == 'created' 44 ? `plus` 45 : data.event_name == 'deactivated' ? `ban` : `trash` 46 }"></i> 47 </small> 48 <br> 49 Post with ID [<strong>${data.id}</strong>] has been ${data.event_name} 50 </p> 51 </div> 52 </div> 53 </article> 54 </div>`; 55 let list = document.querySelector("#events") 56 list.innerHTML += html; 57 } 58 </script> 59 </body> 60 </html>
In the code above, we define an eventHandler()
function which acts as callback for all the events we’re listening for. The function simply gets the event which was triggered and lists it as seen in the image below:
And that’s it! To run our app:
python app.py
In a few easy steps, we have been able to build both a realtime blog page, and an activity feed to show events happening on the blog — this shows how well Pusher works with Flask for creating quick realtime applications.
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!