Build an activity feed with Flask

Introduction

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:

activity-feed-flask-demo

Prerequisites

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

Setup and Configuration

Installing Flask

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

Setting up Pusher

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

File and Folder Structure

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.

Building the backend

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.

Creating Our App Views

The blog page

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.

The feed page

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:

activity-feed-flask-post-created

And that’s it! To run our app:

    python app.py

Conclusion

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!