# -*- coding: utf-8 -*- from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.core.exceptions import ObjectDoesNotExist import json import unidecode from django.template.defaultfilters import slugify from maps.models import CourseRoute, CourseMap from lms.tools import decode_base64, get_real_name from lms.global_decorators import transaction_decorator from library.models import Tags from storage.models import Storage import random COURSE_LEVEL = ( ('B', 'Базовый'), ('A', 'Продвинутый'), ('E', 'Экспертный'), ('B+A', 'Базовый + Продвинутый'), ) COURSE_DIRECTION = ( (3, 'Бизнес'), (2, 'Веб-дизайн'), (1, 'Разработка'), (4, 'Рисование'), ) class CourseManager(models.Manager): @transaction_decorator def update_or_create_course(self, image=None, big_image=None, id=0, big_mobile_image=None, mentors=None, slug=None, teachers=None, level=None, direction=None, **kwargs): slug = slug if slug else slugify(unidecode.unidecode(kwargs['title'])) if image: kwargs['image'] = decode_base64(image, 'course/image%s.png' % slug) if big_image: kwargs['big_image'] = decode_base64(big_image, 'course/big_image%s.png' % slug) if big_mobile_image: kwargs['big_mobile_image'] = decode_base64(big_mobile_image, 'course/big_mobile_image%s.png' % slug) if level: kwargs['level'] = get_real_name(COURSE_LEVEL, level) if direction: kwargs['direction'] = get_real_name(COURSE_DIRECTION, direction) try: course = self.get(id=id) for i in kwargs: if kwargs[i]: setattr(course, i, kwargs[i]) course.save() except ObjectDoesNotExist: kwargs['slug'] = slug course = self.create(**kwargs) CourseMap.objects.create(course=course) if mentors: for email in mentors: course.mentors.add(get_user_model().objects.get(email=email)) if teachers: for email in teachers: course.teachers.add(get_user_model().objects.get(email=email)) return course class Course(models.Model): hidden = models.BooleanField(verbose_name='Видно только оплатившим', default=False) level = models.CharField(verbose_name='Уровень', choices=COURSE_LEVEL, default='B', max_length=3) slug = models.SlugField(max_length=255, blank=True, default='', unique=True, editable=False) direction = models.SmallIntegerField(choices=COURSE_DIRECTION, verbose_name='Направление', null=True) sort = models.SmallIntegerField(null=True, verbose_name="Порядок сортировки") mentors = models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='Кураторы', blank=True, related_name='course_mentors') public = models.BooleanField(verbose_name='Опубликовать', default=False) title = models.CharField(verbose_name="Заголовок", max_length=255) description = models.TextField(verbose_name='Описание', blank=True) image = models.URLField(verbose_name='Изображение', blank=True, max_length=255) big_image = models.URLField(verbose_name='Большое изображение', blank=True, max_length=255) big_mobile_image = models.URLField(verbose_name='Под мобилку', blank=True, null=True, help_text='Большая картинка для мобильной версии', max_length=255) teachers = models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='Преподаватели', related_name='course_teachers') route = models.OneToOneField(to=CourseRoute, verbose_name="Порядок прохождения по умолчанию", blank=True, null=True) def __str__(self): return self.title def get_teacher(self): return random.choice(self.teachers.all()) def get_map(self, user): route = self.route if user.is_authenticated: route = user.progress_set.get(course=self).get_template() map_list = route.get_sorted_maps() return self.route.maps.all()[0] def get_tree(self, serializer): """ Способ отображения дочерних элементов. Принимает на вход сериалайзер узла """ course_map = json.loads(CourseMap.objects.get(course=self).dependent_elements) def helper(tree_id): acc = [] for j, i in enumerate(tree_id): if type([]) == type(i): acc[-1]['children'] = helper(i) else: acc.append(serializer(Vertex.objects.get(id=i)).data) return acc return helper(course_map) def get_vertexes(self, vertex_type=None): course_map = CourseMap.objects.get(course=self).map_to_list() if vertex_type: return self.vertex_set.filter(content_type__model=vertex_type, id__in=course_map) return course_map def get_statistic(self): """ Минималистичная статистика по уроку, количество тем, уроков, домашек. """ return { "topic_count": self.get_vertexes('topic').count(), "tutorial_count": self.get_vertexes('tutorial').count(), "task_count": self.get_vertexes('task').count(), } def get_first(self, vertex_model_list=None): if vertex_model_list is None: vertex_model_list = ['topic', 'tutorial', 'task'] else: for i in vertex_model_list: if i not in ['topic', 'tutorial', 'task']: raise ValueError('undefined model: ' + i) vertex = Vertex.objects.get(id=self.coursemap_set.get(sort=0).get_first()) if vertex.content_type.model in vertex_model_list: return vertex return vertex.get_next(vertex_model_list) def get_last(self, vertex_model_list=None): if vertex_model_list is None: vertex_model_list = ['topic', 'tutorial', 'task'] else: for i in vertex_model_list: if i not in ['topic', 'tutorial', 'task']: raise ValueError('undefined model: ' + i) vertex = Vertex.objects.get(id=self.coursemap_set.get(sort=0).get_last()) if vertex.content_type.model in vertex_model_list: return vertex return vertex.get_previous(vertex_model_list) objects = CourseManager() class Meta: verbose_name = "Курс" verbose_name_plural = "Курсы" class VertexManager(models.Manager): # Менеджер вершин графа. @transaction_decorator def create_with_dependencies(self, model, course, title, description, extra_data=None, free=True, materials=None, parents=None, children=None): extra_data = json.loads(extra_data) content_type = ContentType.objects.get(app_label='courses', model=model) obj = content_type.model_class().objects.create(**extra_data) [obj.materials.add(i) for i in materials] if materials else None res = self.create( content_type=content_type, object_id=obj.id, course=course, title=title, description=description, free=free, ) if children: for child in children: res.children.add(child) if parents: for parent in parents: parent.children.add(res) return res class Vertex(models.Model): """ Основная структурная единица узел графа курса. Позволяет работать со структурой курса на более высоком уровне абстракции. """ course = models.ForeignKey(to=Course) title = models.CharField(verbose_name='Название', max_length=255) free = models.BooleanField(default=True, verbose_name='Привилегии для узла не будут проверяться') description = models.TextField(verbose_name='Описание', default='', blank=True, null=True) children = models.ManyToManyField(to='Vertex', blank=True) content_type = models.ForeignKey(to=ContentType) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') objects = VertexManager() def __str__(self): return self.title + ': ' + str(self.content_type.model) def get_next(self, vertex_model_list=None): if vertex_model_list is None: vertex_model_list = ['topic', 'tutorial', 'task'] else: for i in vertex_model_list: if i not in ['topic', 'tutorial', 'task']: raise ValueError('undefined model: ' + i) vertex_id = CourseMap.objects.get(course=self.course).get_next(self.id) vertex = Vertex.objects.get(id=int(vertex_id), ) if vertex.content_type.model in vertex_model_list: return vertex return vertex.get_next(vertex_model_list) def get_previous(self, vertex_model_list=None): if vertex_model_list is None: vertex_model_list = ['topic', 'tutorial', 'task'] else: for i in vertex_model_list: if i not in ['topic', 'tutorial', 'task']: raise ValueError('undefined model: ' + i) vertex_id = CourseMap.objects.get(course=self.course).get_previous(self.id) vertex = Vertex.objects.get(id=int(vertex_id), ) if vertex.content_type.model in vertex_model_list: return vertex return vertex.get_previous(vertex_model_list) def is_more(self, vertex) -> bool: if not self.course == vertex.course: raise ValueError('Vertexes of different course') course_map = CourseMap.objects.get(course=self.course) return course_map.map_to_list().index(self.id) > course_map.map_to_list().index(vertex.id) def check_vertex(self, user) -> bool: if self.free: return True if not user.is_authenticated: return False try: progress = self.course.progress_set.get(user=user) except ObjectDoesNotExist: return False return progress.is_access(self) def get_number(self, vertex_type=None): """ Возврощает порядковый номер узла с определённым типом. Пример мы хотим определит какой по счёту теме принадлежит конкретно взятый урок. """ vertex_list = list(self.course.get_vertexes(vertex_type)) try: res = vertex_list.index(self) except ValueError: parents = self.vertex_set.all() if parents.count() == 1: res = vertex_list.index(parents[0]) else: res = None return res # Модели нового API со временем всё, что выше будет выпилено class Tutorial(models.Model): """ Модель урока. Урок может быть открыт для комментирования и закрыт, по дефолту открыт, вероятно закрывать нужно будет крайне редко. Видео к уроку фрейм который лежит прямо в базе, конечно же костыль и со временем мы уйдём от этого, все видео хостятся на двух онлайн сервисах на клиент нужно передовать id и сервис на котором, это дело хостится, а правило отображения оставить клиенту. Материалы для урока по сути FileField, нужна только для создания лишней связи в таблице и дублирования метазаголовков файла """ on_comment = models.BooleanField(verbose_name=u'Комментарии', default=False) video = models.TextField(verbose_name=u'Код видео', default='', blank=True) materials = models.ManyToManyField(Storage, verbose_name=u'Материалы урока', blank=True) class Task(models.Model): """ Модель таска. Исторически сложилось, что на сервере хостятся два типа тасков отличающихся лишь наименованием домашние работы и экзамены, не нужно быть гением, чтобы понять для чего нужно булево значение is_exam Материалы для урока по сути FileField, нужна только для создания лишней связи в таблице и дублирования метазаголовков файла """ materials = models.ManyToManyField(Storage, verbose_name='Материалы для домашней работы', blank=True) is_exam = models.BooleanField(default=False, verbose_name='Экзамен или домашка') class Topic(models.Model): """ Модель темы, нужно просто для объединения тасков и уроков. У некоторых тем есть иконка. Возможно поле icon перекачует в Vertex, а данная модель отвалится за ненадобностью """ icon = models.ImageField(verbose_name='Иконка темы', null=True, blank=True)