Search code examples
javascriptpythondjangowebchat

Django - How to display user's profile picture with a message after its sent?


I have a live chat application and I'm trying to display a profile picture with the message after its sent with Javascript. Here is my code...

Models.py - here is my Message and Profile model

class Message(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    room = models.CharField(max_length = 255)
    content = models.TextField()
    date_added = models.DateTimeField(auto_now_add = True)

    class Meta:
        ordering = ('date_added', )

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    image = models.ImageField(default='default.png', upload_to='profile_pics')

    def __str__(self):
        return f'{self.user.username} Profile'

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

        img = Image.open(self.image.path)

        if img.height > 300 or img.width > 300:
            output_size = (300, 300)
            img.thumbnail(output_size)
            img.save(self.image.path)

Consumers.py

class ChatRoomConsumer(AsyncWebsocketConsumer):
async def connect(self):
    self.room_name = self.scope['url_route']['kwargs']['room_name']
    self.room_group_name = 'chat_%s' % self.room_name
    print(self.room_group_name)

    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):
    text_data_json = json.loads(text_data)
    message = text_data_json['message']
    username = text_data_json['username']
    room = text_data_json['room']

    await self.save_message(username, room, message)

    await self.channel_layer.group_send(
        self.room_group_name,
        {
            'type': 'chatroom_message',
            'message': message,
            'username': username,
        }
    )

async def chatroom_message(self, event):
    message = event['message']
    username = event['username']

    await self.send(text_data=json.dumps({
        'message': message,
        'username': username,
    }))

@sync_to_async
def save_message(self, username, room, message):
    user = User.objects.get(username = username)
    Message.objects.create(author = user, room = room, content = message)

pass

And here is the Javascript code for the live chat...

{{ request.user.username|json_script:"user_username" }}
{{ room_name|json_script:"room-name" }}
<script>
    const user_username = JSON.parse(document.getElementById('user_username').textContent);
    document.querySelector('#submit').onclick = function (e) {
        const messageInputDom = document.querySelector('#input');
        const message = messageInputDom.value;
        if(message.trim() == ''){
        }
        else {
            chatSocket.send(JSON.stringify({
            'message': message,
            'username': user_username,
            'room': roomName,
        }));
        messageInputDom.value = '';
        }
    };
    const roomName = JSON.parse(document.getElementById('room-name').textContent);

    const chatSocket = new WebSocket(
        'ws://' +
        window.location.host +
        '/ws/chat/' +
        roomName +
        '/'
    );

    chatSocket.onmessage = function (e) {
        const data = JSON.parse(e.data);
        console.log(data)
        if (data.message) {
                document.querySelector('#chat-text').innerHTML += ('<img id="imageid" src="{{ message.author.profile.image.url }}">' + data.username + '<br>' + data.message + '<br>');
            }
        else {

        }
    }
</script>

The problem is with this <img id="imageid" src="{{ message.author.profile.image.url }}"> in the Javascript code. So my question is, how can I display the user's profile picture with the message after its sent without refreshing the page?


Solution

  • You don't need to use AJAX here, you are communicating with the server via WebSockets, you can get the data you need through it.

    Looks like you are using django-channels. Check django-channels documentation about authentication. If you enable it, you will be able to access the current user through self.scope['user'].

    So, you will able to do something like this:

    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        username = self.scope['user'].username
        profile_pic = self.scope['user'].profile.image.url  # None handling required
        room = text_data_json['room']
    
        await self.save_message(username, room, message)
    
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                'type': 'chatroom_message',
                'message': message,
                'username': username,
                'profile_pic': profile_pic
            }
        )
    
    async def chatroom_message(self, event):
        message = event['message']
        username = event['username']
        profile_pic = event['profile_pic']
    
        await self.send(text_data=json.dumps({
            'message': message,
            'username': username,
            'profile_pic': profile_pic
        }))
    
     # on the frontend
     chatSocket.onmessage = function (e) {
            const data = JSON.parse(e.data);
            console.log(data)
            if (data.message) {
                    document.querySelector('#chat-text').innerHTML += (`<img id="imageid" src="${data.profile_pic}">` + data.username + '<br>' + data.message + '<br>');
                }
            else {
                
            }
    

    Note that you can't make database calls from async context because database connector works in a synchronous manner. When you are getting a profile pic self.scope['user'].profile.image.url you are actually making a DB call to get user's profile.

    What you need to do in such cases is make a db call using either database_sync_to_async or sync_to_async decorators.

    For example:

    from channels.db import database_sync_to_async
    
    @database_sync_to_async
    def get_user_profile(self):
        return self.scope['user'].profile
     
    # and in receive method you call it
    async def receive(self, text_data):
        profile = await self.get_user_profile()
        profile_pic = profile.image.url
        ....