Add chat models and scaffold pages/posts apps
Implemented comprehensive models for chat functionality, including Chat, Message, MessageHistory, MessageReaction, and MessageFile. Updated ChatConsumer to enforce authentication and improve message handling. Added initial scaffolding for 'pages' and 'posts' Django apps with basic files.
This commit is contained in:
@@ -9,19 +9,34 @@ from channels.generic.websocket import AsyncWebsocketConsumer
|
|||||||
|
|
||||||
class ChatConsumer(AsyncWebsocketConsumer):
|
class ChatConsumer(AsyncWebsocketConsumer):
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
|
user = self.scope["user"]
|
||||||
|
|
||||||
|
if not user.is_authenticated:
|
||||||
|
await self.close(code=4401) # unauthorized
|
||||||
|
return
|
||||||
|
|
||||||
await self.accept()
|
await self.accept()
|
||||||
|
|
||||||
async def disconnect(self, close_code):
|
async def disconnect(self, close_code):
|
||||||
|
self.disconnect()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def receive(self, text_data):
|
async def receive(self, data):
|
||||||
text_data_json = json.loads(text_data)
|
if data["type"] == "chat_message":
|
||||||
message = text_data_json["message"]
|
pass
|
||||||
|
else:
|
||||||
|
self.close(reason="Unsupported message type")
|
||||||
|
|
||||||
|
|
||||||
|
# -- CUSTOM METHODS --
|
||||||
|
|
||||||
|
async def send_message_to_chat_group(self, event):
|
||||||
|
message = event["message"]
|
||||||
|
await create_new_message()
|
||||||
await self.send(text_data=json.dumps({"message": message}))
|
await self.send(text_data=json.dumps({"message": message}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@database_sync_to_async
|
@database_sync_to_async
|
||||||
def get_user_profile(user_id):
|
def create_new_message(user_id):
|
||||||
return UserProfile.objects.get(pk=user_id)
|
return None
|
||||||
@@ -1,3 +1,178 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
# Create your models here.
|
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'
|
||||||
|
)
|
||||||
|
|
||||||
|
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}"
|
||||||
0
backend/social/pages/__init__.py
Normal file
0
backend/social/pages/__init__.py
Normal file
3
backend/social/pages/admin.py
Normal file
3
backend/social/pages/admin.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
backend/social/pages/apps.py
Normal file
6
backend/social/pages/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PagesConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'pages'
|
||||||
0
backend/social/pages/migrations/__init__.py
Normal file
0
backend/social/pages/migrations/__init__.py
Normal file
3
backend/social/pages/models.py
Normal file
3
backend/social/pages/models.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
3
backend/social/pages/tests.py
Normal file
3
backend/social/pages/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
3
backend/social/pages/views.py
Normal file
3
backend/social/pages/views.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
0
backend/social/posts/__init__.py
Normal file
0
backend/social/posts/__init__.py
Normal file
3
backend/social/posts/admin.py
Normal file
3
backend/social/posts/admin.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
backend/social/posts/apps.py
Normal file
6
backend/social/posts/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PostsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'posts'
|
||||||
0
backend/social/posts/migrations/__init__.py
Normal file
0
backend/social/posts/migrations/__init__.py
Normal file
3
backend/social/posts/models.py
Normal file
3
backend/social/posts/models.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
3
backend/social/posts/tests.py
Normal file
3
backend/social/posts/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
3
backend/social/posts/views.py
Normal file
3
backend/social/posts/views.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
Reference in New Issue
Block a user