diff --git a/access/views.py b/access/views.py index b141a2e..b782575 100644 --- a/access/views.py +++ b/access/views.py @@ -144,8 +144,11 @@ class DetailUserView(APIView): acc = request.JSON.get('account', None) if not acc['b_day'] is None: - b_date = datetime.datetime.strptime(acc['b_day'], '%d.%m.%Y') # TODO вынести форматы в настройки - acc['b_day'] = b_date.strftime('%Y-%m-%d') + try: + b_day = datetime.datetime.strptime(acc['b_day'], '%d.%m.%Y') # TODO вынести форматы в настройки + except ValueError: + b_day = datetime.datetime.strptime(acc['b_day'], '%d-%m-%Y') + acc['b_day'] = b_day.strftime('%Y-%m-%d') acc['gender'] = 0 if acc['gender'] == "undefined" else 1 if acc['gender'] == "male" else 2 @@ -253,7 +256,7 @@ class LoginView(APIView): try: auth.login(request, user) except AttributeError: - return Response("Неверный пароль", status=404) + return Response("Неверный пароль", status=403) serialized_user = UserSelfSerializer(user).data serialized_user['is_i'] = True diff --git a/courses/migrations/0004_auto_20180222_1756.py b/courses/migrations/0004_auto_20180222_1756.py new file mode 100644 index 0000000..2abd356 --- /dev/null +++ b/courses/migrations/0004_auto_20180222_1756.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-02-22 17:56 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0003_auto_20180219_1323'), + ] + + operations = [ + migrations.AlterField( + model_name='lesson', + name='sort', + field=models.SmallIntegerField(verbose_name='Поле сортировки'), + ), + migrations.AlterField( + model_name='topic', + name='sort', + field=models.SmallIntegerField(verbose_name='Поле сортировки'), + ), + migrations.AlterUniqueTogether( + name='lesson', + unique_together=set([('sort', 'topic')]), + ), + migrations.AlterUniqueTogether( + name='topic', + unique_together=set([('sort', 'course')]), + ), + ] diff --git a/courses/migrations/0005_auto_20180222_1911.py b/courses/migrations/0005_auto_20180222_1911.py new file mode 100644 index 0000000..5ee4961 --- /dev/null +++ b/courses/migrations/0005_auto_20180222_1911.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-02-22 19:11 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models +import lms.tools + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0004_auto_20180222_1756'), + ] + + operations = [ + migrations.AlterField( + model_name='lesson', + name='material_tokens', + field=django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(editable=False, verbose_name='Токен материала'), blank=True, default=lms.tools.get_empty_list, size=None, verbose_name='Материалы курса'), + ), + ] diff --git a/courses/models.py b/courses/models.py index 3bfd01a..7785b4c 100755 --- a/courses/models.py +++ b/courses/models.py @@ -37,9 +37,10 @@ class Lesson(models.Model): models.UUIDField(verbose_name="Токен материала", editable=False), default=get_empty_list, verbose_name='Материалы курса', + blank=True ) free = models.BooleanField(default=False, verbose_name='Привилегии для узла не будут проверяться') - sort = models.SmallIntegerField(unique=True) + sort = models.SmallIntegerField(verbose_name='Поле сортировки') is_hm = models.BooleanField(default=False) #TODO костыли old_id = models.IntegerField(null=True, blank=True) @@ -51,6 +52,7 @@ class Lesson(models.Model): verbose_name = "Урок" verbose_name_plural = "Уроки" ordering = ('sort', ) + unique_together = ('sort', 'topic') class Topic(models.Model): @@ -58,30 +60,42 @@ class Topic(models.Model): title = models.CharField(verbose_name='Название', max_length=255) description = models.TextField(verbose_name='Описание', blank=True, null=True) icon = models.ImageField(verbose_name='Иконка темы', null=True, blank=True) - sort = models.SmallIntegerField(unique=True) + sort = models.SmallIntegerField(verbose_name='Поле сортировки') + + def __str__(self): + return self.title class Meta: verbose_name = "Тема" verbose_name_plural = "Темы" ordering = ('sort',) + unique_together = ('sort', 'course') class CourseManager(models.Manager): - def update_or_create_course(self, image=None, big_image=None, id=0, - big_mobile_image=None, slug=None, + def update_or_create_course(self, image=None, big_image=None, statistic=None, + big_mobile_image=None, slug=None, teachers=None, level=None, direction=None, **kwargs): slug = slug if slug else slugify(unidecode.unidecode(kwargs['title'])) + kwargs['teacher_tokens'] = teachers + if image: - kwargs['image'] = decode_base64(image, 'course/image%s.png' % slug) + path = 'course/image%s.png' % slug + decode_base64(image, path) + kwargs['image'] = path if big_image: - kwargs['big_image'] = decode_base64(big_image, 'course/big_image%s.png' % slug) + path = 'course/big_image%s.png' % slug + decode_base64(image, path) + kwargs['big_image'] = path if big_mobile_image: - kwargs['big_mobile_image'] = decode_base64(big_mobile_image, 'course/big_mobile_image%s.png' % slug) + path = 'course/big_mobile_image%s.png' % slug + decode_base64(image, path) + kwargs['big_mobile_image'] = path if level: kwargs['level'] = get_real_name(COURSE_LEVEL, level) @@ -90,7 +104,7 @@ class CourseManager(models.Manager): kwargs['direction'] = get_real_name(COURSE_DIRECTION, direction) try: - course = self.get(id=id) + course = self.get(slug=slug) for i in kwargs: if kwargs[i]: setattr(course, i, kwargs[i]) diff --git a/courses/serializers.py b/courses/serializers.py index c8034fa..3309f1e 100644 --- a/courses/serializers.py +++ b/courses/serializers.py @@ -29,6 +29,31 @@ class LessonSerializer(MiniLessonSerializer): exclude = ('id', 'topic', 'key') +class TeacherLessonSerializer(MiniLessonSerializer): + topic_sort = serializers.SerializerMethodField() + topic_title = serializers.SerializerMethodField() + course_title = serializers.SerializerMethodField() + + class Meta: + model = Lesson + fields = ('topic_sort', 'description', 'title', 'course_title', 'token', 'topic_title') + + @staticmethod + def get_topic_sort(self): + for topic_idx, topic in enumerate(self.topic.course.topic_set.all()): + if topic == self.topic: + return topic_idx + 1 + return None + + @staticmethod + def get_topic_title(self): + return self.topic.title + + @staticmethod + def get_course_title(self): + return self.topic.course.title + + class CourseInitSerializer(serializers.ModelSerializer): class Meta: diff --git a/courses/urls.py b/courses/urls.py index d1eb8b7..d30f49b 100644 --- a/courses/urls.py +++ b/courses/urls.py @@ -4,6 +4,8 @@ from courses import views as views urlpatterns = [ url(r'vertex/(?P.+)/$', views.LessonDetail.as_view()), + url(r'lesson/teacher/(?P.+)/$', views.LessonInfoView.as_view()), url(r'tree/(?P.+)/$', views.TreeView.as_view()), + url(r'detail/(?P.+)/$', views.CourseDetailView.as_view()), url(r'^$', views.CourseListView.as_view()), ] \ No newline at end of file diff --git a/courses/views.py b/courses/views.py index a6c1624..ea9536f 100644 --- a/courses/views.py +++ b/courses/views.py @@ -2,8 +2,9 @@ from courses.models import Course, Lesson from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.views import APIView +from django.contrib.auth import get_user_model -from courses.serializers import CourseDetailSerializer, CourseTreeSerializer, LessonSerializer +from courses.serializers import CourseDetailSerializer, CourseTreeSerializer, LessonSerializer, TeacherLessonSerializer from progress.models import ProgressLesson @@ -43,6 +44,9 @@ class CourseListView(APIView): location: form ... """ + # TODO: Костыль + teachers_emails = request.JSON.get('teachers', []) + request.JSON['teachers'] = [get_user_model().objects.get(email=i).out_key for i in teachers_emails] course = Course.objects.update_or_create_course(**request.JSON.dict()) return Response(CourseDetailSerializer(course).data, status=self.status_code) @@ -55,6 +59,36 @@ class CourseListView(APIView): return Response(res, self.status_code) +class CourseDetailView(APIView): + renderer_classes = (JSONRenderer,) + status_code = 200 + + @staticmethod + def delete(request, slug): + try: + Course.objects.get(slug=slug).delete() + except Course.DoesNotExist: + return Response("Курса не существует", status=404) + return Response(status=204) + + def get(self, request, slug): + return Response(CourseDetailSerializer(Course.objects.get(slug=slug)).data, self.status_code) + + +class LessonInfoView(APIView): + renderer_classes = (JSONRenderer,) + status_code = 200 + + def get(self, request, token): + try: + lesson = Lesson.objects.get(token=token) + except Lesson.DoesNotExist: + return Response('Урок не найден', status=404) + if request.user.is_authenticated and request.user.out_key in lesson.topic.course.teacher_tokens: + return Response(TeacherLessonSerializer(lesson).data, self.status_code) + return Response("Пользователь не является преподователем по курсу", status=403) + + class LessonDetail(APIView): renderer_classes = (JSONRenderer,) diff --git a/finance/views.py b/finance/views.py index ebd2c37..ba2fa6c 100644 --- a/finance/views.py +++ b/finance/views.py @@ -37,13 +37,16 @@ class BillListView(APIView): or request.user.is_superuser): bill = request.JSON.get('bill') children = request.JSON.get('children', []) + bill_kwarg = dict() if bill: - bill['user'] = get_user_model().objects.get(email=bill['user']) - bill['opener'] = get_user_model().objects.get(email=bill['opener']) - bill.pop('invoices', None) + bill_kwarg['user'] = get_user_model().objects.get(email=bill['user']) + bill_kwarg['opener'] = get_user_model().objects.get(email=bill['opener']) + bill_kwarg['description'] = bill['description'] + bill_kwarg['comment'] = bill['comment'] + bill_kwarg['course_token'] = bill['course_token'] - bill_obj, is_create = Bill.objects.update_or_create(**bill) + bill_obj, is_create = Bill.objects.update_or_create(**bill_kwarg) invoices = bill_obj.invoice_set.all() for i in children: @@ -173,15 +176,16 @@ def get_invoices(request): invoices = invoices.filter(date__gte=date_from) if date_from else invoices response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename="%s"' % file_name + response['Content-Disposition'] = 'attachment; filename="%s.csv"' % file_name writer = csv.writer(response) - writer.writerow(['date', 'student_email', 'full_name', 'course', 'price', 'real_price', 'key']) + writer.writerow(['date', 'time', 'student_email', 'full_name', 'course', 'price', 'real_price', 'key']) for i in invoices.order_by('-date'): course_api = CourseParamsApi(i.bill.course_token) writer.writerow([ - i.date, + i.date.date(), + i.date.time(), i.bill.user.email, i.bill.user.get_full_name(), course_api.get_slug_and_title()['title'], diff --git a/lms/settings.py b/lms/settings.py index f76b57a..6576e6f 100644 --- a/lms/settings.py +++ b/lms/settings.py @@ -68,6 +68,8 @@ DATABASES = { SESSION_ENGINE = 'redis_sessions.session' CELERY_EMAIL_CHUNK_SIZE = 1 +DATA_UPLOAD_MAX_MEMORY_SIZE = 12621440 + CACHES = { 'default': env.cache(), } @@ -187,49 +189,48 @@ RAVEN_CONFIG = { 'dsn': 'http://1a09557dbd144e52af4b14bea569c114:fbb5dfaa39e64f02a1b4cc7ac665d7d7@sentry.skillbox.ru/7' } -# LOGGING = { -# 'version': 1, -# 'disable_existing_loggers': True, -# 'root': { -# 'level': 'WARNING', -# 'handlers': ['sentry'], -# }, -# 'formatters': { -# 'verbose': { -# 'format': '%(levelname)s %(asctime)s %(module)s ' -# '%(process)d %(thread)d %(message)s' -# }, -# }, -# 'handlers': { -# 'sentry': { -# 'level': 'ERROR', # To capture more than ERROR, change to WARNING, INFO, etc. -# 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', -# 'tags': {'custom-tag': 'x'}, -# }, -# 'console': { -# 'level': 'DEBUG', -# 'class': 'logging.StreamHandler', -# 'formatter': 'verbose' -# } -# }, -# 'loggers': { -# 'django.db.backends': { -# 'level': 'ERROR', -# 'handlers': ['console'], -# 'propagate': False, -# }, -# 'raven': { -# 'level': 'DEBUG', -# 'handlers': ['console'], -# 'propagate': False, -# }, -# 'sentry.errors': { -# 'level': 'DEBUG', -# 'handlers': ['console'], -# 'propagate': False, -# }, -# }, -# } +LOGGING = { + 'version': 1, + 'disable_existing_loggers': True, + 'root': { + 'level': 'WARNING', + 'handlers': ['sentry'], + }, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(module)s ' + '%(process)d %(thread)d %(message)s' + }, + }, + 'handlers': { + 'sentry': { + 'level': 'WARNING', # To capture more than ERROR, change to WARNING, INFO, etc. + 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', + 'tags': {'custom-tag': 'x'}, + }, + 'yandex_money': { + 'level': 'DEBUG', + 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', + 'tags': {'custom-tag': 'yandex'}, + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose' + } + }, + 'loggers': { + '': { + 'level': 'WARNING', + 'handlers': ['sentry'], + }, + 'yandex_money': { + 'handlers': ['yandex_money'], + 'level': 'DEBUG', + 'propagate': False + }, + }, +} SWAGGER_SETTINGS = { 'USE_SESSION_AUTH': True, diff --git a/progress/migrations/0007_progresslesson_last_update.py b/progress/migrations/0007_progresslesson_last_update.py new file mode 100644 index 0000000..a055a52 --- /dev/null +++ b/progress/migrations/0007_progresslesson_last_update.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-02-25 18:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('progress', '0006_auto_20180219_1323'), + ] + + operations = [ + migrations.AddField( + model_name='progresslesson', + name='last_update', + field=models.DateTimeField(auto_now=True, verbose_name='Дата последнего изменения'), + ), + ] diff --git a/progress/models.py b/progress/models.py index db2ecec..12a4274 100644 --- a/progress/models.py +++ b/progress/models.py @@ -57,6 +57,7 @@ class ProgressLesson(models.Model): finish_date = models.DateTimeField(verbose_name='Дата зачтения задания', blank=True, null=True) start_date = models.DateTimeField(verbose_name='Дата начала прохождения задания', auto_now_add=True) status = models.CharField(choices=STATUSES, default=STATUSES.start, max_length=20) + last_update = models.DateTimeField(verbose_name='Дата последнего изменения', auto_now=True) comment_tokens = ArrayField(models.UUIDField(verbose_name="Токен комента", editable=False), default=get_empty_list) def __str__(self): diff --git a/progress/serializers.py b/progress/serializers.py index 5467b6d..e156bd3 100644 --- a/progress/serializers.py +++ b/progress/serializers.py @@ -39,7 +39,7 @@ class ProgressLessonSerializer(serializers.ModelSerializer): class Meta: model = ProgressLesson - exclude = ('id', 'progress', 'checker') + exclude = ('progress', 'checker') @staticmethod def get_teacher(self): diff --git a/progress/views.py b/progress/views.py index f161389..f744103 100644 --- a/progress/views.py +++ b/progress/views.py @@ -23,6 +23,8 @@ class StudentWorkView(APIView): @staticmethod def get(request, teacher_token): client_status = request.GET.get('status', 'in_progress') + client_max_body = 50 + last_id = request.GET.get('last_id', 0) server_status = Q(status='fail') if \ client_status == 'not_done' else Q(status='wait') if client_status == 'in_progress' else Q(status='done') if request.user.is_authenticated() and request.user.groups.filter(name__in=['teachers', 'admin']).exists(): @@ -31,7 +33,8 @@ class StudentWorkView(APIView): ~Q(progress__user__out_key=teacher_token), server_status, checker__out_key=teacher_token, - ) + id__gt=last_id + )[:client_max_body] return Response([ProgressLessonSerializer(i).data for i in progress_lessons], status=200) except ValidationError: return Response("Bad request", status=400) diff --git a/storage/models.py b/storage/models.py index 658b88a..9946f71 100755 --- a/storage/models.py +++ b/storage/models.py @@ -10,9 +10,10 @@ class FileManager(models.Manager): def upload_as_base64(self, file_base64): if "data:" in file_base64: - my_str = file_base64[file_base64.index("base64,") + 7:] - ext = my_str.split('/')[-1] - file = self.create(original=ContentFile(base64.b64decode(my_str), name='time.' + ext)) + my_str = file_base64[file_base64.index(";base64,")+8:] + content_type = file_base64[:file_base64.index(";base64,")].split('/')[1] + file_source = ContentFile(base64.b64decode(my_str), name='time.' + content_type) + file = self.create(original=file_source) return file raise ValueError()