Send notification on post_save signal in django - 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

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'

problem changing from WebsocketConsumer to AsyncWebsocketConsumer

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)

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

Django 3.0 + Channels + ASGI + TokenAuthMiddleware

I upgraded to Django 3.0 and now I get this error when using websockets + TokenAuthMiddleware:
SynchronousOnlyOperation
You cannot call this from an async context - use a thread or sync_to_async.
The problem is that you can't access synchronous code from an asynchronous context. Here is a TokenAuthMiddleware for Django 3.0:
# myproject.myapi.utils.py
from channels.auth import AuthMiddlewareStack
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from rest_framework.authtoken.models import Token
#database_sync_to_async
def get_user(headers):
try:
token_name, token_key = headers[b'authorization'].decode().split()
if token_name == 'Token':
token = Token.objects.get(key=token_key)
return token.user
except Token.DoesNotExist:
return AnonymousUser()
class TokenAuthMiddleware:
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
return TokenAuthMiddlewareInstance(scope, self)
class TokenAuthMiddlewareInstance:
"""
Yeah, this is black magic:
https://github.com/django/channels/issues/1399
"""
def __init__(self, scope, middleware):
self.middleware = middleware
self.scope = dict(scope)
self.inner = self.middleware.inner
async def __call__(self, receive, send):
headers = dict(self.scope['headers'])
if b'authorization' in headers:
self.scope['user'] = await get_user(headers)
inner = self.inner(self.scope)
return await inner(receive, send)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
Use it like this:
# myproject/routing.py
from myapi.utils import TokenAuthMiddlewareStack
from myapi.websockets import WSAPIConsumer
application = ProtocolTypeRouter({
"websocket": TokenAuthMiddlewareStack(
URLRouter([
path("api/v1/ws", WSAPIConsumer),
]),
),
})
application = SentryAsgiMiddleware(application)
As #tapion stated this solution doesn't work anymore since channels 3.x
Newer solution can be a little bit tweaked:
class TokenAuthMiddleware:
def __init__(self, inner):
self.inner = inner
async def __call__(self, scope, receive, send):
headers = dict(scope['headers'])
if b'authorization' in headers:
scope['user'] = await get_user(headers)
return await self.inner(scope, receive, send)

Current user in pre_delete signal in django

Is it possible to get a signed in django user (that calls model's delete method) in a callback connected to pre_delete signal?
The pre_delete signal doesn't pass the request instance, but you can add a decorator, that adds it, and to use that decorator on a view, which deletes the specified Model.
Assuming that this is the callback function:
def pre_delete_cb(sender, instance, using, **kwargs):
pass
which is being added in the decorator:
from django.db.models.signals import pre_delete
from functools import wraps
from django.utils.decorators import available_attrs
def pre_delete_dec(cb, sender):
def decorator(view_func):
#wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view(request, *args, **kwargs):
cb.request = request # here we add the request to the callback function
pre_delete.connect(cb, sender=sender) # and connecting the real pre_delete to the callback
return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator
And use the decorator on the view in the way you call it - instead of pre_delete.connect(pre_delete_cb, MyModel), use:
#pre_delete_dec(pre_delete_cb, MyModel)
def myview(request):
and then in the callback you'll have access to the request as:
def pre_delete_cb(sender, instance, using, **kwargs):
current_user = pre_delete_cb.request.user
You can add this on global level, not just per view - using a Middleware:
from django.db.models.signals import pre_delete
def pre_delete_cb(sender, instance, using, **kwargs):
current_user = pre_delete_cb.request.user
class PreDeleteMiddleware(object):
def process_view(self, request, view_func, view_args, view_kwargs):
pre_delete_cb.request = request
from myapp.models import MyModel
pre_delete.connect(pre_delete_cb, sender=MyModel)