Chat Application with Django-channels & Htmx
WebSocket is a computer communications protocol, that provides simultaneous two-way communication channels over a single Transmission Control Protocol connection. WebSockets enable communication between the server and the client without having to poll the server for the response.
In Django, Channels wraps Django’s native asynchronous view support, allowing Django projects to handle not only HTTP, but protocols that require long-running connections too — WebSockets, MQTT, chatbots, amateur radio, and more. WebSockets are popularly used when there’s a need to have real-time communication such as chatting applications or streaming events from the server. In this article, I am using WebSockets to demonstrate its use in developing a chat application.
We will start by installing django and django-channels. We will use those libraries to create the application. I am assuming that you are familiar with setting up a Django project and creating a Python environment.
#installing dependancies
python -m pip install django
python -m pip install -U 'channels[daphne]'
Notice ‘channels[daphne]’ this will install channels as well as the daphne ASGI server for development purposes. Daphne server will take over the management command from Django’s default management command. The official channels library uses the daphne development server but you can also install the ASGI server you want. Daphne server is placed at the top in the installed app list in Django settings.
After installation, we will add channels to the installed apps in Django settings.
# Application definition
INSTALLED_APPS = [
'daphne',
'django.contrib.admin',
...
'chat.apps.ChatConfig', #chat application
]
In the Django myproject/asgi.py file we will add the following lines of code.
import os
from channels.routing import ProtocolTypeRouter
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')#replace with your app settings
# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
django_asgi_app = get_asgi_application()
application = ProtocolTypeRouter({
"http": django_asgi_app,
# Just HTTP for now. (We can add other protocols later.)
})
We will update the settings.py file and add the following configuration to point to the ASGI application that we have created above so that all aync requests in Django in our case, it is the WebSockets will be routed to the ASGI application.
ASGI_APPLICATION = "myproject.asgi.application"
After we have set up channels and Django we have to define the purpose of our application. It will be a simple chat application that will have two models; Message and room models. We will also create a template that will have the chat page. In the chat page, we will add a link for htmx cdn.
Let's define the models.
In the file above we have three models; one is the user model we have imported above and the second one is the Room model, the room model will have the chat groups think of it as the usual chat groups found in popular social media such as WhatsApp and Telegram and lastly we have the Message model which will contain the content of the chat that is the message being passed from one User to another.
What we are trying to achieve is a user will log in to the application proceed to join a Room which will be a chat group then proceed to write a message in the chat and it will be broadcast to all the users in the group.
Let’s create a template for the chatting page. To create a template we have to create its view function that will render the page. We will use Tailwind CSS to style our page.
Notice in the base.html file I have included the htmx cdn.
@login_decorator
def talk(request):
"""chat page view"""
return render(request, ".html", {'groups':Room.objects.all()})
The method above will render the chat.html page. We are rendering all the Room objects that we have in our database. For the purpose of simplicity, we are going to create the room objects using the Django admin page. Create several rooms such as music, movies, or games. Remember those rooms will be the chat groups. Remember to add the @login_decorator to limit only authenticated users only. Therefore we will have to create the authentication system.
The above code demonstrates a simple authentication system for our application. Users must be authenticated to use the app therefore we will use the login required decorator.
Let’s focus on channels. To use channels we have to create two files routing.py and consumers.py those two files are akin to Django urls.py and views.py respectively. Let us define the consumers.py file
In the consumers.py file, we are creating a Class called TalkConsumer that inherits from the WebSocketConsumer class. The Talk class will be responsible for receiving and sending chats using the WebSockets.
TalkConsumer class has only three main methods.
- connect(self)
This method is first called when the client is initially connected such as accepting or rejecting the connection based on certain criteria or sending an initial message to the client. From our example above this method does the following. It gets the room ID of a room and then creates a group name. A room instance is retrieved from the scope. A scope has all the information about a WebSocket connection. It is similar to requests in Django views. From the scope, we also retrieve the user instance that is logged in. self.accept(), this method accepts a WebSocket connection therefore initiating a bi-directional connection with the client. After the connection is created a user is added to a group and remember the group is the rooms that we retrieved based on the ID sent by the WebSocket and in channels they are called a group. Channels gives an arbitrary name to the group but we want to control the naming of our group that’s why we are using rooms and the group names.
# join the room/group
async_to_sync(self.channel_layer.group_add)(self.group_name, self.channel_name)
aysnc_to_sync is a method imported from the async.grief in Django. It converts an async method to a synchronous method. It converts channel_layer.group_add to synchronous and then (self.group_name, self.channel_name) takes the group name and the channel_name from the scope and adds the channel name to the group. One thing to note is that each connection is established via a WebSocket a unique channel is created and now we are putting all those unique channels into one group which will enable all those channels to talk to other channels (users) who have their own channels and by doing that. That’s how a chatting feature is created.
You may ask, why do we have to convert the asynchronous method to the synchronous method? Because channels and Django are different, that is Django executes code synchronously for instance when accessing the database but channels will use a channel layer which channels will have to access asynchronously.
Before we proceed further, we have to configure a channel layer. A channel layer is a kind of communication system, which allows multiple parts of our application to exchange messages, without shuttling all the messages or events through the database. We need a channel layer to give consumers the ability to talk to one another.
docker run --rm -p 6379:6379 redis:7
python3 -m pip install channels_redis
From the snippet above we have created a docker container that has a Redis image which will be used as a channels layer and then we have installed the channels_redis which will interface our Django application with Redis. After installing Redis and channels_redis we have to configure it in our settings file.
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
html = get_template("partial/join.html").render(context={"user":self.user})
self.send(text_data=html)
From the snippet above we are using a template file called join.html passing the user instance from the scope and sending it to the client side this template is a notification to all group members and it will notify the name of the user who has joined the room.
2. disconnect(self)
This method is called when the client terminates the connection, it takes the close_code parameter which is an integer that denotes the reason why the connection is closed. This method has the following function inside the function block.
async_to_sync(self.channel_layer.group_discard)(self.group_name,self.channel_name)
html = get_template("partial/leave.html").render(context={"user":self.user})
self.send(
text_data=html
)
self.room.online.remove(self.user)
The method above will remove the user from the group and send a notification to the Group on the client side with a message the user has left the group and the user is removed from the room.
3. receive(self, text_data=None, bytes_data=None)
This method is invoked when the consumer receives a message from the client over the WebSocket. It handles the incoming data and defines how the server responds to these messages. In our case, the method is deserializing the client's message and creates an instance of the message then it responds with a html file that has all the messages of that particular room. It uses a reverse relationship to get all messages for a particular room.
Our focus here will be on htmx. Htmx is a library that allows you to access modern browser features directly from HTML, rather than using JavaScript.
On our chat page, there’s a code block that entails all groups available in our application.
{% for group in groups %}
<button class="flex flex-row items-center hover:bg-gray-100 rounded-xl p-2"
hx-post="{% url 'group' group.id %}" hx-target="#chat-window"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<div class="flex items-center justify-center h-8 w-8 bg-gray-200 rounded-full">
{{forloop.counter}}
</div>
<div class="ml-2 text-sm font-semibold">{{group.name}}</div>
<div class="flex items-center justify-center ml-auto text-xs text-white bg-red-500 h-4 w-4 rounded leading-none">
{{group.get_online_count}}
</div>
</button>
{% endfor %}
This section has some htmx attributes that are similar to HTML tag attributes and they are used as HTML tag attributes.
- hx-post = “{% url ‘group’ group.id %}” — send a post request to the group URL with a parameter of group id.
- hx-target= “#chat-window” — swap the response to a div with the id of “#chat-window”.
- hx-headers=”{ ‘X-CSRFToken”: ’{{csrf_token}}’ }” — this attribute is used to add a CSRF token to the request header. We are adding a CSRF token because this is a POST request.
The response from that POST request will be swapped in the inner part of #chat-window element.
On the bottom left are the various rooms/groups called active communities that I have created. The response from the server that will sent once we click on any group, will be swapped on the right side of the page where we have an empty grey space. For instance, I have clicked on dev group and the response will look like this.
Notice on the top right is a notification that a user named Chris has joined the group, That is a notification sent when the user first joins the group and all users present in the group are able to see it. and at the bottom part, we have the chat input element tag where a message will be written and sent and the response will appear on the grey shaded area.
The response that was sent by the server when we clicked on the dev group it contains some htmx attributes that enable us to use WebSockets and establish a connection to the server.
<div class="flex flex-col flex-auto flex-shrink-0 rounded-2xl bg-gray-100 h-full p-4" hx-ext="ws"
ws-connect="/talk/{{room_name}}/">
The ws-connect=’/talk/{{room_name}}/” will establish a connection to the WebSocket’s URL. Remember, we already defined the WebSocket’s URL in the routing.py file. The {{room_name}} is the room ID or the group ID of the room we joined. The {{room_name}} context variable is sent by the view that sends a response when the group is selected.
@login_required
def group(request, group_name):
"""chat window view"""
group = Room.objects.get(id=group_name)
messages = group.message_set.all()
return render(request, "partial/group-chat.html",{"messages":messages, "room_name":group_name,"name":group})
From the server response above we have the following snippet of code in the form element.
<form id="chat-form" class="w-100">
<input type="text"
class="flex w-full border rounded-xl focus:outline-none focus:border-indigo-300 pl-4 h-10 w-100"
name="message" placeholder="chat here" />
<button ws-send
class="absolute flex items-center justify-center h-full w-12 right-0 top-0 text-gray-400 hover:text-gray-600">
<svg class="w-4 h-4 transform rotate-45 -mt-px" fill="none"
stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
</svg>
</button>
</form>
Notice at the button tag I have added the ws-send htmx attribute, this attribute will send a message to the nearest WebSocket based on the trigger value. Since it is a button, the trigger value will always be onClick event or when you press the Enter key on your keyboard. Htmx will send the message in JSON format and that’s why in the Consumer’s receive method we have to deserialize the text_data sent by the client. When Htmx sends the message to the server via the WebSocket, the receive method in the consumer class will get the message and save it in the database then it will retrieve all the messages in that room and send it back to the client in a HTML format.
This will be the response sent to the client by the server. Take note of the following htmx attribute found in line 14 of the code snippet above hx-swap-oob=” true”. This will prompt the htmx to swap all the elements with the same ID as that of all elements in the response and by using this mechanism that’s how a notification is sent to the user because there’s an empty notification element that gets swapped by the responses in the server. This process enables us to change the input tag and render all our chats because the response sends new elements that swap the current elements in the DOM. When you look at our chats.html file. Elements that have IDs are swapped by new elements in the response which have the same ID value.
Conclusion
Our consumer class is synchronous, It is possible to make it asynchronous by using Python asyncio. To make asynchronous consumers you have to create a consumer class that inherits from AsyncWebsocketConsumer. Read more
Htmx is an awesome library and it integrates well with the Django template framework because it uses HTML elements. Htmx helps you to do more with less.
Resources.
- htmx documentation
2. Django documentation
3. Django channels documentation
4. Docker with Redis
5. Project GitHub
7. Further reading
8. WebSockets