Using django signals in channels consumer classes
Asked Answered
C

3

26

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.

Cowpox answered 6/10, 2017 at 22:3 Comment(0)
F
17

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

Freddafreddi answered 19/4, 2018 at 21:29 Comment(1)
I have to say that I had to struggled with this implementation. Here is the link to my repository that successfully implements this example - Django signals over WebSocketsDingus
A
12

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)]
Arsenical answered 7/6, 2019 at 9:48 Comment(2)
Thanks! This answer was very helpful.Pompey
I have to say that I had to struggled with this implementation. Here is the link to my repository that successfully implements this example - Django signals over WebSocketsDingus
G
6

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

Gerhardt answered 14/2, 2018 at 19:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.