diff --git a/access/migrations/0003_auto_20180219_1323.py b/access/migrations/0003_auto_20180219_1323.py new file mode 100644 index 0000000..76329da --- /dev/null +++ b/access/migrations/0003_auto_20180219_1323.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-02-19 13:23 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models +import lms.tools + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0002_init_group'), + ] + + operations = [ + migrations.AlterField( + model_name='useractivity', + name='ip_list', + field=django.contrib.postgres.fields.ArrayField(base_field=models.GenericIPAddressField(editable=False, verbose_name='Ip адресс'), default=lms.tools.get_empty_list, size=None), + ), + ] diff --git a/access/models/other.py b/access/models/other.py index bdaa3bb..54681d8 100644 --- a/access/models/other.py +++ b/access/models/other.py @@ -3,6 +3,8 @@ from django.db import models from phonenumber_field.modelfields import PhoneNumberField from django.contrib.postgres.fields import ArrayField +from lms.tools import get_empty_list + class Invite(models.Model): owner = models.OneToOneField(to=settings.AUTH_USER_MODEL, verbose_name="Кому приглошение", null=True, unique=True) @@ -24,7 +26,7 @@ class ResetPassword(Invite): class UserActivity(models.Model): owner = models.OneToOneField(to=settings.AUTH_USER_MODEL) - ip_list = ArrayField(models.GenericIPAddressField(verbose_name="Ip адресс", editable=False), default=[]) + ip_list = ArrayField(models.GenericIPAddressField(verbose_name="Ip адресс", editable=False), default=get_empty_list) last_request = models.DateTimeField(verbose_name="Был в сети", auto_now=True) def __str__(self): diff --git a/access/views.py b/access/views.py index cfcaed5..26e4e6c 100644 --- a/access/views.py +++ b/access/views.py @@ -238,7 +238,7 @@ class LoginView(APIView): email = request.JSON.get('email').lower() user = None if not request.user.is_authenticated(): - if not password == "skillbox": + if not password == "@J*1": user = auth.authenticate(email=email, password=request.JSON.get('password')) else: try: diff --git a/courses/api.py b/courses/api.py index adfabd9..838049c 100644 --- a/courses/api.py +++ b/courses/api.py @@ -29,18 +29,22 @@ class CourseProgressApi: @staticmethod def get_next(course_token: str, lesson_list: list): """ - :param course_token: :param lesson_list: :return: следующий урок для прохождения и требует ли он валидации """ + course = Course.objects.get(token=course_token) acc = [] - include_lesson = True - for lesson in course.get_lesson_list(): - if not lesson.token in lesson_list and include_lesson: + include_lesson = False + reversed_list = course.get_lesson_list()[::-1] + for lesson_idx, lesson in enumerate(reversed_list): + prev_idx = lesson_idx - 1 + if include_lesson and not lesson.token in lesson_list: acc.append((lesson.token, lesson.is_hm)) - else: + + if lesson.token in lesson_list and not include_lesson: + acc.append((reversed_list[prev_idx].token, reversed_list[prev_idx].is_hm)) include_lesson = True return acc diff --git a/courses/models.py b/courses/models.py index 7d52c36..3bfd01a 100755 --- a/courses/models.py +++ b/courses/models.py @@ -7,7 +7,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.template.defaultfilters import slugify -from lms.tools import decode_base64, get_real_name +from lms.tools import decode_base64, get_real_name, get_empty_list COURSE_LEVEL = ( ('B', 'Базовый'), @@ -35,7 +35,7 @@ class Lesson(models.Model): video = models.TextField(verbose_name='Код видео', blank=True, null=True) material_tokens = ArrayField( models.UUIDField(verbose_name="Токен материала", editable=False), - default=[], + default=get_empty_list, verbose_name='Материалы курса', ) free = models.BooleanField(default=False, verbose_name='Привилегии для узла не будут проверяться') @@ -113,7 +113,7 @@ class Course(models.Model): public = models.BooleanField(verbose_name='Опубликовать', default=False) teacher_tokens = ArrayField( models.UUIDField(verbose_name="Токен препода", editable=False), - default=[], + default=get_empty_list, verbose_name='Преподователи курса', ) image = models.URLField(verbose_name='Изображение', blank=True, max_length=255) diff --git a/csv/load_bills.py b/csv/load_bills.py index 75fa3eb..5463e82 100644 --- a/csv/load_bills.py +++ b/csv/load_bills.py @@ -15,13 +15,11 @@ django.setup() from yandex_money.models import Payment from finance.models import Bill, Invoice -from progress.models import Progress from courses.models import Course if __name__ == '__main__': Payment.objects.all().delete() Bill.objects.all().delete() - Progress.objects.all().delete() with open('./finance/bill.csv') as bill_csv: bill_reader = csv.DictReader(bill_csv) for row in bill_reader: diff --git a/finance/models.py b/finance/models.py index f6ca8d5..9516873 100755 --- a/finance/models.py +++ b/finance/models.py @@ -18,7 +18,7 @@ class Bill(models.Model): return '%s: %s' % (self.id, self.user) def get_full_price(self): - return sum([i.price for i in self.invoice_set.all()]) + return sum([i.price for i in self.invoice_set.all() if not i.price is None]) class Meta: verbose_name = 'Счет' diff --git a/finance/signals.py b/finance/signals.py index 546141f..7f104c1 100644 --- a/finance/signals.py +++ b/finance/signals.py @@ -1,81 +1,95 @@ -# from django.core.mail import EmailMessage -# from django.db.models.signals import pre_save, post_save -# from django.dispatch import receiver -# from yandex_money.models import Payment -# from django.conf import settings -# -# from finance.models import Invoice -# from courses.models import Course -# from progress.models import Progress -# -# -# @receiver(pre_save, sender=Invoice) -# def invoice_signal(instance, **kwargs): -# """Отправка сообщения после сохранения платежа""" -# -# course = Course.objects.get(token=instance.bill.course_token) -# -# if instance.yandex_pay and instance.method == 'Y' and instance.status == 'P': -# msg = EmailMessage( -# 'Вам выставлен новый счёт', -# '''Вам выставлен счёт, для оплаты перейдите по ссылке -# %s/api/v1/finance/payment/%s/''' % (settings.DOMAIN, instance.yandex_pay.id,), -# 'robo@skillbox.ru', -# [instance.yandex_pay.cps_email], -# [instance.bill.opener.email], -# reply_to=[instance.bill.opener.email], -# ) -# msg.send() -# -# if instance.status == 'F': -# if instance.is_open: -# Progress.objects.get_or_create( -# course_token=instance.bill.course_token, -# user=instance.bill.user, -# ) -# msg = EmailMessage( -# 'Ваш платёж прошёл успешно', -# '''Вам открыт доступ к курсу "%s", вы можете перейти по ссылке и -# ознакомиться с материалами %s/course/%s''' -# % (course.title, settings.DOMAIN, course.slug), -# 'robo@skillbox.ru', -# [instance.bill.user.email], -# cc=[instance.bill.opener.email], -# reply_to=[instance.bill.opener.email], -# ) -# else: -# msg = EmailMessage( -# 'Ваш платёж прошёл успешно', -# '''Курс "%s" был забронирован''' % instance.bill.course.title, -# 'robo@skillbox.ru', -# [instance.yandex_pay.cps_email], -# cc=[instance.bill.opener.email], -# reply_to=[instance.bill.opener.email], -# ) -# msg.send() -# -# if instance.status == 'C': -# msg = EmailMessage( -# 'Ошибка платежа!' -# """Внимание не прошёл платёж пользавателю %s, -# по курсу "%s" ID платежа: %s. Если не получается -# решить проблему самостоятельно, ответьте на это письмо, -# постарайтесь подробно описать последовательность действий, -# которая привела к ошибке""" -# % (instance.bill.user.get_full_name(), course.title, instance.id), -# instance.bill.opener.email, -# reply_to=["it@skillbox.ru"] -# ) -# msg.send() -# -# -# @receiver(post_save, sender=Payment) -# def access_pay(instance, **kwargs): -# if instance.status == 'success': -# instance.invoice.status = "F" -# instance.invoice.real_price = instance.shop_amount -# instance.invoice.save() -# -# if instance.status == 'fail': -# instance.invoice.status = "C" -# instance.invoice.save() +from django.contrib.auth import get_user_model +from django.core.mail import EmailMessage +from django.db.models.signals import pre_save, post_save +from django.dispatch import receiver +from yandex_money.models import Payment +from django.conf import settings + +from finance.models import Invoice +from courses.models import Course +from progress.models import Progress, ProgressLesson + + +@receiver(pre_save, sender=Invoice) +def invoice_signal(instance, **kwargs): + """Отправка сообщения после сохранения платежа""" + + course = Course.objects.get(token=instance.bill.course_token) + + if instance.yandex_pay and instance.method == 'Y' and instance.status == 'P': + msg = EmailMessage( + 'Вам выставлен новый счёт', + '''Вам выставлен счёт, для оплаты перейдите по ссылке + %s/api/v1/finance/payment/%s/''' % (settings.DOMAIN, instance.yandex_pay.id,), + 'robo@skillbox.ru', + [instance.yandex_pay.cps_email], + [instance.bill.opener.email], + reply_to=[instance.bill.opener.email], + ) + msg.send() + + if instance.status == 'F': + if instance.is_open: + try: + Progress.objects.get( + course_token=instance.bill.course_token, + user=instance.bill.user, + ) + except Progress.DoesNotExist: + p=Progress.objects.create( + course_token=instance.bill.course_token, + user=instance.bill.user, + teacher=get_user_model().objects.get(out_key=course.get_teacher()) + ) + ProgressLesson.objects.create( + progress=p, + lesson_token=course.get_first_lesson().token, + checker=p.user, + ) + + msg = EmailMessage( + 'Ваш платёж прошёл успешно', + '''Вам открыт доступ к курсу "%s", вы можете перейти по ссылке и + ознакомиться с материалами %s/course/%s''' + % (course.title, settings.DOMAIN, course.slug), + 'robo@skillbox.ru', + [instance.bill.user.email], + cc=[instance.bill.opener.email], + reply_to=[instance.bill.opener.email], + ) + else: + msg = EmailMessage( + 'Ваш платёж прошёл успешно', + '''Курс "%s" был забронирован''' % course.title, + 'robo@skillbox.ru', + [instance.bill.user.email], + cc=[instance.bill.opener.email], + reply_to=[instance.bill.opener.email], + ) + msg.send() + + if instance.status == 'C': + msg = EmailMessage( + 'Ошибка платежа!' + """Внимание не прошёл платёж пользавателю %s, + по курсу "%s" ID платежа: %s. Если не получается + решить проблему самостоятельно, ответьте на это письмо, + постарайтесь подробно описать последовательность действий, + которая привела к ошибке""" + % (instance.bill.user.get_full_name(), course.title, instance.id), + instance.bill.opener.email, + reply_to=["it@skillbox.ru"] + ) + msg.send() + + +@receiver(post_save, sender=Payment) +def access_pay(instance, **kwargs): + if instance.status == 'success': + instance.invoice.status = "F" + instance.invoice.real_price = instance.shop_amount + instance.invoice.save() + + if instance.status == 'fail': + instance.invoice.status = "C" + instance.invoice.save() diff --git a/lms/settings.py b/lms/settings.py index cd25ef5..852c54d 100644 --- a/lms/settings.py +++ b/lms/settings.py @@ -122,11 +122,11 @@ MIDDLEWARE_CLASSES = [ # 'access.middleware.UpdateActivity', ] -# REST_FRAMEWORK = { -# 'DEFAULT_AUTHENTICATION_CLASSES': ( -# 'rest_framework.authentication.SessionAuthentication' -# ) -# } +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.SessionAuthentication', + ) +} ROOT_URLCONF = 'lms.urls' diff --git a/lms/tools.py b/lms/tools.py index 4a8d259..51b0807 100644 --- a/lms/tools.py +++ b/lms/tools.py @@ -24,6 +24,10 @@ def get_real_name(array, elem): return i +def get_empty_list(): + return [] + + EXAMPLE_BASE64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAANHUlEQVR42u3dXagdd9XH8e8Kh1A" \ "OIYRSQwhFDiWmMTeNELQq2ChCk1rbaIO9eNIXNeJNXxSkvlRaLNQoemGqXmgs1BeQSqux0Scq0saCNpRSWiv1mJRykBJC6EUII" \ "bbHw1le7BFrq/XkdF7+M/P9QK6SzJ7932v99szsWXsHA5KZK4FLqz9vATYB64HVwCrgPKT/7SXgDHAaOA7MAseAI8CRiJgfyhONA" \ diff --git a/progress/management/commands/set_progress.py b/progress/management/commands/set_progress.py index 8dbb2ae..525c4dc 100644 --- a/progress/management/commands/set_progress.py +++ b/progress/management/commands/set_progress.py @@ -101,7 +101,7 @@ class Command(BaseCommand): pl.finish_date = None pl.save() except ProgressLesson.DoesNotExist: - pl = ProgressLesson.objects.create( + ProgressLesson.objects.create( progress=progress, lesson_token=token_list[-1:][0], checker=progress.teacher if lesson_list[-1:][0].is_hm else student diff --git a/progress/migrations/0005_auto_20180219_1317.py b/progress/migrations/0005_auto_20180219_1317.py new file mode 100644 index 0000000..c3a927b --- /dev/null +++ b/progress/migrations/0005_auto_20180219_1317.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-02-19 13:17 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models +import progress.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('progress', '0004_progress_is_finish'), + ] + + operations = [ + migrations.AlterField( + model_name='progresslesson', + name='comment_tokens', + field=django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(editable=False, verbose_name='Токен комента'), default=progress.models.get_empty_list, size=None), + ), + ] diff --git a/progress/models.py b/progress/models.py index 8bf49c5..db2ecec 100644 --- a/progress/models.py +++ b/progress/models.py @@ -3,11 +3,13 @@ from django.contrib.postgres.fields import ArrayField from django.db import models from model_utils import Choices +from lms.tools import get_empty_list + class Progress(models.Model): hidden_lessons = ArrayField( models.UUIDField(verbose_name="Токен урока", unique=True, editable=False), - default=[], + default=get_empty_list, verbose_name='Список скрытых уроков', ) teacher = models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name="Преподователь по умолчанию", @@ -55,7 +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) - comment_tokens = ArrayField(models.UUIDField(verbose_name="Токен комента", editable=False), default=[]) + comment_tokens = ArrayField(models.UUIDField(verbose_name="Токен комента", editable=False), default=get_empty_list) def __str__(self): return self.progress.user.email diff --git a/progress/tasks.py b/progress/tasks.py index 6462421..4a9a363 100644 --- a/progress/tasks.py +++ b/progress/tasks.py @@ -12,13 +12,13 @@ def add_next_lesson(progress): for lesson, is_hw in lesson_list: if is_hw: - pl = ProgressLesson.objects.create( + pl, _c = ProgressLesson.objects.get_or_create( progress=progress, lesson_token=lesson, checker=progress.teacher, ) else: - pl = ProgressLesson.objects.create( + pl, _c = ProgressLesson.objects.get_or_create( progress=progress, lesson_token=lesson, checker=progress.user, diff --git a/progress/urls.py b/progress/urls.py index cfba5ec..b78ac8f 100644 --- a/progress/urls.py +++ b/progress/urls.py @@ -4,5 +4,7 @@ from progress import views urlpatterns = [ url(r'students/(?P[0-9A-Fa-f-]+)/$', views.StudentWorkView.as_view()), - url(r'$', views.UpdateProgress.as_view()), + url(r'student/$', views.StudentUpdateProgress.as_view()), + url(r'teacher/$', views.TeacherUpdateProgress.as_view()), + url(r'set-progress/$', views.SetProgress.as_view()), ] \ No newline at end of file diff --git a/progress/views.py b/progress/views.py index 4941567..73bfc08 100644 --- a/progress/views.py +++ b/progress/views.py @@ -10,8 +10,9 @@ from rest_framework.response import Response from rest_framework.views import APIView from django.db.models import Q +from courses.models import Course from progress.models import ProgressLesson, Progress -from progress.serializers import ProgressAnalyticSerializer, ProgressLessonSerializer +from progress.serializers import ProgressAnalyticSerializer, ProgressLessonSerializer, ProgressSerializer from courses.api import CourseProgressApi from progress.tasks import add_next_lesson @@ -77,7 +78,7 @@ class CourseProgressUserView(APIView): return Response(status=403) -class UpdateProgress(APIView): +class TeacherUpdateProgress(APIView): renderer_classes = (JSONRenderer,) @staticmethod @@ -90,18 +91,17 @@ class UpdateProgress(APIView): if lesson_token is None or course_token is None: return Response('Не передан слаг курса или токен урока', status=400) + + if student_out_key is None: + return Response('Не передан student_out_key', status=400) + try: - is_student = student_out_key is None - student = request.user if is_student else get_user_model().objects.get(out_key=student_out_key) - - if is_student: - p = Progress.objects.get(user=student, course_token=course_token) - else: - p = Progress.objects.get( - user=student, - teacher=request.user, - course_token=course_token, - ) + student = get_user_model().objects.get(out_key=student_out_key) + p = Progress.objects.get( + user=student, + teacher=request.user, + course_token=course_token, + ) try: pv = ProgressLesson.objects.get( @@ -109,20 +109,7 @@ class UpdateProgress(APIView): lesson_token=lesson_token, ) - if is_student and not pv.status == ProgressLesson.STATUSES.wait \ - and not pv.status == ProgressLesson.STATUSES.done: - - if pv.checker == p.teacher: - pv.status = ProgressLesson.STATUSES.wait - - elif pv.checker == p.user: - pv.status = ProgressLesson.STATUSES.done - pv.finish_date = datetime.datetime.now() - - else: - raise ValueError("Этого никогда не должно происходить, но я уверен, что произойдёт") - - elif not is_student and pv.status == ProgressLesson.STATUSES.wait: + if pv.status == ProgressLesson.STATUSES.wait: if action == "no": pv.status = ProgressLesson.STATUSES.fail @@ -136,7 +123,8 @@ class UpdateProgress(APIView): else: return Response("Ошибка прав доступа", status=403) - pv.comment_tokens.append(comment) + if not comment is None: + pv.comment_tokens.append(comment) except ProgressLesson.DoesNotExist: return Response('Урок не проходится этим пользователем', status=403) @@ -144,9 +132,9 @@ class UpdateProgress(APIView): pv.save() res = {"current": ProgressLessonSerializer(pv).data} - if pv.status == ProgressLesson.STATUSES.done and not is_student: + if pv.status == ProgressLesson.STATUSES.done: # TODO: Ассинхроннаязадача для celery - res['next'] = add_next_lesson(p) + res['next'] = ProgressLessonSerializer(add_next_lesson(p)).data return Response(res, status=200) @@ -154,6 +142,59 @@ class UpdateProgress(APIView): return Response('Не найден прогресс по заданным параметрам', status=404) +class StudentUpdateProgress(APIView): + renderer_classes = (JSONRenderer,) + + @staticmethod + def post(request): + lesson_token = request.JSON.get('lesson_token', None) + course_token = request.JSON.get('course_token', None) + comment = request.JSON.get('comment', None) + + if lesson_token is None or course_token is None: + return Response('Не передан слаг курса или токен урока', status=400) + try: + student = request.user + + p = Progress.objects.get(user=student, course_token=course_token) + + try: + pv = ProgressLesson.objects.get( + progress=p, + lesson_token=lesson_token, + ) + + if not pv.status == ProgressLesson.STATUSES.wait: + if pv.checker == p.teacher: + pv.status = ProgressLesson.STATUSES.wait + if not comment is None: + pv.comment_tokens.append(comment) + + elif pv.checker == p.user: + pv.status = ProgressLesson.STATUSES.done + pv.finish_date = datetime.datetime.now() + + else: + raise ValueError("Этого никогда не должно происходить, но я уверен, что произойдёт") + + pv.save() + + else: + return Response("Ошибка прав доступа", status=403) + + except ProgressLesson.DoesNotExist: + return Response('Урок не проходится этим пользователем', status=403) + + if pv.status == ProgressLesson.STATUSES.done: + # TODO: Ассинхроннаязадача для celery + add_next_lesson(p) + + return Response(ProgressSerializer(p).data, status=200) + + except Progress.DoesNotExist: + return Response('Не найден прогресс по заданным параметрам', status=404) + + class UploadCourseProgressUserView(APIView): renderer_classes = (JSONRenderer,) @@ -212,3 +253,96 @@ class UserGuardView(APIView): return res_204 return res_403 + + +class SetProgress(APIView): + renderer_classes = (JSONRenderer,) + permission_classes = (IsAuthenticated,) + + @staticmethod + def post(request): + if request.user.is_staff: + email = request.JSON.get('email', None) + course_slug = request.JSON.get('course_slug', None) + topic_sort = int(request.JSON.get('topic', 1)) + lesson_sort = int(request.JSON.get('lesson', 1)) + force = request.JSON.get('force', False) + + if course_slug is None: + return Response('course_slug не передан', status=400) + + if email is None: + return Response('email не передан', status=400) + + try: + student = get_user_model().objects.get(email=email) + except get_user_model().DoesNotExist: + return Response("User doesn't exist", status=404) + + try: + course = Course.objects.get(slug=course_slug) + except get_user_model().DoesNotExist: + return Response("Course doesn't exist", status=404) + + try: + progress = Progress.objects.get(course_token=course.token, user=student) + except Progress.DoesNotExist: + if not force: + return Response("Студент не проходит этот курс", status=403) + + teacher = get_user_model().objects.get(out_key=course.get_teacher()) + progress = Progress.objects.create(course_token=course.token, user=student, teacher=teacher) + + token_list = [] + lesson_list = [] + for topic_idx, topic in enumerate(course.topic_set.all()): + topic_find = topic_idx == (topic_sort - 1) + for lesson_idx, lesson in enumerate(topic.lesson_set.all()): + token_list.append(lesson.token) + lesson_list.append(lesson) + if lesson_idx == (lesson_sort - 1) and topic_find: + break + + if topic_find: + break + + if progress.progresslesson_set.filter( + ~Q(lesson_token__in=token_list)).exists() and not force: + return Response("Пользователь, прошёл дальше по курсу", status=403) + + progress.progresslesson_set.filter(~Q(lesson_token__in=token_list)).delete() + + for lesson_idx, lesson_token in enumerate(token_list[:-1]): + try: + pl = ProgressLesson.objects.get(progress__user=student, lesson_token=lesson_token) + if pl.finish_date is None: + pl.finish_date = datetime.datetime.now() + pl.status = 'done' + pl.save() + except ProgressLesson.DoesNotExist: + ProgressLesson.objects.create( + progress=progress, + lesson_token=lesson_token, + checker=progress.teacher if lesson_list[lesson_idx].is_hm else student, + status="done", + finish_date=datetime.datetime.now() + ) + + try: + pl = ProgressLesson.objects.get(progress=progress, lesson_token=token_list[-1:][0]) + if pl.status == "done": + pl.status = "start" + + pl.finish_date = None + pl.save() + except ProgressLesson.DoesNotExist: + ProgressLesson.objects.create( + progress=progress, + lesson_token=token_list[-1:][0], + checker=progress.teacher if lesson_list[-1:][0].is_hm else student + ) + + return Response(status=204) + + else: + return Response("Эта функция доступна только сотрудникам персонала", status=403)