From 5877c462dce75465cc6c52b16ede4547fea55dd1 Mon Sep 17 00:00:00 2001 From: Chiemezuo Date: Wed, 8 Oct 2025 23:14:29 +0100 Subject: [PATCH 1/5] Added async chat app structure --- example/asgi.py | 11 ++++++++++- example/async_/urls.py | 1 + example/async_/views.py | 5 +++++ example/templates/chat/index.html | 27 +++++++++++++++++++++++++++ example/templates/index.html | 1 + example/urls.py | 2 ++ requirements_dev.txt | 1 + 7 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 example/templates/chat/index.html diff --git a/example/asgi.py b/example/asgi.py index 7c5c501f6..8f0fc2876 100644 --- a/example/asgi.py +++ b/example/asgi.py @@ -9,8 +9,17 @@ import os +from channels.routing import ProtocolTypeRouter from django.core.asgi import get_asgi_application 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, + # Just HTTP for now. (We can add other protocols later.) + } +) diff --git a/example/async_/urls.py b/example/async_/urls.py index ad19cbc83..bc32f2386 100644 --- a/example/async_/urls.py +++ b/example/async_/urls.py @@ -6,4 +6,5 @@ urlpatterns = [ path("async/db/", views.async_db_view, name="async_db_view"), *sync_urlpatterns, + path("async/chat", views.async_chat_index, name="chat"), ] diff --git a/example/async_/views.py b/example/async_/views.py index 7326e0d0b..ddb48f57f 100644 --- a/example/async_/views.py +++ b/example/async_/views.py @@ -1,5 +1,6 @@ from django.contrib.auth.models import User from django.http import JsonResponse +from django.shortcuts import render async def async_db_view(request): @@ -7,3 +8,7 @@ async def async_db_view(request): 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") diff --git a/example/templates/chat/index.html b/example/templates/chat/index.html new file mode 100644 index 000000000..d78899eae --- /dev/null +++ b/example/templates/chat/index.html @@ -0,0 +1,27 @@ + + + + + + + Chat Rooms + + + What chat room would you like to enter?
+
+ + + + + diff --git a/example/templates/index.html b/example/templates/index.html index a10c2b5ac..3753516a8 100644 --- a/example/templates/index.html +++ b/example/templates/index.html @@ -16,6 +16,7 @@

