How to send data to a particular user in django channels? - django

This is my class:
class MyClass(models.Model):
name = models.CharField(max_length=128, unique=True)
members = models.ManyToManyField("Employee")
def save(self, *args, **kwargs):
super().save(*args,**kwargs)
channel_layer = get_channel_layer()
data = {"current_obj":self.name}
async_to_sync(channel_layer.group_send)(
"test_consumer_group_1",{
'type':'send_notification',
'value':json.dumps(data)
}
)
This is my consumer file.
class MySyncGroupConsumer(SyncConsumer):
def websocket_connect(self,event):
print("Web socket connected", event)
self.send({
'type':'websocket.accept'
})
print("Channel layer ",self.channel_layer)
print("Channel name", self.channel_name)
async_to_sync(self.channel_layer.group_add)("test_consumer_group_1", self.channel_name)
def websocket_receive(self,event):
print("Web socket recieved",event)
print(event["text"])
sync_to_sync(self.channel_layer.group_send)("test_consumer_group_1", {
'type':'chat.message',
'message':event['text']
})
#event handler, which is sending data to client
def chat_message(self,event):
print('Event...', event)
print('Event...', event["message"])
self.send({
"type":"websocket.send",
"text":event['message']
})
def send_notification(self, event):
print("send_notification called")
print('Event...', event['value'])
# Get the current user from the channel layer
user = self.scope['user']
print(user)
# Check if the user is authenticated and is a member of the team
if user.is_authenticated and user.employee in self.team.members.all():
self.send({
"type":"websocket.send",
"text":event['value']
})
def websocket_disconnect(self,event):
print("Web socket disconnect", event)
async_to_sync(self.channel_layer.group_discard)("test_consumer_group_1", self.channel_name)
raise StopConsumer()
This is my asgi.py file:
import os
import app.routing
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
application = get_asgi_application()
application = ProtocolTypeRouter({
'http': get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
app.routing.websocket_urlpatterns
)
),
})
When an object of MyClass is created, send_notification is called.
Now I want to ask two things:
Inside send_notification, user = self.scope['user'] always returns anonymous user, even if user is authenticated.
I want to send notification to particular users onlyi.e. employees who are added in MyClass object and are logged in, instead of broadcasting the message
How to do that?

Related

Send value from __init__.py to consumer.py websocket Django channels

I want to send data from the firebase stream(pyrebase) via django channels websocket. Web scoket is working fine, But I can't imagine how I can use consumer.py file send message function in init_.py.
Here my django channel web socket Consumer.py file.
import json
from random import randint
from asyncio import sleep
from channels.generic.websocket import AsyncWebsocketConsumer
from django.conf import settings
class WSConsumer(AsyncWebsocketConsumer):
group_name = settings.STREAM_SOCKET_GROUP_NAME
async def connect(self):
# Joining group
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
# Leave group
await self.channel_layer.group_discard(
self.group_name,
self.channel_name
)
async def receive(self, text_data):
print(text_data)
# Send data to group
await self.channel_layer.group_send(
self.group_name,
{
'type': 'system_load',
'data': text_data
}
)
async def system_load(self, event):
# Receive data from group
print('sending message to client')
await self.send(text_data=json.dumps(event['data']))
This file works fine.
This is my inti.py file. In this file, I want to send data to the WebSocket in this stream_handler function.
import pyrebase
from configFiles.config import config
firebase = pyrebase.initialize_app(config)
authe = firebase.auth()
database = firebase.database()
safe_temperature = 20
safe_humidity = 40
def stream_handler(message):
if "Humidity" in message["path"]:
if message["data"] > safe_humidity:
database.child("Controller").child("Light").set(1)
else:
database.child("Controller").child("Light").set(0)
if "Temperature" in message["path"]:
if message["data"] > safe_humidity:
database.child("Controller").child("Fan").set(1)
else:
database.child("Controller").child("Fan").set(0)
# Here place I want to send data which is message["data"] to the WebSocket
mystream = database.child("Sensor").stream(stream_handler)
Thank you!

can't get the profile object firstname

