Django Realtime Chat App Tutorial - Simple Django Tutorial With Channels And Redis

Django Realtime Chat App Tutorial - Simple Django Tutorial With Channels And Redis

/ #Django


Learn how to build a simple realtime chat application using Django, Web sockets, Channels and Redis.

Table of contents

Introduction

In this tutorial, you will learn how to build a simple chat application from scratch using Django as the backend. To connect with the backend, I use Web Sockets (Vanilla JavaScript). To add async support to Django, I use Channels.

Redis is used as a in-memory cache for Channels. This can be skipped if you want, and use regular in-memory cache instead.

So that's basically what we're building. Let's open up a command line and get started!

Setup and installation

Let's begin with setting up the environment we're working in and activate it.

$ virtualenv chatty_env
$ source chatty_env/bin/activate

I always recommend using virtual environments for you projects. This makes it easier to maintain later, it's easier to deploy and similar.

Now that the environment is activated, we can install Django into. All packages we now install using pip, will only be available for this environment.

$ pip install django

This will install the latest stable version of Django and a few dependencies.

Great, but we also need a few more packages.

$ pip install channels channels_redis

So, what is Channels?
To quote the creators of it:
"Channels wraps Django’s native asynchronous view support, allowing Django projects to handle not only HTTP, but protocols that require long-running connections too."

Redis is used as a backing store for Channels. Like an in-memory cache.

And the last thing we need to install is a Redis server.

I'm going to use Docker for this to make things as simple as possible.

If you don't have docker installed on your computer, you need to install it before you can continue.

When you have docker running, you can run this command to start the redis server:

$ docker run -p 6379:6379 -d redis:5

Perfect, all of the software is installed and we can create the Django project.

$ django-admin startproject chatty
$ cd chatty

I want to have one Django app for storing the messages, serving the templates etc.

$ python manage.py startapp chat

Before I forget it, I just want to add the app to the list of installed apps in the settings file.

'chat',

And since we're here, we can add 'channels' as well to the list.

'channels'

And we can also configure Redis and tell Django which ASGI_APPLICATION to use, so everything there is okay.

ASGI_APPLICATION = "chatty.asgi.application"
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

The front page

Okay, let's create the frontpage where we can select chat room and fill in our username.

Inside the chat app, create a new folder called "templates", and a folder called "chat" inside there.

Then inside there, a file called "index.html".

<!DOCTYPE html>

<html>
  <head>
    <meta charset="utf-8"/>
    <title>Chatty</title>
    <!-- Include Bulma CSS framework -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.2/css/bulma.min.css">
  </head>

  <body>
    <section class="section">
      <div class="container">
        <div class="columns is-multiline">
          <div class="column is-6 is-offset-3 mb-6">
            <section class="hero is-primary">
              <div class="hero-body">
                <p class="title">Chatty</p>
                <p class="subtitle">A simple chat built with Django, Channels and Redis</p>
              </div>
            </section>
          </div>

          <div class="column is-4 is-offset-4">
            <div class="field">
              <label>Room name</label>

              <div class="control">
                <input class="input" type="text" placeholder="Room name" id="room-name-input">
              </div>
            </div>

            <div class="field">
              <label>Username</label>

              <div class="control">
                <input class="input" type="text" placeholder="Username" id="username-input">
              </div>
            </div>

            <div class="field">
              <div class="control">
                <a class="button is-info" id="room-name-submit">Submit</a>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>

    <script>
    </script>
  </body>
</html>

Let's also create a view to render this (chat/views.py)

def index(request):
  return render(request, 'chat/index.html')

Then to access this, we create a new file called "urls.py" in the chat folder.

from django.urls import path

from . import views

urlpatterns = [
  path('', views.index, name='index'),
]

Then we just need to include this in the main urls.py file inside the chatty folder.

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
  path('admin/', admin.site.urls),
  path('', include('chat.urls')),
]

Great, let's run the webserver, and open up http://127.0.0.1:8000 in your browser to see the results.

Let's add some functionality to the index page to handle the room name and user name.

// Automatically set the focus to the room name input
document.querySelector('#room-name-input').focus();

// If you hit "enter" on the keyboard, trigger the click method
document.querySelector('#room-name-input').onkeyup = function(e) {
  if (e.keyCode === 13) {
    document.querySelector('#room-name-submit').click();
  }
};

