diff --git a/.gitignore b/.gitignore index a385b2b..ba1bdd8 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,5 @@ coverage.xml # Celery celerybeat-schedule -/config_app/settings/dev.env -/config_app/settings/test.env +/config_app/settings/local.env + diff --git a/Envoy.blade.php b/Envoy.blade.php index d3d66be..0564a73 100644 --- a/Envoy.blade.php +++ b/Envoy.blade.php @@ -29,9 +29,12 @@ echo '{{ $new_release_dir }}' @task('create_symlinks', ['on' => 'localhost']) echo '>> Создание симлинков' -@if ($branch) - ln -nfs {{ $app_dir }}/configs/master/prod.env {{ $new_release_dir }}/config_app/settings/prod.env - ln -nfs {{ $app_dir }}/configs/dev/dev.env {{ $new_release_dir }}/config_app/settings/dev.env +@if ($branch == 'master') + ln -nfs {{ $app_dir }}/configs/master/prod.env {{ $new_release_dir }}/config_app/settings/local.env + ln -nfs {{ $app_dir }}/media/master {{ $new_release_dir }}/media +@endif +@if ($branch == 'dev') + ln -nfs {{ $app_dir }}/configs/dev/dev.env {{ $new_release_dir }}/config_app/settings/local.env ln -nfs {{ $app_dir }}/media/master {{ $new_release_dir }}/media @endif @endtask diff --git a/access/models/other.py b/access/models/other.py index 54681d8..e4ec078 100644 --- a/access/models/other.py +++ b/access/models/other.py @@ -53,6 +53,13 @@ class Account(models.Model): def __str__(self): return self.owner.email + def get_phone(self): + try: + return '' if self.phone is None else ('' if self.phone.national_number else \ + (self.phone.national_number if self.phone.country_code is None else str(self.phone))) + except AttributeError: + return self.phone + class Meta: verbose_name = 'Дополнительная информация о пользователе' verbose_name_plural = 'Дополнительная информация о пользователе' diff --git a/access/serializers.py b/access/serializers.py index 2aea54b..9923df3 100644 --- a/access/serializers.py +++ b/access/serializers.py @@ -3,11 +3,12 @@ from rest_framework import serializers from access.models.other import Account from achievements.serialers import DiplomaSerializer, AchievementsSerializer -from progress.serializers import SecureProgressSerializer +from progress.serializers import SecureProgressSerializer, SupportProgressSerializer class AccountSerializer(serializers.ModelSerializer): gender = serializers.SerializerMethodField() + phone = serializers.SerializerMethodField() class Meta: model = Account @@ -17,6 +18,10 @@ class AccountSerializer(serializers.ModelSerializer): def get_gender(self): return self.get_gender_display() + @staticmethod + def get_phone(self): + return self.get_phone() + class UserSelfSerializer(serializers.ModelSerializer): account = serializers.SerializerMethodField() @@ -48,7 +53,7 @@ class UserSelfSerializer(serializers.ModelSerializer): @staticmethod def get_progresses(self): - return [SecureProgressSerializer(i).data for i in self.progress_set.all()] + return [SecureProgressSerializer(i).data for i in self.progress_set.filter(is_freeze=False)] class UserProfileSerializer(serializers.ModelSerializer): @@ -84,7 +89,7 @@ class UserSearchSerializer(serializers.ModelSerializer): @staticmethod def get_phone(self): - return None if self.account.phone is None else self.account.phone.raw_input + return self.account.get_phone() @staticmethod def get_pay(self): @@ -101,3 +106,22 @@ class UserSearchSerializer(serializers.ModelSerializer): @staticmethod def get_last_request(self): return self.useractivity.last_request + + +class UserProgressSearchSerializer(serializers.ModelSerializer): + phone = serializers.SerializerMethodField() + progresses = serializers.SerializerMethodField() + + class Meta: + model = get_user_model() + fields = ('out_key', 'email', 'first_name', + 'last_name', 'phone', 'progresses') + + @staticmethod + def get_phone(self): + return self.account.get_phone() + + @staticmethod + def get_progresses(self): + return [SupportProgressSerializer(i).data for i in self.progress_set.all()] + diff --git a/access/urls.py b/access/urls.py index 4198a6a..d445565 100644 --- a/access/urls.py +++ b/access/urls.py @@ -10,7 +10,6 @@ urlpatterns = [ url(r'detail/(?P[0-9A-Fa-f-]+)/$', views.DetailUserView.as_view()), url(r'detail/$', views.DetailUserView.as_view()), url(r'info/(?P[0-9A-Fa-f-]+)/$', views.MinUserView.as_view()), - url(r'guard/(?P[0-9]{1,99})/(?P.+)/$', progress.views.UserGuardView.as_view()), url(r'find/$', views.FindUserView.as_view()), url(r'registration/$', views.RegistrationView.as_view()), url(r'change_password/$', views.ChangePasswordView.as_view()), diff --git a/access/views.py b/access/views.py index 146364c..8542bc7 100644 --- a/access/views.py +++ b/access/views.py @@ -28,7 +28,10 @@ class TeacherListView(APIView): status_code = 200 def get(self, request): - return Response([i.email for i in get_user_model().objects.filter(groups__name='teachers')], self.status_code) + return Response([{ + 'email': i.email, + 'token': i.out_key, + } for i in get_user_model().objects.filter(groups__name='teachers')], self.status_code) class ResetPasswordView(APIView): diff --git a/config_app/settings/prod.env b/config_app/settings/prod.env deleted file mode 100644 index 6f33e00..0000000 --- a/config_app/settings/prod.env +++ /dev/null @@ -1,5 +0,0 @@ -DEBUG=False -SECRET_KEY='!eiquy7_+2#vn3z%zfp51$m-=tmvtcv*cj*@x$!v(_9btq0w=$' -DATABASE_URL='psql://team:nu5Xefise@127.0.0.1:5432/new_lms' -EMAIL_URL='smtp+tls://robo@skillbox.ru:nu5Xefise@smtp.gmail.com:587' -CACHE_URL=rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient \ No newline at end of file diff --git a/config_app/settings/prod.env.skeleton b/config_app/settings/prod.env.skeleton new file mode 100644 index 0000000..0f438fb --- /dev/null +++ b/config_app/settings/prod.env.skeleton @@ -0,0 +1,5 @@ +DEBUG=False +SECRET_KEY='....' +DATABASE_URL='psql://:@127.0.0.1:5432/' +EMAIL_URL='smtp+tls://:@smtp.gmail.com:587' +CACHE_URL=rediscache://127.0.0.1:6379/?client_class=django_redis.client.DefaultClient \ No newline at end of file diff --git a/courses/migrations/0006_auto_20180323_1743.py b/courses/migrations/0006_auto_20180323_1743.py new file mode 100644 index 0000000..a39f900 --- /dev/null +++ b/courses/migrations/0006_auto_20180323_1743.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-03-23 17:43 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0005_auto_20180222_1911'), + ] + + operations = [ + migrations.RemoveField( + model_name='topic', + name='description', + ), + migrations.RemoveField( + model_name='topic', + name='icon', + ), + ] diff --git a/courses/models.py b/courses/models.py index 3bc61ac..ce50bf7 100755 --- a/courses/models.py +++ b/courses/models.py @@ -58,8 +58,6 @@ class Lesson(models.Model): class Topic(models.Model): course = models.ForeignKey(to="Course", verbose_name='курс') 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(verbose_name='Поле сортировки') def __str__(self): @@ -75,12 +73,12 @@ class Topic(models.Model): class CourseManager(models.Manager): def update_or_create_course(self, image=None, big_image=None, statistic=None, - big_mobile_image=None, slug=None, teachers=None, + big_mobile_image=None, slug=None, teacher_tokens=None, level=None, direction=None, **kwargs): slug = slug if slug else slugify(unidecode.unidecode(kwargs['title'])) - kwargs['teacher_tokens'] = teachers + kwargs['teacher_tokens'] = teacher_tokens if image: path = 'course/image%s.png' % slug @@ -101,7 +99,7 @@ class CourseManager(models.Manager): kwargs['level'] = get_real_name(COURSE_LEVEL, level) if direction: - kwargs['direction'] = get_real_name(COURSE_DIRECTION, direction) + kwargs['direction'] = get_real_name(COURSE_DIRECTION, direction[0]) try: course = self.get(slug=slug) diff --git a/courses/serializers.py b/courses/serializers.py index 359386b..7b2434b 100644 --- a/courses/serializers.py +++ b/courses/serializers.py @@ -1,6 +1,8 @@ from rest_framework import serializers +from django.conf import settings from courses.models import Course, Lesson, Topic +import os class TopicSerializer(serializers.ModelSerializer): @@ -19,11 +21,12 @@ class MiniLessonSerializer(serializers.ModelSerializer): class Meta: model = Lesson - fields = ('title', 'free', 'token', 'is_hm') + fields = ('title', 'free', 'token', 'is_hm', 'sort') class LessonSerializer(MiniLessonSerializer): course_slug = serializers.SerializerMethodField() + materials = serializers.SerializerMethodField() class Meta: model = Lesson @@ -33,6 +36,15 @@ class LessonSerializer(MiniLessonSerializer): def get_course_slug(self): return self.topic.course.slug + @staticmethod + def get_materials(self): + try: + prefix = 'materials/%s/%s/%s' % (self.topic.course.token, self.topic.id, self.token) + name_list = os.listdir('%s/%s/' % (settings.MEDIA_ROOT, prefix)) + return ["%s%s/%s" % (settings.MEDIA_URL, prefix, i) for i in name_list] + except FileNotFoundError: + return [] + class TeacherLessonSerializer(MiniLessonSerializer): topic_sort = serializers.SerializerMethodField() diff --git a/courses/urls.py b/courses/urls.py index 819f814..c2f8309 100644 --- a/courses/urls.py +++ b/courses/urls.py @@ -3,9 +3,13 @@ from django.conf.urls import url from courses import views as views urlpatterns = [ + url(r'lesson/update/$', views.UpdateLessonView.as_view()), + url(r'lesson/delete/(?P.+)/$', views.DeleteLessonView.as_view()), url(r'lesson/teacher/(?P.+)/$', views.LessonInfoView.as_view()), url(r'lesson/(?P.+)/$', views.LessonDetail.as_view()), url(r'tree/(?P.+)/$', views.TreeView.as_view()), url(r'detail/(?P.+)/$', views.CourseDetailView.as_view()), + url(r'topic/update/$', views.UpdateTopicView.as_view()), + url(r'topic/delete/(?P[0-9]{1,99})/$', views.DeleteTopicView.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 5db4617..0964098 100644 --- a/courses/views.py +++ b/courses/views.py @@ -1,10 +1,9 @@ from jwt import DecodeError -from courses.models import Course, Lesson +from courses.models import Course, Lesson, Topic 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, TeacherLessonSerializer import jwt @@ -31,27 +30,6 @@ class CourseListView(APIView): status_code = 200 def post(self, request): - """ - This API endpoint create/update course. - --- - parameters: - - name: level - type: string - required: true - location: form - - name: direction - type: string - required: true - location: form - - name: statistic - type: string - required: true - 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) @@ -80,6 +58,141 @@ class CourseDetailView(APIView): return Response(CourseDetailSerializer(Course.objects.get(slug=slug)).data, self.status_code) +class DeleteTopicView(APIView): + renderer_classes = (JSONRenderer,) + + @staticmethod + def delete(request, topic_id): + if request.user and request.user.is_staff: + try: + t = Topic.objects.get(id=topic_id) + except Topic.DoesNotExist: + return Response("Темы не существует", status=404) + t.delete() + return Response(CourseTreeSerializer(t.course).data, status=200) + + +class DeleteLessonView(APIView): + renderer_classes = (JSONRenderer,) + + @staticmethod + def delete(request, lesson_token): + if request.user and request.user.is_staff: + try: + l = Lesson.objects.get(token=lesson_token) + except Lesson.DoesNotExist: + return Response("Темы не существует", status=404) + l.delete() + return Response(CourseTreeSerializer(l.topic.course).data, status=200) + + +class UpdateLessonView(APIView): + renderer_classes = (JSONRenderer,) + + @staticmethod + def post(request): + lesson_token = request.JSON.get('token', None) + sort = request.JSON.get('sort', None) + topic_id = request.JSON.get('topic', None) + title = request.JSON.get('title', None) + description = request.JSON.get('description', None) + video = request.JSON.get('video', None) + free = request.JSON.get('free', None) + is_hm = request.JSON.get('is_hm', None) + + if topic_id is None: + return Response("topic не передан", status=400) + + if sort is None: + return Response("sort не передан", status=400) + + try: + topic = Topic.objects.get(id=topic_id) + except Topic.DoesNotExist: + return Response("Тема не найдена", status=404) + + if lesson_token is None: + if title is None: + return Response("Название урока не переданно", status=400) + for lesson in reversed(topic.lesson_set.filter(sort__gte=sort)): + lesson.sort = lesson.sort + 1 + lesson.save() + l = Lesson.objects.create( + title=title, + topic=topic, + sort=sort, + ) + else: + try: + l = Lesson.objects.get(token=lesson_token) + except Lesson.DoesNotExist: + return Response("Урок не найден", status=404) + + l.title = l.title if title is None else title + l.video = l.video if video is None else video + l.free = l.free if free is None else free + l.is_hm = l.is_hm if is_hm is None else is_hm + l.description = l.description if description is None else description + if not l.sort == sort: + for lesson in reversed(topic.lesson_set.filter(sort__gte=sort)): + lesson.sort = lesson.sort + 1 + lesson.save() + l.sort = sort + l.save() + + return Response(CourseTreeSerializer(topic.course).data, status=200) + + +class UpdateTopicView(APIView): + renderer_classes = (JSONRenderer,) + + @staticmethod + def post(request): + topic_id = request.JSON.get('id', None) + sort = request.JSON.get('sort', None) + course_token = request.JSON.get('course_token', None) + title = request.JSON.get('title', None) + + if course_token is None: + return Response("Не передан course_token", status=400) + + if sort is None: + return Response("Не передан sort", status=400) + + try: + course = Course.objects.get(token=course_token) + except Course.DoesNotExist: + return Response("Курс не найден", status=404) + + try: + if topic_id: + t = Topic.objects.get(id=topic_id) + if not t.sort == sort: + for topic in reversed(course.topic_set.filter(sort__gte=sort)): + topic.sort = topic.sort + 1 + topic.save() + t.sort = sort + t.title = t.title if title is None else title + t.save() + else: + raise Topic.DoesNotExist() + + except Topic.DoesNotExist: + if title is None: + return Response("Не передан title", status=400) + + for topic in reversed(course.topic_set.filter(sort__gte=sort)): + topic.sort = topic.sort + 1 + topic.save() + + Topic.objects.create( + course=course, + title=title, + sort=sort, + ) + return Response(CourseTreeSerializer(course).data, status=200) + + class LessonInfoView(APIView): renderer_classes = (JSONRenderer,) status_code = 200 @@ -109,7 +222,7 @@ class LessonDetail(APIView): l = LessonSerializer(lesson).data try: - payload = None if jwt_token is None\ + payload = None if jwt_token is None \ else jwt.decode(jwt_token, settings.COURSE_PROGRESS_SECRET_KEY, algorithms=['HS256']) except DecodeError: payload = None @@ -117,7 +230,7 @@ class LessonDetail(APIView): course = lesson.topic.course if payload is None: - if not lesson.free: + if not (lesson.free or request.user.is_authenticated and request.user.is_staff): return Response("Bad token", status=400) else: @@ -144,7 +257,7 @@ class LessonDetail(APIView): if not new_lesson: return Response("Permission denied", status=403) - #TODO Задача для селери + # TODO Задача для селери add_lesson(request.user.out_key, course.token, lesson.token, course.get_teacher(), lesson.is_hm) return Response(l, status=200) diff --git a/finance/migrations/0004_bill_date.py b/finance/migrations/0004_bill_date.py new file mode 100644 index 0000000..84a07fe --- /dev/null +++ b/finance/migrations/0004_bill_date.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-03-23 17:43 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('finance', '0003_auto_20180315_1358'), + ] + + operations = [ + migrations.AddField( + model_name='bill', + name='date', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Дата выставления'), + preserve_default=False, + ), + ] diff --git a/finance/migrations/0005_auto_20180329_1346.py b/finance/migrations/0005_auto_20180329_1346.py new file mode 100644 index 0000000..eb89e9b --- /dev/null +++ b/finance/migrations/0005_auto_20180329_1346.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-03-29 13:46 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('finance', '0004_bill_date'), + ] + + operations = [ + migrations.CreateModel( + name='InstallmentPlan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('invoice_amount', models.IntegerField(verbose_name='Количество платежей')), + ('price', models.IntegerField(verbose_name='Цена одного платежа')), + ('bill', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='finance.Bill', verbose_name='Связный счёт')), + ], + options={ + 'verbose_name': 'Рассрочка', + 'verbose_name_plural': 'Рассрочки', + }, + ), + migrations.AlterField( + model_name='invoice', + name='date', + field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'), + ), + ] diff --git a/finance/models.py b/finance/models.py index 04209aa..956caa7 100755 --- a/finance/models.py +++ b/finance/models.py @@ -3,8 +3,6 @@ from django.conf import settings from django.db import models from yandex_money.models import Payment -from courses.models import Course, Lesson - class Bill(models.Model): course_token = models.UUIDField(verbose_name="Токен курса", editable=False) @@ -13,6 +11,7 @@ class Bill(models.Model): comment = models.TextField(verbose_name='Комментарий продавца', help_text='Будет показано пользователю', blank=True, editable=False) description = models.TextField(verbose_name='Внутренняя заметка', blank=True) + date = models.DateTimeField(verbose_name="Дата выставления", auto_now_add=True) def __str__(self): return '%s: %s' % (self.id, self.user) @@ -20,6 +19,12 @@ class Bill(models.Model): def get_full_price(self): return sum([i.price for i in self.invoice_set.all() if not i.price is None]) + def check_validate(self, invoice_id): + return self.invoice_set.filter(is_open=True).exclude(id=invoice_id).count() == 1 + + def check_pay(self): + return self.invoice_set.filter(status="F").exists() + class Meta: verbose_name = 'Счет' verbose_name_plural = 'Счета' @@ -43,7 +48,7 @@ class Invoice(models.Model): ('C', 'Отклонен'), ) status = models.CharField(verbose_name='Статус', max_length=1, default='W', choices=BILL_STATUSES) - price = models.IntegerField(verbose_name='Сумма', editable=False, null=True, blank=True) + price = models.IntegerField(verbose_name='Сумма', editable=False, null=True, blank=True) #Todo На самом деле тут не далжно быть значений null real_price = models.FloatField(verbose_name='Полученная сумма', null=True, blank=True, help_text='Сумма, минус комиссия', editable=False) method = models.CharField(verbose_name='Способ оплаты', max_length=2, default='Y', choices=BILL_METHOD) @@ -63,3 +68,17 @@ class Invoice(models.Model): class Meta: verbose_name = 'Платёж' verbose_name_plural = 'Платежи' + + +class InstallmentPlan(models.Model): + bill = models.OneToOneField(to=Bill, verbose_name="Связный счёт") + date = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") + invoice_amount = models.IntegerField(verbose_name="Количество платежей") + price = models.IntegerField(verbose_name="Цена одного платежа") + + def __str__(self): + return '%s' % self.bill.user.email + + class Meta: + verbose_name = 'Рассрочка' + verbose_name_plural = 'Рассрочки' diff --git a/finance/serializers.py b/finance/serializers.py index a166c1c..4549c16 100644 --- a/finance/serializers.py +++ b/finance/serializers.py @@ -28,18 +28,18 @@ class BillSerializer(serializers.ModelSerializer): class InvoiceSerializer(serializers.ModelSerializer): status = serializers.SerializerMethodField() method = serializers.SerializerMethodField() - yandex_pay_id = serializers.SerializerMethodField() + yandex_pay = serializers.SerializerMethodField() class Meta: model = Invoice - exclude = ('bill',) + fields = '__all__' @staticmethod def get_status(self): return self.get_status_display() @staticmethod - def get_yandex_pay_id(self): + def get_yandex_pay(self): return None if self.yandex_pay is None else self.yandex_pay.id @staticmethod diff --git a/finance/signals.py b/finance/signals.py index ac85544..9d62f16 100644 --- a/finance/signals.py +++ b/finance/signals.py @@ -19,11 +19,10 @@ def invoice_signal(instance, **kwargs): if instance.yandex_pay and instance.method == 'Y' and instance.status == 'P' and not instance.rebilling: 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], + """%s для оплаты перейдите по ссылке + %s/api/v1/finance/payment/%s/""" % (instance.get_comment(), settings.DOMAIN, instance.yandex_pay.id), + to=[instance.yandex_pay.cps_email], + bcc=[instance.bill.opener.email], reply_to=[instance.bill.opener.email], ) msg.send() @@ -52,8 +51,7 @@ def invoice_signal(instance, **kwargs): '''Вам открыт доступ к курсу "%s", вы можете перейти по ссылке и ознакомиться с материалами %s/course/%s''' % (course.title, settings.DOMAIN, course.slug), - 'robo@skillbox.ru', - [instance.bill.user.email], + to=[instance.bill.user.email], bcc=[instance.bill.opener.email], reply_to=[instance.bill.opener.email], ) diff --git a/finance/urls.py b/finance/urls.py index 5da326d..5b875b5 100644 --- a/finance/urls.py +++ b/finance/urls.py @@ -7,5 +7,6 @@ urlpatterns = [ url(r'bills/([0-9]{1,99})/$', views.BillDetailView.as_view()), url(r'bills_find/$', views.FindBillView.as_view()), url(r'yandex/fail/$', views.YandexFailView.as_view()), + url(r'invoice/([0-9]{1,99})/$', views.InvoiceDetailView.as_view()), url(r'invoices/$', views.get_invoices), ] \ No newline at end of file diff --git a/finance/views.py b/finance/views.py index 8e122ea..87d9a66 100644 --- a/finance/views.py +++ b/finance/views.py @@ -4,6 +4,7 @@ import logging import requests from django.contrib.auth import get_user_model from django.core.mail import EmailMessage +from django.db import IntegrityError from django.db.models import Q from django.http import HttpResponse, HttpResponseForbidden from django.shortcuts import redirect @@ -12,6 +13,9 @@ from rest_framework.response import Response from rest_framework.views import APIView from yandex_money.models import Payment from django.conf import settings +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string +from django.utils.html import strip_tags from courses.models import Course from courses.api import CourseParamsApi @@ -41,84 +45,144 @@ class BillListView(APIView): def post(self, request): if request.user.is_authenticated and (request.user.groups.filter(name__in=['managers','lead_managers']).exists() or request.user.is_superuser): - bill = request.JSON.get('bill') - children = request.JSON.get('children', []) + user = get_user_model().objects.get(email=request.JSON.get('user')) + opener = get_user_model().objects.get(email=request.JSON.get('opener')) + description = request.JSON.get('description', None) + comment = request.JSON.get('comment', None) + course_token = request.JSON.get('course_token', None) - if bill: - user = get_user_model().objects.get(email=bill['user']) - opener = get_user_model().objects.get(email=bill['opener']) - description = bill['description'] - comment = bill['comment'] - course_token = bill['course_token'] + if course_token is None: + return Response("Идентификатор курса не передан", status=400) + try: + bill_obj = Bill.objects.get(user=user, course_token=course_token) + except Bill.DoesNotExist: try: - bill_obj = Bill.objects.get(user=user, course_token=course_token) - except Bill.DoesNotExist: bill_obj = Bill.objects.create(user=user, course_token=course_token) + except IntegrityError: + return Response("У пользователя уже есть счёт на этот курс", status=400) + + bill_obj.opener = bill_obj.opener if opener is None else opener + bill_obj.description = bill_obj.description if description is None else description + bill_obj.comment = bill_obj.comment if comment is None else comment + bill_obj.save() + + return Response(bill_obj.id, status=200) + + return Response("Ошибка доступа, возможно вы разлогинились из другой вкладки браузера", status=403) + + +class InvoiceDetailView(APIView): + renderer_classes = (JSONRenderer,) + + @staticmethod + def delete(request, invoice_id): + + try: + i = Invoice.objects.get(id=invoice_id) + if not i.status == "F" and not (i.bill.check_pay() and i.is_open): + i.delete() + except Invoice.DoesNotExist: + pass + + return Response(status=204) + + @staticmethod + def post(request, invoice_id): + if request.user.is_authenticated and (request.user.groups.filter(name__in=['managers','lead_managers']).exists() + or request.user.is_superuser): + invoice_id = int(invoice_id) + bill_id = request.JSON.get('bill', None) + is_open = request.JSON.get('is_open', None) + method = request.JSON.get('method', None) + status = request.JSON.get('status', None) + price = request.JSON.get('price', None) + comment = request.JSON.get('comment', None) + real_price = request.JSON.get('real_price', None) + + if bill_id is None: + return Response("Не передан id счёта", status=400) + + if is_open is None or method is None or status is None or price is None: + return Response("Не передан один из пораметров is_open, method, status, price", status=400) - bill_obj.opener = opener - bill_obj.description = description - bill_obj.comment = comment - bill_obj.save() - - for i in children: - status = get_real_name(elem=i['status'], array=Invoice.BILL_STATUSES) - try: - invoice_id = i['id'] - except KeyError: - invoice_id = None - - try: - if not invoice_id is None: - invoice = Invoice.objects.get(id=i['id']) - if invoice.status == "P" or invoice.status == status: - continue - else: - raise Invoice.DoesNotExist - - except Invoice.DoesNotExist: - i['method'] = get_real_name(elem=i['method'], array=Invoice.BILL_METHOD) - i['status'] = status - i['bill'] = bill_obj - i['yandex_pay'] = None - invoice = Invoice.objects.create(**i) - - if i['method'] == 'Y' and invoice.yandex_pay is None: - yandex_pay = Payment.objects.create( - order_amount=i['price'], - shop_amount=0, - customer_number=bill_obj.user.id, - user=bill_obj.user, - cps_email=bill_obj.user.email, - ) - invoice.yandex_pay = yandex_pay - invoice.save() - - msg = EmailMessage( - 'Выставлен новый счёт.', - '''Менеджер %s выставил счёт пользователю %s на курс "%s".''' - % ( - invoice.bill.opener.get_full_name(), - invoice.bill.user.email, - Course.objects.get(token=invoice.bill.course_token).title, - ), - 'robo@skillbox.ru', - [invoice.bill.opener.email], - bcc=['dmitry.dolya@skillbox.ru'], - ) - - msg.send() - - res = { - "bill": BillSerializer(bill_obj).data, - "children": [InvoiceSerializer(i).data for i in bill_obj.invoice_set.all()], + try: + bill = Bill.objects.get(id=bill_id) + except Bill.DoesNotExist: + return Response('Не найден счёт с id=%s' % bill_id, status=404) + + method = get_real_name(elem=method[0], array=Invoice.BILL_METHOD) + status = get_real_name(elem=status[0], array=Invoice.BILL_STATUSES) + + if bill.check_validate(invoice_id) and is_open: + return Response("Уже есть платёж открывающий курс", status=400) + + try: + invoice = Invoice.objects.get(id=invoice_id) + except Invoice.DoesNotExist: + if not invoice_id == 0: + return Response("Платёж не найден", status=404) + + if bill.check_pay(): + return Response( + "Нельзя добавить новый платёж, так как один из платежей по счёту уже оплачен", status=400) + invoice = Invoice.objects.create( + bill=bill, + method=method, + status=status, + is_open=is_open, + ) + + if invoice.status == "F": + return Response(InvoiceSerializer(invoice).data, status=200) + + invoice.real_price = None + invoice.method = method + invoice.status = status + + if invoice.status == "F": + invoice.real_price = invoice.real_price if real_price is None else real_price + + if bill.check_pay() and (invoice.price < price): + return Response("""Нельзя менять стоимость по счёту в большую сторону, + когда один из платежей оплачен""", status=400) + + invoice.price = price + invoice.is_open = is_open + invoice.comment = comment + + if invoice.method == 'Y' and invoice.yandex_pay is None: + yandex_pay = Payment.objects.create( + order_amount=invoice.price, + shop_amount=0, + customer_number=bill.user.id, + user=bill.user, + cps_email=bill.user.email, + ) + invoice.yandex_pay = yandex_pay + + context = { + 'user_email': invoice.bill.user.email, + 'opener_full_name': invoice.bill.opener.get_full_name(), + 'course_title': Course.objects.get(token=invoice.bill.course_token).title, + 'date': str(invoice.date), + 'price': invoice.price, } - return Response(res, status=200) + subject, to = 'Выставлен новый счёт', invoice.bill.opener.email - return Response("Bill not set", status=400) + html_content = render_to_string('mail/sales/back_set_bill.html', context) + text_content = strip_tags(html_content) - return Response("Course detail access only for manager users", status=403) + msg = EmailMultiAlternatives(subject, text_content, to=[to], bcc=['dmitry.dolya@skillbox.ru']) + msg.attach_alternative(html_content, "text/html") + msg.send() + + invoice.save() + + return Response(InvoiceSerializer(invoice).data, status=200) + + return Response("Invoice detail access only for manager users", status=403) class BillDetailView(APIView): @@ -159,7 +223,9 @@ class FindBillView(APIView): if key: res = Bill.objects.filter( - Q(opener__email__contains=key.lower()) | Q(user__email__contains=key.lower()) + Q(opener__email__contains=key.lower()) + | Q(user__email__contains=key.lower()) + | Q(id__contains=key) ) else: @@ -346,16 +412,23 @@ class YandexAvisoView(APIView): 'response': xml_res, }) + context = { + 'user_email': pay.invoice.bill.user.email, + 'opener_full_name': pay.invoice.bill.opener.get_full_name(), + 'course_title': Course.objects.get(token=pay.invoice.bill.course_token).title, + 'date': str(pay.invoice.date), + 'price': pay.invoice.price, + 'finish_date': pay.performed_datetime, + } + + subject, to = 'Счёт оплачен', pay.invoice.bill.opener.email - msg = EmailMessage( - 'Успешная оплата.', - '''Пользователь "%s", перевёл %s рублей. Номер платежа в яндекс кассе %s''' - % (pay.invoice.bill.user.email, str(pay.invoice.price), str(data['invoiceId'])), - 'robo@skillbox.ru', - [pay.invoice.bill.opener.email], - bcc=['dmitry.dolya@skillbox.ru', 'vera.procenko@skillbox.ru'], - ) + html_content = render_to_string('mail/sales/pay_access.html', context) + text_content = strip_tags(html_content) + msg = EmailMultiAlternatives( + subject, text_content, to=[to], bcc=['dmitry.dolya@skillbox.ru', 'vera.procenko@skillbox.ru']) + msg.attach_alternative(html_content, "text/html") msg.send() if pay.invoice.rebilling_on: diff --git a/lms/settings.py b/lms/settings.py index d1cf541..9bc3f60 100644 --- a/lms/settings.py +++ b/lms/settings.py @@ -8,22 +8,10 @@ import socket root = environ.Path(__file__) - 2 env = environ.Env() -MOD = os.environ.get('MOD', 'Prod') -DEBUG = os.environ.get('DEBUG', 'False') -MASTER_PASSWORD = os.environ.get('MASTER_PASSWORD', '@J*1') - -if MOD == 'Test': - environ.Env.read_env(str(root) + '/config_app/settings/test.env') - -elif MOD == 'Dev': - environ.Env.read_env(str(root) + '/config_app/settings/dev.env') +environ.Env.read_env(str(root) + '/config_app/settings/local.env') -elif MOD == 'Prod': - environ.Env.read_env(str(root) + '/config_app/settings/prod.env') - -else: - raise ImportError('no such environ ' + MOD) +MASTER_PASSWORD = os.environ.get('MASTER_PASSWORD', '@J*1') EMAIL_CONFIG = env.email_url('EMAIL_URL',) vars().update(EMAIL_CONFIG) diff --git a/progress/migrations/0009_progress_is_freeze.py b/progress/migrations/0009_progress_is_freeze.py new file mode 100644 index 0000000..9b96892 --- /dev/null +++ b/progress/migrations/0009_progress_is_freeze.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-03-27 13:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('progress', '0008_auto_20180227_1803'), + ] + + operations = [ + migrations.AddField( + model_name='progress', + name='is_freeze', + field=models.BooleanField(default=False, verbose_name='Прохождение было преостановленно'), + ), + ] diff --git a/progress/models.py b/progress/models.py index 2a6e5dc..918ce8d 100644 --- a/progress/models.py +++ b/progress/models.py @@ -12,6 +12,7 @@ class Progress(models.Model): user = models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='Студент') course_token = models.UUIDField(verbose_name="Токен курса", editable=False) is_finish = models.BooleanField(verbose_name="Окончен ли курс", default=False) + is_freeze = models.BooleanField(verbose_name="Прохождение было преостановленно", default=False) only_watch = models.BooleanField(verbose_name="Только просмотр", default=False) def progress_status(self, sorted_token_list): diff --git a/progress/serializers.py b/progress/serializers.py index c2570e9..83d1ce6 100644 --- a/progress/serializers.py +++ b/progress/serializers.py @@ -17,6 +17,23 @@ class ProgressSerializer(serializers.ModelSerializer): return [ProgressLessonSerializer(i).data for i in self.progresslesson_set.all()] +class SupportProgressSerializer(serializers.ModelSerializer): + teacher_email = serializers.SerializerMethodField() + mod = serializers.SerializerMethodField() + + class Meta: + model = Progress + fields = ('id', 'course_token', 'mod', 'teacher_email', "is_freeze") + + @staticmethod + def get_teacher_email(self): + return self.teacher.email + + @staticmethod + def get_mod(self): + return "Без дз" if self.only_watch else "Стандарт" + + class SecureProgressSerializer(serializers.ModelSerializer): jwt_token = serializers.SerializerMethodField() diff --git a/progress/urls.py b/progress/urls.py index 44b7bd1..eeb675f 100644 --- a/progress/urls.py +++ b/progress/urls.py @@ -5,6 +5,9 @@ from progress import views urlpatterns = [ url(r'students/(?P[0-9A-Fa-f-]+)/$', views.StudentWorkView.as_view()), url(r'student/$', views.StudentUpdateProgress.as_view()), + url(r'find/$', views.FindProgressView.as_view()), + url(r'freeze/$', views.FreezeProgressView.as_view()), + url(r'change_teacher/$', views.ChangeTeacherView.as_view()), url(r'teacher/$', views.TeacherUpdateProgress.as_view()), url(r'set-progress/$', views.SetProgress.as_view()), url(r'get_hw_pay/$', views.get_teachers_pay), diff --git a/progress/views.py b/progress/views.py index 01513e8..58dc891 100644 --- a/progress/views.py +++ b/progress/views.py @@ -12,6 +12,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from django.db.models import Q +from access.serializers import UserProgressSearchSerializer from courses.models import Course from progress.models import ProgressLesson, Progress from progress.serializers import ProgressAnalyticSerializer, ProgressLessonSerializer, ProgressSerializer, \ @@ -201,6 +202,13 @@ class StudentUpdateProgress(APIView): not pv.progress.progresslesson_set.filter(status=ProgressLesson.STATUSES.wait).exists(): pv.status = ProgressLesson.STATUSES.wait pv.comment_tokens.append(comment) + msg = EmailMessage( + 'Студент оставил комментарий', + '''Студент "%s" оставил вам комментарий.''' % request.user.get_full_name(), + 'robo@skillbox.ru', + [pv.checker.email], + ) + msg.send() elif comment is None: return Response("Не преложен комментарий", status=400) @@ -248,36 +256,74 @@ class UploadCourseProgressUserView(APIView): return Response(status=403) -class UserGuardView(APIView): +class FindProgressView(APIView): + renderer_classes = (JSONRenderer,) + permission_classes = (IsAuthenticated,) + + @staticmethod + def get(request): + if not request.user.is_staff: + return Response("Только сотрудники персонала могут запрашивать инфо по прогрессу", status=403) + key = request.GET.get('key', None) + count = int(request.GET.get('count', '10')) + if key: + res = get_user_model().objects.filter( + Q(id__contains=key) | Q(email__contains=key.lower()) | Q(first_name__contains=key) | + Q(last_name__contains=key) | Q(account__phone__contains=key) + ) + + else: + res = get_user_model().objects.all() + + res = res[:(count if len(res) > count else len(res))] + return Response([UserProgressSearchSerializer(i).data for i in res], status=200) + + +class FreezeProgressView(APIView): renderer_classes = (JSONRenderer,) permission_classes = (IsAuthenticated,) @staticmethod - def get(request, pk, page): + def post(request): + if not request.user.is_staff: + return Response("Только сотрудники персонала могут вносить изменение в прогресс", status=403) + key = request.JSON.get('id', None) + is_freeze = request.JSON.get('is_freeze', False) + try: - user = get_user_model().objects.get(out_key=pk) - except get_user_model().DoesNotExist: - return Response("User doesn't exist", status=404) + p = Progress.objects.get(id=key) + p.is_freeze = is_freeze + p.save() + except Progress.DoesNotExist: + return Response("не найден прогресс по заданному id", status=404) - is_i = request.user == user - res_403 = Response('Permission denied', status=403) - res_204 = Response(status=204) + return Response(status=204) - if is_i and not request.user.groups.filter(name='teachers').exists() and page == 'homeworks': - return res_403 - if is_i and not \ - request.user.groups.filter(name__in=['students', 'managers', 'lead_managers']).exists() \ - and page == 'payment': - return res_403 +class ChangeTeacherView(APIView): + renderer_classes = (JSONRenderer,) + permission_classes = (IsAuthenticated,) - if is_i: - return res_204 + @staticmethod + def post(request): + if not request.user.is_staff: + return Response("Только сотрудники персонала могут вносить изменение в прогресс", status=403) + key = request.JSON.get('id', None) + teacher_email = request.JSON.get('teacher_email', False) - if page == 'profile' and (request.user.is_superuser or request.user.is_staff): - return res_204 + try: + p = Progress.objects.get(id=key) + try: + teacher = get_user_model().objects.get(email=teacher_email.lower()) + except get_user_model().DoesNotExist: + return Response("Нет пользователя c таким email", status=404) + p.teacher = teacher + p.progresslesson_set.filter(status=ProgressLesson.STATUSES.wait).update(checker=teacher) + p.save() + except Progress.DoesNotExist: + return Response("не найден прогресс по заданному id", status=404) - return res_403 + return Response(status=204) class SetProgress(APIView): diff --git a/templates/mail/sales/back_set_bill.html b/templates/mail/sales/back_set_bill.html new file mode 100644 index 0000000..03a3cd5 --- /dev/null +++ b/templates/mail/sales/back_set_bill.html @@ -0,0 +1,6 @@ +
Выставлен новый счет
+
Пользователь: {{ user_email }}
+
Продавец: {{ opener_full_name }}
+
Курс: {{ course_title }}
+
Дата продажи: {{ date }}
+
Сумма: {{ price }} руб.
\ No newline at end of file diff --git a/templates/mail/sales/pay_access.html b/templates/mail/sales/pay_access.html new file mode 100644 index 0000000..de7fb90 --- /dev/null +++ b/templates/mail/sales/pay_access.html @@ -0,0 +1,7 @@ +
Успешный платёж
+
Пользователь: {{ user_email }}
+
Продавец: {{ opener_full_name }}
+
Курс: {{ course_title }}
+
Дата продажи: {{ date }}
+
Дата оплаты: {{ finish_date }}
+
Сумма: {{ price }} руб.
\ No newline at end of file