"""
Модуль моделей базы данных приложения core.
Содержит описание таблиц для пользователей, документов, комментариев и системы жалоб.
"""
import uuid
import base64
import gzip
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
[документация]
class Report(models.Model):
"""
Модель жалобы пользователя на другого пользователя.
:param reporter: Пользователь, отправивший жалобу.
:type reporter: django.contrib.auth.models.User
:param reported_user: Пользователь, на которого отправлена жалоба.
:type reported_user: django.contrib.auth.models.User
:param reason: Причина жалобы.
:type reason: str
:param created_at: Дата и время создания жалобы.
:type created_at: datetime
:param resolved: Статус решения жалобы.
:type resolved: bool
"""
reporter = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="sent_reports"
)
reported_user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="complaints_received"
)
reason = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
resolved = models.BooleanField(default=False)
def __str__(self):
return f"{self.reporter} -> {self.reported_user}"
[документация]
class Document(models.Model):
"""
Модель документа или онлайн-доски.
:param title: Название документа.
:type title: str
:param content: Содержимое документа (текст или JSON доски).
:type content: str
:param doc_type: Тип документа ('text' или 'paint').
:type doc_type: str
:param owner: Владелец документа.
:type owner: django.contrib.auth.models.User
"""
DOC_TYPES = [
('text', 'Текстовый документ'),
('paint', 'Онлайн доска'),
]
title = models.CharField(max_length=255, default="Без названия")
content = models.TextField(blank=True, default="")
doc_type = models.CharField(max_length=10, choices=DOC_TYPES, default='text')
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='owned_documents')
editors = models.ManyToManyField(User, related_name='editable_documents', blank=True)
viewers = models.ManyToManyField(User, related_name='viewable_documents', blank=True)
invite_token = models.CharField(max_length=64, unique=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
[документация]
def set_content(self, data):
"""
Сохраняет контент документа в gzip-сжатом виде.
:param data: Данные для сохранения.
:type data: str или bytes
"""
if data is None:
data = ""
if isinstance(data, bytes):
raw_bytes = data
else:
raw_bytes = str(data).encode("utf-8")
self.content = gzip.compress(raw_bytes)
[документация]
def get_content(self):
"""
Возвращает исходный контент документа в виде строки.
:return: Распакованное содержимое документа.
:rtype: str
"""
if isinstance(self.content, bytes):
try:
return gzip.decompress(self.content).decode("utf-8")
except Exception:
return self.content.decode("utf-8", errors="ignore")
if isinstance(self.content, str) and self.content.startswith("gz:"):
try:
payload = base64.b64decode(self.content[3:].encode("ascii"))
return gzip.decompress(payload).decode("utf-8")
except Exception:
return ""
return self.content
[документация]
def save(self, *args, **kwargs):
"""
Переопределенный метод сохранения документа.
Генерирует уникальный токен приглашения при первом создании
и обрабатывает сериализацию сжатого контента.
"""
if not self.invite_token:
self.invite_token = uuid.uuid4().hex + uuid.uuid4().hex
content_was_bytes = isinstance(self.content, bytes)
content_cache = self.content if content_was_bytes else None
if content_was_bytes:
self.content = "gz:" + base64.b64encode(self.content).decode("ascii")
super().save(*args, **kwargs)
if content_was_bytes:
self.content = content_cache
[документация]
def user_has_access(self, user):
"""
Проверяет, есть ли у пользователя права на доступ к документу.
:param user: Пользователь для проверки.
:type user: django.contrib.auth.models.User
:return: True, если доступ разрешен, иначе False.
:rtype: bool
"""
if user.is_authenticated:
return user == self.owner or \
self.editors.filter(id=user.id).exists() or \
self.viewers.filter(id=user.id).exists()
return False
[документация]
def can_edit_doc(self, user):
"""
Проверяет, есть ли у пользователя права на редактирование документа.
:param user: Пользователь для проверки.
:type user: django.contrib.auth.models.User
:return: True, если пользователь может редактировать документ, иначе False.
:rtype: bool
"""
if not user.is_authenticated:
return False
return user == self.owner or self.editors.filter(id=user.id).exists()
def __str__(self):
return f"{self.title} ({self.doc_type})"
[документация]
class PasswordResetCode(models.Model):
"""
Модель для хранения кодов сброса пароля.
:param user: Пользователь, запросивший сброс.
:type user: django.contrib.auth.models.User
:param code: Шестизначный код подтверждения.
:type code: str
"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
code = models.CharField(max_length=6)
created_at = models.DateTimeField(auto_now_add=True)
[документация]
def is_valid(self):
"""
Проверяет, действителен ли код сброса (не истек ли срок 15 минут).
:return: True, если код действителен.
:rtype: bool
"""
from django.utils import timezone
from datetime import timedelta
return timezone.now() < self.created_at + timedelta(minutes=15)
[документация]
class Profile(models.Model):
"""
Профиль пользователя с дополнительными данными (аватар).
:param user: Связанный пользователь.
:type user: django.contrib.auth.models.User
:param avatar: Путь к изображению аватара.
:type avatar: str
"""
user = models.OneToOneField(User, on_delete=models.CASCADE)
avatar = models.CharField(max_length=255, default="/static/core/images/profile/penguin.png")
def __str__(self):
return self.user.username
[документация]
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
"""Сигнал для автоматического создания профиля при регистрации пользователя."""
if created:
Profile.objects.create(user=instance)
[документация]
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
"""Сигнал для сохранения профиля при сохранении пользователя."""
if hasattr(instance, 'profile'):
instance.profile.save()
[документация]
class UserPunishment(models.Model):
"""
Модель наказаний пользователя (баны и муты).
"""
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name="punishment"
)
reports_count = models.PositiveIntegerField(default=0)
is_banned = models.BooleanField(default=False)
banned_at = models.DateTimeField(null=True, blank=True)
ban_reason = models.TextField(blank=True, default="")
muted_until = models.DateTimeField(null=True, blank=True)
muted_at = models.DateTimeField(null=True, blank=True)
mute_reason = models.TextField(blank=True, default="")
[документация]
def is_muted(self):
"""
Проверяет, замучен ли пользователь на данный момент.
"""
return self.muted_until is not None and timezone.now() < self.muted_until
[документация]
def mute_left(self):
"""
Возвращает время окончания мута.
"""
if not self.is_muted():
return None
return self.muted_until
def __str__(self):
return self.user.username
[документация]
class Complaint(models.Model):
"""
Модель жалобы на комментарий в системе.
"""
reporter = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="sent_complaints"
)
reported_user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="reports"
)
comment = models.ForeignKey(
Comment,
on_delete=models.CASCADE
)
reason = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
[документация]
class UserActivity(models.Model):
"""
Модель отслеживания сетевой активности пользователя.
"""
user = models.OneToOneField(User, on_delete=models.CASCADE)
last_seen = models.DateTimeField(auto_now=True)
[документация]
def is_online(self):
"""
Проверяет статус онлайна пользователя (активен в течение последних 5 минут).
"""
return timezone.now() - self.last_seen < timezone.timedelta(minutes=5)