In the previous post I explained about configuring Flask-SocketIO, Nginx and Gunicorn. This post includes integrating Flask-SocketIO library to display notifications to users in real time.
Flask Config
For development we use the default web server that ships with Flask. For this, Flask-SocketIO fallsback to long-polling as its transport mechanism, instead of WebSockets. So to properly test SocketIO I wanted to work directly with Gunicorn (hence the previous post about configuring development environment). Also, not everyone needs to be bothered with the changes required to run it.
class DevelopmentConfig(Config):
DEVELOPMENT = True
DEBUG = True
# If Env Var `INTEGRATE_SOCKETIO` is set to 'true', then integrate SocketIO
socketio_integration = os.environ.get('INTEGRATE_SOCKETIO')
if socketio_integration == 'true':
INTEGRATE_SOCKETIO = True
else:
INTEGRATE_SOCKETIO = False
# Other stuff
SocketIO is integrated (in development env) if the developer has set the INTEGRATE_SOCKETIO
environment variable to “true”. In Production, our application runs on Gunicorn, and SocketIO integration must always be there.
Flow
To send message to a particular connection (or a set of connections) Flask-SocketIO provides Rooms. The connections are made to join a room and the message is sent in the room. So to send message to a particular user we need him to join a room, and then send the message in that room. The room name needs to be unique and related to just one user. The User database Ids could be used. I decided to keep user_{id}
as the room name for a user with id {id}
. This information (room name) would be needed when making the user join a room, so I stored it for every user that logged in.
@expose('/login/', methods=('GET', 'POST'))
def login_view(self):
if request.method == 'GET':
# Render template
if request.method == 'POST':
# Take email and password from form and check if
# user exists. If he does, log him in.
login.login_user(user)
# Store user_id in session for socketio use
session['user_id'] = login.current_user.id
# Redirect
After the user logs in, a connection request from the client is sent to the server. With this connection request the connection handler at server makes the user join a room (based on the user_id
stored previously).
@socketio.on('connect', namespace='/notifs')
def connect_handler():
if current_user.is_authenticated():
user_room = 'user_{}'.format(session['user_id'])
join_room(user_room)
emit('response', {'meta': 'WS connected'})
The client side is somewhat similar to this:
<script src="{{ url_for('static', filename='path/to/socket.io-client/socket.io.js') }}"></script>
<script type="text/javascript">
$(document).ready(function() {
var namespace = '/notifs';
var socket = io.connect(location.protocol + "//" + location.host + namespace, {reconnection: false});
socket.on('response', function(msg) {
console.log(msg.meta);
// If `msg` is a notification, display it to the user.
});
});
</script>
Namespaces helps when making multiple connections over the same socket.
So now that the user has joined a room we can send him notifications. The notification data sent to the client should be standard, so the message always has the same format. I defined a get_unread_notifs
method for the User
class that fetches unread notifications.
class User(db.Model):
# Other stuff
def get_unread_notifs(self, reverse=False):
"""Get unread notifications with titles, humanized receiving time
and Mark-as-read links.
"""
notifs = []
unread_notifs = Notification.query.filter_by(user=self, has_read=False)
for notif in unread_notifs:
notifs.append({
'title': notif.title,
'received_at': humanize.naturaltime(datetime.now() - notif.received_at),
'mark_read': url_for('profile.mark_notification_as_read', notification_id=notif.id)
})
if reverse:
return list(reversed(notifs))
else:
return notifs
This class method is used when a notification is added in the database and has to be pushed into the user SocketIO room.
def create_user_notification(user, action, title, message):
"""
Create a User Notification
:param user: User object to send the notification to
:param action: Action being performed
:param title: The message title
:param message: Message
"""
notification = Notification(user=user,
action=action,
title=title,
message=message,
received_at=datetime.now())
saved = save_to_db(notification, 'User notification saved')
if saved:
push_user_notification(user)
def push_user_notification(user):
"""
Push user notification to user socket connection.
"""
user_room = 'user_{}'.format(user.id)
emit('response',
{'meta': 'New notifications',
'notif_count': user.get_unread_notif_count(),
'notifs': user.get_unread_notifs()},
room=user_room,
namespace='/notifs')