problem changing from WebsocketConsumer to AsyncWebsocketConsumer - django

i'm making a chat app with django,
everything is fine and working with the WebsocketConsumer and async_to_sync, but when i change to the AsyncWebSocket i get an error :
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
any idea or suggestion on why is that happening or what i can do here ? thanks for your help.
this is my consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.generic.websocket import WebsocketConsumer
#from asgiref.sync import async_to_sync
from django.utils import timezone
from .models import Message
from django.shortcuts import render, redirect, get_object_or_404
from courses.models import Course
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.user = self.scope['user']
self.id = self.scope['url_route']['kwargs']['course_id']
self.room_group_name = 'chat_%s' % self.id
# join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name)
# accept connection
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']
now = timezone.now()
course = get_object_or_404(Course,id=self.id)
messages = Message.objects.create(author=self.scope['user'], content=message,course=course)
# send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'user': self.user.username,
'datetime': now.isoformat(),
}
)
# receive message from room group
async def chat_message(self, event):
# Send message to WebSocket
await self.send(text_data=json.dumps(event))
UPDATE
I found the Solution
those 2 lines are the rcause of the error:
course = get_object_or_404(Course,id=self.id)
messages = Message.objects.create(author=self.scope['user'],
content=message,course=course)
i'm using ththem to save messages in the database, aparently it's not possible in the async mode.
so according to the docs this is the modification i added :
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
then my save method:
#database_sync_to_async
def save_message(self,message):
course = get_object_or_404(Course,id=self.id)
messages = Message.objects.create(author=self.scope['user'],
content=message,course=course)
return messages
and finally replace the 2 line (the cause of problem) with this line in the receive() method :
await self.save_message(message)

Related

Django sending data from outside consumer class

I am trying to get use Django channels to send data over a websocket to my react native application from django. I have read all the available documentation on this subject on Django and have went through numerous stackoverflow posts, but I don't think they are applicable to me because they use redis and I decided not to use redis.
Whenever I try to send data right now, nothing sends.
These are my files.
models.py
from django.db import models
import json
from .consumers import DBUpdateConsumer
from django.db.models.signals import post_save
from django.dispatch import receiver
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
channel_layer = get_channel_layer()
class Connect(models.Model):
id = models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')
neighborhood = models.CharField(max_length=50, choices=neighborhood_choices, default='all')
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.CharField(max_length=100)
phone = models.CharField(max_length=50)
def save(self, *args, **kwargs):
super().save(self, *args, **kwargs)
print("def save")
async_to_sync(channel_layer.send)("hello", {"type": "something", "text": "hellooo"})
class Meta:
managed = False
db_table = 'connect'
settings.py
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
consumers.py
import json
from channels.generic.websocket import AsyncJsonWebsocketConsumer
#used https://blog.logrocket.com/django-channels-and-websockets/
#https://channels.readthedocs.io/en/latest/topics/consumers.html
class DBUpdateConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
self.send_message(self, "UPDATE")
await self.accept()
await self.send(text_data=json.dumps({
"payload": "UPDATE",
}))
print("connect!")
async def disconnect(self, close_code):
print("Disconnected")
async def receive(self, text_data):
"""
Receive message from WebSocket.
Get the event and send the appropriate event
"""
response = json.loads(text_data)
#event = response.get("event", None)
#message = response.get("message", None)
print(response)
#classmethod
async def send_message(cls, self, res):
# Send message to WebSocket
print("send msg")
await self.send(text_data=json.dumps({
"payload": res,
}))
print("send msg")
What I am trying to do is whenever a new value is stored in my database, I am trying to send a message through a websocket that connects my react native app and my django backend. The websocket currently connects fine, but I am having trouble using the send_message function contained within my consumers.py file from outside consumers.py. So what I am trying to do is in my models.py file, send a message to all the channels that are open to eventually update my database. Currently, I am just trying to send test messages through, but no matter what I do, nothing goes through, and being a newbie to Django, I have no idea why.
Thank you!
Solved, with some help from a friend!
consumers.py
import json
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from .models import Client
from asgiref.sync import sync_to_async
#used https://blog.logrocket.com/django-channels-and-websockets/
#https://channels.readthedocs.io/en/latest/topics/consumers.html
class DBUpdateConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
print("channel name is " + self.channel_name)
await sync_to_async(Client.objects.create)(channel_name=self.channel_name)
await self.accept()
await self.send(text_data=json.dumps({
"payload": "UPDATE",
}))
print("connect!")
async def disconnect(self, close_code):
print("Disconnected")
# Leave room group
"""await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)"""
async def update(self, message):
print("Sent message " + message["text"])
await self.send(text_data=json.dumps({
"payload": "UPDATE",
}))
async def receive(self, text_data):
"""
Receive message from WebSocket.
Get the event and send the appropriate event
"""
response = json.loads(text_data)
#event = response.get("event", None)
#message = response.get("message", None)
print(response)
"""if event == 'MOVE':
# Send message to room group
await self.channel_layer.group_send(self.room_group_name, {
'type': 'send_message',
'message': message,
"event": "MOVE"
})
if event == 'START':
# Send message to room group
await self.channel_layer.group_send(self.room_group_name, {
'type': 'send_message',
'message': message,
'event': "START"
})
if event == 'END':
# Send message to room group
await self.channel_layer.group_send(self.room_group_name, {
'type': 'send_message',
'message': message,
'event': "END"
})"""
# #classmethod
# async def send_message(cls, self, res):
# # Send message to WebSocket
# print("send msg")
# await self.send(text_data=json.dumps({
# "payload": res,
# }))
# print("send msg")
models.py
class Connect(models.Model):
id = models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')
neighborhood = models.CharField(max_length=50, choices=neighborhood_choices, default='all')
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.CharField(max_length=100)
phone = models.CharField(max_length=50)
def save(self, *args, **kwargs):
super().save(self, *args, **kwargs)
clients = Client.objects.all()
for client in clients:
async_to_sync(channel_layer.send)(client.channel_name, {"type": "update", "text": "hellooo"})
class Meta:
managed = False
db_table = 'connect'