I'm trying to get the profile firstname but it gives me this error
File "/home/marwan/Desktop/Gowd/venv/lib/python3.6/site-packages/django/utils/asyncio.py", line 24, in inner
raise SynchronousOnlyOperation(message)
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
WebSocket DISCONNECT /public_chat/1/ [127.0.0.1:53742]
INFO 2021-03-12 01:43:30,570 runserver 119412 140507688003328 WebSocket DISCONNECT /public_chat/1/ [127.0.0.1:53742]
I think that this error has something to do with trying to get the user profile in line 56
"profile": self.scope["user"].profile.firstname,
if I remove it will work fine
this is my consumers.py file
from channels.generic.websocket import AsyncJsonWebsocketConsumer
import json
from django.contrib.auth import get_user_model
from gowd_project.users.models import User, Profile
User = get_user_model()
# Example taken from:
# https://github.com/andrewgodwin/channels-examples/blob/master/multichat/chat/consumers.py
class PublicChatConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
"""
Called when the websocket is handshaking as part of initial connection.
"""
print("PublicChatConsumer: connect: " + str(self.scope["user"]))
# let everyone connect. But limit read/write to authenticated users
await self.accept()
# Add them to the group so they get room messages
await self.channel_layer.group_add(
"public_chatroom_1",
self.channel_name,
)
async def disconnect(self, code):
"""
Called when the WebSocket closes for any reason.
"""
# leave the room
print("PublicChatConsumer: disconnect")
pass
async def receive_json(self, content):
"""
Called when we get a text frame. Channels will JSON-decode the payload
for us and pass it as the first argument.
"""
# Messages will have a "command" key we can switch on
command = content.get("command", None)
print("PublicChatConsumer: receive_json: " + str(command))
print("PublicChatConsumer: receive_json: message: " + str(content["message"]))
if command == "send":
if len(content["message"].lstrip()) == 0:
raise Exception("You can't send an empty message.")
await self.send_message(content["message"])
async def send_message(self, message):
await self.channel_layer.group_send(
"public_chatroom_1",
{
"type": "chat.message",
"user_id": self.scope["user"].id,
"user_name": self.scope["user"].name,
"profile": self.scope["user"].profile.firstname,
"message": message,
}
)
async def chat_message(self, event):
"""
Called when someone has messaged our chat.
"""
# Send a message down to the client
print("PublicChatConsumer: chat_message from user #" + str(event["user_id"]))
await self.send_json(
{
"user_id": event["user_id"],
"user_name": event["user_name"],
"message": event["message"],
},
)
sync_to_async takes a callable, not the result. so, I need to do this:
#sync_to_async
def get_user_profile(self, user):
return user.profile
async def send_message(self, message):
profile = await PublicChatConsumer.get_user_profile(self.scope["user"])
profile_name = profile.firstname
await self.channel_layer.group_send(
"public_chatroom_1",
{
"type": "chat.message",
"user_id": self.scope["user"].id,
"username": profile_name,
"message": message,
}
)
full documentation
Similar problem

How to create task which sends data continuously and disconnect safely in consumers, django channels 2?

From this answer, which helps to send data from consumers for every n seconds.
Tried to handle the disconnection properly, using creat_task method, tried to stop while-loop(which is used to send data for every n seconds) by sending a flag=False(Assuming, this flag is not sent to the same instance which is created the task).
consumers.py:
class AutoUpdateConsumer(AsyncConsumer):
async def websocket_connect(self, event):
print("connected", event)
await self.send({
"type": "websocket.accept"
})
await self.create_task(True)
async def websocket_receive(self, event):
print("receive", event)
async def websocket_disconnect(self, event):
await self.create_task(False)
print("disconnected", event)
async def create_task(self, flag=True):
while flag:
await asyncio.sleep(2)
df= pd.DataFrame(data=[random.sample(range(100), 4) for _ in range(5)])
await self.send({
'type': 'websocket.send',
'text': df.to_html(),
})
Warning:
2019-09-11 14:40:06,400 - WARNING - server - Application instance
<Task pending coro=<SessionMiddlewareInstance.__call__() running at
D:\Django\Django channels\django_channels_env\lib\site-packages\channels\sessions.py:175>
wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at
0x000001870E06C618>()] for connection <WebSocketProtocol client=
['127.0.0.1', 63789] path=b'/ws/home'> took too long to shut down and was
killed.
How to stop_task safely instead of waiting for channels to kill task?
Or
How to stop infinite while loop running in a method, from another method in same class?
Versions:
Django == 2.0.7
channels == 2.1.2
I would suggest creating a group when connecting to the consumer. That way you can trigger a message from anywhere in your django project as long as you know the group name (auto_update).
from channels.generic.websocket import AsyncWebsocketConsumer
class AutoUpdateConsumer(AsyncWebsocketConsumer):
async def connect(self):
print('connect')
# join the group
self.group_name = 'auto_update'
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, event):
print('disconnect')
# leave the group
await self.channel_layer.group_discard(
self.group_name,
self.channel_name
)
async def receive(self, event):
print('receive')
async def auto_update(self, event):
print('sending df')
df = event['df']
await self.send({
'text': df
})
To send the message I would use a custom management command. To stop the command I would create a singleton model (a model with only one instance) that has a boolean field that can be periodically checked to see if the loop should be stopped.
First use get_channel_layer() to get the active layer that communicates with redis, then in the loop call group_send to invoke a consumer method specified by the type key.
# /project/app/management/commands/auto_update.py
from django.core.management.base import BaseCommand
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from config.models import AutoUpdateSettings
class Command(BaseCommand):
help = 'Command to start auto updating'
def handle(self, *args, **kwargs):
settings = AutoUpdateSettings.objects.first()
settings.keep_running = True
settings.save()
group_name = 'auto_update'
channel_layer = get_channel_layer()
while True:
settings.refresh_from_db()
if not settings.keep_running:
break
df= pd.DataFrame(data=[random.sample(range(100), 4) for _ in range(5)])
async_to_sync(channel_layer.group_send)(
group_name,
{
'type': 'auto_update', # this is the name of your consumer method
'df': df.to_html()
}
)
To start the loop that sends the message to the group you would call the command python manage.py auto_update. To stop the command you would use the admin page and set keep_running to false.

