from datetime import timedelta from django.core.validators import RegexValidator from django.db import models from django.db.models import Q from django.utils import timezone ACTIVE_BOOKING_STATUSES = ("new", "confirmed") class ConsultationSlotQuerySet(models.QuerySet): def available(self): return ( self.filter(is_active=True, start_at__gte=timezone.now()) .exclude(bookings__status__in=ACTIVE_BOOKING_STATUSES) .distinct() ) class ConsultationSlot(models.Model): class Subject(models.TextChoices): ANY = "any", "Физика или математика" PHYSICS = "physics", "Физика" MATHEMATICS = "mathematics", "Математика" class Format(models.TextChoices): ONLINE = "online", "Онлайн" OFFLINE = "offline", "Очно" start_at = models.DateTimeField("Начало консультации", unique=True) duration_minutes = models.PositiveSmallIntegerField( "Длительность, минут", choices=((45, "45 минут"), (60, "60 минут"), (90, "90 минут"), (120, "120 минут")), default=60, ) subject = models.CharField( "Доступный предмет", max_length=20, choices=Subject.choices, default=Subject.ANY ) format = models.CharField("Формат", max_length=20, choices=Format.choices, default=Format.ONLINE) is_active = models.BooleanField("Открыт для записи", default=True) teacher_note = models.CharField("Заметка преподавателя", max_length=300, blank=True) created_at = models.DateTimeField("Создан", auto_now_add=True) objects = ConsultationSlotQuerySet.as_manager() class Meta: verbose_name = "Свободное время" verbose_name_plural = "Расписание" ordering = ("start_at",) def __str__(self): local_start = timezone.localtime(self.start_at) return f"{local_start:%d.%m.%Y, %H:%M} · {self.get_format_display()}" @property def end_at(self): return self.start_at + timedelta(minutes=self.duration_minutes) @property def is_available(self): return self.is_active and self.start_at >= timezone.now() and not self.bookings.filter( status__in=ACTIVE_BOOKING_STATUSES ).exists() class ConsultationRequest(models.Model): class Subject(models.TextChoices): PHYSICS = "physics", "Физика" MATHEMATICS = "mathematics", "Математика" class Exam(models.TextChoices): OGE = "oge", "ОГЭ" EGE = "ege", "ЕГЭ" SCHOOL = "school", "Школьная программа" OTHER = "other", "Другая консультация" class Format(models.TextChoices): ONLINE = "online", "Онлайн" OFFLINE = "offline", "Очно" ANY = "any", "Не имеет значения" class Status(models.TextChoices): NEW = "new", "Новая" CONFIRMED = "confirmed", "Подтверждена" COMPLETED = "completed", "Проведена" CANCELLED = "cancelled", "Отменена" NO_SHOW = "no_show", "Не пришёл" name = models.CharField("Имя ученика или родителя", max_length=100) phone = models.CharField( "Телефон", max_length=30, validators=[ RegexValidator( regex=r"^[0-9+()\-\s]{7,30}$", message="Введите телефон в привычном формате.", ) ], ) contact = models.CharField("Telegram или другой способ связи", max_length=100, blank=True) student_grade = models.PositiveSmallIntegerField( "Класс ученика", choices=tuple((grade, f"{grade} класс") for grade in range(5, 12)), blank=True, null=True ) subject = models.CharField("Предмет", max_length=20, choices=Subject.choices) exam = models.CharField("Цель подготовки", max_length=20, choices=Exam.choices) format = models.CharField("Предпочтительный формат", max_length=20, choices=Format.choices, default=Format.ONLINE) slot = models.ForeignKey( ConsultationSlot, verbose_name="Выбранное время", related_name="bookings", on_delete=models.SET_NULL, blank=True, null=True, ) preferred_date = models.DateField("Дата консультации", blank=True, null=True) preferred_time = models.TimeField("Время консультации", blank=True, null=True) comment = models.TextField("Комментарий ученика", max_length=1000, blank=True) status = models.CharField("Статус", max_length=20, choices=Status.choices, default=Status.NEW) teacher_note = models.TextField("Заметка преподавателя", max_length=2000, blank=True) meeting_link = models.URLField("Ссылка на встречу", blank=True) cancellation_reason = models.CharField("Причина отмены", max_length=500, blank=True) reminder_sent = models.BooleanField("Напоминание отправлено", default=False) consent_given = models.BooleanField("Согласие на обработку данных", default=False) created_at = models.DateTimeField("Создана", auto_now_add=True) updated_at = models.DateTimeField("Изменена", auto_now=True) class Meta: verbose_name = "Запись на консультацию" verbose_name_plural = "Записи на консультации" ordering = ("preferred_date", "preferred_time", "-created_at") constraints = [ models.UniqueConstraint( fields=("slot",), condition=Q(status__in=ACTIVE_BOOKING_STATUSES), name="unique_active_slot_booking", ) ] def save(self, *args, **kwargs): if self.slot_id: local_start = timezone.localtime(self.slot.start_at) self.preferred_date = local_start.date() self.preferred_time = local_start.time() if self.format == self.Format.ANY: self.format = self.slot.format super().save(*args, **kwargs) def __str__(self): if self.preferred_date and self.preferred_time: return f"{self.name}: {self.get_subject_display()} — {self.preferred_date:%d.%m.%Y}, {self.preferred_time:%H:%M}" return f"{self.name}: {self.get_subject_display()}"