Index of Tests

  • Hotwire Turbo
  • htmx
  • Bad form
  • +
  • Chat app
  • Django Admin

    {% endcache %} diff --git a/example/urls.py b/example/urls.py index 86e6827fc..f07885c68 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,3 +1,4 @@ +from async_.views import async_chat_index from django.contrib import admin from django.urls import path from django.views.generic import TemplateView @@ -20,6 +21,7 @@ ), path("jinja/", jinja2_view, name="jinja"), path("async/", async_home, name="async_home"), + path("async/chat/", async_chat_index, name="async_chat"), 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")), diff --git a/requirements_dev.txt b/requirements_dev.txt index 90e490192..4e8544469 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -7,6 +7,7 @@ Jinja2 # Django Async daphne whitenoise # To avoid dealing with static files +channels # Testing From b7c088384780a9718d61f6e94a2b6aa2f784ee44 Mon Sep 17 00:00:00 2001 From: Chiemezuo Date: Tue, 14 Oct 2025 10:36:29 +0100 Subject: [PATCH 2/5] implement Django channels chat server --- example/asgi.py | 10 +++++-- example/async_/consumers.py | 41 ++++++++++++++++++++++++++ example/async_/routing.py | 8 +++++ example/async_/urls.py | 1 - example/async_/views.py | 4 +++ example/settings.py | 8 +++++ example/templates/chat/index.html | 2 +- example/templates/chat/room.html | 49 +++++++++++++++++++++++++++++++ example/urls.py | 3 +- requirements_dev.txt | 1 + 10 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 example/async_/consumers.py create mode 100644 example/async_/routing.py create mode 100644 example/templates/chat/room.html diff --git a/example/asgi.py b/example/asgi.py index 8f0fc2876..90df0854c 100644 --- a/example/asgi.py +++ b/example/asgi.py @@ -9,9 +9,13 @@ import os -from channels.routing import ProtocolTypeRouter +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. @@ -20,6 +24,8 @@ application = ProtocolTypeRouter( { "http": django_asgi_app, - # Just HTTP for now. (We can add other protocols later.) + "websocket": AllowedHostsOriginValidator( + AuthMiddlewareStack(URLRouter(websocket_urlpatterns)) + ), } ) diff --git a/example/async_/consumers.py b/example/async_/consumers.py new file mode 100644 index 000000000..313bf279f --- /dev/null +++ b/example/async_/consumers.py @@ -0,0 +1,41 @@ +# This is the consumer logic for the Django Channels "Web Socket" chat app +import json + +from asgiref.sync import async_to_sync +from channels.generic.websocket import WebsocketConsumer + + +class ChatConsumer(WebsocketConsumer): + 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 + async_to_sync(self.channel_layer.group_add)( + self.room_group_name, self.channel_name + ) + + self.accept() + + def disconnect(self, close_code): + # Leave room group + async_to_sync(self.channel_layer.group_discard)( + self.room_group_name, self.channel_name + ) + + # Receive message from WebSocket + def receive(self, text_data): + text_data_json = json.loads(text_data) + message = text_data_json["message"] + + # Send message to room group + async_to_sync(self.channel_layer.group_send)( + self.room_group_name, {"type": "chat.message", "message": message} + ) + + # Receive message from room group + def chat_message(self, event): + message = event["message"] + + # send message to WebSocket + self.send(text_data=json.dumps({"message": message})) diff --git a/example/async_/routing.py b/example/async_/routing.py new file mode 100644 index 000000000..3175e503c --- /dev/null +++ b/example/async_/routing.py @@ -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\w+)/$", consumers.ChatConsumer.as_asgi()), +] diff --git a/example/async_/urls.py b/example/async_/urls.py index bc32f2386..ad19cbc83 100644 --- a/example/async_/urls.py +++ b/example/async_/urls.py @@ -6,5 +6,4 @@ urlpatterns = [ path("async/db/", views.async_db_view, name="async_db_view"), *sync_urlpatterns, - path("async/chat", views.async_chat_index, name="chat"), ] diff --git a/example/async_/views.py b/example/async_/views.py index ddb48f57f..142b646af 100644 --- a/example/async_/views.py +++ b/example/async_/views.py @@ -12,3 +12,7 @@ async def async_db_view(request): 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}) diff --git a/example/settings.py b/example/settings.py index ffaa09fe5..1bf25db61 100644 --- a/example/settings.py +++ b/example/settings.py @@ -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 diff --git a/example/templates/chat/index.html b/example/templates/chat/index.html index d78899eae..6d65ca3f0 100644 --- a/example/templates/chat/index.html +++ b/example/templates/chat/index.html @@ -20,7 +20,7 @@ } document.querySelector('#room-name-submit').onclick = function(e) { var roomName = document.querySelector('#room-name-input').value; - window.location.pathname = '/chat/' + roomName + '/'; + window.location.pathname = 'async/chat/' + roomName + '/'; } diff --git a/example/templates/chat/room.html b/example/templates/chat/room.html new file mode 100644 index 000000000..0065467b7 --- /dev/null +++ b/example/templates/chat/room.html @@ -0,0 +1,49 @@ + + + + + + Chat Room + + +
    +
    + + {{ room_name|json_script:"room-name" }} + + + diff --git a/example/urls.py b/example/urls.py index f07885c68..8b748b9bf 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,4 +1,4 @@ -from async_.views import async_chat_index +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 @@ -22,6 +22,7 @@ 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//", 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")), diff --git a/requirements_dev.txt b/requirements_dev.txt index 4e8544469..577398dc6 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -8,6 +8,7 @@ Jinja2 daphne whitenoise # To avoid dealing with static files channels +channels_redis # Testing From c28970cd5e87e3695a81b03c83b980f1669b27a0 Mon Sep 17 00:00:00 2001 From: Chiemezuo Date: Tue, 14 Oct 2025 10:55:26 +0100 Subject: [PATCH 3/5] Rewrite chat server as asynchronous --- example/async_/consumers.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/example/async_/consumers.py b/example/async_/consumers.py index 313bf279f..2682685b9 100644 --- a/example/async_/consumers.py +++ b/example/async_/consumers.py @@ -1,41 +1,36 @@ # This is the consumer logic for the Django Channels "Web Socket" chat app import json -from asgiref.sync import async_to_sync -from channels.generic.websocket import WebsocketConsumer +from channels.generic.websocket import AsyncWebsocketConsumer -class ChatConsumer(WebsocketConsumer): - def connect(self): +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 - async_to_sync(self.channel_layer.group_add)( - self.room_group_name, self.channel_name - ) + await self.channel_layer.group_add(self.room_group_name, self.channel_name) - self.accept() + await self.accept() - def disconnect(self, close_code): + async def disconnect(self, close_code): # Leave room group - async_to_sync(self.channel_layer.group_discard)( - self.room_group_name, self.channel_name - ) + await self.channel_layer.group_discard(self.room_group_name, self.channel_name) # Receive message from WebSocket - def receive(self, text_data): + async def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json["message"] # Send message to room group - async_to_sync(self.channel_layer.group_send)( + await self.channel_layer.group_send( self.room_group_name, {"type": "chat.message", "message": message} ) # Receive message from room group - def chat_message(self, event): + async def chat_message(self, event): message = event["message"] - # send message to WebSocket - self.send(text_data=json.dumps({"message": message})) + # Send message to WebSocket + await self.send(text_data=json.dumps({"message": message})) From 34e22ac213c80b634cd393680428320eb6586e7c Mon Sep 17 00:00:00 2001 From: Chiemezuo Date: Tue, 14 Oct 2025 11:04:33 +0100 Subject: [PATCH 4/5] Added changes to changelog --- docs/changes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changes.rst b/docs/changes.rst index 452242279..da70a49aa 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -13,6 +13,7 @@ Pending class instance, regardless if any data was generated. * Fixed selenium tests for CI by using psycopg for Python 3.13 runs. * Added ``CommunityPanel`` containing links to documentation and resources. +* Added Django Channels chat app to the example project. 6.0.0 (2025-07-22) ------------------ From e945d92cddcc726bb6cb9f4ad000f6f7f0906ca1 Mon Sep 17 00:00:00 2001 From: Chiemezuo Date: Mon, 20 Oct 2025 12:48:39 +0100 Subject: [PATCH 5/5] Separate concerns in example app --- example/async_/consumers.py | 1 + example/async_/routing.py | 1 + example/async_/settings.py | 9 +++++++++ example/async_/urls.py | 4 ++++ example/settings.py | 9 --------- example/templates/chat/index.html | 6 +++++- example/templates/chat/room.html | 6 +++++- example/templates/index.html | 2 +- example/urls.py | 3 --- 9 files changed, 26 insertions(+), 15 deletions(-) diff --git a/example/async_/consumers.py b/example/async_/consumers.py index 2682685b9..04d0af6d3 100644 --- a/example/async_/consumers.py +++ b/example/async_/consumers.py @@ -1,4 +1,5 @@ # This is the consumer logic for the Django Channels "Web Socket" chat app +# https://channels.readthedocs.io/en/latest/tutorial/index.html import json from channels.generic.websocket import AsyncWebsocketConsumer diff --git a/example/async_/routing.py b/example/async_/routing.py index 3175e503c..ddb7724f6 100644 --- a/example/async_/routing.py +++ b/example/async_/routing.py @@ -1,4 +1,5 @@ # This is the routing logic for the Django Channels "Web Socket" chat app +# https://channels.readthedocs.io/en/latest/tutorial/index.html from django.urls import re_path from . import consumers diff --git a/example/async_/settings.py b/example/async_/settings.py index f3bef673a..96eedaf6c 100644 --- a/example/async_/settings.py +++ b/example/async_/settings.py @@ -3,3 +3,12 @@ from ..settings import * # noqa: F403 ROOT_URLCONF = "example.async_.urls" + +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [("127.0.0.1", 6379)], + }, + } +} diff --git a/example/async_/urls.py b/example/async_/urls.py index ad19cbc83..7711ab305 100644 --- a/example/async_/urls.py +++ b/example/async_/urls.py @@ -3,7 +3,11 @@ from example.async_ import views from example.urls import urlpatterns as sync_urlpatterns +from .views import async_chat_index, async_chat_room + urlpatterns = [ path("async/db/", views.async_db_view, name="async_db_view"), + path("async/chat/", async_chat_index, name="async_chat"), + path("async/chat//", async_chat_room, name="room"), *sync_urlpatterns, ] diff --git a/example/settings.py b/example/settings.py index 1bf25db61..1fe6aa79a 100644 --- a/example/settings.py +++ b/example/settings.py @@ -70,15 +70,6 @@ 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 CACHES = {"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}} diff --git a/example/templates/chat/index.html b/example/templates/chat/index.html index 6d65ca3f0..1996352a0 100644 --- a/example/templates/chat/index.html +++ b/example/templates/chat/index.html @@ -1,4 +1,8 @@ - +{% comment %} + This template is from the Django Channels "Web Socket" chat app + https://channels.readthedocs.io/en/latest/tutorial/index.html +{% endcomment %} + diff --git a/example/templates/chat/room.html b/example/templates/chat/room.html index 0065467b7..d2621cd0c 100644 --- a/example/templates/chat/room.html +++ b/example/templates/chat/room.html @@ -1,4 +1,8 @@ - +{% comment %} + This template is from the Django Channels "Web Socket" chat app + https://channels.readthedocs.io/en/latest/tutorial/index.html +{% endcomment %} + diff --git a/example/templates/index.html b/example/templates/index.html index 3753516a8..cf1b233ec 100644 --- a/example/templates/index.html +++ b/example/templates/index.html @@ -16,7 +16,6 @@

    Index of Tests

  • Hotwire Turbo
  • htmx
  • Bad form
  • -
  • Chat app
  • Django Admin

    {% endcache %} @@ -27,6 +26,7 @@

    Index of Tests

    {% comment %} +
  • Chat app
  • {% endcomment %} diff --git a/example/urls.py b/example/urls.py index 8b748b9bf..86e6827fc 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,4 +1,3 @@ -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 @@ -21,8 +20,6 @@ ), 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//", 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")),