django-channels 2.x How to implement login required on the consumer class?

I am using django-channels 2.1.2. Since it had a drastic change from 1.x to 2.x, I would like to know the way to enforce login on the consumer class.
So far:
from channels.generic.websocket import AsyncWebsocketConsumer
import json
from channels.consumer import SyncConsumer
from doclet.models import *
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
to_room_id = self.scope['url_route']['kwargs']['to_room_id']
self.user = self.scope["user"]
if self.user.is_authenticated:
if self.user.rooms.filter(pk=int(to_room_id)).exists():
self.to_room = Room.objects.get(pk=int(to_room_id))
self.room_name = 'room_%s' %self.to_room.id
else:
self.close() #do something to create room for the user
# Join room group
await self.channel_layer.group_add(
self.room_name,
self.channel_name
)
await self.accept()
You need to set this up in your Application stack.
application = ProtocolTypeRouter({
"websocket": AuthMiddlewareStack(
URLRouter([
url(r"^front(end)/$", consumers.AsyncChatConsumer),
])
),
})
And then in your connect method, you can access the user from self.scope['user'] feel free to save this onto your consumer instance. By trying to read the user from the scope you will ensure a user object was resolved.
you can do this before calling self.accept() in the connect method if you so wish to ensure the WebSocket connection is not accepted if the user auth failed.

Using django signals in channels consumer classes

