From deb853b564986ffe9a927ccb120f020d3c51f6bc Mon Sep 17 00:00:00 2001 From: Brunobrno Date: Fri, 26 Dec 2025 04:48:39 +0100 Subject: [PATCH] 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. --- backend/social/chat/consumers.py | 25 ++- backend/social/chat/models.py | 177 +++++++++++++++++++- backend/social/pages/__init__.py | 0 backend/social/pages/admin.py | 3 + backend/social/pages/apps.py | 6 + backend/social/pages/migrations/__init__.py | 0 backend/social/pages/models.py | 3 + backend/social/pages/tests.py | 3 + backend/social/pages/views.py | 3 + backend/social/posts/__init__.py | 0 backend/social/posts/admin.py | 3 + backend/social/posts/apps.py | 6 + backend/social/posts/migrations/__init__.py | 0 backend/social/posts/models.py | 3 + backend/social/posts/tests.py | 3 + backend/social/posts/views.py | 3 + 16 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 backend/social/pages/__init__.py create mode 100644 backend/social/pages/admin.py create mode 100644 backend/social/pages/apps.py create mode 100644 backend/social/pages/migrations/__init__.py create mode 100644 backend/social/pages/models.py create mode 100644 backend/social/pages/tests.py create mode 100644 backend/social/pages/views.py create mode 100644 backend/social/posts/__init__.py create mode 100644 backend/social/posts/admin.py create mode 100644 backend/social/posts/apps.py create mode 100644 backend/social/posts/migrations/__init__.py create mode 100644 backend/social/posts/models.py create mode 100644 backend/social/posts/tests.py create mode 100644 backend/social/posts/views.py diff --git a/backend/social/chat/consumers.py b/backend/social/chat/consumers.py index 3fa4c01..1b61ebb 100644 --- a/backend/social/chat/consumers.py +++ b/backend/social/chat/consumers.py @@ -9,19 +9,34 @@ from channels.generic.websocket import AsyncWebsocketConsumer class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): + user = self.scope["user"] + + if not user.is_authenticated: + await self.close(code=4401) # unauthorized + return + await self.accept() async def disconnect(self, close_code): + self.disconnect() pass - async def receive(self, text_data): - text_data_json = json.loads(text_data) - message = text_data_json["message"] + async def receive(self, data): + if data["type"] == "chat_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})) @database_sync_to_async -def get_user_profile(user_id): - return UserProfile.objects.get(pk=user_id) \ No newline at end of file +def create_new_message(user_id): + return None \ No newline at end of file diff --git a/backend/social/chat/models.py b/backend/social/chat/models.py index 71a8362..f8e3b79 100644 --- a/backend/social/chat/models.py +++ b/backend/social/chat/models.py @@ -1,3 +1,178 @@ 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}" \ No newline at end of file diff --git a/backend/social/pages/__init__.py b/backend/social/pages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/social/pages/admin.py b/backend/social/pages/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/social/pages/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/social/pages/apps.py b/backend/social/pages/apps.py new file mode 100644 index 0000000..cdd024b --- /dev/null +++ b/backend/social/pages/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PagesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'pages' diff --git a/backend/social/pages/migrations/__init__.py b/backend/social/pages/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/social/pages/models.py b/backend/social/pages/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/social/pages/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/social/pages/tests.py b/backend/social/pages/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/social/pages/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/social/pages/views.py b/backend/social/pages/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/social/pages/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/social/posts/__init__.py b/backend/social/posts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/social/posts/admin.py b/backend/social/posts/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/social/posts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/social/posts/apps.py b/backend/social/posts/apps.py new file mode 100644 index 0000000..b18ed0d --- /dev/null +++ b/backend/social/posts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PostsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'posts' diff --git a/backend/social/posts/migrations/__init__.py b/backend/social/posts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/social/posts/models.py b/backend/social/posts/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/social/posts/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/social/posts/tests.py b/backend/social/posts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/social/posts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/social/posts/views.py b/backend/social/posts/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/social/posts/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.