TypeError: can not serialize 'User' object in Chat Application

I want to integrate a chat app with my instagram-like project. My primary goal is to provide the users of this website with the possibility to chat with each other real-time. I have the following code but I keep getting the error:
TypeError: can not serialize 'User' object
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
import json
from .models import Message
from django.contrib.auth.models import User
class ChatConsumer(WebsocketConsumer):
def fetch_messages(self, data):
messages = Message.last_10_messages()
content = {
'messages': self.messages_to_json(messages)
}
self.send_message(content)
def new_message(self, data):
author = data['from']
author_user = User.objects.get(username = author)
message = Message.objects.create(author=author_user, content=data['message'])
content ={
'command' : 'new_message',
'message': self.message_to_json(message)
}
return self.send_chat_message(content)
def messages_to_json(self, messages):
result = []
for message in messages:
result.append(self.message_to_json(message))
return result
def message_to_json(self, message):
return {
'author' : message.author,
'content' : message.content,
'timestamp': str(message.timestamp)
}
commands = {
'fetch_messages': fetch_messages,
'new_message' : new_message
}
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
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
)
def receive(self, text_data):
data = json.loads(text_data)
self.commands[data['command']](self, data)
def send_chat_message(self, data):
message = data['message']
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)
def send_message(self, message):
self.send(text_data=json.dumps(message))
def chat_message(self, event):
message = event['message']
self.send(text_data=json.dumps(message))
The views.py:
from django.shortcuts import render
from django.utils.safestring import mark_safe
import json
def index(request):
return render(request, 'chat/index.html', {})
def room(request, room_name):
return render(request, 'chat/room.html', {
'room_name_json': mark_safe(json.dumps(room_name)),
'username' : mark_safe(json.dumps(request.user.username))
})
The routing.py
from django.urls import re_path, path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
The asgi.py in the project root:
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", "mysite.settings")
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})
I am following the tutorial on the official website of Django Channels. I am also trying to customise my consumer to save it on database and the model for that is as following:
from django.db import models
from django.contrib.auth.models import User
class Message(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='author')
content = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.author.username
def last_10_messages(self):
return Message.objects.order_by('-timestamp').all()[:10]
self.channel_layer.group_send requires the message dict to contain the following values only:
Byte strings
Unicode strings
Integers (within the signed 64 bit range)
Floating point numbers (within the IEEE 754 double precision range)
Lists (tuples should be encoded as lists)
Dicts (keys must be unicode strings)
Booleans
None
Source: https://channels.readthedocs.io/en/latest/channel_layer_spec.html#messages

