import arrow from random import shuffle from uuid import uuid4 from django.db import models from django.core.exceptions import ValidationError from django.utils import timezone from django.utils.timezone import now from django.contrib.auth import get_user_model from django.urls import reverse_lazy from django.conf import settings from polymorphic_tree.models import PolymorphicMPTTModel, PolymorphicTreeForeignKey from apps.school.models import LiveLesson from project.mixins import BaseModel, DeactivatedMixin from apps.content.models import ImageObject, Gallery, Video, ContestWork User = get_user_model() class Like(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) ip = models.GenericIPAddressField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) def default_slug(): return str(uuid4()) def deferred_start_at_validator(value): if value < now(): raise ValidationError( 'Дата и время начала курса не может быть меньше текущих.', ) class Course(BaseModel, DeactivatedMixin): DRAFT = 0 PENDING = 1 PUBLISHED = 2 ARCHIVED = 3 DENIED = 4 STATUS_CHOICES = ( (DRAFT, 'Draft'), (PENDING, 'Pending'), (PUBLISHED, 'Published'), (ARCHIVED, 'Archived'), (DENIED, 'Denied') ) AGE_LT5 = 1 AGE_510 = 2 AGE_1018 = 3 AGE_GT18 = 4 AGE_CHOICES = ( (0, 'Любой возраст'), (AGE_LT5, 'до 5'), (AGE_510, '5-10'), (AGE_1018, '10-18'), (AGE_GT18, 'от 18'), ) slug = models.SlugField( allow_unicode=True, null=True, blank=True, max_length=100, unique=True, db_index=True, ) author = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, blank=True ) title = models.CharField('Название курса', default='', max_length=100, db_index=True) short_description = models.TextField( 'Краткое описание курса', default='', db_index=True ) from_author = models.TextField( 'От автора', default='', null=True, blank=True ) cover = models.ForeignKey( ImageObject, related_name='course_covers', verbose_name='Обложка курса', on_delete=models.SET_NULL, null=True, blank=True, ) old_price = models.DecimalField( 'Старая цена курса', max_digits=10, decimal_places=2, null=True, blank=True ) price = models.DecimalField( 'Цена курса', help_text='Если цена не выставлена, то курс бесплатный', max_digits=10, decimal_places=2, null=True, blank=True ) age = models.SmallIntegerField(choices=AGE_CHOICES, default=0) is_infinite = models.BooleanField(default=False) deferred_start_at = models.DateTimeField( 'Отложенный запуск курса', help_text='Заполнить если курс отложенный', null=True, blank=True, validators=[deferred_start_at_validator], ) category = models.ForeignKey('Category', null=True, blank=True, on_delete=models.PROTECT, related_name='courses') duration = models.IntegerField('Продолжительность курса', default=0) is_featured = models.BooleanField(default=False) status = models.PositiveSmallIntegerField( 'Статус', default=DRAFT, choices=STATUS_CHOICES ) likes = models.ManyToManyField(Like, blank=True) materials = models.ManyToManyField('Material', blank=True) gallery = models.ForeignKey( Gallery, verbose_name='Галерея работ', on_delete=models.CASCADE, null=True, blank=True, related_name='results_gallery', ) created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = 'Курс' verbose_name_plural = 'Курсы' ordering = ['-is_featured', ] def __str__(self): return str(self.id) + ' ' + self.title # def save(self, *args, **kwargs): # if not self.slug: # self.slug = slugify( # self.title[:90], # allow_unicode=True # ) # if Course.objects.filter(slug=self.slug).exclude(id=self.id).exists(): # self.slug += '_' + str(uuid4())[:6] # return super().save() @property def url(self): return self.get_absolute_url() def get_absolute_url(self): return reverse_lazy('course', args=[self.id]) @property def is_free(self): if self.price: return False return True @property def deferred_start_at_humanize(self): return arrow.get(self.deferred_start_at, settings.TIME_ZONE).humanize(locale='ru') @property def created_at_humanize(self): return arrow.get(self.created_at, settings.TIME_ZONE).humanize(locale='ru') @property def is_deferred_start(self): if not self.deferred_start_at: return False if timezone.now() < self.deferred_start_at: return True return False @property def count_videos_in_lessons(self): return Video.objects.filter(lesson__in=self.lessons.all()).count() @classmethod def shuffle(cls, qs, order_is_featured = True): if order_is_featured: featured = [] other = [] for c in list(qs): if c.is_featured: featured.append(c) else: other.append(c) shuffle(featured) shuffle(other) return featured + other else: all_courses = list(qs) shuffle(all_courses) return all_courses class Category(models.Model): title = models.CharField('Название категории', max_length=100) def __str__(self): return self.title class Meta: verbose_name = 'Категория' verbose_name_plural = 'Категории' ordering = ['title'] class Lesson(BaseModel, DeactivatedMixin): title = models.CharField('Название урока', max_length=100) short_description = models.TextField('Краткое описание урока') author = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, blank=True ) course = models.ForeignKey( Course, on_delete=models.CASCADE, related_name='lessons' ) cover = models.ForeignKey( ImageObject, related_name='lesson_covers', verbose_name='Обложка урока', on_delete=models.SET_NULL, null=True, blank=True, ) created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) position = models.PositiveSmallIntegerField( 'Положение на странице', default=1, ) def __str__(self): return self.title def set_last_position(self): if self.course: self.position = self.course.lessons.count() def save(self, *args, **kwargs): if not self.author and self.course and self.course.author: self.author = self.course.author super().save(*args, **kwargs) class Meta: verbose_name = 'Урок' verbose_name_plural = 'Уроки' ordering = ('title',) class Material(models.Model): title = models.CharField('Название материала', max_length=100) cover = models.ForeignKey( ImageObject, related_name='material_covers', verbose_name='Обложка материала', on_delete=models.SET_NULL, null=True, blank=True, ) short_description = models.TextField('Краткое описание материала') created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) def __str__(self): return self.title class Meta: verbose_name = 'Материал' verbose_name_plural = 'Материалы' ordering = ('title',) class Comment(PolymorphicMPTTModel, DeactivatedMixin): OBJ_TYPE_COURSE = 'course' OBJ_TYPE_LESSON = 'lesson' OBJ_TYPE_LIVE_LESSON = 'live-lesson' content = models.TextField('Текст комментария', default='') author = models.ForeignKey(User, on_delete=models.CASCADE) parent = PolymorphicTreeForeignKey( 'self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.PROTECT ) created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) @property def created_at_humanize(self): return arrow.get(self.created_at, settings.TIME_ZONE).humanize(locale='ru') def __str__(self): return self.content class Meta: ordering = ('-created_at',) class MPTTMeta: order_insertion_by = ['-created_at'] class CourseComment(Comment): course = models.ForeignKey( Course, on_delete=models.CASCADE, related_name='comments' ) class Meta(Comment.Meta): verbose_name = 'Комментарий курса' verbose_name_plural = 'Комментарии курсов' class LessonComment(Comment): lesson = models.ForeignKey( Lesson, on_delete=models.CASCADE, related_name='comments' ) class Meta(Comment.Meta): verbose_name = 'Комментарий урока' verbose_name_plural = 'Комментарии уроков' class LiveLessonComment(Comment): live_lesson = models.ForeignKey( LiveLesson, on_delete=models.CASCADE, related_name='comments' ) class Meta(Comment.Meta): verbose_name = 'Комментарий урока школы' verbose_name_plural = 'Комментарии уроков школы' class ContestWorkComment(Comment): contest_work = models.ForeignKey(ContestWork, on_delete=models.CASCADE, related_name='comments')