Django Channels ORM Database Query - django

I don't know what I did wrong
I can't get Database Data.
class AsyncChatConsumer(AsyncWebsocketConsumer):
async def receive(self, text_data):
users = await self.get_users()
for user in users:
print(user.id)
#database_sync_to_async
def get_users(self):
return User.objects.all()
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
The information I checked and the official documents are all written in this way.
But why am I getting an error?

Your problem is duplicate on stackoverflow:
It's already explained there: Django Channels Error: you cannot use AsyncToSync in the same thread as an async event loop

Related

Terminate previous Celery task with same task id and run again if created

In my django project, I have made a view class by using TemplateView class. Again, I am using django channels and have made a consumer class too. Now, I am trying to use celery worker to pull queryset data whenever a user refreshes the page. But the problem is, if user again refreshes the page before the task gets finished, it create another task which causes overload.
Thus I have used revoke to terminate the previous running task. But I see, the revoke permanently revoked the task id. I don't know how to clear this. Because, I want to run the task again whenever user call it.
views.py
class Analytics(LoginRequiredMixin,TemplateView):
template_name = 'app/analytics.html'
login_url = '/user/login/'
def get_context_data(self, **kwargs):
app.control.terminate(task_id=self.request.user.username+'_analytics')
print(app.control.inspect().revoked())
context = super().get_context_data(**kwargs)
context['sub_title'] = 'Analytics'
return context
consumers.py
class AppConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
await self.accept()
analytics_queryset_for_selected_devices.apply_async(
args=[self.scope['user'].username],
task_id=self.scope['user'].username+'_analytics'
)
Right now I am solving the problem in this following way. In the consumers.py I made a disconnect function which revoke the task when the web socket get closed.
counter = 0
class AppConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
await self.accept()
analytics_queryset_for_selected_devices.apply_async(args=[self.scope['user'].username],
task_id=self.scope['user'].username+str(counter))
async def disconnect(self, close_code):
global counter
app.control.terminate(task_id=self.scope['user'].username+str(counter), signal='SIGKILL')
counter += 1
await self.close()
counter is used for making new unique task id. But in this method, for every request makes a new task id is added in the revoke list which cause load in memory. To minimize the issue I limited the revoke list size to 20.
from celery.utils.collections import LimitedSet
from celery.worker import state
state.revoked = LimitedSet(maxlen=20, expires=3600)

Why is it not possible to use the AsyncClient to login without using sync_to_async?

According to the documentation about testing async functions in Django 3.1, it should be possible to simply await the async client methods in an async context without having to wrap it in sync_to_async or database_sync_to_async as it implements all methods. Is there a way around this?
The first request works, but the second and third do not.
class TestAsyncClient(TransactionTestCase):
def setUp(self):
self.async_client = AsyncClient()
self.user = get_user_model().objects.create_user("user_a#example.com", "passwd_a")
async def test_async_login(self):
"""Use the async client to login without using sync_to_async"""
request = await self.async_client.get("/")
self.assertTrue(request)
logged_in = await self.async_client.login(
username="user_a#example.com", password="passwd_a"
)
self.assertTrue(logged_in)
logged_in = await self.async_client.force_login(self.user)
self.assertTrue(logged_in)
First of all, you get a response, not a request.
What evaluates to True, would be response.ok.
However, async_client.login is not an async method and does not send a request, same with force_login. They simply fake a session with a logged in user and are synchronous (with database access if your SESSION_STORE is ModelBackend).

Remove a specific user from a Django Channels group?

I have a couple of Django Channels groups that I use to send various messages to the client with.
Can I remove a specific user from one of these groups, using only the ID of the user that I want to remove?
Another potential option would be to force disconnect the user's connection using just their ID.
First, you have to save the user's channel_name to their model
and we assume that you had the group_name of channels too
then you can use group_discard for deleting user from group like this:
group_name = 'name_of_channels_group'
user = User.objects.get(id=id)
channel_name = user.channel_name
async_to_sync(self.channel_layer.group_discard)(group_name, channel_name)
https://channels.readthedocs.io/en/stable/topics/channel_layers.html?highlight=group_send#groups
I have thought couple a days about this problem and have an idea how this can be implemented, but can't test it currently. You should try to change your receive() method like this:
async def receive(self, text_data=None, bytes_data=None):
text_data_json = json.loads(text_data)
message = text_data_json['message']
users_to_kick = text_data_json['kick']
# you should inspect scope['user'] cuz I am not sure in what place
# user's id is placed, but there is 'user' object.
if self.scope['user']['id'] in list(map(int, users_to_kick)):
await self.close()
else:
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'some_method',
'message': message
}
)
You have to have Authorisation system enabled, you can't kick Anonymous users. And you have to send from Front-End a list of users which you want to kick.
You need to prevent connect based on scope['user'] in consumer connect method like this:
class MyConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
id_cannot_connect = 1
if self.scope['user'] == id_cannot_connect:
await self.close()
else:
await self.accept()
If you want to make lists of users allowed to connect to specific groups you'll need to store groups and group users in your database and use it the same way in connect method like above.
edit: You can discard user's channel from the group with group_discard in receive_json where you still have access to self.scope['user'] to filter needed users.
I solved this in the end by storing each channel name (self.channel_name) on channel connection, and removing them on disconnect. These are then tied to a Django user object.
Now, if I want to remove a user from a group, I can just loop over all stored channel names tied to a user object, and run group_discard.

Django Channels: Can someone provide a very simple working example of data binding?

