uchill/backend/apps/board/models.py

575 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Модели для интерактивной доски.
"""
from django.db import models
from django.utils import timezone
import uuid
class Board(models.Model):
"""
Модель интерактивной доски.
Miro-подобная доска для совместной работы.
"""
ACCESS_CHOICES = [
('private', 'Приватная'),
('mentor_student', 'Для пары ментор-студент'),
('public', 'Публичная'),
]
# Уникальный идентификатор
board_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
verbose_name='ID доски'
)
# Основная информация
title = models.CharField(
max_length=255,
verbose_name='Название'
)
description = models.TextField(
blank=True,
verbose_name='Описание'
)
# Владелец
owner = models.ForeignKey(
'users.User',
on_delete=models.CASCADE,
related_name='owned_boards',
verbose_name='Владелец'
)
# Связь ментор-студент (для персональной доски)
mentor = models.ForeignKey(
'users.User',
on_delete=models.CASCADE,
related_name='mentor_boards',
null=True,
blank=True,
verbose_name='Ментор'
)
student = models.ForeignKey(
'users.User',
on_delete=models.CASCADE,
related_name='student_boards',
null=True,
blank=True,
verbose_name='Студент'
)
# Поле lesson удалено - доска привязана к паре mentor-student, а не к уроку
# Доска доступна и вне занятия
# Доступ
access_type = models.CharField(
max_length=20,
choices=ACCESS_CHOICES,
default='private',
verbose_name='Тип доступа',
db_index=True
)
# Участники (для приватных досок)
participants = models.ManyToManyField(
'users.User',
related_name='boards',
blank=True,
verbose_name='Участники'
)
# Настройки доски
background_color = models.CharField(
max_length=7,
default='#FFFFFF',
verbose_name='Цвет фона'
)
grid_enabled = models.BooleanField(
default=True,
verbose_name='Сетка включена'
)
width = models.IntegerField(
default=5000,
verbose_name='Ширина (px)'
)
height = models.IntegerField(
default=5000,
verbose_name='Высота (px)'
)
# Статус
is_active = models.BooleanField(
default=True,
verbose_name='Активна',
db_index=True
)
is_template = models.BooleanField(
default=False,
verbose_name='Шаблон'
)
# Тип доски
BOARD_TYPE_CHOICES = [
('tldraw', 'Tldraw'),
('whiteboard', 'Whiteboard'),
('spacedeck', 'Spacedeck'),
]
board_type = models.CharField(
max_length=20,
choices=BOARD_TYPE_CHOICES,
default='whiteboard',
verbose_name='Тип доски',
db_index=True
)
# Tldraw данные
tldraw_snapshot = models.JSONField(
default=dict,
blank=True,
verbose_name='Tldraw состояние',
help_text='Полное состояние Tldraw доски'
)
# Spacedeck данные
spacedeck_id = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name='Spacedeck Space ID'
)
spacedeck_edit_hash = models.CharField(
max_length=255,
blank=True,
null=True,
unique=True,
verbose_name='Spacedeck Edit Hash'
)
# Статистика
views_count = models.IntegerField(
default=0,
verbose_name='Количество просмотров'
)
elements_count = models.IntegerField(
default=0,
verbose_name='Количество элементов'
)
last_edited_by = models.ForeignKey(
'users.User',
on_delete=models.SET_NULL,
related_name='last_edited_boards',
null=True,
blank=True,
verbose_name='Последний редактор'
)
last_edited_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='Дата последнего редактирования'
)
# Временные метки
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name='Дата создания'
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name='Дата обновления'
)
class Meta:
db_table = 'boards'
verbose_name = 'Доска'
verbose_name_plural = 'Доски'
ordering = ['-last_edited_at', '-created_at']
unique_together = [('mentor', 'student')]
indexes = [
models.Index(fields=['board_id']),
models.Index(fields=['owner', 'is_active']),
models.Index(fields=['mentor', 'student']),
models.Index(fields=['access_type', 'is_active']),
]
def __str__(self):
return self.title
def increment_views(self):
"""Увеличить счетчик просмотров."""
self.views_count += 1
self.save(update_fields=['views_count'])
def update_elements_count(self):
"""Обновить количество элементов."""
self.elements_count = self.elements.filter(is_deleted=False).count()
self.save(update_fields=['elements_count'])
def get_files_count(self):
"""Получить количество файлов изображений из tldraw_snapshot."""
if not self.tldraw_snapshot or not isinstance(self.tldraw_snapshot, dict):
return 0
files = self.tldraw_snapshot.get('files', {})
if not isinstance(files, dict):
return 0
# Подсчитываем только файлы с dataURL (валидные изображения)
count = 0
for file_id, file_data in files.items():
if isinstance(file_data, dict) and file_data.get('dataURL'):
count += 1
return count
def get_elements_count_from_snapshot(self):
"""Получить количество элементов из tldraw_snapshot."""
if not self.tldraw_snapshot or not isinstance(self.tldraw_snapshot, dict):
return 0
elements = self.tldraw_snapshot.get('elements', [])
if not isinstance(elements, list):
return 0
return len(elements)
def mark_edited(self, user):
"""Отметить изменение доски."""
self.last_edited_by = user
self.last_edited_at = timezone.now()
self.save(update_fields=['last_edited_by', 'last_edited_at'])
def has_access(self, user):
"""Проверка доступа пользователя к доске."""
# Владелец всегда имеет доступ
if self.owner == user:
return True
# Публичные доски доступны всем
if self.access_type == 'public':
return True
# Участники имеют доступ
if self.participants.filter(id=user.id).exists():
return True
# Для досок пары ментор-студент проверяем, что пользователь - ментор или студент
if self.access_type == 'mentor_student':
if self.mentor and self.student:
return user in [self.mentor, self.student]
return False
class BoardElement(models.Model):
"""
Элемент доски (текст, фигура, изображение, рисунок).
"""
TYPE_CHOICES = [
('text', 'Текст'),
('shape', 'Фигура'),
('image', 'Изображение'),
('drawing', 'Рисунок'),
('sticky', 'Стикер'),
('arrow', 'Стрелка'),
('line', 'Линия'),
]
SHAPE_CHOICES = [
('rectangle', 'Прямоугольник'),
('circle', 'Круг'),
('triangle', 'Треугольник'),
('star', 'Звезда'),
]
# Доска
board = models.ForeignKey(
Board,
on_delete=models.CASCADE,
related_name='elements',
verbose_name='Доска'
)
# Тип элемента
element_type = models.CharField(
max_length=20,
choices=TYPE_CHOICES,
verbose_name='Тип элемента',
db_index=True
)
# Позиция и размер
x = models.FloatField(
verbose_name='Позиция X'
)
y = models.FloatField(
verbose_name='Позиция Y'
)
width = models.FloatField(
default=100,
verbose_name='Ширина'
)
height = models.FloatField(
default=100,
verbose_name='Высота'
)
rotation = models.FloatField(
default=0,
verbose_name='Поворот (градусы)'
)
z_index = models.IntegerField(
default=0,
verbose_name='Z-индекс (слой)'
)
# Содержимое (для текста)
content = models.TextField(
blank=True,
verbose_name='Содержимое'
)
# Стиль текста
font_size = models.IntegerField(
default=16,
verbose_name='Размер шрифта'
)
font_family = models.CharField(
max_length=100,
default='Arial',
verbose_name='Шрифт'
)
font_weight = models.CharField(
max_length=20,
default='normal',
verbose_name='Жирность'
)
text_align = models.CharField(
max_length=20,
default='left',
verbose_name='Выравнивание'
)
text_color = models.CharField(
max_length=7,
default='#000000',
verbose_name='Цвет текста'
)
# Стиль фигуры
shape_type = models.CharField(
max_length=20,
choices=SHAPE_CHOICES,
blank=True,
verbose_name='Тип фигуры'
)
fill_color = models.CharField(
max_length=7,
default='#FFFFFF',
verbose_name='Цвет заливки'
)
stroke_color = models.CharField(
max_length=7,
default='#000000',
verbose_name='Цвет границы'
)
stroke_width = models.FloatField(
default=1,
verbose_name='Толщина границы'
)
opacity = models.FloatField(
default=1.0,
verbose_name='Прозрачность (0-1)'
)
# Для изображений
image_url = models.URLField(
blank=True,
max_length=500,
verbose_name='URL изображения'
)
# Для рисунков (SVG path или JSON)
drawing_data = models.TextField(
blank=True,
verbose_name='Данные рисунка'
)
# Связь со стрелкой
arrow_start_element = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
related_name='arrow_targets',
null=True,
blank=True,
verbose_name='Начало стрелки'
)
arrow_end_element = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
related_name='arrow_sources',
null=True,
blank=True,
verbose_name='Конец стрелки'
)
# Автор
created_by = models.ForeignKey(
'users.User',
on_delete=models.SET_NULL,
related_name='board_elements',
null=True,
verbose_name='Автор'
)
# Блокировка редактирования
locked = models.BooleanField(
default=False,
verbose_name='Заблокирован'
)
locked_by = models.ForeignKey(
'users.User',
on_delete=models.SET_NULL,
related_name='locked_elements',
null=True,
blank=True,
verbose_name='Заблокировал'
)
# Удаление (мягкое)
is_deleted = models.BooleanField(
default=False,
verbose_name='Удален',
db_index=True
)
deleted_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='Дата удаления'
)
# Временные метки
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name='Дата создания'
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name='Дата обновления'
)
class Meta:
db_table = 'board_elements'
verbose_name = 'Элемент доски'
verbose_name_plural = 'Элементы доски'
ordering = ['z_index', 'created_at']
indexes = [
models.Index(fields=['board', 'is_deleted']),
models.Index(fields=['element_type']),
models.Index(fields=['z_index']),
]
def __str__(self):
return f"{self.get_element_type_display()} - {self.board.title}"
def soft_delete(self):
"""Мягкое удаление элемента."""
self.is_deleted = True
self.deleted_at = timezone.now()
self.save()
# Обновляем счетчик элементов доски
self.board.update_elements_count()
def lock(self, user):
"""Заблокировать элемент для редактирования."""
self.locked = True
self.locked_by = user
self.save(update_fields=['locked', 'locked_by'])
def unlock(self):
"""Разблокировать элемент."""
self.locked = False
self.locked_by = None
self.save(update_fields=['locked', 'locked_by'])
class BoardSnapshot(models.Model):
"""
Снимок доски (для истории изменений).
"""
board = models.ForeignKey(
Board,
on_delete=models.CASCADE,
related_name='snapshots',
verbose_name='Доска'
)
# Снимок данных (JSON)
snapshot_data = models.JSONField(
verbose_name='Данные снимка'
)
# Автор изменений
created_by = models.ForeignKey(
'users.User',
on_delete=models.SET_NULL,
related_name='board_snapshots',
null=True,
verbose_name='Автор'
)
# Описание изменений
description = models.CharField(
max_length=255,
blank=True,
verbose_name='Описание'
)
# Временная метка
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name='Дата создания'
)
class Meta:
db_table = 'board_snapshots'
verbose_name = 'Снимок доски'
verbose_name_plural = 'Снимки досок'
ordering = ['-created_at']
def __str__(self):
return f"Снимок {self.board.title} - {self.created_at}"