// When you submit the form, redirect the user to the room page
document.querySelector('#room-name-submit').onclick = function(e) {
  var roomName = document.querySelector('#room-name-input').value;
  var userName = document.querySelector('#username-input').value;

  window.location.replace(roomName + '/?username=' + userName);
};

So when you click the submit button, you will be redirected to the chat page and the username will also be added to the url.

Nothing inside the room is ready yet.

Chat page

Let's begin with the view for this (chat/views.py)

def room(request, room_name):
  username = request.GET.get('username', 'Anonymous')

  return render(request, 'chat/room.html', {'room_name': room_name, 'username': username})

Now we have a view for the rooms. This will make it possible to get the room name and username in the frontend.

We can also add this to the urlpatterns as well.

path('/', views.room, name='room'),

Next step is to create a basic template.

Create a new file called 'room.html'

<!DOCTYPE html>

<html>
  <head>
    <meta charset="utf-8"/>
    <title>Chatty</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.2/css/bulma.min.css">
  </head>

  <body>
    <section class="section">
      <div class="container">
        <div class="columns is-multiline">
            <div class="column is-6 is-offset-3">
              <section class="hero is-primary">
                <div class="hero-body">
                  <p class="title">Chatty</p>
                  <p class="subtitle">A simple chat built with Django, Channels and Redis</p>
                </div>
              </section>
            </div>

            <div class="column is-6 is-offset-3">
              <div class="box">     
                <div id="chat-messages">
                </div>
              </div>

              <div class="field">
                <div class="control">
                  <input class="input" type="text" placeholder="Message" id="chat-message-input">
                </div>
              </div>

              <div class="field">
                <div class="control">
                  <a class="button is-info" id="chat-message-submit">Submit</a>
                </div>
              </div>

              <small class="has-text-grey-light">Your username: {{ username }}</small>
            </div>
          </div>
       </div>
    </section>

    <script>
    </script>
  </body>
</html>

It doesn't have any functionality yet, but at least it showing your username and a few other things.

Above the script tags, I want to make it easy to access the username and room name for javascript.

{{ room_name|json_script:"json-roomname" }}
{{ username|json_script:"json-username" }}

Great, this will create a script tag with the information we need.

Next, we can create two variables inside the script tag to get this information.

const roomName = JSON.parse(document.getElementById('json-roomname').textContent);
const userName = JSON.parse(document.getElementById('json-username').textContent);

Now we've made a little bit of progress for the frontend and it's time to make some changes in the backend.

To communicate between the frontend with sockets and the backend, we need to create a consumer in the chat app.

So let's create a new file called "consumers.py"

import json

from channels.generic.websocket import AsyncWebsocketConsumer # The class we're using
from asgiref.sync import sync_to_async # Implement later

class ChatConsumer(AsyncWebsocketConsumer):
  async def connect(self):
    self.room_name = self.scope['url_route']['kwargs']['room_name']
    self.room_group_name = 'chat_%s' % self.room_name

    # Join room group
    await self.channel_layer.group_add(
      self.room_group_name,
      self.channel_name
    )

    await self.accept()

  async def disconnect(self, close_code):
    # Leave room group
    await self.channel_layer.group_discard(
      self.room_group_name,
      self.channel_name
  )

Next step is to set up the routing for this.

It's the same thing as the usual urls.py file, but I want it specific for the web socket routes.

Create a new file called "routing.py

from django.urls import path

from . import consumers

websocket_urlpatterns = [
  path('ws//', consumers.ChatConsumer.as_asgi()), # Using asgi
]

Open up "chatty/asgi.py

import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application

import chat.routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chatty.settings')

application = ProtocolTypeRouter({
  "http": get_asgi_application(),
  "websocket": AuthMiddlewareStack(
    URLRouter(
      chat.routing.websocket_urlpatterns
    )
  )
})

Things are becoming a little bit complicated now, but I hope it's possible to follow along.

Next step now is to add more functionality to the script inside room.html to connect to the server,

const chatSocket = new WebSocket(
  'ws://'
  + window.location.host
  + '/ws/'
  + roomName
  + '/'
);

chatSocket.onmessage = function(e) {
  console.log('onMessage');
};

