uchill/backend/apps/schedule/models.py

1147 lines
37 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, MaxValueValidator, FileExtensionValidator
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from datetime import timedelta
import uuid
import os
class Subject(models.Model):
"""
Модель предмета обучения.
Общие предметы, доступные всем менторам.
"""
name = models.CharField(
max_length=100,
unique=True,
verbose_name='Название предмета',
help_text='Название предмета обучения'
)
is_active = models.BooleanField(
default=True,
verbose_name='Активен',
help_text='Доступен ли предмет для использования'
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name='Дата создания'
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name='Дата обновления'
)
class Meta:
db_table = 'subjects'
verbose_name = 'Предмет'
verbose_name_plural = 'Предметы'
ordering = ['name']
indexes = [
models.Index(fields=['is_active', 'name']),
]
def __str__(self):
return self.name
class MentorSubject(models.Model):
"""
Кастомный предмет ментора.
Используется для предметов, которые еще не добавлены в общую модель Subject.
Если предмет используется более чем 10 менторами, он должен быть перенесен в Subject.
"""
mentor = models.ForeignKey(
'users.User',
on_delete=models.CASCADE,
related_name='mentor_subjects',
limit_choices_to={'role': 'mentor'},
verbose_name='Ментор'
)
name = models.CharField(
max_length=100,
verbose_name='Название предмета',
help_text='Название кастомного предмета'
)
usage_count = models.IntegerField(
default=0,
verbose_name='Количество использований',
help_text='Сколько раз этот предмет был использован в занятиях'
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name='Дата создания'
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name='Дата обновления'
)
class Meta:
db_table = 'mentor_subjects'
verbose_name = 'Предмет ментора'
verbose_name_plural = 'Предметы менторов'
ordering = ['mentor', 'name']
indexes = [
models.Index(fields=['mentor', 'name']),
models.Index(fields=['name', 'usage_count']),
]
constraints = [
models.UniqueConstraint(
fields=['mentor', 'name'],
name='unique_mentor_subject'
),
]
def __str__(self):
return f"{self.name} ({self.mentor.get_full_name()})"
def increment_usage(self):
"""Увеличить счетчик использования."""
self.usage_count += 1
self.save(update_fields=['usage_count', 'updated_at'])
class Lesson(models.Model):
"""
Модель занятия.
Представляет конкретное занятие в определенное время.
"""
STATUS_CHOICES = [
('scheduled', 'Запланировано'),
('in_progress', 'В процессе'),
('completed', 'Завершено'),
('cancelled', 'Отменено'),
('rescheduled', 'Перенесено'),
]
# Участники
mentor = models.ForeignKey(
'users.User',
on_delete=models.CASCADE,
related_name='mentor_lessons',
limit_choices_to={'role': 'mentor'},
verbose_name='Ментор'
)
client = models.ForeignKey(
'users.Client',
on_delete=models.CASCADE,
related_name='lessons',
verbose_name='Клиент'
)
# Время и продолжительность
start_time = models.DateTimeField(
verbose_name='Время начала',
db_index=True
)
end_time = models.DateTimeField(
verbose_name='Время окончания',
db_index=True
)
duration = models.IntegerField(
validators=[MinValueValidator(15), MaxValueValidator(480)],
default=60,
verbose_name='Длительность (минуты)',
help_text='Длительность занятия в минутах (15-480)'
)
# Информация о занятии
title = models.CharField(
max_length=200,
verbose_name='Название',
help_text='Краткое название занятия'
)
description = models.TextField(
blank=True,
verbose_name='Описание',
help_text='Подробное описание занятия'
)
# Предмет (может быть общим или кастомным)
subject = models.ForeignKey(
'Subject',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='lessons',
verbose_name='Предмет',
help_text='Общий предмет обучения'
)
# Кастомный предмет ментора (если не выбран общий предмет)
mentor_subject = models.ForeignKey(
'MentorSubject',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='lessons',
verbose_name='Кастомный предмет',
help_text='Кастомный предмет ментора (если не выбран общий предмет)'
)
# Для обратной совместимости - храним название предмета как строку
subject_name = models.CharField(
max_length=100,
blank=True,
verbose_name='Название предмета (legacy)',
help_text='Название предмета для обратной совместимости'
)
# Статус
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='scheduled',
verbose_name='Статус',
db_index=True
)
# Связь с шаблоном (если создано из шаблона)
template = models.ForeignKey(
'LessonTemplate',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='lessons',
verbose_name='Шаблон'
)
# Отмена
cancelled_by = models.ForeignKey(
'users.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='cancelled_lessons',
verbose_name='Отменено пользователем'
)
cancellation_reason = models.TextField(
blank=True,
verbose_name='Причина отмены'
)
cancelled_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='Дата отмены'
)
# Перенос
rescheduled_from = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='rescheduled_to',
verbose_name='Перенесено из'
)
# Ссылки
meeting_url = models.URLField(
blank=True,
max_length=500,
verbose_name='Ссылка на встречу',
help_text='Ссылка на видеоконференцию'
)
# Заметки
mentor_notes = models.TextField(
blank=True,
verbose_name='Заметки ментора',
help_text='Приватные заметки ментора о занятии'
)
# Домашнее задание
homework_text = models.TextField(
blank=True,
verbose_name='Домашнее задание',
help_text='Описание домашнего задания, выданного по результатам занятия'
)
# Оценки
mentor_grade = models.IntegerField(
null=True,
blank=True,
verbose_name='Оценка ментора',
help_text='Оценка работы студента на занятии (0-100)'
)
school_grade = models.IntegerField(
null=True,
blank=True,
verbose_name='Оценка в школе',
help_text='Текущая оценка студента в школе (0-100)'
)
# Стоимость
price = models.DecimalField(
max_digits=10,
decimal_places=2,
null=True,
blank=True,
verbose_name='Стоимость',
help_text='Стоимость занятия в рублях'
)
# Группа (для групповых занятий)
group = models.ForeignKey(
'users.Group',
on_delete=models.SET_NULL,
related_name='lessons',
null=True,
blank=True,
verbose_name='Группа',
help_text='Учебная группа, если занятие групповое'
)
# Напоминания
reminder_sent = models.BooleanField(
default=False,
verbose_name='Напоминание отправлено',
help_text='Устаревшее поле, используйте reminder_24h_sent, reminder_1h_sent, reminder_15m_sent'
)
reminder_24h_sent = models.BooleanField(
default=False,
verbose_name='Напоминание за 24 часа отправлено',
help_text='Отправлено ли напоминание за 24 часа до занятия'
)
reminder_1h_sent = models.BooleanField(
default=False,
verbose_name='Напоминание за 1 час отправлено',
help_text='Отправлено ли напоминание за 1 час до занятия'
)
reminder_15m_sent = models.BooleanField(
default=False,
verbose_name='Напоминание за 15 минут отправлено',
help_text='Отправлено ли напоминание за 15 минут до занятия'
)
# Подтверждение присутствия
attendance_confirmation_sent = models.BooleanField(
default=False,
verbose_name='Запрос подтверждения присутствия отправлен',
help_text='Отправлен ли запрос о подтверждении присутствия за 3 часа до занятия'
)
attendance_confirmed = models.BooleanField(
null=True,
blank=True,
verbose_name='Присутствие подтверждено',
help_text='Ответ студента на запрос о присутствии (True - будет, False - не будет, None - не ответил)'
)
attendance_response_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='Время ответа о присутствии'
)
# reminder_sent_at временно удалено из модели, так как было удалено в миграции 0004
# TODO: Добавить обратно при необходимости
# reminder_sent_at = models.DateTimeField(
# null=True,
# blank=True,
# verbose_name='Время отправки напоминания'
# )
# Метрики подключения к видеокомнате (заполняются при подключении ментора/студента)
mentor_connected_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='Ментор подключился',
help_text='Время подключения ментора к видеокомнате'
)
client_connected_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='Студент подключился',
help_text='Время подключения студента к видеокомнате'
)
# Фактическое время завершения (если занятие завершено досрочно)
completed_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='Фактическое время завершения',
help_text='Время, когда занятие было фактически завершено (может отличаться от end_time)'
)
# LiveKit видеоконференция
livekit_room_name = models.CharField(
max_length=255,
blank=True,
verbose_name='Название LiveKit комнаты',
help_text='Уникальное название комнаты LiveKit для этого занятия',
db_index=True
)
livekit_access_token = models.TextField(
blank=True,
verbose_name='Токен доступа LiveKit',
help_text='JWT токен для подключения к LiveKit комнате (действителен 24 часа)'
)
# Повторяющиеся занятия
is_recurring = models.BooleanField(
default=False,
verbose_name='Постоянное время занятия',
help_text='Если отмечено, занятие будет повторяться каждую неделю в этот день и время'
)
recurring_series_id = models.UUIDField(
null=True,
blank=True,
verbose_name='ID серии повторяющихся занятий',
help_text='Уникальный ID для группировки занятий одной серии',
db_index=True
)
parent_lesson = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='recurring_lessons',
verbose_name='Родительское занятие',
help_text='Ссылка на первое занятие в серии повторяющихся занятий'
)
# Временные метки
created_at = models.DateTimeField(
auto_now_add=True,
null=True,
blank=True,
verbose_name='Дата создания'
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name='Дата обновления'
)
class Meta:
db_table = 'lessons'
verbose_name = 'Занятие'
verbose_name_plural = 'Занятия'
ordering = ['start_time']
indexes = [
models.Index(fields=['mentor', 'start_time']),
models.Index(fields=['client', 'start_time']),
models.Index(fields=['status', 'start_time']),
models.Index(fields=['start_time', 'end_time']),
models.Index(fields=['recurring_series_id']),
models.Index(fields=['mentor', 'status']),
models.Index(fields=['subject']),
models.Index(fields=['created_at']),
]
constraints = [
models.CheckConstraint(
check=models.Q(end_time__gt=models.F('start_time')),
name='lesson_end_after_start'
),
]
def __str__(self):
return f"{self.title} - {self.client.user.get_full_name()} ({self.start_time.strftime('%d.%m.%Y %H:%M')})"
def save(self, *args, **kwargs):
"""Автоматический расчет end_time на основе start_time и duration."""
if self.start_time and self.duration:
self.end_time = self.start_time + timedelta(minutes=self.duration)
super().save(*args, **kwargs)
@property
def is_upcoming(self):
"""Проверка, является ли занятие предстоящим."""
return self.start_time > timezone.now() and self.status == 'scheduled'
@property
def is_past(self):
"""Проверка, прошло ли занятие."""
return self.end_time < timezone.now()
@property
def is_in_progress(self):
"""Проверка, идет ли занятие сейчас."""
now = timezone.now()
return self.start_time <= now <= self.end_time and self.status == 'in_progress'
@property
def can_be_cancelled(self):
"""Можно ли отменить занятие."""
return self.status in ['scheduled'] and self.start_time > timezone.now()
@property
def can_be_rescheduled(self):
"""Можно ли перенести занятие."""
return self.status in ['scheduled'] and self.start_time > timezone.now()
def cancel(self, user, reason=''):
"""Отменить занятие."""
if not self.can_be_cancelled:
raise ValueError('Занятие нельзя отменить')
self.status = 'cancelled'
self.cancelled_by = user
self.cancellation_reason = reason
self.cancelled_at = timezone.now()
self.save()
def reschedule(self, new_start_time):
"""Перенести занятие на новое время."""
if not self.can_be_rescheduled:
raise ValueError('Занятие нельзя перенести')
# Создаем новое занятие
new_lesson = Lesson.objects.create(
mentor=self.mentor,
client=self.client,
start_time=new_start_time,
duration=self.duration,
title=self.title,
description=self.description,
subject=self.subject,
mentor_subject=self.mentor_subject,
subject_name=self.subject_name or (self.subject.name if self.subject else '') or (self.mentor_subject.name if self.mentor_subject else ''),
template=self.template,
meeting_url=self.meeting_url,
rescheduled_from=self,
)
# Отмечаем старое как перенесенное
self.status = 'rescheduled'
self.save()
return new_lesson
class LessonTemplate(models.Model):
"""
Шаблон занятия.
Используется для быстрого создания повторяющихся занятий.
"""
# Владелец шаблона
mentor = models.ForeignKey(
'users.User',
on_delete=models.CASCADE,
related_name='lesson_templates',
limit_choices_to={'role': 'mentor'},
verbose_name='Ментор'
)
# Информация о шаблоне
title = models.CharField(
max_length=200,
verbose_name='Название'
)
description = models.TextField(
blank=True,
verbose_name='Описание'
)
subject = models.ForeignKey(
'Subject',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='templates',
verbose_name='Предмет'
)
mentor_subject = models.ForeignKey(
'MentorSubject',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='templates',
verbose_name='Кастомный предмет'
)
subject_name = models.CharField(
max_length=100,
blank=True,
verbose_name='Название предмета (legacy)'
)
duration = models.IntegerField(
validators=[MinValueValidator(15), MaxValueValidator(480)],
default=60,
verbose_name='Длительность (минуты)'
)
# Настройки
is_active = models.BooleanField(
default=True,
verbose_name='Активен'
)
meeting_url = models.URLField(
blank=True,
max_length=500,
verbose_name='Ссылка на встречу'
)
# Цвет для календаря
color = models.CharField(
max_length=7,
default='#3B82F6',
verbose_name='Цвет',
help_text='HEX код цвета для отображения в календаре'
)
# Временные метки
created_at = models.DateTimeField(
auto_now_add=True,
null=True,
blank=True,
verbose_name='Дата создания'
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name='Дата обновления'
)
class Meta:
db_table = 'lesson_templates'
verbose_name = 'Шаблон занятия'
verbose_name_plural = 'Шаблоны занятий'
ordering = ['mentor', 'title']
def __str__(self):
return f"{self.title} ({self.mentor.get_full_name()})"
def create_lesson(self, client, start_time):
"""Создать занятие из шаблона."""
return Lesson.objects.create(
mentor=self.mentor,
client=client,
start_time=start_time,
duration=self.duration,
title=self.title,
description=self.description,
subject=self.subject,
mentor_subject=self.mentor_subject,
subject_name=self.subject_name or (self.subject.name if self.subject else '') or (self.mentor_subject.name if self.mentor_subject else ''),
template=self,
meeting_url=self.meeting_url,
)
class TimeSlot(models.Model):
"""
Временной слот.
Представляет конкретный временной интервал для бронирования.
"""
# Ментор
mentor = models.ForeignKey(
'users.User',
on_delete=models.CASCADE,
related_name='time_slots',
limit_choices_to={'role': 'mentor'},
verbose_name='Ментор'
)
# Время
start_time = models.DateTimeField(
verbose_name='Время начала',
db_index=True
)
end_time = models.DateTimeField(
verbose_name='Время окончания',
db_index=True
)
# Статус
is_available = models.BooleanField(
default=True,
verbose_name='Доступен',
help_text='Свободен ли слот для бронирования'
)
is_booked = models.BooleanField(
default=False,
verbose_name='Забронирован'
)
# Связь с занятием (если забронирован)
lesson = models.OneToOneField(
'Lesson',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='time_slot',
verbose_name='Занятие'
)
# Повторяющийся слот
is_recurring = models.BooleanField(
default=False,
verbose_name='Повторяющийся',
help_text='Повторяется ли этот слот еженедельно'
)
recurring_day = models.IntegerField(
null=True,
blank=True,
validators=[MinValueValidator(0), MaxValueValidator(6)],
verbose_name='День недели',
help_text='0=Понедельник, 6=Воскресенье'
)
# Временные метки
created_at = models.DateTimeField(
auto_now_add=True,
null=True,
blank=True,
verbose_name='Дата создания'
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name='Дата обновления'
)
class Meta:
db_table = 'time_slots'
verbose_name = 'Временной слот'
verbose_name_plural = 'Временные слоты'
ordering = ['start_time']
indexes = [
models.Index(fields=['mentor', 'start_time']),
models.Index(fields=['is_available', 'is_booked']),
models.Index(fields=['start_time', 'end_time']),
]
constraints = [
models.CheckConstraint(
check=models.Q(end_time__gt=models.F('start_time')),
name='timeslot_end_after_start'
),
]
def __str__(self):
return f"{self.mentor.get_full_name()} - {self.start_time.strftime('%d.%m.%Y %H:%M')}-{self.end_time.strftime('%H:%M')}"
def book(self, lesson):
"""Забронировать слот для занятия."""
if self.is_booked:
raise ValueError('Слот уже забронирован')
if not self.is_available:
raise ValueError('Слот недоступен')
self.is_booked = True
self.lesson = lesson
self.save()
def release(self):
"""Освободить слот."""
self.is_booked = False
self.lesson = None
self.save()
class Availability(models.Model):
"""
Доступность ментора.
Определяет когда ментор доступен для занятий.
"""
DAY_CHOICES = [
(0, 'Понедельник'),
(1, 'Вторник'),
(2, 'Среда'),
(3, 'Четверг'),
(4, 'Пятница'),
(5, 'Суббота'),
(6, 'Воскресенье'),
]
# Ментор
mentor = models.ForeignKey(
'users.User',
on_delete=models.CASCADE,
related_name='availabilities',
limit_choices_to={'role': 'mentor'},
verbose_name='Ментор'
)
# День недели (для повторяющейся доступности)
day_of_week = models.IntegerField(
choices=DAY_CHOICES,
null=True,
blank=True,
verbose_name='День недели',
help_text='Для еженедельного повторения'
)
# Конкретная дата (для разовой доступности)
specific_date = models.DateField(
null=True,
blank=True,
verbose_name='Конкретная дата',
help_text='Для разовой доступности'
)
# Время
start_time = models.TimeField(
verbose_name='Время начала'
)
end_time = models.TimeField(
verbose_name='Время окончания'
)
# Тип
is_recurring = models.BooleanField(
default=True,
verbose_name='Повторяющаяся',
help_text='Повторяется ли еженедельно'
)
# Статус
is_active = models.BooleanField(
default=True,
verbose_name='Активна'
)
# Исключения (даты, когда доступность не действует)
exception_dates = models.JSONField(
default=list,
blank=True,
verbose_name='Даты исключений',
help_text='Список дат в формате YYYY-MM-DD'
)
# Заметки
notes = models.TextField(
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 = 'availabilities'
verbose_name = 'Доступность'
verbose_name_plural = 'Доступность'
ordering = ['mentor', 'day_of_week', 'start_time']
indexes = [
models.Index(fields=['mentor', 'is_active']),
models.Index(fields=['day_of_week', 'start_time']),
models.Index(fields=['specific_date']),
]
constraints = [
models.CheckConstraint(
check=models.Q(end_time__gt=models.F('start_time')),
name='availability_end_after_start'
),
]
def __str__(self):
if self.is_recurring:
day_name = dict(self.DAY_CHOICES)[self.day_of_week]
return f"{self.mentor.get_full_name()} - {day_name} {self.start_time.strftime('%H:%M')}-{self.end_time.strftime('%H:%M')}"
else:
return f"{self.mentor.get_full_name()} - {self.specific_date} {self.start_time.strftime('%H:%M')}-{self.end_time.strftime('%H:%M')}"
def is_available_on(self, date):
"""Проверка доступности на конкретную дату."""
if not self.is_active:
return False
# Проверка исключений
date_str = date.strftime('%Y-%m-%d')
if date_str in self.exception_dates:
return False
# Проверка для повторяющейся доступности
if self.is_recurring:
return date.weekday() == self.day_of_week
# Проверка для разовой доступности
return date == self.specific_date
def lesson_file_upload_path(instance, filename):
"""Путь для загрузки файлов уроков."""
ext = filename.split('.')[-1]
new_filename = f"{uuid.uuid4()}.{ext}"
return os.path.join('lessons', str(instance.lesson.id), new_filename)
class LessonFile(models.Model):
"""
Файлы, прикрепленные к уроку.
Могут быть загружены при завершении урока или выбраны из учебных материалов.
"""
SOURCE_CHOICES = [
('uploaded', 'Загружен при завершении'),
('material', 'Из учебных материалов'),
]
lesson = models.ForeignKey(
Lesson,
on_delete=models.CASCADE,
related_name='files',
verbose_name='Урок'
)
# Файл (если загружен)
file = models.FileField(
upload_to=lesson_file_upload_path,
blank=True,
null=True,
max_length=500,
validators=[FileExtensionValidator(allowed_extensions=['pdf', 'doc', 'docx', 'txt', 'jpg', 'jpeg', 'png', 'zip', 'rar'])],
verbose_name='Файл'
)
# Ссылка на материал (если выбран из материалов)
material = models.ForeignKey(
'materials.Material',
on_delete=models.SET_NULL,
related_name='lesson_files',
null=True,
blank=True,
verbose_name='Учебный материал'
)
source = models.CharField(
max_length=20,
choices=SOURCE_CHOICES,
default='uploaded',
verbose_name='Источник'
)
filename = models.CharField(
max_length=255,
verbose_name='Название файла'
)
file_size = models.BigIntegerField(
null=True,
blank=True,
verbose_name='Размер файла (bytes)'
)
description = models.TextField(
blank=True,
verbose_name='Описание'
)
uploaded_by = models.ForeignKey(
'users.User',
on_delete=models.SET_NULL,
related_name='uploaded_lesson_files',
null=True,
verbose_name='Загрузил'
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name='Дата создания'
)
class Meta:
db_table = 'lesson_files'
verbose_name = 'Файл урока'
verbose_name_plural = 'Файлы уроков'
ordering = ['-created_at']
indexes = [
models.Index(fields=['lesson']),
models.Index(fields=['material']),
]
def __str__(self):
return f"{self.filename} - {self.lesson.title}"
def get_file_url(self):
"""Получить URL файла."""
if self.file:
return self.file.url
elif self.material and self.material.file:
return self.material.file.url
return None
def get_file_size_display(self):
"""Отформатированный размер файла."""
if not self.file_size:
return '0 B'
for unit in ['B', 'KB', 'MB', 'GB']:
if self.file_size < 1024.0:
return f"{self.file_size:.1f} {unit}"
self.file_size /= 1024.0
return f"{self.file_size:.1f} TB"
def lesson_homework_submission_file_path(instance, filename):
"""Путь для загрузки файлов ответов на ДЗ по уроку."""
ext = filename.split('.')[-1]
new_filename = f"{uuid.uuid4()}.{ext}"
return os.path.join('lessons', str(instance.lesson.id), 'homework_submissions', new_filename)
class LessonHomeworkSubmission(models.Model):
"""
Ответ ученика на домашнее задание по уроку.
"""
STATUS_CHOICES = [
('pending', 'Ожидает проверки'),
('graded', 'Проверено'),
('returned', 'Возвращено на доработку'),
]
lesson = models.ForeignKey(
Lesson,
on_delete=models.CASCADE,
related_name='homework_submissions',
verbose_name='Урок'
)
student = models.ForeignKey(
'users.User',
on_delete=models.CASCADE,
related_name='lesson_homework_submissions',
limit_choices_to={'role': 'client'},
verbose_name='Ученик'
)
# Содержимое ответа
content = models.TextField(
blank=True,
verbose_name='Текст ответа',
help_text='Текстовый ответ на домашнее задание'
)
# Файл ответа
attachment = models.FileField(
upload_to=lesson_homework_submission_file_path,
blank=True,
null=True,
max_length=500,
validators=[FileExtensionValidator(allowed_extensions=['pdf', 'doc', 'docx', 'txt', 'jpg', 'jpeg', 'png', 'zip', 'rar'])],
verbose_name='Файл ответа'
)
# Статус и проверка
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='pending',
verbose_name='Статус',
db_index=True
)
# Оценка
score = models.IntegerField(
null=True,
blank=True,
validators=[MinValueValidator(0), MaxValueValidator(100)],
verbose_name='Оценка',
help_text='Оценка от 0 до 100'
)
# Отзыв ментора
feedback = models.TextField(
blank=True,
verbose_name='Отзыв ментора'
)
checked_by = models.ForeignKey(
'users.User',
on_delete=models.SET_NULL,
related_name='checked_lesson_homework_submissions',
null=True,
blank=True,
limit_choices_to={'role': 'mentor'},
verbose_name='Проверил'
)
checked_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='Дата проверки'
)
# Временные метки
submitted_at = models.DateTimeField(
auto_now_add=True,
verbose_name='Дата отправки'
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name='Дата обновления'
)
class Meta:
db_table = 'lesson_homework_submissions'
verbose_name = 'Ответ на ДЗ по уроку'
verbose_name_plural = 'Ответы на ДЗ по урокам'
ordering = ['-submitted_at']
indexes = [
models.Index(fields=['lesson', 'student']),
models.Index(fields=['student', 'status']),
models.Index(fields=['status', 'submitted_at']),
]
constraints = [
models.UniqueConstraint(
fields=['lesson', 'student'],
name='unique_lesson_student_submission'
),
]
def __str__(self):
return f"{self.student.get_full_name()} - {self.lesson.title} ({self.get_status_display()})"
def grade(self, score, feedback, checked_by):
"""Выставить оценку."""
self.score = score
self.feedback = feedback
self.checked_by = checked_by
self.checked_at = timezone.now()
self.status = 'graded'
self.save()
def return_for_revision(self, feedback):
"""Вернуть на доработку."""
self.feedback = feedback
self.status = 'returned'
self.save()