""" Модели для интерактивной доски. """ 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}"