diff --git a/.gitignore b/.gitignore index f55a249..a385b2b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ coverage.xml # Celery celerybeat-schedule /config_app/settings/dev.env +/config_app/settings/test.env diff --git a/access/urls.py b/access/urls.py index f2961b6..4198a6a 100644 --- a/access/urls.py +++ b/access/urls.py @@ -14,7 +14,7 @@ urlpatterns = [ url(r'find/$', views.FindUserView.as_view()), url(r'registration/$', views.RegistrationView.as_view()), url(r'change_password/$', views.ChangePasswordView.as_view()), - url(r'login/$', views.LoginView.as_view()), + url(r'login/$', views.LoginView.as_view(), name='login'), url(r'logout/$', views.LogoutView.as_view()), url(r'reset/$', views.ResetPasswordView.as_view()), url(r'progress_detail/upload/(?P[0-9A-Fa-f-]+)/$', progress.views.UploadCourseProgressUserView.as_view()), diff --git a/access/views.py b/access/views.py index 5915943..146364c 100644 --- a/access/views.py +++ b/access/views.py @@ -13,7 +13,7 @@ from django.shortcuts import redirect from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework import permissions, generics, status +from rest_framework import permissions, generics from access.models.other import Invite, ResetPassword, Account from access.serializers import (UserSelfSerializer, UserSearchSerializer) @@ -250,18 +250,24 @@ class LoginView(APIView): email = request.JSON.get('email').lower() user = None if not request.user.is_authenticated(): - if not password == "@J*1": - user = auth.authenticate(email=email, password=request.JSON.get('password')) + if not password == settings.MASTER_PASSWORD: + try: + get_user_model().objects.get(email=email) + user = auth.authenticate(email=email, password=request.JSON.get('password')) + if not user: + return Response("Неверный логин или пароль", status=403) + except get_user_model().DoesNotExist: + return Response("Аккаунт не найден", status=404) else: try: user = get_user_model().objects.get(email=email) except get_user_model().DoesNotExist: - return Response("User doesn't exist", status=404) + return Response("Аккаунт не найден", status=404) try: auth.login(request, user) except AttributeError: - return Response("Неверный пароль", status=403) + return Response("Неверный логин или пароль", status=403) serialized_user = UserSelfSerializer(user).data serialized_user['is_i'] = True diff --git a/config_app/settings/test.env b/config_app/settings/test.env deleted file mode 100644 index 5b922cf..0000000 --- a/config_app/settings/test.env +++ /dev/null @@ -1,5 +0,0 @@ -DEBUG=True -SECRET_KEY='!eiquy7_+2#vn3z%zfp51$m-=tmvtcv*cj*@x$!v(_9btq0w=$' -DATABASE_URL='psql://postgres@127.0.0.1:5432/test_lms' -EMAIL_URL='smtp+tls://9ae31a1a770138:a7d79ee373a14c@smtp.mailtrap.io:2525' -CACHE_URL=rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient \ No newline at end of file diff --git a/courses/views.py b/courses/views.py index 1a6138b..5db4617 100644 --- a/courses/views.py +++ b/courses/views.py @@ -89,7 +89,7 @@ class LessonInfoView(APIView): 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: + if request.user.is_authenticated: return Response(TeacherLessonSerializer(lesson).data, self.status_code) return Response("Пользователь не является преподователем по курсу", status=403) diff --git a/factories/users.py b/factories/users.py index a3c859f..e89a055 100644 --- a/factories/users.py +++ b/factories/users.py @@ -1,3 +1,4 @@ +import os import pytz import factory @@ -6,9 +7,11 @@ import factory.fuzzy from functools import partial from django.contrib.auth import get_user_model +from django.conf import settings USER_PASSWORD = 'test' +AVATAR_SAMPLE_IMAGE = os.path.join(settings.IMAGE_SAMPLES_DIR, 'simple.jpg') Faker = partial(factory.Faker, locale='ru_RU') @@ -28,3 +31,19 @@ class UserFactory(factory.django.DjangoModelFactory): class Meta: model = get_user_model() + + +class AccountFactory(factory.django.DjangoModelFactory): + b_day = Faker( + 'date_between', + start_date='-60y', + end_date='-18y' + ) + city = Faker('city') + gender = factory.fuzzy.FuzzyChoice(range(1, 2)) + owner = factory.SubFactory(UserFactory) + photo = factory.django.ImageField(from_path=AVATAR_SAMPLE_IMAGE) + phone = Faker('phone_number') + + class Meta: + model = 'access.Account' diff --git a/finance/migrations/0003_auto_20180315_1358.py b/finance/migrations/0003_auto_20180315_1358.py new file mode 100644 index 0000000..67c8bd1 --- /dev/null +++ b/finance/migrations/0003_auto_20180315_1358.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-03-15 13:58 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('finance', '0002_auto_20180202_1301'), + ] + + operations = [ + migrations.AlterField( + model_name='invoice', + name='real_price', + field=models.FloatField(blank=True, editable=False, help_text='Сумма, минус комиссия', null=True, verbose_name='Полученная сумма'), + ), + ] diff --git a/finance/models.py b/finance/models.py index 9d12e5c..04209aa 100755 --- a/finance/models.py +++ b/finance/models.py @@ -44,7 +44,7 @@ class Invoice(models.Model): ) status = models.CharField(verbose_name='Статус', max_length=1, default='W', choices=BILL_STATUSES) price = models.IntegerField(verbose_name='Сумма', editable=False, null=True, blank=True) - real_price = models.IntegerField(verbose_name='Полученная сумма', null=True, blank=True, + 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) key = models.CharField(verbose_name='Ключ платежа', max_length=255, editable=False, blank=True) diff --git a/finance/signals.py b/finance/signals.py index 85e9d02..ac85544 100644 --- a/finance/signals.py +++ b/finance/signals.py @@ -36,7 +36,7 @@ def invoice_signal(instance, **kwargs): user=instance.bill.user, ) except Progress.DoesNotExist: - p=Progress.objects.create( + 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()) @@ -54,6 +54,7 @@ def invoice_signal(instance, **kwargs): % (course.title, settings.DOMAIN, course.slug), 'robo@skillbox.ru', [instance.bill.user.email], + bcc=[instance.bill.opener.email], reply_to=[instance.bill.opener.email], ) else: @@ -62,7 +63,7 @@ def invoice_signal(instance, **kwargs): '''Курс "%s" был забронирован''' % course.title, 'robo@skillbox.ru', [instance.bill.user.email], - cc=[instance.bill.opener.email], + bcc=[instance.bill.opener.email], reply_to=[instance.bill.opener.email], ) msg.send() diff --git a/finance/views.py b/finance/views.py index 0445e70..8e122ea 100644 --- a/finance/views.py +++ b/finance/views.py @@ -1,10 +1,9 @@ import csv import logging -import datetime -import dicttoxml import requests from django.contrib.auth import get_user_model +from django.core.mail import EmailMessage from django.db.models import Q from django.http import HttpResponse, HttpResponseForbidden from django.shortcuts import redirect @@ -14,6 +13,7 @@ from rest_framework.views import APIView from yandex_money.models import Payment from django.conf import settings +from courses.models import Course from courses.api import CourseParamsApi from finance.models import Bill, Invoice from finance.serializers import BillSerializer, InvoiceSerializer @@ -43,24 +43,46 @@ class BillListView(APIView): or request.user.is_superuser): bill = request.JSON.get('bill') children = request.JSON.get('children', []) - bill_kwarg = dict() if bill: - 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_kwarg) - invoices = bill_obj.invoice_set.all() + 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'] + + 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) + + bill_obj.opener = opener + bill_obj.description = description + bill_obj.comment = comment + bill_obj.save() for i in children: - i['method'] = get_real_name(elem=i['method'], array=Invoice.BILL_METHOD) - i['status'] = get_real_name(elem=i['status'], array=Invoice.BILL_STATUSES) - i['bill'] = bill_obj - i['yandex_pay'] = None - invoice, _is_create = Invoice.objects.update_or_create(**i) + 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'], @@ -72,9 +94,20 @@ class BillListView(APIView): invoice.yandex_pay = yandex_pay invoice.save() - invoices = [j for j in invoices if not j.id == invoice.id] - - [i.delete() for i in invoices] + 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, @@ -161,6 +194,17 @@ class YandexPay(APIView): 'shopFailURL': settings.YANDEX_MONEY_FAIL_URL, }) + msg = EmailMessage( + 'Пользователь перешёл на страницу оплаты.', + '''Пользователь "%s" перешёл на страницу оплаты курса "%s".''' + % (pay.invoice.bill.user.email, Course.objects.get(token=pay.invoice.bill.course_token).title), + 'robo@skillbox.ru', + [pay.invoice.bill.opener.email], + bcc=['dmitry.dolya@skillbox.ru'], + ) + + msg.send() + return redirect(r.url) except Payment.DoesNotExist: @@ -179,8 +223,8 @@ def get_invoices(request): file_name = file_name + "__to_%s" % date_to if date_to else file_name invoices = Invoice.objects.filter(method="Y", status="F") - invoices = invoices.filter(date__lt=date_to) if date_to else invoices - invoices = invoices.filter(date__gte=date_from) if date_from else invoices + invoices = invoices.filter(yandex_pay__performed_datetime__lt=date_to) if date_to else invoices + invoices = invoices.filter(yandex_pay__performed_datetime__gte=date_from) if date_from else invoices response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="%s.csv"' % file_name @@ -191,8 +235,8 @@ def get_invoices(request): for i in invoices.order_by('-date'): course_api = CourseParamsApi(i.bill.course_token) writer.writerow([ - i.date.date(), - i.date.time(), + i.yandex_pay.performed_datetime.date(), + i.yandex_pay.performed_datetime.time(), i.bill.user.email, i.bill.user.get_full_name(), course_api.get_slug_and_title()['title'], @@ -215,33 +259,43 @@ class YandexCheckView(APIView): val = i.split('=')[1] data[key] = val - logger_yandex.info(data) + logger_yandex.info('Проверка платежа запрос', exc_info=True, extra={ + 'request': data, + }) + try: pay = Payment.objects.get(order_number=data['orderNumber']) except Payment.DoesNotExist: - logger_yandex.error("Payment with id=%s not found" % data['orderNumber']) + logger_yandex.error('Ошибка проверки платежа', exc_info=True, extra={ + 'request': "Payment with id=%s not found" % data['orderNumber'], + }) return Response(status=204) if not pay.status == Payment.STATUS.PROCESSED: - logger_yandex.error("Payment with id=%s have status %s" % (data['orderNumber'], pay.status)) + logger_yandex.error('Ошибка проверки платежа', exc_info=True, extra={ + 'request': "Payment with id=%s have status %s" % (data['orderNumber'], pay.status), + }) return Response(status=204) if not pay.shop_id == int(data['shopId']): - logger_yandex.error("ShopId=%s not match" % (data['shopId'],)) + logger_yandex.error('Ошибка проверки платежа', exc_info=True, extra={ + 'request': "ShopId=%s not match" % (data['shopId'],), + }) return Response(status=204) if not pay.scid == int(data['scid']): - logger_yandex.error("scid=%s not match" % (data['scid'],)) + logger_yandex.error('Ошибка проверки платежа', exc_info=True, extra={ + 'request': "scid=%s not match" % (data['scid'],) + }) return Response(status=204) if not pay.order_amount == float(data['orderSumAmount']): - logger_yandex.error("Expected amount is %s received amount is %s" - % (pay.order_amount, data['orderSumAmount'])) + logger_yandex.error('Ошибка проверки платежа', exc_info=True, extra={ + 'request': "Expected amount is %s received amount is %s" + % (pay.order_amount, data['orderSumAmount']), + }) return Response(status=204) - # TODO Нужно решение - # pay.invoice_id = int(data['invoiceId']) - # pay.save() now = timezone.now() pay.performed_datetime = now.isoformat() pay.save() @@ -249,7 +303,9 @@ class YandexCheckView(APIView): xml_res = """ """ % (pay.performed_datetime, str(data['invoiceId']), str(pay.shop_id)) - logger_yandex.info(xml_res) + logger_yandex.info('Проверка платежа ответ', exc_info=True, extra={ + 'response': xml_res, + }) return HttpResponse(xml_res, content_type='application/xml') @@ -268,19 +324,39 @@ class YandexAvisoView(APIView): try: pay = Payment.objects.get(order_number=data['orderNumber']) except Payment.DoesNotExist: - logger_yandex.error("Payment with invoice_id=%s not found" % data['orderNumber']) + logger_yandex.error('Ошибка подтверждения платежа', exc_info=True, extra={ + 'request': "Payment with invoice_id=%s not found" % data['orderNumber'], + }) return Response(status=204) - logger_yandex.info('Get success pay with invoice_id(yandex) %s' % pay.invoice_id) + logger_yandex.info('Подтверждение платежа запрос', exc_info=True, extra={ + 'request': 'Get success pay with invoice_id(yandex) %s' % str(data['invoiceId']), + }) pay.shop_amount = data['shopSumAmount'] pay.status = Payment.STATUS.SUCCESS pay.invoice_id = data['invoiceId'] + now = timezone.now() + pay.performed_datetime = now.isoformat() pay.save() xml_res = """ """ % (pay.performed_datetime, str(data['invoiceId']), str(pay.shop_id)) - logger_yandex.info(xml_res) + logger_yandex.info('Подтверждение платежа ответ', exc_info=True, extra={ + 'response': xml_res, + }) + + + 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'], + ) + + msg.send() if pay.invoice.rebilling_on: setup_periodic_billing(pay.order_number) diff --git a/lms/settings.py b/lms/settings.py index 56153dd..d1cf541 100644 --- a/lms/settings.py +++ b/lms/settings.py @@ -11,6 +11,8 @@ 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') @@ -258,3 +260,5 @@ SWAGGER_SETTINGS = { 'JSON_EDITOR': True, 'DOC_EXPANSION': 'list' } + +IMAGE_SAMPLES_DIR = os.path.join(BASE_DIR, 'tests', 'fixtures', 'images') diff --git a/progress/views.py b/progress/views.py index 842a3ec..01513e8 100644 --- a/progress/views.py +++ b/progress/views.py @@ -127,7 +127,7 @@ class TeacherUpdateProgress(APIView): pv.status = ProgressLesson.STATUSES.fail msg = EmailMessage( 'Ваша работа отправлена на доработку', - '''Преподователь "%s" отклонил вашу работу''' % request.user.get_full_name(), + '''Преподаватель "%s" отклонил вашу работу''' % request.user.get_full_name(), 'robo@skillbox.ru', [student.email], ) @@ -190,7 +190,7 @@ class StudentUpdateProgress(APIView): ) if pv.status == ProgressLesson.STATUSES.done: - Response(SecureProgressSerializer(pv.progress).data, status=200) + return Response(SecureProgressSerializer(pv.progress).data, status=200) if not pv.status == ProgressLesson.STATUSES.wait: if pv.checker == pv.progress.user: diff --git a/tests/fixtures/images/simple.jpg b/tests/fixtures/images/simple.jpg new file mode 100644 index 0000000..5dfea4c Binary files /dev/null and b/tests/fixtures/images/simple.jpg differ diff --git a/tests/fixtures/users.py b/tests/fixtures/users.py index 8116bd7..7c95a99 100644 --- a/tests/fixtures/users.py +++ b/tests/fixtures/users.py @@ -1,6 +1,6 @@ import pytest -from factories.users import UserFactory +from factories.users import UserFactory, AccountFactory @pytest.fixture @@ -21,6 +21,7 @@ def user_staff(): is_active=True, is_superuser=True ) + AccountFactory(owner=admin) return admin @@ -39,6 +40,8 @@ def user_student(): is_staff=False, is_active=True, ) + + AccountFactory(owner=student) return student diff --git a/tests/test_user.py b/tests/test_user.py index b85b636..5afc79d 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -7,9 +7,11 @@ from django.urls import reverse from rest_framework import status from rest_framework.generics import get_object_or_404 +from factories.users import USER_PASSWORD + @pytest.mark.django_db -@mock.patch('django.core.mail.send_mail') +@mock.patch('django.core.mail.EmailMessage.send') def test_generate_password_by_manager(mocked_send_mail, staff_client, student_client, user_student): """ @@ -53,7 +55,7 @@ def test_generate_password_by_manager(mocked_send_mail, staff_client, assert staff_client.post( reverse('users:management-password'), data=wrong_email, - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_404_NOT_FOUND ) @@ -70,5 +72,53 @@ def test_generate_password_by_manager_for_not_active_student(staff_client, assert staff_client.post( reverse('users:management-password'), data=data, - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_201_CREATED + ) + + +@pytest.mark.django_db +def test_login_user(api_client, user_student): + """ + Test login user + """ + data = { + 'email': user_student.email, + 'password': USER_PASSWORD + } + assert api_client.post( + reverse('users:login'), + data=data, + status=status.HTTP_200_OK + ) + + +@pytest.mark.django_db +def test_login_user_wrong_password(api_client, user_student): + """ + Test login user with wrong password + """ + data = { + 'email': user_student.email, + 'password': USER_PASSWORD + '1' + } + assert api_client.post( + reverse('users:login'), + data=data, + status=status.HTTP_403_FORBIDDEN + ) + + +@pytest.mark.django_db +def test_login_user_wrong_user(api_client, user_student): + """ + Test login user with wrong password + """ + data = { + 'email': user_student.email + '1', + 'password': USER_PASSWORD + } + assert api_client.post( + reverse('users:login'), + data=data, + status=status.HTTP_404_NOT_FOUND )