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?
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
....