chatSocket.onclose = function(e) {
  console.error('The socket closed unexpectedly');
};

Before we test this, we need to initialize the database where the sessions are stored.

$ python manage.py makemigrations
$ python manage.py migrate

We can also add the javascript for sending and receiving messages.

chatSocket.onmessage = function(e) {
  const data = JSON.parse(e.data);

  if (data.message) {
    document.querySelector('#chat-messages').innerHTML += ('' + data.username + ': ' + data.message + '
'); } else { alert('The message was empty!') } }; document.querySelector('#chat-message-input').focus(); document.querySelector('#chat-message-input').onkeyup = function(e) { if (e.keyCode === 13) { document.querySelector('#chat-message-submit').click(); } }; document.querySelector('#chat-message-submit').onclick = function(e) { const messageInputDom = document.querySelector('#chat-message-input'); const message = messageInputDom.value; chatSocket.send(JSON.stringify({ 'message': message, 'username': userName, 'room': roomName })); messageInputDom.value = ''; };

Now everything in the frontend should be ready.

But there's still some thing that needs to be done in the backend.

# Receive message from WebSocket
async def receive(self, text_data):
  data = json.loads(text_data)
  message = data['message']
  username = data['username']
  room = data['room']

  # Send message to room group
  await self.channel_layer.group_send(
    self.room_group_name,
    {
      'type': 'chat_message',
      'message': message,
      'username': username
    }
  )

# Receive message from room group
async def chat_message(self, event):
  message = event['message']
  username = event['username']

  # Send message to WebSocket
  await self.send(text_data=json.dumps({
    'message': message,
    'username': username
  }))

So if we test now in two windows, we should be able to talk to eachother.

The problem now is that if we refresh, the messages are gone.

Or if you open one more window, the messages is also gone.

To fix this, we need to store the messages in a database.

Storing messages

from django.db import models

class Message(models.Model):
  username = models.CharField(max_length=255)
  room = models.CharField(max_length=255)
  content = models.TextField()
  date_added = models.DateTimeField(auto_now_add=True)

  class Meta:
    ordering = ('date_added',)

We also need to get the messages in the view.

# Import the messages model
from .models import Message

# Get the messages from the database
messages = Message.objects.filter(room=room_name)[0:25]

# Add the messages to the context
'messages': messages

And we need to add them to the template:

{% for m in messages %}<b>{{ m.username }}</b>: {{ m.content }}<br>{% endfor %}

So now we get the messages there, but we also need to store them when their sent.

from .models import Message

@sync_to_async
def save_message(self, username, room, message):
  Message.objects.create(username=username, room=room, content=message)
# Place this line:
await self.save_message(username, room, message)

# Above this line
await self.channel_layer.group_send(

Scrolling to the bottom

<!-- Change the style attribute -->
<div id="chat-messages" style="max-height: 300px; overflow-y: scroll;">

And add some JavaScript:

/**
 * A function for findind the messages element, and scroll to the bottom of it.
 */
function scrollToBottom() {
    let objDiv = document.getElementById("chat-messages");
    objDiv.scrollTop = objDiv.scrollHeight;
}

// Add this below the function to trigger the scroll on load.
scrollToBottom();

And then add this line at the bottom of this function (chatSocket.onmessage = function(e)) to trigger the scroll after a message is sent:

scrollToBottom();

And that was it for this time.

I hope you liked this tutorial and if you want more content like this, please subscribe to my channel and remember to click the bell.

Video


Comments

Aditya Priyadarshi | Jun 18, 21 03:21

That was very nice, but how can I host it on heroku, like installing redis and all on and server, and btw the the comment box the placeholder text is Your Name instead of Comment


Stein Ove Helset | Jun 21, 21 07:47

Thank you :-D I don't have any experience with Heroku, so I really don't know :/ Whoops, that's fixed now :-)

Baqir Hussien | Jul 04, 21 10:18

Hello this was so good, and how to host this on pythonanywhere.com


Stein Ove Helset | Jul 05, 21 06:48

Thank you Baqir! I have never used python anywhere before, so I can't really help you. Doesn't they have any guides for hosting Django projects? There might not be possibilities for using Redis though.

Add comment

Newsletter

Subscribe to my weekly newsletter. One time per week I will send you a short summary of the tutorials I have posted in the past week.