uchill/backend/apps/materials/models.py

576 lines
17 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.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'])