/
lizzza
/
labaweb
Обзор
Документация
Войти
/
lizzza
/
labaweb
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
main/models.py
455 строк
20 KB
ligzzza
все выполненные лабораторные
05 май 2026, 19:57
05 май 2026, 19:57
fa35051
Код
Авторство
О чём код?
from django.db import models from django.contrib.auth.models import AbstractUser from django.core.validators import MinValueValidator, MaxValueValidator from django.utils import timezone from django.urls import reverse from django.db.models import Q, Count from django.core.exceptions import ValidationError from simple_history.models import HistoricalRecords class ApprovedMasterClassManager(models.Manager): """Менеджер для получения только одобренных и предстоящих мастер-классов""" def get_queryset(self): return super().get_queryset().filter( Q(schedule_type='single', status='approved', start_datetime__gte=timezone.now()) | Q(schedule_type='recurring', status='approved', sessions__status='active', sessions__start_datetime__gte=timezone.now()) ).distinct() def by_category(self, category_slug): """Метод менеджера: фильтр по категории""" return self.filter(category__slug=category_slug) # ← ДЕМОНСТРАЦИЯ __ (связанная таблица) def popular(self, limit=5): """Популярные МК (по количеству бронирований)""" return self.annotate( booking_count=models.Count('bookings') ).order_by('-booking_count')[:limit] # ============================================================ # 1. ПОЛЬЗОВАТЕЛЬ (расширенная модель пользователя Django) # ============================================================ class User(AbstractUser): """Модель пользователя с ролями Участник/Организатор/Администратор""" ROLE_CHOICES = [ ('participant', 'Участник'), ('organizer', 'Организатор'), ('admin', 'Администратор'), ] email = models.EmailField(unique=True, verbose_name="Email (логин)") role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='participant', verbose_name="Роль") phone = models.CharField(max_length=20, blank=True, verbose_name="Телефон") avatar = models.ImageField(upload_to='avatars/', blank=True, null=True, verbose_name="Аватар") is_active = models.BooleanField(default=True, verbose_name="Активен") date_joined = models.DateTimeField(auto_now_add=True, verbose_name="Дата регистрации") last_login = models.DateTimeField(blank=True, null=True, verbose_name="Последний вход") # Дополнительные поля для организатора organization_name = models.CharField(max_length=200, blank=True, verbose_name="Название студии/организации") organization_description = models.TextField(blank=True, verbose_name="Описание студии") class Meta: verbose_name = "Пользователь" verbose_name_plural = "Пользователи" swappable = 'AUTH_USER_MODEL' # 👈 ЭТА СТРОКА ВАЖНА! Указывает, что это кастомная модель def __str__(self): return f"{self.get_full_name() or self.username} ({self.get_role_display()})" @property def is_admin(self): return self.role == 'admin' or self.is_superuser @property def is_organizer(self): return self.role == 'organizer' @property def is_participant(self): return self.role == 'participant' # ============================================================ # 2. КАТЕГОРИЯ # ============================================================ class Category(models.Model): """Категория мастер-классов (Кулинария, Творчество и т.д.)""" name = models.CharField(max_length=100, unique=True, verbose_name="Название категории") slug = models.SlugField(max_length=100, unique=True, verbose_name="URL-имя") description = models.TextField(blank=True, verbose_name="Описание") image = models.ImageField(upload_to='categories/', blank=True, null=True, verbose_name="Изображение") created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") class Meta: verbose_name = "Категория" verbose_name_plural = "Категории" ordering = ['name'] def __str__(self): return self.name # ============================================================ # 3. МАСТЕР-КЛАСС # ============================================================ class MasterClass(models.Model): """Мастер-класс (мероприятие)""" FORMAT_CHOICES = [ ('online', 'Онлайн'), ('offline', 'Офлайн'), ] STATUS_CHOICES = [ ('pending', 'На модерации'), ('approved', 'Одобрен'), ('rejected', 'Отклонен'), ('completed', 'Завершен'), ('cancelled', 'Отменен'), ] SCHEDULE_CHOICES = [ ('single', 'Однократный'), ('recurring', 'С расписанием'), ] title = models.CharField(max_length=200, verbose_name="Название") description = models.TextField(verbose_name="Описание") category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='masterclasses', verbose_name="Категория") organizer = models.ForeignKey(User, on_delete=models.CASCADE, related_name='masterclasses', limit_choices_to={'role': 'organizer'}, verbose_name="Организатор") city = models.CharField(max_length=100, verbose_name="Город") address = models.CharField(max_length=255, blank=True, verbose_name="Адрес") format = models.CharField(max_length=10, choices=FORMAT_CHOICES, default='offline', verbose_name="Формат") price = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(0)], verbose_name="Цена (руб)") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="Статус") moderation_comment = models.TextField(blank=True, verbose_name="Комментарий модератора") schedule_type = models.CharField( max_length=20, choices=SCHEDULE_CHOICES, # ← теперь видит default='single', verbose_name="Тип расписания" ) start_datetime = models.DateTimeField(null=True, blank=True, verbose_name="Дата начала") end_datetime = models.DateTimeField(null=True, blank=True, verbose_name="Дата окончания") max_participants = models.PositiveIntegerField(null=True, blank=True) current_participants = models.PositiveIntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления") online_link = models.URLField( blank=True, null=True, verbose_name="Ссылка на трансляцию" ) #Задание 12 Django 4 program_file = models.FileField( upload_to='programs/', blank=True, null=True, verbose_name="Программа (PDF)" ) objects = models.Manager() # стандартный менеджер approved_objects = ApprovedMasterClassManager() # ManyToMany через Booking (уже существующую модель) participants = models.ManyToManyField( User, # с кем связь through='Booking', # через какую таблицу through_fields=('masterclass', 'participant'), # какие поля связывают related_name='joined_masterclasses', # обратная связь verbose_name="Участники" ) history = HistoricalRecords() class Meta: verbose_name = "Мастер-класс" verbose_name_plural = "Мастер-классы" ordering = ['-start_datetime'] indexes = [ models.Index(fields=['city']), models.Index(fields=['start_datetime']), models.Index(fields=['category']), models.Index(fields=['status']), ] def __str__(self): if self.start_datetime: return f"{self.title} - {self.city} ({self.start_datetime.strftime('%d.%m.%Y')})" return f"{self.title} - {self.city} (расписание)" @property def is_upcoming(self): if self.start_datetime is None: # Для МК с расписанием проверяем, есть ли активные сеансы в будущем return self.sessions.filter(start_datetime__gte=timezone.now(), status='active').exists() return self.start_datetime > timezone.now() @property def has_free_places(self): if self.schedule_type == 'recurring': # Для МК с расписанием — проверяем любой активный сеанс return self.sessions.filter(status='active', current_participants__lt=models.F('max_participants')).exists() return self.current_participants < self.max_participants @property def free_places(self): if self.schedule_type == 'recurring': # Возвращаем общее количество свободных мест по всем активным сеансам from django.db.models import Sum, F total = self.sessions.filter(status='active').aggregate( free=Sum(F('max_participants') - F('current_participants')) )['free'] or 0 return total return self.max_participants - self.current_participants def get_absolute_url(self): """← ДЕМОНСТРАЦИЯ get_absolute_url и reverse""" return reverse('masterclass_detail', kwargs={'pk': self.pk}) def is_full(self): """Проверка заполненности""" return self.current_participants >= self.max_participants # ============================================================ # СЕАНСЫ МАСТЕР-КЛАССА (НОВАЯ МОДЕЛЬ) # ============================================================ class MasterClassSession(models.Model): masterclass = models.ForeignKey(MasterClass, on_delete=models.CASCADE, related_name='sessions', verbose_name="Мастер-класс") start_datetime = models.DateTimeField(verbose_name="Дата и время начала") # ← ВОТ ЭТА СТРОКА end_datetime = models.DateTimeField(verbose_name="Дата и время окончания") max_participants = models.PositiveIntegerField(verbose_name="Всего участников") current_participants = models.PositiveIntegerField(default=0, verbose_name="Занято мест") status = models.CharField( max_length=20, choices=[ ('active', 'Активен'), ('cancelled', 'Отменён'), ('completed', 'Завершён'), ], default='active', verbose_name="Статус" ) created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан") class Meta: verbose_name = "Сеанс мастер-класса" verbose_name_plural = "Сеансы мастер-классов" ordering = ['start_datetime'] def __str__(self): return f"{self.masterclass.title} — {self.start_datetime.strftime('%d.%m.%Y %H:%M')}" @property def free_places(self): return self.max_participants - self.current_participants @property def has_free_places(self): return self.free_places > 0 # ============================================================ # 4. ИЗОБРАЖЕНИЕ # ============================================================ class Image(models.Model): """Изображения для мастер-класса""" masterclass = models.ForeignKey(MasterClass, on_delete=models.CASCADE, related_name='images', verbose_name="Мастер-класс") image = models.ImageField(upload_to='masterclasses/', verbose_name="Изображение") is_main = models.BooleanField(default=False, verbose_name="Главное фото") uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата загрузки") class Meta: verbose_name = "Изображение" verbose_name_plural = "Изображения" def __str__(self): return f"Фото для {self.masterclass.title}" # ============================================================ # 5. БРОНИРОВАНИЕ # ============================================================ class Booking(models.Model): """Бронирование места на мастер-класс""" STATUS_CHOICES = [ ('pending', 'Ожидает оплаты'), ('confirmed', 'Подтверждено'), ('cancelled', 'Отменено'), ('completed', 'Завершено'), ] PAYMENT_STATUS_CHOICES = [ ('pending', 'Ожидает оплаты'), ('paid', 'Оплачено'), ('refunded', 'Возврат'), ] participant = models.ForeignKey(User, on_delete=models.CASCADE, related_name='bookings', limit_choices_to={'role': 'participant'}, verbose_name="Участник") masterclass = models.ForeignKey(MasterClass, on_delete=models.CASCADE, related_name='bookings', verbose_name="Мастер-класс") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="Статус") payment_status = models.CharField(max_length=20, choices=PAYMENT_STATUS_CHOICES, default='pending', verbose_name="Статус оплаты") participants_count = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], verbose_name="Количество мест") total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Цена на момент бронирования") created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") session = models.ForeignKey( MasterClassSession, on_delete=models.CASCADE, related_name='bookings', null=True, blank=True, verbose_name="Сеанс" ) history = HistoricalRecords() class Meta: verbose_name = "Бронирование" verbose_name_plural = "Бронирования" ordering = ['-created_at'] # Сортировка по дате (новые сверху) def session_info(self): if not self.session: return "Без сеанса" return f"{self.session.start_datetime.strftime('%d.%m.%Y %H:%M')} - {self.session.end_datetime.strftime('%H:%M')}" def get_event_datetime_display(self): dt = self.get_event_datetime() if not dt: return "Не указано" return dt.strftime('%d.%m.%Y %H:%M') def clean(self): if self.session and self.session.masterclass != self.masterclass: raise ValidationError("Сеанс не принадлежит выбранному мастер-классу") def save(self, *args, **kwargs): if not self.total_price: self.total_price = self.masterclass.price * self.participants_count # Проверка лимита if self.pk is None and self.status in ['confirmed', 'pending']: current = self.masterclass.current_participants max_parts = self.masterclass.max_participants if current + self.participants_count > max_parts: from django.core.exceptions import ValidationError raise ValidationError( f'Недостаточно мест! Свободно: {max_parts - current}' ) self.masterclass.current_participants += self.participants_count self.masterclass.save() super().save(*args, **kwargs) # ============================================================ # 6. ОТЗЫВ # ============================================================ class Review(models.Model): """Отзыв на мастер-класс""" STATUS_CHOICES = [ ('pending', 'На модерации'), ('approved', 'Одобрен'), ('rejected', 'Отклонен'), ] author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reviews', verbose_name="Автор") masterclass = models.ForeignKey(MasterClass, on_delete=models.CASCADE, related_name='reviews', verbose_name="Мастер-класс") booking = models.OneToOneField(Booking, on_delete=models.CASCADE, related_name='review', verbose_name="Бронирование") rating = models.PositiveSmallIntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)], verbose_name="Оценка (1-5)") text = models.TextField(verbose_name="Текст отзыва") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="Статус") created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") class Meta: verbose_name = "Отзыв" verbose_name_plural = "Отзывы" ordering = ['-created_at'] def __str__(self): return f"Отзыв от {self.author.get_full_name()} на {self.masterclass.title} (оценка: {self.rating})" # ============================================================ # 7. ИЗБРАННОЕ # ============================================================ class Favorite(models.Model): """Избранные мастер-классы пользователя""" user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='favorites', verbose_name="Пользователь") masterclass = models.ForeignKey(MasterClass, on_delete=models.CASCADE, related_name='favorited_by', verbose_name="Мастер-класс") created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата добавления") class Meta: verbose_name = "Избранное" verbose_name_plural = "Избранное" unique_together = ['user', 'masterclass'] # Нельзя добавить одно и то же дважды def __str__(self): return f"{self.user.get_full_name()} → {self.masterclass.title}" # ============================================================ # 8. УВЕДОМЛЕНИЕ # ============================================================ class Notification(models.Model): """Уведомления пользователей""" TYPE_CHOICES = [ ('booking_confirmed', 'Бронирование подтверждено'), ('booking_cancelled', 'Бронирование отменено'), ('moderation_result', 'Результат модерации'), ('new_booking', 'Новое бронирование'), ('reminder', 'Напоминание'), ] user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications', verbose_name="Пользователь") type = models.CharField(max_length=30, choices=TYPE_CHOICES, verbose_name="Тип") title = models.CharField(max_length=200, verbose_name="Заголовок") message = models.TextField(verbose_name="Текст") is_read = models.BooleanField(default=False, verbose_name="Прочитано") created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") class Meta: verbose_name = "Уведомление" verbose_name_plural = "Уведомления" ordering = ['-created_at'] def __str__(self): return f"{self.title} для {self.user.get_full_name()}"