import arrow from random import shuffle from uuid import uuid4 from unidecode import unidecode from django.db import models from django.core.exceptions import ValidationError from django.utils import timezone from django.utils.timezone import now from django.utils.text import slugify 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() def default_slug(): return str(uuid4()) def deferred_start_at_validator(value): if value < now(): raise ValidationError( 'Дата и время начала курса не может быть меньше текущих.', ) 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) class Tag(models.Model): tag = models.CharField(max_length=20,) class CourseTags(models.Model): tag = models.ForeignKey(Tag, on_delete=models.CASCADE) course = models.ForeignKey('Course', on_delete=models.CASCADE) 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') access_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', ) tags = models.ManyToManyField('Tag', through=CourseTags, blank=True) 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 and self.title: self.slug = slugify(unidecode(self.title[:90])) if self.slug: if self.slug.isdigit(): self.slug = 'course%s' % self.slug slug = self.slug while Course.objects.filter(slug__iexact=self.slug).exclude(id=self.id).exists(): self.slug = '%s_%s' % (slug, str(uuid4())[-4:]) return super().save() @property def age_str(self): ages = dict(self.AGE_CHOICES) return ages.get(self.age) @property def url(self): return self.get_absolute_url() def get_absolute_url(self): return reverse_lazy('course', args=[self.slug.lower() if self.slug else 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')