I'm learning how to implement Django channels into my website but I have problems understand the documentation (http://channels.readthedocs.io/en/latest/binding.html). There are not many examples on the internet. Can someone provide me a working source code of data binding with decent commenting?
For django-channels 1 - https://bitbucket.org/voron-raven/chat/src/aec8536dba2cc5f0faa42305dcd7a49d330a8b54/core/models.py?at=master&fileviewer=file-view-default#models.py-242
It's simple chat site with a chatbot - http://chat.mkeda.me
Example of data binding:
class MessageBinding(WebsocketBinding):
model = Message
stream = 'messages'
fields = ['__all__']
#classmethod
def group_names(cls, instance):
"""
Returns the iterable of group names to send the object to based on the
instance and action performed on it.
"""
return ['thread-{}'.format(instance.thread.pk)]
def has_permission(self, user, action, pk):
"""
Return True if the user can do action to the pk, False if not.
User may be AnonymousUser if no auth hooked up/they're not logged in.
Action is one of "create", "delete", "update".
"""
if action == 'create':
return True
return user.is_superuser
In django-channels 2 binding was removed - http://channels.readthedocs.io/en/latest/one-to-two.html?highlight=binding#removed-components
You can use signals to send updates to your groups.

Sending a message to a single user using django-channels

I have been trying out django-channels including reading the docs and playing around with the examples.
I want to be able to send a message to a single user that is triggered by saving a new instance to a database.
My use case is creating a new notification (via a celery task) and once the notification has saved, sending this notification to a single user.
This sounds like it is possible (from the django-channels docs)
...the crucial part is that you can run code (and so send on
channels) in response to any event - and that includes ones you
create. You can trigger on model saves, on other incoming messages, or
from code paths inside views and forms.
However reading the docs further and playing around with the django-channels examples, I can't see how I can do this. The databinding and liveblog examples demonstrate sending to a group, but I can't see how to just send to a single user.
Little update since Groups work differently with channels 2 than they did with channels 1. There is no Group class anymore, as mentioned here.
The new groups API is documented here. See also here.
What works for me is:
# Required for channel communication
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
def send_channel_message(group_name, message):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
'{}'.format(group_name),
{
'type': 'channel_message',
'message': message
}
)
Do not forget to define a method to handle the message type in the Consumer!
# Receive message from the group
def channel_message(self, event):
message = event['message']
# Send message to WebSocket
self.send(text_data=json.dumps({
'message': message
}))
Expanding on #Flip's answer of creating a group for that particular user.
In your python function in your ws_connect function you can add that user into a a group just for them:
consumers.py
from channels.auth import channel_session_user_from_http
from channels import Group
#channel_session_user_from_http
def ws_connect(message):
if user.is_authenticated:
Group("user-{}".format(user.id)).add(message.reply_channel)
To send that user a message from your python code:
my view.py
import json
from channels import Group
def foo(user):
if user.is_authenticated:
Group("user-{}".format(user.id)).send({
"text": json.dumps({
"foo": 'bar'
})
})
If they are connected they will receive the message. If the user is not connected to a websocket it will fail silently.
You will need to also ensure that you only connect one user to each user's Group, otherwise multiple users could receive a message that you intended for only a specific user.
Have a look at django channels examples, particularly multichat for how to implement routing, creating the websocket connection on the client side and setting up django_channels.
Make sure you also have a look at the django channels docs.
In Channels 2, you can save self.channel_name in a db on connect method that is a specific hash for each user. Documentation here
from asgiref.sync import async_to_sync
from channels.generic.websocket import AsyncJsonWebsocketConsumer
import json
class Consumer(AsyncJsonWebsocketConsumer):
async def connect(self):
self.room_group_name = 'room'
if self.scope["user"].is_anonymous:
# Reject the connection
await self.close()
else:
# Accept the connection
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
print( self.channel_name )
Last line returns something like specific.WxuYsxLK!owndoeYTkLBw
This specific hash you can save in user's table.
The best approach is to create the Group for that particular user. When ws_connect you can add that user into Group("%s" % <user>).add(message.reply_channel)
Note: My websocket url is ws://127.0.0.1:8000/<user>
Just to extend #luke_aus's answer, if you are working with ResourceBindings, you can also make it so, that only users "owning" an object retrieve updates for these:
Just like #luke_aus answer we register the user to it's own group where we can publish actions (update, create) etc that should only be visible to that user:
from channels.auth import channel_session_user_from_http,
from channels import Group
#channel_session_user_from_http
def ws_connect(message):
Group("user-%s" % message.user).add(message.reply_channel)
Now we can change the corresponding binding so that it only publishes changes if the bound object belongs to that user, assuming a model like this:
class SomeUserOwnedObject(models.Model):
owner = models.ForeignKey(User)
Now we can bind this model to our user group and all actions (update, create, etc) will only be published to this one user:
class SomeUserOwnedObjectBinding(ResourceBinding):
# your binding might look like this:
model = SomeUserOwnedObject
stream = 'someuserownedobject'
serializer_class = SomeUserOwnedObjectSerializer
queryset = SomeUserOwnedObject.objects.all()
# here's the magic to only publish to this user's group
#classmethod
def group_names(cls, instance, action):
# note that this will also override all other model bindings
# like `someuserownedobject-update` `someuserownedobject-create` etc
return ['user-%s' % instance.owner.pk]
Although it's late but I have a direct solution for channels 2 i.e using send instead of group_send
send(self, channel, message)
| Send a message onto a (general or specific) channel.
use it as -
await self.channel_layer.send(
self.channel_name,
{
'type':'bad_request',
'user':user.username,
'message':'Insufficient Amount to Play',
'status':'400'
}
)
handel it -
await self.send(text_data=json.dumps({
'type':event['type'],
'message': event['message'],
'user': event['user'],
'status': event['status']
}))
Thanks