updated to selfdelete inheritance
This commit is contained in:
@@ -171,5 +171,30 @@ class CustomUser(SoftDeleteModel, AbstractUser):
|
|||||||
"""Return the singleton anonymous user."""
|
"""Return the singleton anonymous user."""
|
||||||
User = CustomUser
|
User = CustomUser
|
||||||
return User.objects.get(username="anonymous")
|
return User.objects.get(username="anonymous")
|
||||||
|
|
||||||
|
def has_blocked(self, user) -> bool:
|
||||||
|
return self.blocking.filter(blocked_user=user).exists()
|
||||||
|
|
||||||
|
def is_blocked_by(self, user) -> bool:
|
||||||
|
return self.blocked_by.filter(blocker=user).exists()
|
||||||
|
|
||||||
|
|
||||||
|
class UserBlock(models.Model):
|
||||||
|
blocker = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='blocking'
|
||||||
|
)
|
||||||
|
blocked_user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='blocked_by'
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('blocker', 'blocked_user')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.blocker} blocked {self.blocked_user}"
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,21 @@ 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
|
from vontor_cz.custom_fields import WebPImageField
|
||||||
|
from vontor_cz.models import SoftDeleteModel
|
||||||
|
|
||||||
|
class Chat(SoftDeleteModel):
|
||||||
|
AUTHOR_FIELD = 'owner'
|
||||||
|
|
||||||
|
class ChatType(models.TextChoices):
|
||||||
|
DM = 'DM', 'Direct Message'
|
||||||
|
GROUP = 'GROUP', 'Group'
|
||||||
|
|
||||||
|
chat_type = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=ChatType.choices,
|
||||||
|
default=ChatType.GROUP,
|
||||||
|
)
|
||||||
|
|
||||||
class Chat(models.Model):
|
|
||||||
owner = models.ForeignKey(
|
owner = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
@@ -50,11 +63,19 @@ class Chat(models.Model):
|
|||||||
self.members.add(self.owner)
|
self.members.add(self.owner)
|
||||||
self.moderators.add(self.owner)
|
self.moderators.add(self.owner)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.chat_type == self.ChatType.DM:
|
||||||
|
member_count = self.members.count() if self.pk else 0
|
||||||
|
if member_count > 2:
|
||||||
|
raise ValidationError("A DM chat cannot have more than 2 members.")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Chat {self.id}"
|
return f"Chat {self.id}"
|
||||||
|
|
||||||
|
|
||||||
class Message(models.Model):
|
class Message(SoftDeleteModel):
|
||||||
|
AUTHOR_FIELD = 'sender'
|
||||||
|
|
||||||
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(
|
||||||
@@ -145,7 +166,7 @@ class Message(models.Model):
|
|||||||
return f"Message {self.id} from {self.sender}"
|
return f"Message {self.id} from {self.sender}"
|
||||||
|
|
||||||
|
|
||||||
class MessageHistory(models.Model):
|
class MessageHistory(SoftDeleteModel):
|
||||||
message = models.ForeignKey(
|
message = models.ForeignKey(
|
||||||
Message,
|
Message,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@@ -158,7 +179,7 @@ class MessageHistory(models.Model):
|
|||||||
ordering = ['-archived_at']
|
ordering = ['-archived_at']
|
||||||
|
|
||||||
|
|
||||||
class MessageReaction(models.Model):
|
class MessageReaction(SoftDeleteModel):
|
||||||
message = models.ForeignKey(
|
message = models.ForeignKey(
|
||||||
Message,
|
Message,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@@ -175,7 +196,7 @@ class MessageReaction(models.Model):
|
|||||||
return f"{self.user} reacted {self.emoji}"
|
return f"{self.user} reacted {self.emoji}"
|
||||||
|
|
||||||
|
|
||||||
class MessageFile(models.Model):
|
class MessageFile(SoftDeleteModel):
|
||||||
message = models.ForeignKey(
|
message = models.ForeignKey(
|
||||||
Message,
|
Message,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
|||||||
@@ -1,17 +1,6 @@
|
|||||||
# Social Feature Ideas
|
# 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
|
## 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
|
## Users
|
||||||
- User blocking — affects message/post visibility across all social features
|
- ~~User blocking — affects message/post visibility across all social features~~ ✓ done (`UserBlock` in account, `has_blocked`/`is_blocked_by` helpers on CustomUser)
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import uuid
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from vontor_cz.custom_fields import WebPImageField
|
from vontor_cz.custom_fields import WebPImageField
|
||||||
|
from vontor_cz.models import SoftDeleteModel
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
|
||||||
|
|
||||||
class Hub(models.Model):
|
class Hub(SoftDeleteModel):
|
||||||
|
AUTHOR_FIELD = 'owner'
|
||||||
|
|
||||||
name = models.CharField(max_length=255, unique=True)
|
name = models.CharField(max_length=255, unique=True)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
@@ -25,12 +29,45 @@ class Hub(models.Model):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
#TODO:
|
is_public = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
transfer_to = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='pending_hub_transfers'
|
||||||
|
)
|
||||||
|
transfer_token = models.UUIDField(null=True, blank=True, unique=True)
|
||||||
|
|
||||||
|
def create_transfer(self, new_owner):
|
||||||
|
self.transfer_to = new_owner
|
||||||
|
self.transfer_token = uuid.uuid4()
|
||||||
|
self.save(update_fields=['transfer_to', 'transfer_token'])
|
||||||
|
|
||||||
|
def verify_transfer(self, input_token, triggering_user):
|
||||||
|
"""Requires uuid and triggering user must match the transfer_to field"""
|
||||||
|
if self.transfer_token and str(self.transfer_token) == str(input_token) and self.transfer_to == triggering_user:
|
||||||
|
self.owner = self.transfer_to
|
||||||
|
|
||||||
|
self.transfer_to = None
|
||||||
|
self.transfer_token = None
|
||||||
|
|
||||||
|
self.save(update_fields=['owner', 'transfer_to', 'transfer_token'])
|
||||||
|
return True
|
||||||
|
return raiseExceptions("Invalid transfer token or user does not match transfer_to field")
|
||||||
|
|
||||||
|
def cancel_transfer(self):
|
||||||
|
self.transfer_to = None
|
||||||
|
self.transfer_token = None
|
||||||
|
self.save(update_fields=['transfer_to', 'transfer_token'])
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
class HubPermission(models.Model):
|
class HubPermission(SoftDeleteModel):
|
||||||
hub = models.ForeignKey(Hub, on_delete=models.CASCADE, related_name='moderators')
|
hub = models.ForeignKey(Hub, on_delete=models.CASCADE, related_name='moderators')
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from vontor_cz.models import SoftDeleteModel
|
||||||
import magic
|
import magic
|
||||||
|
|
||||||
mime = magic.Magic(mime=True)
|
mime = magic.Magic(mime=True)
|
||||||
|
|
||||||
|
|
||||||
class Post(models.Model):
|
class Post(SoftDeleteModel):
|
||||||
|
AUTHOR_FIELD = 'author'
|
||||||
|
|
||||||
content = models.TextField(blank=True)
|
content = models.TextField(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)
|
||||||
@@ -23,11 +25,19 @@ class Post(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
reply_to = models.ForeignKey(
|
||||||
|
'self',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='replies'
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Post {self.id}"
|
return f"Post {self.id}"
|
||||||
|
|
||||||
class PostContent(models.Model):
|
class PostContent(SoftDeleteModel):
|
||||||
post = models.ForeignKey(Post, related_name='contents', on_delete=models.CASCADE)
|
post = models.ForeignKey(Post, related_name='contents', on_delete=models.CASCADE)
|
||||||
mime_type = models.CharField(max_length=100)
|
mime_type = models.CharField(max_length=100)
|
||||||
file = models.FileField(upload_to='post_contents/', null=True, blank=True) # For images/videos
|
file = models.FileField(upload_to='post_contents/', null=True, blank=True) # For images/videos
|
||||||
@@ -39,4 +49,22 @@ class PostContent(models.Model):
|
|||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.file and not self.file._committed:
|
if self.file and not self.file._committed:
|
||||||
self.mime_type = mime.from_buffer(self.file.read(1024))
|
self.mime_type = mime.from_buffer(self.file.read(1024))
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class PostVote(SoftDeleteModel):
|
||||||
|
class VoteChoice(models.IntegerChoices):
|
||||||
|
UP = 1, 'Upvote'
|
||||||
|
DOWN = -1, 'Downvote'
|
||||||
|
|
||||||
|
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='votes')
|
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
|
vote = models.SmallIntegerField(choices=VoteChoice.choices)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('post', 'user')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
label = 'up' if self.vote == self.VoteChoice.UP else 'down'
|
||||||
|
return f"{self.user} voted {label} on Post {self.post_id}"
|
||||||
@@ -1,9 +1,24 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
class ActiveManager(models.Manager):
|
class ActiveManager(models.Manager):
|
||||||
|
class _QS(models.QuerySet):
|
||||||
|
def visible_to(self, user):
|
||||||
|
from account.models import UserBlock
|
||||||
|
author_field = getattr(self.model, 'AUTHOR_FIELD', 'author')
|
||||||
|
|
||||||
|
blocked_ids = UserBlock.objects.filter(blocker=user).values_list('blocked_id', flat=True)
|
||||||
|
blocking_ids = UserBlock.objects.filter(blocked=user).values_list('blocker_id', flat=True)
|
||||||
|
|
||||||
|
return (
|
||||||
|
self.exclude(**{f'{author_field}__in': blocked_ids})
|
||||||
|
.exclude(**{f'{author_field}__in': blocking_ids})
|
||||||
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().filter(is_deleted=False)
|
return self._QS(self.model, using=self._db).filter(is_deleted=False)
|
||||||
|
|
||||||
|
|
||||||
class AllManager(models.Manager):
|
class AllManager(models.Manager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -18,11 +33,6 @@ class SoftDeleteModel(models.Model):
|
|||||||
is_deleted = models.BooleanField(default=False)
|
is_deleted = models.BooleanField(default=False)
|
||||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
def delete(self, using=None, keep_parents=False):
|
|
||||||
self.is_deleted = True
|
|
||||||
self.deleted_at = timezone.now()
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
objects = ActiveManager()
|
objects = ActiveManager()
|
||||||
all_objects = AllManager()
|
all_objects = AllManager()
|
||||||
|
|
||||||
@@ -30,32 +40,9 @@ class SoftDeleteModel(models.Model):
|
|||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
# Soft delete self
|
|
||||||
self.is_deleted = True
|
self.is_deleted = True
|
||||||
self.deleted_at = timezone.now()
|
self.deleted_at = timezone.now()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def hard_delete(self, using=None, keep_parents=False):
|
def hard_delete(self, using=None, keep_parents=False):
|
||||||
super().delete(using=using, keep_parents=keep_parents)
|
super().delete(using=using, keep_parents=keep_parents)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# SiteSettings model for managing site-wide settings
|
|
||||||
"""class SiteSettings(models.Model):
|
|
||||||
bank = models.CharField(max_length=100, blank=True)
|
|
||||||
support_email = models.EmailField(blank=True)
|
|
||||||
logo = models.ImageField(upload_to='settings/', blank=True, null=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Site Settings"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = "Site Settings"
|
|
||||||
verbose_name_plural = "Site Settings"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_solo(cls):
|
|
||||||
obj, created = cls.objects.get_or_create(id=1)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
"""
|
|
||||||
@@ -317,7 +317,13 @@ REST_FRAMEWORK = {
|
|||||||
'rest_framework.permissions.AllowAny',
|
'rest_framework.permissions.AllowAny',
|
||||||
),
|
),
|
||||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||||
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
|
# Default filter backends (ordering, search, filtering)
|
||||||
|
'DEFAULT_FILTER_BACKENDS': [
|
||||||
|
'django_filters.rest_framework.DjangoFilterBackend',
|
||||||
|
'rest_framework.filters.OrderingFilter',
|
||||||
|
'rest_framework.filters.SearchFilter',
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
# Enable default pagination so custom list actions (e.g., /orders/detail) paginate
|
# Enable default pagination so custom list actions (e.g., /orders/detail) paginate
|
||||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||||
@@ -328,7 +334,7 @@ REST_FRAMEWORK = {
|
|||||||
'user': '2000/hour', # authenticated
|
'user': '2000/hour', # authenticated
|
||||||
},
|
},
|
||||||
|
|
||||||
'EXCEPTION_HANDLER': 'trznice.utils.custom_exception_handler',
|
'EXCEPTION_HANDLER': 'vontor_cz.utils.custom_exception_handler',
|
||||||
|
|
||||||
}
|
}
|
||||||
#--------------------------------END REST FRAMEWORK 🛠️-------------------------------------
|
#--------------------------------END REST FRAMEWORK 🛠️-------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user