# -*- 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 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 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: 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) 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') def __str__(self): return self.title 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_statistic(self): """ Минималистичная статистика по уроку, количество тем, уроков, домашек. """ topic_count = Vertex.objects.filter(course=self, content_type__model='topic').count() task_count = Vertex.objects.filter(course=self, content_type__model='task').count() tutorial_count = Vertex.objects.filter(course=self, content_type__model='tutorial').count() return {"topic_count": topic_count, "tutorial_count": tutorial_count, "task_count": 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.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.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 = u"Курс" verbose_name_plural = u"Курсы" class Skills(models.Model): title = models.CharField(verbose_name='Наименование', max_length=255) color = models.CharField(verbose_name='Цвет', max_length=255) icon = models.ImageField(verbose_name='Большая картинка', upload_to='skills', null=True, help_text='65x65') description = models.TextField(verbose_name='Описание', blank=True) def __str__(self): return '%s' % self.title class Meta: verbose_name = u'Навык' verbose_name_plural = u'Навыки' class SkillJ(models.Model): skill = models.ForeignKey(to="Skills", verbose_name='Навык') lesson = models.ForeignKey(to="Vertex", verbose_name='Урок') def __str__(self): return '%s' % self.skill class Meta: verbose_name = 'Размер навыка' verbose_name_plural = 'Размеры навыков' class Achievements(models.Model): course = models.ForeignKey(to="Course") icon = models.ImageField(verbose_name='Отображение достижения', upload_to='diplomas', blank=True, null=True) user = models.ForeignKey(to=settings.AUTH_USER_MODEL) def __str__(self): return 'Студенту %s за курс %s' % (self.user.username, self.course.title) class Meta: verbose_name = 'Достижение' verbose_name_plural = 'Достижения' class DiplomaGen(models.Model): course = models.ForeignKey(to=Course) template = models.URLField(verbose_name="Путь до шаблона") def __str__(self): return 'Шаблон можно найти по адресу: %s, диплом выдаётся за курс %s' % (self.template, self.course.title) class Meta: verbose_name = 'Генератор дипломов' verbose_name_plural = 'Генераторы дипловов' class Diploma(models.Model): icon = models.ImageField(verbose_name='Иконка', upload_to='diplomas') template = models.ForeignKey(to=DiplomaGen, verbose_name='Использовать шаблон', blank=True, null=True) user = models.ForeignKey(to=settings.AUTH_USER_MODEL) def __str__(self): return 'Студенту %s за курс %s' % (self.user.username, self.template.course.title) 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=u'Название', max_length=255) free = models.BooleanField(default=True, verbose_name=u'Привилегии для узла не будут проверяться') description = models.TextField(verbose_name=u'Описание', 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 if self.extraprivilege_set.filter(user=user).exists(): return True try: progress = self.course.progress_set.get(user=user) except ObjectDoesNotExist: return False return progress.is_access(self) # Модели нового 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=u'Материалы для домашней работы', blank=True) is_exam = models.BooleanField(default=False, verbose_name=u'Экзамен или домашка') class Topic(models.Model): """ Модель темы, нужно просто для объединения тасков и уроков. У некоторых тем есть иконка. Возможно поле icon перекачует в Vertex, а данная модель отвалится за ненадобностью """ icon = models.ImageField(verbose_name=u'Иконка темы', null=True, blank=True) class CourseMap(models.Model): """ Так как курс евляется связным графом мы можем отобразить его бесконечным количеством способов, а нам нужен один самый красивый, мы должный в явном виде указать способ отображения. """ course = models.OneToOneField(to=Course) dependent_elements = models.TextField(default='[]') independent_elements = models.TextField(default='[]') def map_to_list(self) -> list: def helper(root_list): res = [] for i in root_list: if type(i) == type([]): res += helper(i) else: res.append(i) return res return helper(json.loads(self.dependent_elements)) def get_next(self, vertex_id) -> int: res_list = self.map_to_list() if not res_list[-1] == vertex_id: return res_list[res_list.index(vertex_id) + 1] error = "vertex_id " + str(vertex_id) + " last object in list\n" + ",".join([str(v) for v in res_list]) raise ValueError(error) def get_previous(self, vertex_id) -> int: res_list = self.map_to_list() if not res_list[0] == vertex_id: return res_list[res_list.index(vertex_id) - 1] error = "vertex_id " + str(vertex_id) + " first object in list\n" + ",".join([str(v) for v in res_list]) raise ValueError(error) def get_first(self): return self.map_to_list()[0] def get_last(self): return self.map_to_list()[-1]