I am trying to develop an auction type system, where a customer makes an order, and then different stores can offer a price for that order.
An interesting part of this system is that when the order is initially created, the available stores will have 60 seconds to make their respective offer. When a first store makes their offer, the "auction" will now only have the next 20 seconds for other stores to make their own offer. If they do make another offer, in this smaller allocated time, then this 20 second is refreshed. Offers can keep on being received as long as there is enough time, which cannot surpass the initial 60 seconds given.
class Order(models.Model):
customer = models.ForeignKey(Customer)
create_time = models.DateTimeField(auto_now_add=True)
update_time = models.DateTimeField(auto_now_add=True)
total = models.FloatField(default=0)
status = models.IntegerField(default=0)
delivery_address = models.ForeignKey(DeliveryAddress)
store = models.ForeignKey(Store, null=True, blank=True, related_name='orders', on_delete=models.CASCADE)
credit_card = models.ForeignKey(CreditCard, null=True, blank=True, related_name='orders')
class OrderOffer(models.Model):
store = models.ForeignKey(Store, related_name="offers", on_delete=models.CASCADE)
order = models.ForeignKey(Order, related_name="offers", on_delete=models.CASCADE)
create_time = models.DateTimeField(auto_now_add=True)
Besides these requirements, I also want to update the client when new offers arrive in real-time. For this, I'm using django-channels implementation of WebSockets.
I have the following consumers.pyfile:
from channels.generic.websockets import WebsocketConsumer
from threading import Timer
from api.models import Order, OrderOffer
from django.db.models.signals import post_save
from django.dispatch import receiver
class OrderConsumer(WebsocketConsumer):
def connect(self, message, **kwargs):
"""
Initialize objects here.
"""
order_id = int(kwargs['order_id'])
self.order = Order.objects.get(id=order_id)
self.timer = Timer(60, self.sendDone)
self.timer.start()
self.message.reply_channel.send({"accept": True})
def sendDone(self):
self.send(text="Done")
# How do I bind self to onOffer?
#receiver(post_save, sender=OrderOffer)
def onOffer(self, sender, **kwargs):
self.send(text="Offer received!")
if (len(self.offers) == 0):
self.offerTimer = Timer(20, self.sendDone)
self.offers = [kwargs['instance'],]
else:
self.offerTimer = Timer(20, self.sendDone)
self.offers.append(kwargs['instance'])
def receive(self, text=None, bytes=None, **kwargs):
# Echo
self.send(text=text, bytes=bytes)
def disconnect(self, message, **kwargs):
"""
Perform necessary disconnect operations.
"""
pass
I have successfully been able to establish a WebSocket communication channel between my client and the server. I've tested sending messages, and everything seems ok. Now I want to detect the creation of new OrderOffer's, and send a notification to the client. For this, I need access to the self variable, to use self.send, which is impossible, as the signals decorator does not send this parameter. I've tried forcing it by declaring onOffer with self, but I get the following error:
TypeError: onOffer() missing 1 required positional argument: 'self'
If I could somehow access the keyword arguments, that signals sets, I could maybe do something like:
context = self.
I would appreciate any help, or even alternative solutions to my original problem.
If someone stumbles upon on that, this is the way I solved it in the signals.py. I have a Job and need to send its status to the client every time it changes. This is my signals.py:
import channels.layers
from asgiref.sync import async_to_sync
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Job
def send_message(event):
'''
Call back function to send message to the browser
'''
message = event['text']
channel_layer = channels.layers.get_channel_layer()
# Send message to WebSocket
async_to_sync(channel_layer.send)(text_data=json.dumps(
message
))
#receiver(post_save, sender=Job, dispatch_uid='update_job_status_listeners')
def update_job_status_listeners(sender, instance, **kwargs):
'''
Sends job status to the browser when a Job is modified
'''
user = instance.owner
group_name = 'job-user-{}'.format(user.username)
message = {
'job_id': instance.id,
'title': instance.title,
'status': instance.status,
'modified': instance.modified.isoformat(),
}
channel_layer = channels.layers.get_channel_layer()
async_to_sync(channel_layer.group_send)(
group_name,
{
'type': 'send_message',
'text': message
}
)
By the way, I have a Consumer class JobUserConsumer(AsyncWebsocketConsumer) where I define the groups:
async def connect(self):
user = self.scope["user"]
self.group_name = 'job-user-{}'.format(user.username)
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.accept()
The project I used this is here: https://github.com/ornl-ndav/django-remote-submission/tree/master/django_remote_submission
For those who still have problems with web sockets, this could be helpful:
from api.models import Order, OrderOffer
from asgiref.sync import async_to_sync
import channels.layers
from channels.generic.websocket import JsonWebsocketConsumer
from django.db.models import signals
from django.dispatch import receiver
class OrderOfferConsumer(JsonWebsocketConsumer):
def connect(self):
async_to_sync(self.channel_layer.group_add)(
'order_offer_group',
self.channel_name
)
self.accept()
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
'order_offer_group',
self.channel_name
)
self.close()
def receive_json(self, content, **kwargs):
print(f"Received event: {content}")
def events_alarm(self, event):
self.send_json(event['data'])
#staticmethod
#receiver(signals.post_save, sender=OrderOffer)
def order_offer_observer(sender, instance, **kwargs):
layer = channels.layers.get_channel_layer()
async_to_sync(layer.group_send)('order_offer_group', {
'type': 'events.alarm',
'data': {
'text': 'Offer received',
'id': instance.pk
}
})
In urls.py you need to register a new webscoket route:
websocket_urlpatterns = [url(r'^order_offer$', OrderOfferConsumer)]
If you want to talk to the consumer from "outside" - in this case, from a model save method - you'll need to use a Channel Layer to talk to it: http://channels.readthedocs.io/en/latest/topics/channel_layers.html
Essentially, you'll need to:
Add the consumer to a Group on startup (probably based on its order ID)
Send a message to the Group whenever there's a new OrderOffer with a custom type - e.g. {"type": "order.new_offer", "order_offer_id": 45}
Define a handler on the Consumer that handles this - it matches the type name, so in this case it would be def order_new_offer(self, event):
In that handler you can then use self.send to talk down the socket (and query the database if you need extra info to send to the client you didn't put into the event message).
You can see a variant of this in the MultiChat example project: https://github.com/andrewgodwin/channels-examples/tree/master/multichat