Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Pending
* Upgraded CI ``postgis`` version to 17-3.5.
* Added how to generate the documentation locally to the contributing
documentation.
* Added Django Channels chat app to the example project.

6.0.0 (2025-07-22)
------------------
Expand Down
17 changes: 16 additions & 1 deletion example/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,23 @@

import os

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

from .async_.routing import websocket_urlpatterns

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.async_.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 = get_asgi_application()
application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
),
Comment on lines +27 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why the toolbar doesn't work with the channels' consumers. When you send a chat message via the websocket, that message isn't visible in the toolbar anywhere. Only the HTTP requests are. It bypasses the middleware that the toolbar works with.

To start, we would need to define a another middleware that is a subclass of channels.middleware.BaseMiddleware. There may be other issues that arise after that. However, I think this is enough for us to declare the toolbar doesn't support Django Channels' consumers yet 😢

}
)
36 changes: 36 additions & 0 deletions example/async_/consumers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# This is the consumer logic for the Django Channels "Web Socket" chat app
import json

from channels.generic.websocket import AsyncWebsocketConsumer


class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
self.room_group_name = f"chat_{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)

# Receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json["message"]

# Send message to room group
await self.channel_layer.group_send(
self.room_group_name, {"type": "chat.message", "message": message}
)

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

# Send message to WebSocket
await self.send(text_data=json.dumps({"message": message}))
8 changes: 8 additions & 0 deletions example/async_/routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This is the routing logic for the Django Channels "Web Socket" chat app
from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
]
9 changes: 9 additions & 0 deletions example/async_/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
from django.contrib.auth.models import User
from django.http import JsonResponse
from django.shortcuts import render


async def async_db_view(request):
names = []
async for user in User.objects.all():
names.append(user.username)
return JsonResponse({"names": names})


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


def async_chat_room(request, room_name):
return render(request, "chat/room.html", {"room_name": room_name})
8 changes: 8 additions & 0 deletions example/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@
WSGI_APPLICATION = "example.wsgi.application"
ASGI_APPLICATION = "example.asgi.application"

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

# Cache and database

Expand Down
27 changes: 27 additions & 0 deletions example/templates/chat/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!-- example/templates/chat/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Rooms</title>
</head>
<body>
What chat room would you like to enter?<br>
<input type="text" id="room-name-input" size="100"><br>
<input type="button" id="room-name-submit" value="Enter">

<script>
document.querySelector('#room-name-input').focus();
document.querySelector('#room-name-input').onkeyup = function(e) {
if (e.key === 'Enter') { // enter, return
document.querySelector('#room-name-submit').click()
}
}
document.querySelector('#room-name-submit').onclick = function(e) {
var roomName = document.querySelector('#room-name-input').value;
window.location.pathname = 'async/chat/' + roomName + '/';
}
</script>
</body>
</html>
49 changes: 49 additions & 0 deletions example/templates/chat/room.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!-- example/templates/chat/room.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Chat Room</title>
</head>
<body>
<textarea id="chat-log" cols="100" rows="20"></textarea><br>
<input type="text" id="chat-message-input" size="100"><br>
<input type="button" id="chat-message-submit" value="Send">
{{ room_name|json_script:"room-name" }}
<script>
const roomName = JSON.parse(document.getElementById('room-name').textContent);

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

chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
document.querySelector('#chat-log').value += (data.message + '\n');
};

chatSocket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
}
document.querySelector('#chat-message-input').focus();
document.querySelector('#chat-message-input').onkeyup = function(e) {
if (e.key === 'Enter') { // enter, return
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
}));
messageInputDom.value = '';
}
</script>
</body>
</html>
1 change: 1 addition & 0 deletions example/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ <h1>Index of Tests</h1>
<li><a href="{% url 'turbo' %}">Hotwire Turbo</a></li>
<li><a href="{% url 'htmx' %}">htmx</a></li>
<li><a href="{% url 'bad_form' %}">Bad form</a></li>
<li><a href="{% url 'async_chat' %}">Chat app</a></li>
</ul>
<p><a href="/admin/">Django Admin</a></p>
{% endcache %}
Expand Down
3 changes: 3 additions & 0 deletions example/urls.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from async_.views import async_chat_index, async_chat_room
from django.contrib import admin
from django.urls import path
from django.views.generic import TemplateView
Expand All @@ -20,6 +21,8 @@
),
path("jinja/", jinja2_view, name="jinja"),
path("async/", async_home, name="async_home"),
path("async/chat/", async_chat_index, name="async_chat"),
path("async/chat/<str:room_name>/", async_chat_room, name="room"),
path("async/db/", async_db, name="async_db"),
path("async/db-concurrent/", async_db_concurrent, name="async_db_concurrent"),
path("jquery/", TemplateView.as_view(template_name="jquery/index.html")),
Expand Down
2 changes: 2 additions & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Jinja2
# Django Async
daphne
whitenoise # To avoid dealing with static files
channels
channels_redis

# Testing

Expand Down