Search code examples
pythondjangodjango-viewsdjango-channels

logic of saving a notification object django websockets


I am having a hard time wrapping my head around how to save notifications when a message is received from another other_user in a 2-person Django Channels websockets chat application.

Right now I have a function def create_notification that is called after def create_chat_message.

def create_chat_message creates a new ChatMessage object each time a new message is sent in the Thread. It takes args thread=thread_obj, user=me, message=msg So obviously each individual message is saved with the user than sent it.

def create_notification takes the last object by id in ChatMessage and creates a new Notification object.

created_notification = Notification.objects.create(notification_user=user, notification_chat=last_chat)

So essentially, the person who sends the message is associated with the notification_user field in the Notification model and saved along with the ChatMessage id.

But if I send a message to Tom, my sent message should only be associated with a notification for Tom, not for myself.

When I go to render out the notification objects, I get a list of all of them, including notifications for messages I've sent obviously.

How can I render out all notifications for each thread I am in with various users?

Am I saving these wrong? Should I configure the save notification function so that it only saves incoming messages from the other user? Or add some kind of if statement?

Don't I need to be associated in some way to the notification so that when they are rendered out, all notifications where I am the recipient will display?

My Notification model has ChatMessage as a ForeignKey which has a thread field, which is a ForeignKey to Thread which contains first and second (representing me and another user in a single thread).

I've been looking at this for days and have a feeling I am missing something simple and am making this far more complicated than it needs to be.

models.py

class ThreadManager(models.Manager):
    def by_user(self, user):
        qlookup = Q(first=user) | Q(second=user)
        qlookup2 = Q(first=user) & Q(second=user)
        qs = self.get_queryset().filter(qlookup).exclude(qlookup2).distinct()
        return qs

    # method to grab the thread for the 2 users
    def get_or_new(self, user, other_username): # get_or_create
        username = user.username
        if username == other_username:
            return None, None
        # looks based off of either username
        qlookup1 = Q(first__username=username) & Q(second__username=other_username)
        qlookup2 = Q(first__username=other_username) & Q(second__username=username)
        qs = self.get_queryset().filter(qlookup1 | qlookup2).distinct()
        if qs.count() == 1:
            return qs.first(), False
        elif qs.count() > 1:
            return qs.order_by('timestamp').first(), False
        else:
            Klass = user.__class__
            try:
                user2 = Klass.objects.get(username=other_username)
            except Klass.DoesNotExist:
                user2 = None
            if user != user2:
                obj = self.model(
                        first=user,
                        second=user2
                    )
                obj.save()
                return obj, True
            return None, False

class Thread(models.Model):
    first        = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chat_thread_first')
    second       = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chat_thread_second')
    updated      = models.DateTimeField(auto_now=True)
    timestamp    = models.DateTimeField(auto_now_add=True)

    objects      = ThreadManager()

    def __str__(self):
        return f'{self.id}'

    @property
    def room_group_name(self):
        return f'chat_{self.id}'

    def broadcast(self, msg=None):
        if msg is not None:
            broadcast_msg_to_chat(msg, group_name=self.room_group_name, user='admin')
            return True
        return False

class ChatMessage(models.Model):
    thread      = models.ForeignKey(Thread, null=True, blank=True, on_delete=models.SET_NULL)
    user        = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='sender', on_delete=models.CASCADE)
    message     = models.TextField()
    timestamp   = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f'{self.id}'

class Notification(models.Model):
    notification_user = models.ForeignKey(User, on_delete=models.CASCADE)
    notification_chat = models.ForeignKey(ChatMessage, on_delete=models.CASCADE)
    notification_read = models.BooleanField(default=False)

    def __str__(self):
        return f'{self.id} attached to {self.notification_user}'

consumers.py

class ChatConsumer(AsyncConsumer):
    async def websocket_connect(self, event):
        print('connected', event)

        other_user = self.scope['url_route']['kwargs']['username']
        me = self.scope['user']
        #print(other_user, me)
        thread_obj = await self.get_thread(me, other_user)
        self.thread_obj = thread_obj
        chat_room = f"thread_{thread_obj.id}"
        self.chat_room = chat_room
        # below creates the chatroom
        await self.channel_layer.group_add(
            chat_room,
            self.channel_name
        )

        await self.send({
            "type": "websocket.accept"
        })

    async def websocket_receive(self, event):
        # when a message is recieved from the websocket
        print("receive", event)

        message_type = json.loads(event.get('text','{}')).get('type')
        print(message_type)
        if message_type == "notification_read":
            user = self.scope['user']
            username = user.username if user.is_authenticated else 'default'
            # Update the notification read status flag in Notification model.
            notification = Notification.objects.filter(notification_user=user)
            notification.notification_read = True
            notification.save()  #commit to DB
            print("notification read")
            return

        front_text = event.get('text', None)
        if front_text is not None:
            loaded_dict_data = json.loads(front_text)
            msg =  loaded_dict_data.get('message')
            user = self.scope['user']
            username = user.username if user.is_authenticated else 'default'
            notification_id = 'default'
            myResponse = {
                'message': msg,
                'username': username,
                'notification': notification_id,
            }
            print(myResponse)
            await self.create_chat_message(user, msg)
            await self.create_notification(user, msg)

            # broadcasts the message event to be sent, the group send layer
            # triggers the chat_message function for all of the group (chat_room)
            await self.channel_layer.group_send(
                self.chat_room,
                {
                    'type': 'chat_message',
                    'text': json.dumps(myResponse)
                }
            )

    # chat_method is a custom method name that we made
    async def chat_message(self, event):
        # sends the actual message
        await self.send({
                'type': 'websocket.send',
                'text': event['text']
        })

    async def websocket_disconnect(self, event):
        # when the socket disconnects
        print('disconnected', event)

    @database_sync_to_async
    def get_thread(self, user, other_username):
        return Thread.objects.get_or_new(user, other_username)[0]

    @database_sync_to_async
    def create_chat_message(self, me, msg):
        thread_obj = self.thread_obj
        return ChatMessage.objects.create(thread=thread_obj, user=me, message=msg)

    @database_sync_to_async
    def create_notification(self, user, msg):
        last_chat = ChatMessage.objects.latest('id')
        created_notification = Notification.objects.create(notification_user=user, notification_chat=last_chat)
        print(created_notification)
        return created_notification

navbar.html

  <div id="notificationsBody" class="notifications">
              {% for notifications in notification|slice:"0:10" %}
              <a href="{% url 'chat:thread' user %}">
                <span id="notification-{{notification.id}}">
                  {{ notifications.notification_chat.message }}
                  via {{ notifications.notification_chat.user }}
                  at {{ notifications.notification_chat.timestamp }}
                </span>
              </a>
              {% endfor %}

Solution

  • The Notification object that you are creating, you need to set Notification.notification_user to other_user. The notification is for the other user, not the user who sent the message.

    other_username = self.scope['url_route']['kwargs']['username']
    other_user = User.objects.get(username=other_username)
    await self.create_notification(other_user, msg)  #other_user, not the current user
    

    However, when updating the Notification as read, the current user is required.