Django Channels Websocket group name

Sorry for bad English :(
I want to create notification system per user. User has own group/room and every notification go to specific user's notification room. Every time when user connect to websocket, user creates the same id of user. self.scope["user"]. Therefore only user_notification_1 group name was created. How it is possible to create group name depending on user?
I use
application = ProtocolTypeRouter(
{
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
}
)
Code:
import json
from channels.generic.websocket import WebsocketConsumer
class NotificationConsumer(WebsocketConsumer):
async def connect(self):
self.user = self.scope["user"]
if self.user.is_authenticated:
self.room_group_name = f"user_notification_{self.user.id}"
else:
self.room_group_name = "anonymous"
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
async def receive(self, text_data=None, bytes_data=None):
self.user = self.scope["user"]
if self.user.is_authenticated:
await self.send(
text_data=json.dumps({"message": "pong"})
)
else:
await self.send(
text_data=json.dumps({"type": "error", "code": "UserNotAuthenticated"})
)
async def new_chat_message(self, event):
await self.send(text_data=json.dumps(event.get("data")))
async def connect_successful(self, event):
await self.send(text_data=json.dumps(event.get("data")))
UPD My problem was solved when I noticed that I disabled REST_SESSION_LOGIN. I use dj-rest-auth

How to send message from django view to consumer (django-channels)?

I want to send some message from Django view to django channels consumer. I have consumer like:
from channels.generic.websocket import AsyncWebsocketConsumer
import json
class KafkaConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_group_name = 'kafka'
# 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': 'kafka_message',
'message': message
}
)
# Receive message from room group
async def kafka_message(self, event):
message = event['message']
# Send message to WebSocket
await self.send(text_data=json.dumps({
'message': message
}))
And, my Django view is like:
from django.views.generic import TemplateView
from django.http import HttpResponse
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
class LogView(TemplateView):
template_name = "kafka/index.html"
def testview(request):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send(
'kafka',
{
'type': 'kafka.message',
'message': 'Test message'
}
))
return HttpResponse('<p>Done</p>')
URL url is like:
from django.urls import path
from .views import LogView, testview
urlpatterns = [
path(r'', LogView.as_view()),
path(r'test/', testview),
]
So, when I do http://mydevhost/test/, consumer do not receive message. However, I can send message from/within consumer i.e. KafkaConsumer.receive in channels consumer.
Pretty much silly mistake on async_to_sync. Actually async_to_sync should wrap only channel_layer.group_send instead of whole i.e. async_to_sync(channel_layer.group_send). So call looks like:
async_to_sync(channel_layer.group_send)(
'kafka',
{
'type': 'kafka.message',
'message': 'Test message'
}
)
All view code with corrected code:
from django.views.generic import TemplateView
from django.http import HttpResponse
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
class LogView(TemplateView):
template_name = "kafka/index.html"
def testview(request):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
'kafka',
{
'type': 'kafka.message',
'message': 'Test message'
}
)
return HttpResponse('<p>Done</p>')

Send notification on post_save signal in django

I have a model called deposit and I am trying to send real time notification when a new row is added in table. Is it possible to do so using django-channels ?
You can use the save method of your Django model, like so:
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
class Info(models.Model):
# your model fields
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
super(Info, self).save(force_insert, force_update, using, update_fields)
# send info to channel
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
'infochannel',
{
'type': 'infochannel.message',
'device_id': str(self.device_id)
}
)
And in the consumer:
from channels.generic.websocket import AsyncWebsocketConsumer
class DataConsumer(AsyncWebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.group_name = ''
async def connect(self):
# we are using one fixed group
self.group_name = 'infochannel'
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard('infochannel', self.channel_name)
async def infochannel_message(self, event):
# Send message to websocket group
await self.send(text_data=event['device_id'])
Where device_id is a field on my model, and of course you also have to set up routing, redis_channels, and so on.
You can simply add a signal for a post_save
#receiver(post_save, sender=Deposit)
def signal_deposit_save(sender, instance, created, **kwargs):
if created: # This means it is a new row
# Send notification using django-channels