Django Channels “For example, updating the user’s online real-time status online
- Tram Ho
In this article, I will do a simple example of using Channels in Django to create users’ online-offline status real-time updates.
1. Installation and configuration
Download the project directory
In this article, I will use myproject / project directory which you can download by:
1 2 | >>> git clone https://github.com/batTrung/myproject-django >>> cd myproject-django |
Activate the virtual environment and run migrate
1 2 3 | >>> source env / bin / activate (env) >>> python manage.py makemigrations (env) >>> python manage.py migrate |
Conduct installation of Channels in virtual environment
1 2 | (env) >>> pip install channels (env) >>> pip install channels_redis |
settings
Edit the settings.py file to add Channels and Projects
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # myproject / settings.py INSTALLED_APPS = [ # ..... 'channels', ] # .... WSGI_APPLICATION = 'myproject.wsgi.application' ASGI_APPLICATION = 'myproject.routing.application' CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('127.0.0.1', 6379)], }, }, } |
routing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # myproject / routing.py from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack from django.urls import path from accounts.consumers import NewUserConsumer application = ProtocolTypeRouter ({ 'websocket': AuthMiddlewareStack ( URLRouter ( [ path ("new-user /", NewUserConsumer), ] ) ) }) |
2. Create Accounts application
We will create an application named accounts.
1 | env) >>> python manage.py startapp accounts |
Our project structure will be as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | myproject-django ─── accounts │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ ├── models.py │ ├── tests.py │ └── views.py ├── db.sqlite3 ├── env / ├── manage.py ├── myproject │ ├── __init__.py │ ├── routing.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── README.md ├── requirements.txt ├── site_static │ ├── css │ │ ├── bootstrap.min.css │ │ └── font-awesome.min.css │ └── js │ ├── bootstrap.min.js │ └── jquery-3.3.1.min.js └── templates ├── base.html └── home.html |
views
At the file views.py of the accounts / directory specified as follows:
1 2 3 4 5 6 7 8 | # accounts / views.py from django.shortcuts import render from django.contrib.auth.models import User def new_user (request): users = User.objects.all () return render (request, 'new_user.html', {'users': users}) |
URL
Create a file urls.py at the accounts directory / to determine the url link to the views.
1 2 3 4 5 6 7 | # accounts / urls.py from django.urls import path from. import views urlpatterns = [ path ('', views.new_user, name = 'new_user'), ] |
In the file urls.py of the mypoject / project directory we edit to link to the above url as follows:
1 2 3 4 5 6 7 8 | # myproject / urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path ('admin /', admin.site.urls), path ("", include ('accounts.urls')), ] |
Template
Create new_user.html file to determine the template. ‘
1 2 3 4 5 6 7 8 9 10 | {% extends "base.html"%} {% load static%} {% block title%} How often when new user {% endblock title%} {% block content%} <h2> NEW USER </h2> {% endblock content%} |
Now open the browser on http://127.0.0.1:8000 to look like this:
3. Consumers
We now determine our consumers.
Create an consumers.py file in the accounts / directory as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # accounts / consumers.py from channels.generic.websocket import AsyncJsonWebsocketConsumer NewUserConsumer class (AsyncJsonWebsocketConsumer): async def connect (self): print ('connect') await self.accept () await self.channel_layer.group_add ("users", self.channel_name) print (f "Add {self.channel_name} channel to users's group") async def receive_json (self, message): print ("receive", message) async def disconnect (self, close_code): await self.channel_layer.group_discard ("users", self.channel_name) print (f "Remove {self.channel_name} channel from users's group") |
Consumer helps communicate handling events, and sends messages back to the browser.
There are several subclasses of consumers you can use in addition to AsyncConsumer and SyncConsumer like:
- WebsocketConsumer and AsyncWebsocketConsumer
- JsonWebsocketConsumer and AsyncJsonWebsocketConsumer
- AsyncHttpConsumer
And here I use AsyncJsonWebsocketConsumer to actually transmit and receive via JSON encryption. It has properties like:
- connect (): Execute when the Consumer is connected.
- receive_json (): Executes when a message is received through the send method of WebSocket
- disconnect (): Executes when a consumer disconnects
- accept () Use accept () to accept WebSocket connection, if not call this method in connect (), it will reject WebSocket and will be closed.
- channel_layer (), channel_name (): Each consumer when the connection is automatically added to the channel_layer () and channel_name () attributes. When a request comes to us, there will be channel_name ().
- Group: We will use group_add to add this channel_name () to the group named “users”, and group_discard to remove this channel_name () from the group when the user exits or this consumer closes.
4. WebSocket
Now we will create WebSocket to communicate with Consumers. Edit the new_user.html file as follows
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | {% extends "base.html"%} {% load static%} {% block title%} How often when new user {% endblock title%} {% block content%} <h2> NEW USER </h2> {% endblock content%} <! - NEW -> {% block scripts%} <script src = "{% static" js / channels / reconnecting-websocket.min.js "%}"> </script> <script src = "{% static" js / channels / new_user.js "%}"> </script> {% endblock scripts%} |
In the first file we use reconnecting-websocket.min.js download at reconnecting-websocket. I will explain why use it later. Create a new_user.js file in the site_static / js / channels / directory as follows: site_static / channels / new_user.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | $ (function () { endpoint = 'ws: //127.0.0.1: 8000 / new-user /' // 1 var socket = new ReconnectingWebSocket (endpoint) // 2 // 3 socket.onopen = function (e) { console.log ("open", e); } socket.onmessage = function (e) { console.log ("message", e) } socket.onerror = function (e) { console.log ("error", e) } socket.onclose = function (e) { console.log ("close", e) } }); |
- First, we will connect WebSocket to the path ws: //127.0.0.1: 8000 / new-user / where we previously defined the router as path (“new-user /”, NewUserConsumer).
These two must match for example if the router is path (“new-user-update /”, NewUserConsumer), the path must be ws: //127.0.0.1: 8000 / new-user-update /
- As mentioned above, here you can use as var socket = new WebSocket (endpoint). However, using ReconnectingWebSocket will make WebSocket automatically reconnect when WebSocket is closed. When it is onclose, it will automatically turn onopen.
- Methods onopen, onclose, onmessage to determine WebSocket status to connect, close and receive messages. Use console.log to make it easier to check errors.
5. Templates
Now everything is ready, but we haven’t done anything yet. The task here is to automatically update the online-offline status of the user. Add the new_user.html file as follows: templates / new_user.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | {% block content%} <h2> NEW USER </h2> <p> Automatically update user information when users edit information or subscribe. </p> <table class = "table"> <thead> <tr> <th> username </th> <th> Firstname </th> <th> Lastname </th> <th> Email </th> </tr> </thead> <tbody id = "new_user"> {% include "includes / users.html"%} </tbody> </table> {% endblock content%} |
Create a users.html file in the templates / includes / directory as follows:
1 2 3 4 5 6 7 8 | {% for user in users%} <tr> <td> {{user.username}} </td> <td> {{user.first_name}} </td> <td> {{user.last_name}} </td> <td> {{user.email}} </td> </tr> {% endfor%} |
Opening the browser at http://127.0.0.1:8000/ will look like this:
There is no information because we have not created any users yet. You add yourself some users at the admin page. After adding 3 users it will look like this:
6. Update status
models
To know the status of online or offline we need to add a status field for the User model. By locating the Profile model and connecting OneToOne to the User as follows: accocunts / models.py
1 2 3 4 5 6 7 8 9 10 | # accounts / models.py from django.db import models from django.conf import settings Profile class (models.Model): user = models.OneToOneField (settings.AUTH_USER_MODEL, on_delete = models.CASCADE, related_name = 'profile') status = models.BooleanField (default = False) def __str __ (self): return f "Profile of {self.user.username}" |
Default will let False be the Offline status
Running migration:
1 2 | (env) >>> python manage.py makemigrations (env) >>> python manage.py migrate |
Consumers
Now go to the most important part, edit the consumers to check the online-offline state.
- When the user is online: Consumer is in connect state -> update status = True -> update the page.
- When the offline user: Cusumer is disconnected -> update status = False -> update the page. Customize the consumer as follows:
accounts / consumers.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | import asyncio import json from channels.consumer import AsyncConsumer from channels.db import database_sync_to_async from channels.generic.websocket import AsyncJsonWebsocketConsumer from .models import Profile from django.contrib.auth.models import User from django.template.loader import render_to_string NewUserConsumer class (AsyncJsonWebsocketConsumer): async def connect (self): await self.accept () await self.channel_layer.group_add ("users", self.channel_name) user = self.scope ['user'] if user.is_authenticated: selfwaiting self.update_user_status (user, True) await self.send_status () async def disconnect (self, code): await self.channel_layer.group_discard ("users", self.channel_name) user = self.scope ['user'] if user.is_authenticated: await self.update_user_status (user, False) await self.send_status () async def send_status (self): users = User.objects.all () html_users = render_to_string ("includes / users.html", {'users': users}) await self.channel_layer.group_send ( 'users', { "type": "user_update", "event": "Change Status", "html_users": html_users } ) async def user_update (self, event): await self.send_json (event) print ('user_update', event) @database_sync_to_async def update_user_status (self, user, status): return Profile.objects.filter (user_id = user.pk) .update (status = status) |
- Because it is not possible to use the synchronous code asynchronously together. So here we use database_sync_to_async provided by Channels to allow us to access the database and edit the Profile.
- The update_user_status () method will be responsible for updating the status of the user.
- The send_status () method uses send_json to send information to Websocket as JSON code. The Websocket side will receive information at the onmessage method with the data name.
- Using the group_send method includes 2 arguments. The first argument is the group name as “users”, the second argument contains the information to be sent.
- type: Specifies the method name to be executed in the consumer. This means that each time you call group_send (), it will execute the method that the type determines.
- html_users: This is the HTML code that contains user information that has been updated to the status. It will be used to replace the old html code with Jquery to update the status.
Websocket
Now WebSocket can receive messages in onmessage. Now we need to deal with that information. Correct the new_user.js file in the site_static / js / channels / directory as follows
1 2 3 4 5 6 7 8 9 | $ (function () { // ... socket.onmessage = function (e) { console.log ("message", e) var userData = JSON.parse (e.data) $ ('# new_user'). html (userData.html_users) } // .... }); |
As mentioned, the information received from consumers sent to Websocket is JSON code with the name data. So here we need to use JSON method in JS to read it. With the information received, we will replace the HTML code at the tag with the new_user id of new_user.html with Jquery.
Achievement
So we are done with the task of updating real-time online status – offline of the user. The result may look like this:
Just a little more
Every time we log out we have to go to admin to do it. That’s a little troublesome during the test. We will add the logout logon button as follows
URL
Edit the urls.py file of the accounts / directory as follows:
1 2 3 4 5 6 7 8 9 10 | # accounts / urls.py from django.urls import path from django.contrib.auth import views as auth_views from. import views urlpatterns = [ path ('', views.new_user, name = 'new_user'), path ('login /', auth_views.LoginView.as_view (), name = 'login'), path ('logout /', auth_views.LogoutView.as_view (), name = 'logout'), ] |
Here I use Django’s available application at django.contrib.auth to perform the login and logout process.
templates
Create registration / directory at templates / directory. In this directory, create 2 files, login.html and logged_out.html. These two files are the files that the auth application will point to.
log in
templates / registration / login.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | {% extends "base.html"%} {% load crispy_forms_tags%} {% block title%} Login {% endblock title%} {% block stylesheet%} <style> .form-signin { max-width: 380px; padding: 15px 35px 45px; margin: 0 auto; margin-top: 30px; background-color: #fff; border: 1px solid rgba (0.0,0,0.1); } </style> {% endblock stylesheet%} {% block content%} <div class = "wrapper"> <form class = "form-signin" method = "post"> {% csrf_token%} {{form | crispy}} <button class = "btn btn-lg btn-primary btn-block" type = "submit"> Login </button> </form> </div> {% endblock content%} |
Log out
templates / registration / logged_out.html
1 2 3 4 5 6 7 8 9 10 | {% extends "base.html"%} {% block title%} Logout {% endblock title%} {% block content%} You have successfully logged out. <a href="{% url'login' %}"> Log in </a> to continue {% endblock content%} |
navbar
Add the navbar with the login and logout buttons as follows: templates / base.html
1 2 3 4 | <! --- .... -> <body> {% include "includes / navbar.html"%} <! - .... -> |
In includes folder / create navbar.html file as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <nav class = "navbar navbar-light bg-light"> <form class = "form-inline"> {% if request.user.is_authenticated%} <div class = "p-2"> <a href="{% url'logout' %}" class="btn btn-secondary btn-lg active" role="button" aria-pressed="true"> Sign out </a> </div> <div class = "p-2" style = "color: # 380e16"> Hi <strong> {{request.user.username}} </strong> </div> {% else%} <a href="{% url'login' %}" class="btn btn-primary btn-lg active" role="button" aria-pressed="true"> Sign in </a> {% endif%} </form> </nav> |
setting
Add the setting file as follows:
1 2 3 4 | # .... LOGIN_REDIRECT_URL = 'new_user' LOGIN_URL = 'login' LOGOUT_URL = 'logout' |
Here will determine the URL for the login, logout, and redirect page after successful login.
Result
Source : viblo