Expanded chat backend with message reply, edit, delete, and reaction support in consumers and models. Updated routing to use chat_id. Added chat-related view stubs. On the frontend, introduced ChatLayout and ChatPage scaffolding, and routed protected routes through ChatLayout.
187 lines
5.4 KiB
Python
187 lines
5.4 KiB
Python
from django.db import models
|
|
from django.conf import settings
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils import timezone
|
|
|
|
class Chat(models.Model):
|
|
owner = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
related_name='owned_chats'
|
|
)
|
|
|
|
members = models.ManyToManyField(
|
|
settings.AUTH_USER_MODEL,
|
|
related_name='chats',
|
|
blank=True
|
|
)
|
|
|
|
moderators = models.ManyToManyField(
|
|
settings.AUTH_USER_MODEL,
|
|
related_name='moderated_chats',
|
|
blank=True
|
|
)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
def save(self, *args, **kwargs):
|
|
is_new = self._state.adding
|
|
super().save(*args, **kwargs)
|
|
|
|
# LOGIC: Ensure owner is always a member and moderator
|
|
if is_new and self.owner:
|
|
self.members.add(self.owner)
|
|
self.moderators.add(self.owner)
|
|
|
|
def __str__(self):
|
|
return f"Chat {self.id}"
|
|
|
|
|
|
class Message(models.Model):
|
|
chat = models.ForeignKey(Chat, related_name='messages', on_delete=models.CASCADE)
|
|
|
|
sender = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='sent_messages'
|
|
)
|
|
|
|
#odpověď na jinou zprávu
|
|
reply_to = models.ForeignKey(
|
|
'self',
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='replies'
|
|
)
|
|
|
|
content = models.TextField(blank=True)
|
|
|
|
# --- TRACKING EDIT STATUS ---
|
|
# We add these so the frontend doesn't need to check MessageHistory table
|
|
is_edited = models.BooleanField(default=False)
|
|
edited_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
def clean(self):
|
|
# VALIDATION: Ensure sender is actually in the chat
|
|
# Note: We check self.id to avoid running this on creation if logic depends on M2M
|
|
# But generally, a sender must be a member.
|
|
if self.chat and self.sender:
|
|
if not self.chat.members.filter(id=self.sender.id).exists():
|
|
raise ValidationError("Sender is not a member of this chat.")
|
|
|
|
def save(self, *args, **kwargs):
|
|
# Optional: Run validation before saving
|
|
# self.full_clean()
|
|
super().save(*args, **kwargs)
|
|
|
|
# --- HELPER METHODS FOR WEBSOCKETS / VIEWS ---
|
|
|
|
def edit_content(self, new_text):
|
|
"""
|
|
Handles the complex logic of editing:
|
|
1. Checks if text actually changed.
|
|
2. Saves old text to History.
|
|
3. Updates current text and timestamps.
|
|
"""
|
|
if self.content == new_text:
|
|
return False # No change happened
|
|
|
|
# 1. Save History
|
|
MessageHistory.objects.create(
|
|
message=self,
|
|
old_content=self.content
|
|
)
|
|
|
|
# 2. Update Self
|
|
self.content = new_text
|
|
self.is_edited = True
|
|
self.edited_at = timezone.now()
|
|
self.save()
|
|
return True
|
|
|
|
def toggle_reaction(self, user, emoji):
|
|
"""
|
|
Handles Add/Remove/Switch logic.
|
|
Returns a tuple: (action, reaction_object)
|
|
action can be: 'added', 'removed', 'switched'
|
|
"""
|
|
try:
|
|
reaction = MessageReaction.objects.get(message=self, user=user)
|
|
|
|
if reaction.emoji == emoji:
|
|
# Same emoji -> Remove it (Toggle)
|
|
reaction.delete()
|
|
return 'removed', None
|
|
else:
|
|
# Different emoji -> Switch it
|
|
reaction.emoji = emoji
|
|
reaction.save()
|
|
return 'switched', reaction
|
|
|
|
except MessageReaction.DoesNotExist:
|
|
# New reaction -> Create it
|
|
reaction = MessageReaction.objects.create(
|
|
message=self,
|
|
user=user,
|
|
emoji=emoji
|
|
)
|
|
return 'added', reaction
|
|
|
|
def __str__(self):
|
|
return f"Message {self.id} from {self.sender}"
|
|
|
|
|
|
class MessageHistory(models.Model):
|
|
message = models.ForeignKey(
|
|
Message,
|
|
on_delete=models.CASCADE,
|
|
related_name='edit_history'
|
|
)
|
|
old_content = models.TextField()
|
|
archived_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
ordering = ['-archived_at']
|
|
|
|
|
|
class MessageReaction(models.Model):
|
|
message = models.ForeignKey(
|
|
Message,
|
|
on_delete=models.CASCADE,
|
|
related_name='reactions'
|
|
)
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
emoji = models.CharField(max_length=10)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
unique_together = ('message', 'user')
|
|
|
|
def __str__(self):
|
|
return f"{self.user} reacted {self.emoji}"
|
|
|
|
|
|
class MessageFile(models.Model):
|
|
message = models.ForeignKey(
|
|
Message,
|
|
on_delete=models.CASCADE,
|
|
related_name='media_files'
|
|
)
|
|
file = models.FileField(upload_to='chat_uploads/%Y/%m/%d/')
|
|
|
|
media_type = models.CharField(max_length=20, choices=[
|
|
('IMAGE', 'Image'),
|
|
('VIDEO', 'Video'),
|
|
('FILE', 'File')
|
|
], default='FILE')
|
|
|
|
uploaded_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
def __str__(self):
|
|
return f"Media {self.id} for Message {self.message.id}" |