diff --git a/backend/requirements.txt b/backend/requirements.txt index 8685169..01d83c8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -88,6 +88,8 @@ django-silk[formatting] yt-dlp +python-magic + weasyprint #tvoření PDFek z html dokumentu + css styly ## -- MISCELLANEOUS -- diff --git a/backend/social/chat/models.py b/backend/social/chat/models.py index d10404c..40085fc 100644 --- a/backend/social/chat/models.py +++ b/backend/social/chat/models.py @@ -2,6 +2,7 @@ from django.db import models from django.conf import settings from django.core.exceptions import ValidationError from django.utils import timezone +from vontor_cz.custom_fields import WebPImageField class Chat(models.Model): owner = models.ForeignKey( @@ -10,6 +11,11 @@ class Chat(models.Model): null=True, related_name='owned_chats' ) + + name = models.CharField(max_length=255, blank=True) + + icon = WebPImageField(upload_to='chat_icons/', null=True, blank=True) + banner = WebPImageField(upload_to='chat_banners/', null=True, blank=True) members = models.ManyToManyField( settings.AUTH_USER_MODEL, @@ -23,6 +29,15 @@ class Chat(models.Model): blank=True ) + hub = models.ForeignKey( + 'pages.Hub', + on_delete=models.CASCADE, + null=True, + blank=True + ) + + + created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -43,8 +58,9 @@ 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, + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, related_name='sent_messages' ) @@ -68,18 +84,10 @@ class Message(models.Model): 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(): + if self.chat_id and self.sender_id: + 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): diff --git a/backend/social/chat/serializers.py b/backend/social/chat/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/social/nápady_nebo_chybějicí.md b/backend/social/nápady_nebo_chybějicí.md new file mode 100644 index 0000000..d6e5958 --- /dev/null +++ b/backend/social/nápady_nebo_chybějicí.md @@ -0,0 +1,17 @@ +# Social Feature Ideas + +## Hub +- Hub visibility flag (`is_public`) — private/public hubs +- Hub owner transfer (current owner passes ownership to another member) + +## Chat +- Chat type field (DM vs group) — enforce 2-member limit on DMs +- Message soft-delete (`is_deleted` flag) — preserve reply context when a message is removed + +## Posts +- Post reactions/likes — equivalent of MessageReaction but for posts +- Post comments — a `Comment` model FK-ing to `Post` +- Upvote/downvote system for posts — reddit style voting with score and sorting by "hotness" + +## Users +- User blocking — affects message/post visibility across all social features diff --git a/backend/social/pages/models.py b/backend/social/pages/models.py index 71a8362..cd81860 100644 --- a/backend/social/pages/models.py +++ b/backend/social/pages/models.py @@ -1,3 +1,50 @@ from django.db import models +from django.conf import settings +from vontor_cz.custom_fields import WebPImageField # Create your models here. + + +class Hub(models.Model): + name = models.CharField(max_length=255, unique=True) + description = models.TextField(blank=True, null=True) + + owner = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + related_name='owned_hubs' + ) + + icon = WebPImageField(upload_to='hub_icons/', null=True, blank=True) + banner = WebPImageField(upload_to='hub_banners/', null=True, blank=True) + + members = models.ManyToManyField( + settings.AUTH_USER_MODEL, + related_name='hubs', + blank=True + ) + + #TODO: + + def __str__(self): + return self.name + +class HubPermission(models.Model): + hub = models.ForeignKey(Hub, on_delete=models.CASCADE, related_name='moderators') + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + + changing_name = models.BooleanField(default=False) + changing_description = models.BooleanField(default=False) + changing_icon = models.BooleanField(default=False) + changing_banner = models.BooleanField(default=False) + + managing_members = models.BooleanField(default=False) + managing_posts = models.BooleanField(default=False) + managing_chats = models.BooleanField(default=False) + + class Meta: + unique_together = ('hub', 'user') + + def __str__(self): + return f"{self.user} moderates {self.hub}" \ No newline at end of file diff --git a/backend/social/posts/models.py b/backend/social/posts/models.py index 71a8362..d0965ff 100644 --- a/backend/social/posts/models.py +++ b/backend/social/posts/models.py @@ -1,3 +1,42 @@ from django.db import models -# Create your models here. +from django.conf import settings +import magic + +mime = magic.Magic(mime=True) + + +class Post(models.Model): + content = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='posts' + ) + + hub = models.ForeignKey( + 'pages.Hub', + on_delete=models.CASCADE, + null=True, + blank=True + ) + + def __str__(self): + return f"Post {self.id}" + +class PostContent(models.Model): + post = models.ForeignKey(Post, related_name='contents', on_delete=models.CASCADE) + mime_type = models.CharField(max_length=100) + file = models.FileField(upload_to='post_contents/', null=True, blank=True) # For images/videos + alt_text = models.TextField(blank=True, null=True) # For text content + + def __str__(self): + return f"Content for Post {self.post.id} - Type: {self.mime_type}" + + def save(self, *args, **kwargs): + if self.file and not self.file._committed: + self.mime_type = mime.from_buffer(self.file.read(1024)) + return super().save(*args, **kwargs) \ No newline at end of file diff --git a/backend/vontor_cz/custom_fields.py b/backend/vontor_cz/custom_fields.py new file mode 100644 index 0000000..77a4071 --- /dev/null +++ b/backend/vontor_cz/custom_fields.py @@ -0,0 +1,51 @@ +import os +from io import BytesIO +from PIL import Image +from django.db import models +from django.core.files.base import ContentFile +import logging + +logger = logging.getLogger(__name__) + + +class WebPImageField(models.ImageField): + """ + A custom ImageField that converts uploaded images to WebP automatically. + + Inherits from models.ImageField (description: "Image"). + Accepts the same arguments: + verbose_name, name, upload_to, storage, + width_field, height_field, **kwargs + """ + + def pre_save(self, model_instance, add): + file_obj = getattr(model_instance, self.attname) + + if file_obj and not file_obj._committed: + self._convert_to_webp(file_obj) + + return super().pre_save(model_instance, add) + + def _convert_to_webp(self, file_obj): + try: + file_obj.open() + image = Image.open(file_obj) + + if image.format == 'WEBP': + image.close() + return + + if image.mode == 'P': + image = image.convert('RGBA') + elif image.mode not in ('RGBA', 'LA'): + image = image.convert('RGB') + + image_io = BytesIO() + image.save(image_io, format='WEBP', quality=85, optimize=True) + image.close() + + new_filename = os.path.splitext(file_obj.name)[0] + '.webp' + file_obj.save(new_filename, ContentFile(image_io.getvalue()), save=False) + + except Exception as e: + logger.error(f"WebP conversion failed: {e}")