In this tutorial, we will use Django Channels to create a real-time application that updates a list of users as they log in and out.
With WebSockets (via Django Channels) managing the communication between the client and the server, whenever a user is authenticated, an event will be broadcasted to every other connected user. Each user’s screen will change automatically, without them having to reload their browsers.
NOTE: We recommend that you have some experience with Django before beginning this tutorial. Also, you should be familiar with the concept of WebSockets.
Our application uses:
- Python (v3.6.0)
- Django (v1.10.5)
- Django Channels (v1.0.3)
- Redis (v3.2.8)
Objectives
By the end of this tutorial, you will be able to…
- Add Web sockets support to a Django project via Django Channels
- Set up a simple connection between Django and a Redis server
- Implement basic user authentication
- Leverage Django Signals to take action when a user logs in or out
Getting Started
First, create a new virtual environment to isolate our project’s dependencies:
12345 |
|
Install Django, Django Channels, and ASGI Redis, and then create a new Django project and app:
12345 |
|
NOTE: During the course of this tutorial, we will create a variety of different files and folders. Please refer to the folder structure from the project’s repository if you get stuck.
Next, download and install Redis. If you’re on a Mac, we recommend using Homebrew:
1 |
|
Start the Redis server in a new terminal window and make sure that it is running on its default port, 6379. The port number will be important when we tell Django how to communicate with Redis.
Complete the setup by updating INSTALLED_APPS
in the project’s settings.py file:
12345678910 |
|
Then Configure the CHANNEL_LAYERS
by setting a default backend and routing:
123456789 |
|
This uses a Redis backend which is also needed in production.
WebSockets 101
Normally, Django uses HTTP to communicate between the client and server:
- The client sends an HTTP request to the server.
- Django parses the request, extracts a URL, and then matches it to a view.
- The view processes the request and returns an HTTP response to the client.
Unlike HTTP, the WebSockets protocol allows bi-directional communication, meaning that the server can push data to the client without being prompted by the user. With HTTP, only the client that made a request receives a response. With WebSockets, the server can communicate with multiple clients simultaneously. As we will see later on in this tutorial, we send WebSockets messages using the ws://
prefix, as opposed to http://
.
NOTE: Before diving in, quickly review the Channels Concepts documentation.
Consumers and Groups
Let’s create our first consumer, which handle the basic connections between the client and the server. Create a new file called example_channels/example/consumers.py:
123456789 |
|
Consumers are the counterpart to Django views. Any user connecting to our app will be added to the “users” group and will receive messages sent by the server. When the client disconnects from our app, the channel is removed from the group, and the user will stop receiving messages.
Next, let’s set up routes, which work in almost the same manner as Django URL configuration, by adding the following code to a new file called example_channels/routing.py:
12345678 |
|
So, we defined channel_routing
instead of urlpatterns
and route()
instead of url()
. Notice that we linked our consumer functions to WebSockets.
Templates
Let’s write up some HTML that can communicate with our server via a WebSocket. Create a “templates” folder within “example” and then add an “example” folder within “templates” – “example_channels/example/templates/example”.
Add a _base.html file:
123456789101112131415161718 |
|
And user_list.html:
1234567891011121314151617 |
|
Now, when the client successfully opens a connection with the server using a WebSocket, we will see a confirmation message print to the console.
Views
Set up a supporting Django view to render our template within example_channels/example/views.py:
12345 |
|
Add the URL to example_channels/example/urls.py:
1234567 |
|
Update the project URL as well in example_channels/example_channels/urls.py:
1234567 |
|
Test
Ready to test?
1 |
|
NOTE: You can alternatively run
python manage.py runserver --noworker
andpython manage.py runworker
in two different terminals to test the interface and worker servers as two separate processes. Both methods work!
When you visit http://localhost:8000/, you should see the connection message print to the terminal:
123 |
|
User Authentication
Now that we have proven that we can open a connection, our next step is to handle user authentication. Remember: We want a user to be able to log into our app and see a list of all of the other users who are subscribed to that user’s group. First, we need a way for users to create accounts and log in. Begin by creating a simple login page that will allow a user to authenticate with a username and password.
Create a new file called log_in.html within “example_channels/example/templates/example”:
123456789101112131415 |
|
Next, update example_channels/example/views.py like so:
12345678910111213141516171819202122232425 |
|
Django comes with forms that support common authentication functionality. We can use the AuthenticationForm
to handle user login. This form checks the supplied username and password, then returns a User
object if a validated user is found. We log in the validated user and redirect them to our homepage. A user should also have to ability to log out of the application, so we create a logout view that provides that functionality and then takes the user back to the login screen.
Then update example_channels/example/urls.py:
123456789 |
|
We also need a way to create new users. Create a sign-up page in the same manner as the login by adding a new file called sign_up.html to “example_channels/example/templates/example”:
123456789101112131415 |
|
Notice that the login page has a link to the sign-up page, and the sign-up page has a link back to the login.
Add the following function to the views:
12345678910 |
|
We use another built-in form for user creation. After successful form validation, we redirect to the login page.
Make sure to import the form:
1 |
|
Update example_channels/example/urls.py again:
12345678910 |
|
At this point, we need to create a user. Run the server and visit http://localhost:8000/sign_up/
in your browser. Fill in the form with a valid username and password and submit it to create our first user.
NOTE: Try using
michael
as the username andjohnson123
as the password.
The sign_up
view redirects us to the log_in
view, and from there we can authenticate our newly created user.
After we log in, we can test our new authentication views.
Use the sign up form to create several new users in preparation for the next section.
Login Alerts
We have basic user authentication working, but we still need to display a list of users and we need the server to tell the group when a user logs in and out. We need to edit our consumer functions so that they send a message right after a client connects and right before a client disconnects. The message data will include the user’s username and connection status.
Update example_channels/example/consumers.py like so:
12345678910111213141516171819202122232425 |
|
Notice that we have added decorators to the functions to get the user from the Django session. Also, all messages must be JSON-serializable, so we dump our data into a JSON string.
Next, update example_channels/example/templates/example/user_list.html:
1234567891011121314151617181920212223242526272829303132333435363738394041424344 |
|
On our homepage, we expand our user list to display a list of users. We store each user’s username as a data attribute to make it easy to find the user item in the DOM. We also add an event listener to our WebSocket that can handle messages from the server. When we receive a message, we parse the JSON data, find the <li>
element for the given user, and update that user’s status.
Django does not track whether a user is logged in, so we need to create a simple model to do that for us. Create a LoggedInUser
model with a one-to-one connection to our User
model in example_channels/example/models.py:
1234567 |
|
Our app will create a LoggedInUser
instance when a user logs in, and the app will delete the instance when the user logs out.
Make the schema migration and then migrate our database to apply the changes.
12 |
|
Next, update our user list view, in example_channels/example/views.py, to retrieve a list of users to render:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152 |
|
If a user has an associated LoggedInUser
, then we record the user’s status as “Online”, and if not, the user is “Offline”. We also add a @login_required
decorator to both our user list and log out views to restrict access only to registered users.
Add the imports as well:
12 |
|
At this point, users can log in and out, which will trigger the server to send messages to the client, but we have no way of knowing which users are logged in when the user first logs in. The user only sees updates when another user’s status changes. This is where the LoggedInUser
comes into play, but we need a way to create a LoggedInUser
instance when a user logs in, and then delete it when that user logs out.
The Django library includes a feature known as signals that broadcasts notifications when certain actions occur. Applications can listen for those notifications and then act on them. We can exploit two helpful, built-in signals (user_logged_in
and user_logged_out
) to handle our LoggedInUser
behavior.
Within “example_channels/example”, add a new file called signals.py:
12345678910111213 |
|
We have to make the signals available in our app configuration, example_channels/example/apps.py:
12345678 |
|
Update example_channels/example/__init__.py as well:
1 |
|
Sanity Check
Now we are finished coding and are ready to connect to our server with multiple users to test our app.
Run the Django server, log in as a user, and visit the homepage. We should see a list of all of the users in our app, each with a status of “Offline”. Next, open a new Incognito window and log in as a different user and watch both screens. Right when we log in, the regular browser updates the user status to “Online”. From our Incognito window, we see that the user logged in also has a status of “Online”. We can test the WebSockets by logging in and out on our different devices with various users.
Observing the developer console on the client and the server activity in our terminal, we can confirm that WebSocket connections are being formed when a user logs in and destroyed when a user logs out.
1234567 |
|
NOTE: You could also use ngrok to expose the local server to the internet securely. Doing this will allow you to hit the local server from various devices such as your phone or tablet.
Closing Thoughts
We covered a lot in this tutorial – Django Channels, WebSockets, user authentication, signals, and some front-end development. The main take away is this: Channels extends the functionality of a traditional Django app by letting us push messages from the server to groups of users via WebSockets.
This is powerful stuff!
Think of some of the applications. We can create chat rooms, multiplayer games, and collaborative apps that allow users to communicate in real time. Even mundane tasks are improved with WebSockets. For example, instead of periodically polling the server to see if a long-running task has completed, the server can push a status update to the client when it finishes.
This tutorial just scratches the surface of what we can do with Django Channels, too. Explore the Django Channels documentation and see what else you can create.
Grab the final code from the django-example-channels repo. Cheers!