Develop See merge request skillbox/go.skillbox.ru!56remotes/origin/pm_task_31703
commit
c6430ebc4f
24 changed files with 449 additions and 73 deletions
@ -1,27 +1,11 @@ |
||||
#**SkillBox LMS** |
||||
|
||||
Подробные шаги для проверки работоспособности CI |
||||
Нужно выполнить миграции и запустить скрипт update_db.py без параметров в модуле courses (скрипт на всякий случай пока не удалять) |
||||
|
||||
1) Проверяем доступ к репозиторию кода https://gitlab.com/skillbox/go.skillbox.ru |
||||
2) Клонируем проект |
||||
3) Вносим изменения в код |
||||
4) Комитим изменения в новый branch (ОБЯЗАТЕЛЬНО в отдельный Branch) |
||||
4.1) Пушим изменения |
||||
5) Ждем выполнения создания тестового сайта https://gitlab.com/skillbox/go.skillbox.ru/pipelines |
||||
6) Сайт будет доступен по адресу <branch_name>.lms.test.spicycms.com |
||||
Фактически в структуре данных изменилось только то, что появилась новая табличка NormalMap. Скрипт собирает все treeview курсов и кладёт в эту табличку |
||||
|
||||
**Правила для разработчика** |
||||
Появилась новая зависимость django_rest_framework. |
||||
|
||||
* Разрабока ведется в отдельном брэнче |
||||
* После комита изменений можно проверить свою работу по следующему адресу: .lms.test.spicycms.com |
||||
* После проверки тестового сайта по имени брэнча, брэнч помечается к мерж реквесту |
||||
* Ответсвенный инженер производит код ревью и запускает/отклоняет мерж |
||||
* Если сайт запустился на демо сайте, код в ручом режиме можно обновить на продакшен сервере |
||||
Изменения никак не трогают старый функционал. |
||||
|
||||
**Замечания** |
||||
|
||||
* Мерж реквест лучше производить в ветку develop(чтобы исключить случайного обновления кода на демо сайте) |
||||
* Внимание! Разработчик обязан проверить работоспособность тестового сайта по имени созданного брэнча .lms.test.spicycms.com |
||||
* Если необходимы тестовые данные разработчик их подготавливает дополнительно |
||||
* Если нет возможности добавить тестовые данные в авто режиме, разработчик добавляет инструкцию в README файл по запуску необходимых команд |
||||
(Под тестовыми данными подразумеваются фикстуры) |
||||
Имеет смысл пробежаться по middl |
||||
|
||||
@ -0,0 +1,7 @@ |
||||
from django.conf.urls import url |
||||
from access import new_view as views |
||||
|
||||
urlpatterns = [ |
||||
url(r'teachers/$', views.TeacherListView.as_view()), |
||||
url(r'check/$', views.CheckUserView.as_view()), |
||||
] |
||||
@ -0,0 +1,48 @@ |
||||
from django.contrib.auth import get_user_model |
||||
from django.contrib import auth |
||||
from rest_framework.views import APIView |
||||
from rest_framework.renderers import JSONRenderer |
||||
from rest_framework.response import Response |
||||
|
||||
from access.serializers import UserInitSerializer |
||||
|
||||
|
||||
class TeacherListView(APIView): |
||||
renderer_classes = (JSONRenderer,) |
||||
status_code = 200 |
||||
|
||||
def get(self, request): |
||||
return Response([teacher.full_name() for teacher in get_user_model().objects.filter( |
||||
in_role='T', |
||||
is_active=True, |
||||
reg_status=4, |
||||
)], self.status_code) |
||||
|
||||
|
||||
class CheckUserView(APIView): |
||||
renderer_classes = (JSONRenderer,) |
||||
status_code = 200 |
||||
|
||||
def get(self, request): |
||||
if request.user.is_authenticated() and (request.user.in_role in ['M', 'S', 'S2', 'A'] or request.user.is_admin): |
||||
return Response(True, status=self.status_code) |
||||
return Response(False, status=self.status_code) |
||||
|
||||
|
||||
class LoginView(APIView): |
||||
renderer_classes = (JSONRenderer,) |
||||
status_code = 200 |
||||
|
||||
def get(self, request): |
||||
if not request.user.is_authenticated(): |
||||
user = auth.authenticate(email=request.JSON.get('email'), password=request.JSON.get('password')) |
||||
return Response(UserInitSerializer(user).data, status=self.status_code) |
||||
return Response(status=403) |
||||
|
||||
|
||||
class LogoutView(APIView): |
||||
renderer_classes = (JSONRenderer,) |
||||
status_code = 204 |
||||
|
||||
def get(self, request): |
||||
return Response(False, status=self.status_code) |
||||
@ -0,0 +1,9 @@ |
||||
from django.contrib.auth import get_user_model |
||||
from rest_framework import serializers |
||||
|
||||
|
||||
class UserInitSerializer(serializers.ModelSerializer): |
||||
|
||||
class Meta: |
||||
model = get_user_model() |
||||
fields = '__all__' |
||||
@ -0,0 +1,6 @@ |
||||
from django.conf.urls import url, include |
||||
|
||||
urlpatterns = [ |
||||
url(r'courses/', include('courses.new_urls')), |
||||
url(r'users/', include('access.new_urls')), |
||||
] |
||||
@ -0,0 +1,24 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Generated by Django 1.9.3 on 2017-09-04 13:56 |
||||
from __future__ import unicode_literals |
||||
|
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('courses', '0047_auto_20170904_1355'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.CreateModel( |
||||
name='NormalMap', |
||||
fields=[ |
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('json_tree', models.TextField(default='')), |
||||
('course', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='courses.Course')), |
||||
], |
||||
), |
||||
] |
||||
@ -0,0 +1,25 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Generated by Django 1.9.3 on 2017-09-07 17:53 |
||||
from __future__ import unicode_literals |
||||
|
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('courses', '0048_normalmap'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AddField( |
||||
model_name='normalmap', |
||||
name='independent_elements', |
||||
field=models.TextField(default=''), |
||||
), |
||||
migrations.AlterField( |
||||
model_name='course', |
||||
name='level', |
||||
field=models.CharField(choices=[('B', 'Базовый'), ('A', 'Продвинутый'), ('E', 'Экспертный'), ('B+A', 'Базовый + Продвинутый')], default='B', max_length=3, verbose_name='Уровень'), |
||||
), |
||||
] |
||||
@ -0,0 +1,11 @@ |
||||
from django.conf.urls import url |
||||
from courses import new_view as views |
||||
|
||||
urlpatterns = [ |
||||
url(r'theme/detail/([0-9]{1,99})/$', views.ThemeDetailView.as_view()), |
||||
url(r'lesson/detail/([0-9]{1,99})/$', views.LessonDetailView.as_view()), |
||||
url(r'detail/([0-9]{1,99})/$', views.CourseDetailView.as_view()), |
||||
url(r'tree/([0-9]{1,99})/$', views.TreeView.as_view()), |
||||
url(r'directions/$', views.DirectionListView.as_view()), |
||||
url(r'^$', views.CourseListView.as_view()), |
||||
] |
||||
@ -0,0 +1,72 @@ |
||||
from rest_framework.views import APIView |
||||
from rest_framework.renderers import JSONRenderer |
||||
from rest_framework.response import Response |
||||
from courses.models import Course, MaterialDirection, CourseTheme, Lesson |
||||
from courses.serializers import CourseTreeSerializer, CourseDetailSerializer, CourseListSerializer,\ |
||||
ThemeSerializer, LessonSerializer |
||||
from finance.models import Bill |
||||
|
||||
|
||||
class DirectionListView(APIView): |
||||
renderer_classes = (JSONRenderer,) |
||||
status_code = 200 |
||||
|
||||
def get(self, request): |
||||
return Response([direction.title for direction in MaterialDirection.objects.all()], self.status_code) |
||||
|
||||
|
||||
class TreeView(APIView): |
||||
renderer_classes = (JSONRenderer,) |
||||
status_code = 200 |
||||
|
||||
@staticmethod |
||||
def post(request): |
||||
print(request) |
||||
return Response(status=204) |
||||
|
||||
def get(self, request, id): |
||||
return Response(CourseTreeSerializer(Course.objects.get(id=id)).data, self.status_code) |
||||
|
||||
|
||||
class ThemeDetailView(APIView): |
||||
renderer_classes = (JSONRenderer,) |
||||
status_code = 200 |
||||
|
||||
def get(self, request, id): |
||||
return Response(ThemeSerializer(CourseTheme.objects.get(id=id)).data, self.status_code) |
||||
|
||||
|
||||
class LessonDetailView(APIView): |
||||
renderer_classes = (JSONRenderer,) |
||||
status_code = 200 |
||||
|
||||
def get(self, request, id): |
||||
return Response(LessonSerializer(Lesson.objects.get(id=id)).data, self.status_code) |
||||
|
||||
|
||||
class CourseDetailView(APIView): |
||||
renderer_classes = (JSONRenderer,) |
||||
status_code = 200 |
||||
|
||||
def get(self, request, id): |
||||
return Response(CourseDetailSerializer(Course.objects.get(id=id)).data, self.status_code) |
||||
|
||||
|
||||
class CourseListView(APIView): |
||||
renderer_classes = (JSONRenderer,) |
||||
status_code = 200 |
||||
|
||||
def get(self, request): |
||||
if not request.GET.get('staff', 'true') == 'false': |
||||
return Response([CourseListSerializer(i).data for i in Course.objects.all()], self.status_code) |
||||
|
||||
res = [] |
||||
for course in Course.objects.all(): |
||||
if course.public: |
||||
course_serialize = CourseListSerializer(course).data |
||||
course_serialize['is_mine'] = False |
||||
if Bill.objects.filter(service__course=course, user=request.user, status='F').exists(): |
||||
course_serialize['is_mine'] = True |
||||
res.append(course_serialize) |
||||
|
||||
return Response(res, self.status_code) |
||||
@ -0,0 +1,159 @@ |
||||
from rest_framework import serializers |
||||
import json |
||||
# from django.contrib.auth import get_user_model |
||||
# from django.core.exceptions import ObjectDoesNotExist |
||||
|
||||
from courses.models import Course, CourseTheme, Lesson, Homework, Exam |
||||
|
||||
|
||||
class LessonSerializer(serializers.ModelSerializer): |
||||
on_comment = serializers.SerializerMethodField() |
||||
|
||||
class Meta: |
||||
model = Lesson |
||||
fields = ( |
||||
'id', 'title', 'on_comment', 'materials', |
||||
'free', 'video', 'video_id', |
||||
) |
||||
|
||||
@staticmethod |
||||
def get_on_comment(self): |
||||
return self.on_comment == 'N' or self.on_comment == 'T' and self.theme.on_comment |
||||
|
||||
|
||||
class ThemeSerializer(serializers.ModelSerializer): |
||||
|
||||
class Meta: |
||||
model = CourseTheme |
||||
exclude = ('price_type', '_type', 'sort', 'on_comment') |
||||
|
||||
|
||||
class CourseListSerializer(serializers.ModelSerializer): |
||||
length = serializers.SerializerMethodField() |
||||
level = serializers.SerializerMethodField() |
||||
direction = serializers.SerializerMethodField() |
||||
|
||||
class Meta: |
||||
model = Course |
||||
fields = ['id', 'title', 'length', |
||||
'level', 'direction', 'image',] |
||||
|
||||
@staticmethod |
||||
def get_length(self): |
||||
summary = 0 |
||||
for theme_slim in json.loads(self.normalmap.json_tree): |
||||
for simple_object in theme_slim['body']: |
||||
if simple_object.split('_')[1] == 'L': |
||||
summary += 1 |
||||
return summary |
||||
|
||||
@staticmethod |
||||
def get_level(self): |
||||
return self.get_level_display() |
||||
|
||||
@staticmethod |
||||
def get_direction(self): |
||||
return self.direction.title |
||||
|
||||
|
||||
class CourseTreeSerializer(serializers.ModelSerializer): |
||||
children = serializers.SerializerMethodField() |
||||
|
||||
class Meta: |
||||
model = Course |
||||
fields = ['id', 'title', 'children'] |
||||
|
||||
@staticmethod |
||||
def get_children(self): |
||||
theme_list = json.loads(self.normalmap.json_tree) |
||||
map = [] |
||||
for theme_slim in theme_list: |
||||
theme = CourseTheme.objects.get(id=theme_slim['id']) |
||||
theme_obj = { |
||||
'id': theme.id, |
||||
'title': theme.title, |
||||
'lessons': [], |
||||
'tasks': [], |
||||
} |
||||
for simple_object in theme_slim['body']: |
||||
val = simple_object.split('_')[0] |
||||
if simple_object.split('_')[1] == 'L': |
||||
lesson = Lesson.objects.get(id=val) |
||||
lesson_obj = {'id': lesson.id, 'title': lesson.title} |
||||
theme_obj['lessons'].append(lesson_obj) |
||||
|
||||
if simple_object.split('_')[1] == 'H': |
||||
task = Homework.objects.get(id=val) |
||||
task_obj = { |
||||
'id': task.id, |
||||
'is_exam': False, |
||||
} |
||||
theme_obj['tasks'].append(task_obj) |
||||
|
||||
if simple_object.split('_')[1] == 'E': |
||||
task = Exam.objects.get(id=val) |
||||
task_obj = { |
||||
'id': task.id, |
||||
'is_exam': True, |
||||
} |
||||
theme_obj['tasks'].append(task_obj) |
||||
|
||||
map.append(theme_obj) |
||||
|
||||
return map |
||||
|
||||
|
||||
class CourseDetailSerializer(serializers.ModelSerializer): |
||||
level = serializers.SerializerMethodField() |
||||
direction = serializers.SerializerMethodField() |
||||
teachers = serializers.SerializerMethodField() |
||||
|
||||
class Meta: |
||||
model = Course |
||||
exclude = ( |
||||
'slug', 'mentors', 'page', 'sort', |
||||
'preview', 'use_fail', 'basic_len', |
||||
'addition_len', 'min_price', 'buy_icon', |
||||
'must_build', 'keywords', 'recommend', |
||||
) |
||||
|
||||
@staticmethod |
||||
def get_level(self): |
||||
return self.get_level_display() |
||||
|
||||
@staticmethod |
||||
def get_direction(self): |
||||
return self.direction.title |
||||
|
||||
@staticmethod |
||||
def get_teachers(self): |
||||
return [teacher.full_name() for teacher in self.teachers.all()] |
||||
|
||||
|
||||
# class UserSerializer(serializers.ModelSerializer): |
||||
# statistics = serializers.SerializerMethodField('get_statistic') |
||||
# games = serializers.SerializerMethodField('get_my_games') |
||||
# is_anonymous = serializers.BooleanField() |
||||
# |
||||
# @staticmethod |
||||
# def get_my_games(self): |
||||
# res = {} |
||||
# try: |
||||
# res['active'] = GameSerializer(self.game_set.get(state__lte=1)).data |
||||
# except ObjectDoesNotExist: |
||||
# res['active'] = {} |
||||
# |
||||
# res['archive'] = [GameSerializer(i).data for i in self.game_set.all().filter(state=2)] |
||||
# return res |
||||
# |
||||
# @staticmethod |
||||
# def get_statistic(self): |
||||
# try: |
||||
# statistics = StatisticSerializer(Statistic.objects.get(user=self)).data |
||||
# except ObjectDoesNotExist: |
||||
# statistics = {} |
||||
# return statistics |
||||
# |
||||
# class Meta: |
||||
# model = get_user_model() |
||||
# fields = ['id', 'username', 'email', 'is_active', 'statistics', 'games', 'is_anonymous'] |
||||
@ -0,0 +1,22 @@ |
||||
import os, sys |
||||
import django, json |
||||
|
||||
sys.path.append("../") |
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lms.settings") |
||||
django.setup() |
||||
|
||||
from courses.models import Course, NormalMap, CourseTheme, Lesson, Homework, Exam |
||||
|
||||
if __name__ == '__main__': |
||||
for course in Course.objects.all(): |
||||
tree = [] |
||||
for theme in CourseTheme.objects.filter(course=course).order_by('sort'): |
||||
tree.append({'id': theme.id, 'body': |
||||
[str(i.id) + "_L" for i in Lesson.objects.filter(theme=theme).order_by('sort')] + |
||||
[str(i.id) + "_H" for i in Homework.objects.filter(theme=theme).order_by('sort')] + |
||||
[str(i.id) + "_E" for i in Exam.objects.filter(theme=theme).order_by('sort')] |
||||
}) |
||||
|
||||
obj, _is_create = NormalMap.objects.get_or_create(course=course) |
||||
obj.json_tree = json.dumps(tree) |
||||
obj.save() |
||||
@ -1,22 +0,0 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Generated by Django 1.9.3 on 2017-08-10 09:04 |
||||
from __future__ import unicode_literals |
||||
|
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('courses', '0045_auto_20170810_0904'), |
||||
('finance', '0070_auto_20170424_1638'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AddField( |
||||
model_name='bill', |
||||
name='flow', |
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='courses.Flow', verbose_name='Поток'), |
||||
), |
||||
] |
||||
Loading…
Reference in new issue