adding features for social network
This commit is contained in:
@@ -88,6 +88,8 @@ django-silk[formatting]
|
|||||||
|
|
||||||
yt-dlp
|
yt-dlp
|
||||||
|
|
||||||
|
python-magic
|
||||||
|
|
||||||
weasyprint #tvoření PDFek z html dokumentu + css styly
|
weasyprint #tvoření PDFek z html dokumentu + css styly
|
||||||
|
|
||||||
## -- MISCELLANEOUS --
|
## -- MISCELLANEOUS --
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from django.db import models
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from vontor_cz.custom_fields import WebPImageField
|
||||||
|
|
||||||
class Chat(models.Model):
|
class Chat(models.Model):
|
||||||
owner = models.ForeignKey(
|
owner = models.ForeignKey(
|
||||||
@@ -10,6 +11,11 @@ class Chat(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
related_name='owned_chats'
|
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(
|
members = models.ManyToManyField(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
@@ -23,6 +29,15 @@ class Chat(models.Model):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
hub = models.ForeignKey(
|
||||||
|
'pages.Hub',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=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)
|
chat = models.ForeignKey(Chat, related_name='messages', on_delete=models.CASCADE)
|
||||||
|
|
||||||
sender = models.ForeignKey(
|
sender = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
related_name='sent_messages'
|
related_name='sent_messages'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,18 +84,10 @@ class Message(models.Model):
|
|||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
# VALIDATION: Ensure sender is actually in the chat
|
if self.chat_id and self.sender_id:
|
||||||
# Note: We check self.id to avoid running this on creation if logic depends on M2M
|
if not self.chat.members.filter(id=self.sender_id).exists():
|
||||||
# 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.")
|
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 ---
|
# --- HELPER METHODS FOR WEBSOCKETS / VIEWS ---
|
||||||
|
|
||||||
def edit_content(self, new_text):
|
def edit_content(self, new_text):
|
||||||
|
|||||||
0
backend/social/chat/serializers.py
Normal file
0
backend/social/chat/serializers.py
Normal file
17
backend/social/nápady_nebo_chybějicí.md
Normal file
17
backend/social/nápady_nebo_chybějicí.md
Normal file
@@ -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
|
||||||
@@ -1,3 +1,50 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
from vontor_cz.custom_fields import WebPImageField
|
||||||
|
|
||||||
# Create your models here.
|
# 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}"
|
||||||
@@ -1,3 +1,42 @@
|
|||||||
from django.db import models
|
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)
|
||||||
51
backend/vontor_cz/custom_fields.py
Normal file
51
backend/vontor_cz/custom_fields.py
Normal file
@@ -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}")
|
||||||
Reference in New Issue
Block a user