""" Модели для учебных материалов. """ from django.db import models from django.core.validators import MinValueValidator import uuid import os import mimetypes def material_file_upload_path(instance, filename): """Путь для загрузки файлов материалов.""" ext = filename.split('.')[-1] new_filename = f"{uuid.uuid4()}.{ext}" return os.path.join('materials', str(instance.owner.id), new_filename) class MaterialFolder(models.Model): """ Папка для организации материалов. """ name = 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='material_folders', verbose_name='Владелец' ) parent = models.ForeignKey( 'self', on_delete=models.CASCADE, related_name='subfolders', null=True, blank=True, verbose_name='Родительская папка' ) # Доступ is_public = models.BooleanField( default=False, verbose_name='Публичная' ) shared_with = models.ManyToManyField( 'users.User', related_name='shared_folders', blank=True, verbose_name='Доступ предоставлен' ) # Статистика materials_count = models.IntegerField( default=0, 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 = 'material_folders' verbose_name = 'Папка материалов' verbose_name_plural = 'Папки материалов' ordering = ['name'] unique_together = ['owner', 'parent', 'name'] indexes = [ models.Index(fields=['owner', 'parent']), models.Index(fields=['owner', 'is_public']), ] def __str__(self): return self.name def get_path(self): """Получить полный путь папки.""" if self.parent: return f"{self.parent.get_path()}/{self.name}" return self.name def update_materials_count(self): """Обновить количество материалов.""" self.materials_count = self.materials.filter(is_deleted=False).count() self.save(update_fields=['materials_count']) class MaterialTag(models.Model): """ Тег для материалов. """ name = models.CharField( max_length=50, unique=True, verbose_name='Название' ) slug = models.SlugField( max_length=50, unique=True, verbose_name='Слаг' ) color = models.CharField( max_length=7, default='#007bff', verbose_name='Цвет' ) materials_count = models.IntegerField( default=0, verbose_name='Количество материалов' ) created_at = models.DateTimeField( auto_now_add=True, verbose_name='Дата создания' ) class Meta: db_table = 'material_tags' verbose_name = 'Тег материала' verbose_name_plural = 'Теги материалов' ordering = ['name'] def __str__(self): return self.name class Material(models.Model): """ Модель учебного материала. """ TYPE_CHOICES = [ ('document', 'Документ'), ('presentation', 'Презентация'), ('video', 'Видео'), ('audio', 'Аудио'), ('image', 'Изображение'), ('archive', 'Архив'), ('link', 'Ссылка'), ('other', 'Другое'), ] ACCESS_CHOICES = [ ('private', 'Приватный'), ('public', 'Публичный'), ('lesson', 'Для занятия'), ('clients', 'Для клиентов'), ] # Основная информация title = models.CharField( max_length=255, verbose_name='Название' ) description = models.TextField( blank=True, verbose_name='Описание' ) # Файл или ссылка file = models.FileField( upload_to=material_file_upload_path, blank=True, max_length=500, verbose_name='Файл' ) file_name = models.CharField( max_length=255, blank=True, verbose_name='Имя файла' ) file_size = models.BigIntegerField( default=0, verbose_name='Размер файла (bytes)' ) file_type = models.CharField( max_length=100, blank=True, verbose_name='MIME тип' ) url = models.URLField( blank=True, max_length=500, verbose_name='Ссылка' ) material_type = models.CharField( max_length=20, choices=TYPE_CHOICES, default='other', verbose_name='Тип материала', db_index=True ) # Владелец и организация owner = models.ForeignKey( 'users.User', on_delete=models.CASCADE, related_name='materials', verbose_name='Владелец' ) folder = models.ForeignKey( MaterialFolder, on_delete=models.SET_NULL, related_name='materials', null=True, blank=True, verbose_name='Папка' ) tags = models.ManyToManyField( MaterialTag, related_name='materials', blank=True, verbose_name='Теги' ) # Связи lesson = models.ForeignKey( 'schedule.Lesson', on_delete=models.SET_NULL, related_name='materials', null=True, blank=True, verbose_name='Занятие' ) homework = models.ForeignKey( 'homework.Homework', on_delete=models.SET_NULL, related_name='materials', null=True, blank=True, verbose_name='Домашнее задание' ) # Доступ access_type = models.CharField( max_length=20, choices=ACCESS_CHOICES, default='private', verbose_name='Тип доступа', db_index=True ) shared_with = models.ManyToManyField( 'users.User', related_name='shared_materials', blank=True, verbose_name='Доступ предоставлен' ) # Настройки allow_download = models.BooleanField( default=True, verbose_name='Разрешить скачивание' ) is_featured = models.BooleanField( default=False, verbose_name='Избранный' ) # Статистика views_count = models.IntegerField( default=0, verbose_name='Количество просмотров' ) downloads_count = models.IntegerField( default=0, 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 = 'materials' verbose_name = 'Материал' verbose_name_plural = 'Материалы' ordering = ['-created_at'] indexes = [ models.Index(fields=['owner', 'is_deleted']), models.Index(fields=['folder', 'is_deleted']), models.Index(fields=['material_type']), models.Index(fields=['access_type', 'is_deleted']), models.Index(fields=['lesson']), models.Index(fields=['homework']), models.Index(fields=['owner', 'created_at']), models.Index(fields=['access_type', 'material_type']), ] def __str__(self): return self.title def save(self, *args, **kwargs): """Переопределяем save для автоматической обработки.""" # Определяем тип материала по MIME типу if self.file and not self.material_type: self.material_type = self.detect_material_type() # Сохраняем имя файла if self.file and not self.file_name: self.file_name = os.path.basename(self.file.name) super().save(*args, **kwargs) # Обновляем счетчик папки if self.folder: self.folder.update_materials_count() def detect_material_type(self): """Определить тип материала по MIME типу.""" if not self.file_type: return 'other' mime = self.file_type.lower() if mime.startswith('image/'): return 'image' elif mime.startswith('video/'): return 'video' elif mime.startswith('audio/'): return 'audio' elif 'pdf' in mime or 'document' in mime or 'text' in mime: return 'document' elif 'presentation' in mime or 'powerpoint' in mime: return 'presentation' elif 'zip' in mime or 'rar' in mime or 'archive' in mime: return 'archive' return 'other' def increment_views(self): """Увеличить счетчик просмотров.""" self.views_count += 1 self.save(update_fields=['views_count']) def increment_downloads(self): """Увеличить счетчик скачиваний.""" self.downloads_count += 1 self.save(update_fields=['downloads_count']) def soft_delete(self): """Мягкое удаление.""" from django.utils import timezone self.is_deleted = True self.deleted_at = timezone.now() self.save() # Обновляем счетчик папки if self.folder: self.folder.update_materials_count() def has_access(self, user): """Проверка доступа пользователя к материалу.""" # Владелец всегда имеет доступ if self.owner == user: return True # Публичные материалы доступны всем if self.access_type == 'public': return True # Доступ предоставлен напрямую if self.shared_with.filter(id=user.id).exists(): return True # Материалы для клиентов if self.access_type == 'clients': # Проверяем что пользователь - клиент владельца from apps.users.models import Client try: client = Client.objects.get(user=user) if self.owner in client.mentors.all(): return True except Client.DoesNotExist: pass # Материалы для занятия if self.access_type == 'lesson' and self.lesson: return user in [self.lesson.mentor, self.lesson.client] return False class MaterialAccess(models.Model): """ Лог доступа к материалам. """ ACTION_CHOICES = [ ('view', 'Просмотр'), ('download', 'Скачивание'), ('share', 'Предоставление доступа'), ] material = models.ForeignKey( Material, on_delete=models.CASCADE, related_name='access_logs', verbose_name='Материал' ) user = models.ForeignKey( 'users.User', on_delete=models.CASCADE, related_name='material_accesses', verbose_name='Пользователь' ) action = models.CharField( max_length=20, choices=ACTION_CHOICES, verbose_name='Действие' ) ip_address = models.GenericIPAddressField( null=True, blank=True, verbose_name='IP адрес' ) user_agent = models.CharField( max_length=500, blank=True, verbose_name='User Agent' ) created_at = models.DateTimeField( auto_now_add=True, verbose_name='Дата', db_index=True ) class Meta: db_table = 'material_access_logs' verbose_name = 'Лог доступа к материалу' verbose_name_plural = 'Логи доступа к материалам' ordering = ['-created_at'] indexes = [ models.Index(fields=['material', 'created_at']), models.Index(fields=['user', 'created_at']), ] def __str__(self): return f"{self.user.email} - {self.get_action_display()} - {self.material.title}" class StorageQuota(models.Model): """ Квота хранилища для пользователя. """ user = models.OneToOneField( 'users.User', on_delete=models.CASCADE, related_name='storage_quota', verbose_name='Пользователь' ) # Лимиты в байтах total_quota = models.BigIntegerField( default=1073741824, # 1 GB verbose_name='Общая квота (bytes)' ) used_space = models.BigIntegerField( default=0, verbose_name='Использовано (bytes)' ) # Временные метки created_at = models.DateTimeField( auto_now_add=True, verbose_name='Дата создания' ) updated_at = models.DateTimeField( auto_now=True, verbose_name='Дата обновления' ) class Meta: db_table = 'storage_quotas' verbose_name = 'Квота хранилища' verbose_name_plural = 'Квоты хранилища' def __str__(self): return f"{self.user.email} - {self.get_used_percentage():.1f}%" def get_used_percentage(self): """Получить процент использования.""" if self.total_quota == 0: return 0 return (self.used_space / self.total_quota) * 100 def get_available_space(self): """Получить доступное пространство.""" return self.total_quota - self.used_space def has_space(self, size): """Проверить достаточно ли места.""" return self.get_available_space() >= size def add_usage(self, size): """Добавить использование.""" self.used_space += size self.save(update_fields=['used_space']) def remove_usage(self, size): """Убрать использование.""" self.used_space = max(0, self.used_space - size) self.save(update_fields=['used_space']) def recalculate(self): """Пересчитать использование.""" total = Material.objects.filter( owner=self.user, is_deleted=False ).aggregate( total=models.Sum('file_size') )['total'] or 0 self.used_space = total self.save(update_fields=['used_space'])