Compare commits

..

8 Commits

  1. 6
      .gitignore
  2. 23
      Envoy.blade.php
  3. 12
      access/factories.py
  4. 25
      access/groups.py
  5. 42
      access/management/commands/set_tokens.py
  6. 7
      access/models/other.py
  7. 33
      access/serializers.py
  8. 56
      access/tests/conftest.py
  9. 122
      access/tests/fixtures.py
  10. 120
      access/tests/test_access_view.py
  11. 2
      access/urls.py
  12. 28
      access/views.py
  13. 2
      api_v1/urls.py
  14. 31
      config_app/management/commands/for_test.py
  15. 5
      config_app/settings/prod.env
  16. 5
      config_app/settings/prod.env.skeleton
  17. 62
      courses/factories.py
  18. 0
      courses/factories/__init__.py
  19. 23
      courses/migrations/0006_auto_20180323_1743.py
  20. 13
      courses/models.py
  21. 14
      courses/serializers.py
  22. 18
      courses/tests/fixtures.py
  23. 4
      courses/urls.py
  24. 165
      courses/views.py
  25. 0
      factories/__init__.py
  26. 8
      finance/admin.py
  27. 72
      finance/factories.py
  28. 56
      finance/loggers.py
  29. 0
      finance/management/__init__.py
  30. 14
      finance/management/commands/yandex_repeat_payment.py
  31. 22
      finance/migrations/0004_bill_date.py
  32. 35
      finance/migrations/0005_auto_20180329_1346.py
  33. 37
      finance/migrations/0006_auto_20180330_1121.py
  34. 23
      finance/migrations/0007_auto_20180330_1452.py
  35. 28
      finance/migrations/0008_invoicerebilling.py
  36. 21
      finance/migrations/0009_invoicerebilling_pay_count.py
  37. 40
      finance/migrations/0010_auto_20180412_1628.py
  38. 126
      finance/models.py
  39. 6
      finance/serializers.py
  40. 15
      finance/signals.py
  41. 81
      finance/tasks.py
  42. 0
      finance/tests.py
  43. 58
      finance/tests/conftest.py
  44. 53
      finance/tests/fixtures.py
  45. 198
      finance/tests/test_finance_view.py
  46. 6
      finance/urls.py
  47. 407
      finance/views.py
  48. 12
      lms/celery.py
  49. 56
      lms/settings.py
  50. 21
      lms/settings_qa.py
  51. 520
      logs/celery.log
  52. 623
      logs/flower.log
  53. 121
      logs/schedule.log
  54. 20
      progress/migrations/0009_progress_is_freeze.py
  55. 20
      progress/migrations/0010_progress_exp_date.py
  56. 2
      progress/models.py
  57. 17
      progress/serializers.py
  58. 5
      progress/urls.py
  59. 176
      progress/views.py
  60. 2
      pytest.ini
  61. 6
      templates/mail/sales/back_set_bill.html
  62. 7
      templates/mail/sales/pay_access.html
  63. 57
      templates/yandex/test_pay.html
  64. 28
      tests/conftest.py
  65. 63
      tests/fixtures/users.py
  66. 124
      tests/test_user.py

6
.gitignore vendored

@ -3,7 +3,6 @@
# use glob syntax. # use glob syntax.
syntax: glob syntax: glob
logs/
docs/_build docs/_build
.idea .idea
.DS_Store .DS_Store
@ -37,5 +36,6 @@ coverage.xml
# Celery # Celery
celerybeat-schedule celerybeat-schedule
/config_app/settings/local.env /config_app/settings/dev.env
/config_app/settings/test.env
/media_qa/

@ -29,16 +29,11 @@ echo '{{ $new_release_dir }}'
@task('create_symlinks', ['on' => 'localhost']) @task('create_symlinks', ['on' => 'localhost'])
echo '>> Создание симлинков' echo '>> Создание симлинков'
ln -s {{ $app_dir }}/configs/master/ssl {{ $new_release_dir }}/ssl @if ($branch)
@if ($branch == 'master') ln -nfs {{ $app_dir }}/configs/master/prod.env {{ $new_release_dir }}/config_app/settings/prod.env
ln -nfs {{ $app_dir }}/configs/master/prod.env {{ $new_release_dir }}/config_app/settings/local.env ln -nfs {{ $app_dir }}/configs/dev/dev.env {{ $new_release_dir }}/config_app/settings/dev.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 ln -nfs {{ $app_dir }}/media/master {{ $new_release_dir }}/media
@endif @endif
ln -nfs {{ $app_dir }}/logs/{{ $branch }} {{ $new_release_dir }}/logs
@endtask @endtask
@task('install_req', ['on' => 'localhost']) @task('install_req', ['on' => 'localhost'])
@ -50,18 +45,10 @@ echo '>> Подтягиваем зависимости'
@endtask @endtask
@task('run_tests', ['on' => 'localhost']) @task('run_tests', ['on' => 'localhost'])
echo '>> Запускаем тесты' echo '>> Запускаем миграции тесты'
@if ($branch)
cd {{ $new_release_dir }}
source /env/bin/activate && python manage.py test
@endif
@endtask
@task('run_migrate', ['on' => 'localhost'])
echo '>> Запускаем миграции'
@if ($branch) @if ($branch)
cd {{ $new_release_dir }} cd {{ $new_release_dir }}
source /env/bin/activate && python manage.py migrate source /env/bin/activate && python manage.py migrate && python manage.py test
@endif @endif
@endtask @endtask

@ -11,7 +11,7 @@ from django.conf import settings
USER_PASSWORD = 'test' USER_PASSWORD = 'test'
AVATAR_SAMPLE_IMAGE = os.path.join(settings.IMAGE_SAMPLES_DIR, 'simple.jpg') SAMPLE_IMAGE = os.path.join(settings.IMAGE_SAMPLES_DIR, 'simple.jpg')
Faker = partial(factory.Faker, locale='ru_RU') Faker = partial(factory.Faker, locale='ru_RU')
@ -32,6 +32,14 @@ class UserFactory(factory.django.DjangoModelFactory):
class Meta: class Meta:
model = get_user_model() model = get_user_model()
@factory.post_generation
def groups(self, create, extracted, **kwargs):
if not create:
return
if extracted:
for group in extracted:
self.groups.add(group)
class AccountFactory(factory.django.DjangoModelFactory): class AccountFactory(factory.django.DjangoModelFactory):
b_day = Faker( b_day = Faker(
@ -42,7 +50,7 @@ class AccountFactory(factory.django.DjangoModelFactory):
city = Faker('city') city = Faker('city')
gender = factory.fuzzy.FuzzyChoice(range(1, 2)) gender = factory.fuzzy.FuzzyChoice(range(1, 2))
owner = factory.SubFactory(UserFactory) owner = factory.SubFactory(UserFactory)
photo = factory.django.ImageField(from_path=AVATAR_SAMPLE_IMAGE) photo = factory.django.ImageField(from_path=SAMPLE_IMAGE)
phone = Faker('phone_number') phone = Faker('phone_number')
class Meta: class Meta:

@ -0,0 +1,25 @@
ADMIN = 1
STUDENTS = 2
TEACHERS = 3
MANAGERS = 4
FINANCE_MANAGERS = 5
LEAD_MANAGERS = 6
CURATORS = 7
PARTNERS = 8
SUPPORTS = 9
FINANCE = 10
PROJECT_MANAGERS = 11
STATUS_CHOICES = (
(ADMIN, 'admin'),
(STUDENTS, 'students'),
(TEACHERS, 'teachers'),
(MANAGERS, 'managers'),
(FINANCE_MANAGERS, 'finance_managers'),
(LEAD_MANAGERS, 'lead_managers'),
(CURATORS, 'curators'),
(PARTNERS, 'partners'),
(SUPPORTS, 'supports'),
(FINANCE, 'finance'),
(PROJECT_MANAGERS, 'project_managers'),
)

@ -1,42 +0,0 @@
import csv
import jwt
from django.conf import settings
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Добавляет 1 или нескольких юзеров в указанные группы'
def add_arguments(self, parser):
parser.add_argument(
'--from',
type=str,
dest='from',
help='Файл подгрузки данных'
)
parser.add_argument(
'--to',
type=str,
dest='to',
help='Файл выгрузки'
)
def handle(self, *args, **options):
from_path = options['from']
to_path = options['to']
with open(from_path) as f:
with open(to_path, 'w') as out_f:
fw = csv.writer(out_f)
fr = csv.reader(f, delimiter=';')
for row in fr:
email = row[0]
course_token = row[1]
period = row[2]
payload = {
'period': period,
'course_token': course_token,
'email': email.lower(),
}
token = jwt.encode(payload, settings.COURSE_PROGRESS_SECRET_KEY, algorithm='HS256').decode("utf-8")
url = "https://go.skillbox.ru/api/v1/progress/progress_token/?token=%s" % str(token)
fw.writerow([email.lower(), url])

@ -53,13 +53,6 @@ class Account(models.Model):
def __str__(self): def __str__(self):
return self.owner.email 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: class Meta:
verbose_name = 'Дополнительная информация о пользователе' verbose_name = 'Дополнительная информация о пользователе'
verbose_name_plural = 'Дополнительная информация о пользователе' verbose_name_plural = 'Дополнительная информация о пользователе'

@ -3,12 +3,11 @@ from rest_framework import serializers
from access.models.other import Account from access.models.other import Account
from achievements.serialers import DiplomaSerializer, AchievementsSerializer from achievements.serialers import DiplomaSerializer, AchievementsSerializer
from progress.serializers import SecureProgressSerializer, SupportProgressSerializer from progress.serializers import SecureProgressSerializer
from django.utils import timezone
class AccountSerializer(serializers.ModelSerializer): class AccountSerializer(serializers.ModelSerializer):
gender = serializers.SerializerMethodField() gender = serializers.SerializerMethodField()
phone = serializers.SerializerMethodField()
class Meta: class Meta:
model = Account model = Account
@ -18,10 +17,6 @@ class AccountSerializer(serializers.ModelSerializer):
def get_gender(self): def get_gender(self):
return self.get_gender_display() return self.get_gender_display()
@staticmethod
def get_phone(self):
return self.get_phone()
class UserSelfSerializer(serializers.ModelSerializer): class UserSelfSerializer(serializers.ModelSerializer):
account = serializers.SerializerMethodField() account = serializers.SerializerMethodField()
@ -53,8 +48,7 @@ class UserSelfSerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_progresses(self): def get_progresses(self):
return [SecureProgressSerializer(i).data for i in return [SecureProgressSerializer(i).data for i in self.progress_set.all()]
self.progress_set.filter(is_freeze=False).exclude(exp_date__lt=timezone.now())]
class UserProfileSerializer(serializers.ModelSerializer): class UserProfileSerializer(serializers.ModelSerializer):
@ -90,7 +84,7 @@ class UserSearchSerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_phone(self): def get_phone(self):
return self.account.get_phone() return None if self.account.phone is None else self.account.phone.raw_input
@staticmethod @staticmethod
def get_pay(self): def get_pay(self):
@ -107,22 +101,3 @@ class UserSearchSerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_last_request(self): def get_last_request(self):
return self.useractivity.last_request 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()]

@ -0,0 +1,56 @@
import os
import shutil
import pytest
from tests.client import BetterAPIClient
pytest_plugins = [
'access.tests.fixtures',
]
def pytest_sessionfinish(session, exitstatus):
""" whole test run finishes. """
print('pytest finish: cleanup')
if os.path.exists('media_qa'):
shutil.rmtree('media_qa')
@pytest.fixture
def api_client():
"""Anonymous client for REST API."""
client = BetterAPIClient()
return client
@pytest.fixture
def admin_client(admin):
"""Authorized as admin(superuser) client for REST API."""
client = BetterAPIClient()
client.force_authenticate(user=admin)
return client
@pytest.fixture
def student_client(student):
"""Authorized as student client for REST API."""
client = BetterAPIClient()
client.force_authenticate(user=student)
return client
@pytest.fixture
def manager_client(manager):
"""Authorized as manager client for REST API."""
client = BetterAPIClient()
client.force_authenticate(user=manager)
return client
@pytest.fixture
def lead_manager_client(lead_manager):
"""Authorized as lead manager client for REST API."""
client = BetterAPIClient()
client.force_authenticate(user=lead_manager)
return client

@ -0,0 +1,122 @@
import pytest
from access.factories import UserFactory, AccountFactory
from access import groups
@pytest.fixture
def admin():
"""
Create user as staff with data:
email = 'admin@example.com'
password = 'test'
is_staff = True
is_active = True
"""
user = UserFactory(
email='admin@example.com',
is_staff=True,
is_active=True,
groups__name=(groups.ADMIN,)
)
AccountFactory(owner=user)
return user
@pytest.fixture
def student():
"""
Create user as student with data:
email = 'student@example.com'
password = 'test'
is_active = True
in groups 'students'
"""
user = UserFactory.create(
email='student@example.com',
is_staff=False,
is_active=True,
groups__name=(groups.STUDENTS,)
)
AccountFactory(owner=user)
return user
@pytest.fixture
def student_not_active():
"""
Create user as student with data:
email = 'notactivestudent@example.com'
password = 'test'
is_active = False
in groups 'students'
"""
user = UserFactory.create(
email='notactivestudent@example.com',
is_staff=False,
is_active=False,
groups__name=(groups.STUDENTS,)
)
return user
@pytest.fixture
def manager():
"""
Create user as manager with data:
email = 'manager@example.com'
password = 'test'
is_staff = True
is_active = True
in groups 'managers'
"""
user = UserFactory.create(
email='manager@example.com',
is_staff=True,
is_active=True,
groups=(groups.MANAGERS,)
)
AccountFactory(owner=user)
return user
@pytest.fixture
def lead_manager():
"""
Create user as lead manager with data:
email = 'lead_manager@example.com'
password = 'test'
is_staff = True
is_active = True
in groups 'lead_managers'
"""
user = UserFactory.create(
email='lead_manager@example.com',
is_staff=True,
is_active=True,
groups=(groups.LEAD_MANAGERS,)
)
AccountFactory(owner=user)
return user
@pytest.fixture
def teacher():
"""
Create user as teacher with data:
email = 'teacher@example.com'
password = 'test'
is_staff = True
is_active = True
in groups 'teachers'
"""
user = UserFactory.create(
email='teacher@example.com',
is_staff=True,
is_active=True,
groups=(groups.TEACHERS,)
)
AccountFactory(owner=user)
return user

@ -0,0 +1,120 @@
import mock
import pytest
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework import status
from rest_framework.generics import get_object_or_404
from access.factories import USER_PASSWORD
@pytest.mark.django_db
@mock.patch('django.core.mail.EmailMessage.send')
def test_generate_password_by_manager(mocked_send_mail, manager_client,
student_client, student):
"""
Test generate new password from admin area by manager
"""
assert manager_client.get(
reverse('users:management-password'),
status=status.HTTP_405_METHOD_NOT_ALLOWED
)
assert student_client.get(
reverse('users:management-password'),
status=status.HTTP_403_FORBIDDEN
)
data = {
'email': student.email,
}
assert manager_client.post(
reverse('users:management-password'),
data=data,
status=status.HTTP_201_CREATED
)
test_user = get_object_or_404(get_user_model(), email=student.email)
assert not test_user.check_password('test')
assert mocked_send_mail.call_count == 1
assert student_client.post(
reverse('users:management-password'),
data=data,
status=status.HTTP_403_FORBIDDEN
)
wrong_data = {
'email': 'no_user@example.com',
}
assert manager_client.post(
reverse('users:management-password'),
data=wrong_data,
status=status.HTTP_404_NOT_FOUND
)
wrong_email = {
'email': 'no_user@example',
}
assert manager_client.post(
reverse('users:management-password'),
data=wrong_email,
status=status.HTTP_404_NOT_FOUND
)
@pytest.mark.django_db
def test_generate_password_by_manager_for_not_active_student(manager_client, student_not_active):
"""
Test generate new password from admin area by manager for not active student
"""
data = {
'email': student_not_active.email,
}
assert manager_client.post(
reverse('users:management-password'),
data=data,
status=status.HTTP_201_CREATED
)
@pytest.mark.django_db
class TestLogin:
def test_login_user(self, api_client, student):
"""
Test login user
"""
data = {
'email': student.email,
'password': USER_PASSWORD
}
assert api_client.post(
reverse('users:login'),
data=data,
status=status.HTTP_200_OK
)
def test_login_user_wrong_password(self, api_client, student):
"""
Test login user with wrong password
"""
data = {
'email': student.email,
'password': USER_PASSWORD + '1'
}
assert api_client.post(
reverse('users:login'),
data=data,
status=status.HTTP_403_FORBIDDEN
)
def test_login_user_wrong_user(self, api_client, student):
"""
Test login user with wrong password
"""
data = {
'email': student.email + '1',
'password': USER_PASSWORD
}
assert api_client.post(
reverse('users:login'),
data=data,
status=status.HTTP_404_NOT_FOUND
)

@ -5,10 +5,12 @@ from access import views
urlpatterns = [ urlpatterns = [
url(r'progress_detail/(?P<token>[0-9A-Fa-f-]+)/$', progress.views.CourseProgressUserView.as_view()), url(r'progress_detail/(?P<token>[0-9A-Fa-f-]+)/$', progress.views.CourseProgressUserView.as_view()),
url(r'progress_dynamic/(?P<token>[0-9A-Fa-f-]+)/$', progress.views.CourseProgressDynamicView.as_view()),
url(r'teachers/$', views.TeacherListView.as_view()), url(r'teachers/$', views.TeacherListView.as_view()),
url(r'detail/(?P<out_key>[0-9A-Fa-f-]+)/$', views.DetailUserView.as_view()), url(r'detail/(?P<out_key>[0-9A-Fa-f-]+)/$', views.DetailUserView.as_view()),
url(r'detail/$', views.DetailUserView.as_view()), url(r'detail/$', views.DetailUserView.as_view()),
url(r'info/(?P<out_key>[0-9A-Fa-f-]+)/$', views.MinUserView.as_view()), url(r'info/(?P<out_key>[0-9A-Fa-f-]+)/$', views.MinUserView.as_view()),
url(r'guard/(?P<pk>[0-9]{1,99})/(?P<page>.+)/$', progress.views.UserGuardView.as_view()),
url(r'find/$', views.FindUserView.as_view()), url(r'find/$', views.FindUserView.as_view()),
url(r'registration/$', views.RegistrationView.as_view()), url(r'registration/$', views.RegistrationView.as_view()),
url(r'change_password/$', views.ChangePasswordView.as_view()), url(r'change_password/$', views.ChangePasswordView.as_view()),

@ -13,7 +13,7 @@ from django.shortcuts import redirect
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import permissions, generics from rest_framework import permissions
from access.models.other import Invite, ResetPassword, Account from access.models.other import Invite, ResetPassword, Account
from access.serializers import (UserSelfSerializer, UserSearchSerializer) from access.serializers import (UserSelfSerializer, UserSearchSerializer)
@ -28,10 +28,7 @@ class TeacherListView(APIView):
status_code = 200 status_code = 200
def get(self, request): def get(self, request):
return Response([{ return Response([i.email for i in get_user_model().objects.filter(groups__name='teachers')], self.status_code)
'email': i.email,
'token': i.out_key,
} for i in get_user_model().objects.filter(groups__name='teachers')], self.status_code)
class ResetPasswordView(APIView): class ResetPasswordView(APIView):
@ -128,7 +125,6 @@ class FindUserView(APIView):
class DetailUserView(APIView): class DetailUserView(APIView):
renderer_classes = (JSONRenderer,) renderer_classes = (JSONRenderer,)
permission_classes = (permissions.IsAuthenticated,)
@staticmethod @staticmethod
def post(request, out_key=None): def post(request, out_key=None):
@ -238,7 +234,7 @@ class ChangePasswordView(APIView):
@staticmethod @staticmethod
def post(request): def post(request):
if request.user.is_authenticated() and not request.user.check_password(request.JSON['old_password']): if request.user.is_authenticated() or not request.user.check_password(request.JSON['old_password']):
return Response("Неверный пароль", status=400) return Response("Неверный пароль", status=400)
request.user.set_password(request.JSON['new_password']) request.user.set_password(request.JSON['new_password'])
request.user.save() request.user.save()
@ -252,16 +248,22 @@ class LoginView(APIView):
def post(request): def post(request):
password = request.JSON.get('password') password = request.JSON.get('password')
email = request.JSON.get('email').lower() email = request.JSON.get('email').lower()
user = None
if not request.user.is_authenticated():
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: try:
user = get_user_model().objects.get(email=email) user = get_user_model().objects.get(email=email)
except get_user_model().DoesNotExist: except get_user_model().DoesNotExist:
return Response("Аккаунт не найден", status=404) return Response("Аккаунт не найден", status=404)
if not request.user.is_authenticated():
if not password == settings.MASTER_PASSWORD and not auth.authenticate(email=email, password=password):
return Response("Неверный пароль", status=403)
try: try:
auth.login(request, user) auth.login(request, user)
except AttributeError: except AttributeError:
@ -294,7 +296,7 @@ class MinUserView(APIView):
return Response("User not found", status=404) return Response("User not found", status=404)
class ManagementPassword(generics.GenericAPIView): class ManagementPassword(APIView):
permission_classes = (permissions.IsAuthenticated, permissions.IsAdminUser) permission_classes = (permissions.IsAuthenticated, permissions.IsAdminUser)
@staticmethod @staticmethod

@ -7,7 +7,7 @@ urlpatterns = [
url(r'courses/', include('courses.urls')), url(r'courses/', include('courses.urls')),
url(r'users/', include('access.urls', namespace='users')), url(r'users/', include('access.urls', namespace='users')),
url(r'library/', include('library.urls')), url(r'library/', include('library.urls')),
url(r'finance/', include('finance.urls')), url(r'finance/', include('finance.urls', namespace='finance')),
url(r'storage/', include('storage.urls')), url(r'storage/', include('storage.urls')),
url(r'progress/', include('progress.urls')), url(r'progress/', include('progress.urls')),
url(r'^docs/$', schema_view, name='api-docs'), url(r'^docs/$', schema_view, name='api-docs'),

@ -1,31 +0,0 @@
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Команда для тестов'
def handle(self, *args, **options):
from xml.dom import minidom
xx = """<?xml version="1.0" encoding="UTF-8"?>
<repeatCardPaymentResponse status="3" error="112" processedDT="2018-04-23T19:37:04.544+03:00" techMessage="Неверное значение параметра invoiceId" clientOrderId="unknown"/>"""
dom = minidom.parseString(xx)
dom.normalize()
node1 = dom.getElementsByTagName("repeatCardPaymentResponse")[0]
print(node1)
print(node1.getAttribute('error'))
print(node1.getAttribute('processedDT'))
print(node1.getAttribute('techMessage'))
# def handle(self, *args, **options):
# from finance.loggers import FinanceLogger
# logger = FinanceLogger()
# try:
# 10 / 0
# except Exception as exc:
# logger.exception('FinanceLogger test log record', extra={'asdf': 3246523},
# invoice_id=42, some_param={'a': 1, 'b': 3, 'привет': 'медвед'}
# )
#

@ -0,0 +1,5 @@
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

@ -1,5 +0,0 @@
DEBUG=False
SECRET_KEY='....'
DATABASE_URL='psql://<name>:<password>@127.0.0.1:5432/<db_name>'
EMAIL_URL='smtp+tls://<name>:<password>@smtp.gmail.com:587'
CACHE_URL=rediscache://127.0.0.1:6379/<db>?client_class=django_redis.client.DefaultClient

@ -0,0 +1,62 @@
import uuid
import factory
import factory.fuzzy
from functools import partial
from access.factories import UserFactory
from access import groups
IMAGE_URL = 'https://dummyimage.com/240x240/000/fff.png'
BIG_IMAGE_URL = 'https://dummyimage.com/1200x800/000/fff.png'
BIG_MOBILE_IMAGE_URL = 'https://dummyimage.com/600x400/000/fff.png'
Faker = partial(factory.Faker, locale='ru_RU')
class CourseFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'courses.Course'
token = factory.LazyFunction(uuid.uuid4)
slug = factory.LazyAttribute(lambda x: factory.Faker('sentence').
generate({'nb_words': 3}).replace(' ', '-').replace('.', ''))
title = factory.LazyAttribute(lambda x: Faker('sentence').generate({'nb_words': 4}))
description = factory.LazyAttribute(
lambda x: '\n'.join(Faker('paragraphs').generate({'nb': 15}))
)
direction = factory.fuzzy.FuzzyChoice(range(1, 5))
public = True
image = IMAGE_URL
big_image = BIG_IMAGE_URL
big_mobile_image = BIG_MOBILE_IMAGE_URL
@factory.lazy_attribute
def teacher_tokens(self):
teacher = UserFactory(groups=(groups.TEACHERS,))
return [teacher.out_key]
class TopicFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'courses.Topic'
course = factory.SubFactory(CourseFactory)
title = factory.LazyAttribute(lambda x: Faker('sentence').generate({'nb_words': 4}))
description = factory.LazyAttribute(
lambda x: '\n'.join(Faker('paragraphs').generate({'nb': 15}))
)
sort = 0
class LessonFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'courses.Lesson'
token = factory.LazyFunction(uuid.uuid4)
key = factory.LazyFunction(uuid.uuid4)
topic = factory.SubFactory(TopicFactory)
title = factory.LazyAttribute(lambda x: Faker('sentence').generate({'nb_words': 4}))
sort = 0

@ -1,23 +0,0 @@
# -*- 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',
),
]

@ -58,6 +58,8 @@ class Lesson(models.Model):
class Topic(models.Model): class Topic(models.Model):
course = models.ForeignKey(to="Course", verbose_name='курс') course = models.ForeignKey(to="Course", verbose_name='курс')
title = models.CharField(verbose_name='Название', max_length=255) 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='Поле сортировки') sort = models.SmallIntegerField(verbose_name='Поле сортировки')
def __str__(self): def __str__(self):
@ -72,13 +74,13 @@ class Topic(models.Model):
class CourseManager(models.Manager): class CourseManager(models.Manager):
def update_or_create_course(self, image=None, big_image=None, statistic=None, old_slug=None, def update_or_create_course(self, image=None, big_image=None, statistic=None,
big_mobile_image=None, slug=None, teacher_tokens=None, big_mobile_image=None, slug=None, teachers=None,
level=None, direction=None, **kwargs): level=None, direction=None, **kwargs):
slug = slug if slug else slugify(unidecode.unidecode(kwargs['title'])) slug = slug if slug else slugify(unidecode.unidecode(kwargs['title']))
kwargs['teacher_tokens'] = teacher_tokens kwargs['teacher_tokens'] = teachers
if image: if image:
path = 'course/image%s.png' % slug path = 'course/image%s.png' % slug
@ -99,14 +101,13 @@ class CourseManager(models.Manager):
kwargs['level'] = get_real_name(COURSE_LEVEL, level) kwargs['level'] = get_real_name(COURSE_LEVEL, level)
if direction: if direction:
kwargs['direction'] = get_real_name(COURSE_DIRECTION, direction[0]) kwargs['direction'] = get_real_name(COURSE_DIRECTION, direction)
try: try:
course = self.get(slug=old_slug) course = self.get(slug=slug)
for i in kwargs: for i in kwargs:
if kwargs[i]: if kwargs[i]:
setattr(course, i, kwargs[i]) setattr(course, i, kwargs[i])
course.slug = slug
course.save() course.save()
except ObjectDoesNotExist: except ObjectDoesNotExist:

@ -1,8 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from django.conf import settings
from courses.models import Course, Lesson, Topic from courses.models import Course, Lesson, Topic
import os
class TopicSerializer(serializers.ModelSerializer): class TopicSerializer(serializers.ModelSerializer):
@ -21,12 +19,11 @@ class MiniLessonSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Lesson model = Lesson
fields = ('title', 'free', 'token', 'is_hm', 'sort') fields = ('title', 'free', 'token', 'is_hm')
class LessonSerializer(MiniLessonSerializer): class LessonSerializer(MiniLessonSerializer):
course_slug = serializers.SerializerMethodField() course_slug = serializers.SerializerMethodField()
materials = serializers.SerializerMethodField()
class Meta: class Meta:
model = Lesson model = Lesson
@ -36,15 +33,6 @@ class LessonSerializer(MiniLessonSerializer):
def get_course_slug(self): def get_course_slug(self):
return self.topic.course.slug 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): class TeacherLessonSerializer(MiniLessonSerializer):
topic_sort = serializers.SerializerMethodField() topic_sort = serializers.SerializerMethodField()

@ -0,0 +1,18 @@
import pytest
from courses.factories import CourseFactory, LessonFactory, TopicFactory
@pytest.fixture
def course():
"""
Create course
"""
_course = CourseFactory()
topic = TopicFactory(
course=_course
)
LessonFactory(
topic=topic
)
return _course

@ -3,13 +3,9 @@ from django.conf.urls import url
from courses import views as views from courses import views as views
urlpatterns = [ urlpatterns = [
url(r'lesson/update/$', views.UpdateLessonView.as_view()),
url(r'lesson/delete/(?P<lesson_token>.+)/$', views.DeleteLessonView.as_view()),
url(r'lesson/teacher/(?P<token>.+)/$', views.LessonInfoView.as_view()), url(r'lesson/teacher/(?P<token>.+)/$', views.LessonInfoView.as_view()),
url(r'lesson/(?P<token>.+)/$', views.LessonDetail.as_view()), url(r'lesson/(?P<token>.+)/$', views.LessonDetail.as_view()),
url(r'tree/(?P<slug>.+)/$', views.TreeView.as_view()), url(r'tree/(?P<slug>.+)/$', views.TreeView.as_view()),
url(r'detail/(?P<slug>.+)/$', views.CourseDetailView.as_view()), url(r'detail/(?P<slug>.+)/$', views.CourseDetailView.as_view()),
url(r'topic/update/$', views.UpdateTopicView.as_view()),
url(r'topic/delete/(?P<topic_id>[0-9]{1,99})/$', views.DeleteTopicView.as_view()),
url(r'^$', views.CourseListView.as_view()), url(r'^$', views.CourseListView.as_view()),
] ]

@ -1,9 +1,10 @@
from jwt import DecodeError from jwt import DecodeError
from courses.models import Course, Lesson, Topic from courses.models import Course, Lesson
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from django.contrib.auth import get_user_model
from courses.serializers import CourseDetailSerializer, CourseTreeSerializer, LessonSerializer, TeacherLessonSerializer from courses.serializers import CourseDetailSerializer, CourseTreeSerializer, LessonSerializer, TeacherLessonSerializer
import jwt import jwt
@ -30,6 +31,27 @@ class CourseListView(APIView):
status_code = 200 status_code = 200
def post(self, request): 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()) course = Course.objects.update_or_create_course(**request.JSON.dict())
return Response(CourseDetailSerializer(course).data, status=self.status_code) return Response(CourseDetailSerializer(course).data, status=self.status_code)
@ -58,141 +80,6 @@ class CourseDetailView(APIView):
return Response(CourseDetailSerializer(Course.objects.get(slug=slug)).data, self.status_code) 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): class LessonInfoView(APIView):
renderer_classes = (JSONRenderer,) renderer_classes = (JSONRenderer,)
status_code = 200 status_code = 200
@ -222,7 +109,7 @@ class LessonDetail(APIView):
l = LessonSerializer(lesson).data l = LessonSerializer(lesson).data
try: 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']) else jwt.decode(jwt_token, settings.COURSE_PROGRESS_SECRET_KEY, algorithms=['HS256'])
except DecodeError: except DecodeError:
payload = None payload = None
@ -230,7 +117,7 @@ class LessonDetail(APIView):
course = lesson.topic.course course = lesson.topic.course
if payload is None: if payload is None:
if not (lesson.free or request.user.is_authenticated and request.user.is_staff): if not lesson.free:
return Response("Bad token", status=400) return Response("Bad token", status=400)
else: else:
@ -257,7 +144,7 @@ class LessonDetail(APIView):
if not new_lesson: if not new_lesson:
return Response("Permission denied", status=403) return Response("Permission denied", status=403)
# TODO Задача для селери #TODO Задача для селери
add_lesson(request.user.out_key, course.token, lesson.token, course.get_teacher(), lesson.is_hm) add_lesson(request.user.out_key, course.token, lesson.token, course.get_teacher(), lesson.is_hm)
return Response(l, status=200) return Response(l, status=200)

@ -1,13 +1,7 @@
# coding=utf-8 # coding=utf-8
from django.contrib import admin from django.contrib import admin
from finance.models import Bill, Invoice, InvoiceRebilling from finance.models import Bill, Invoice
class InvoiceAdmin(admin.ModelAdmin):
list_display = ('__str__', 'rebilling_on',)
admin.site.register(Bill) admin.site.register(Bill)
admin.site.register(Invoice) admin.site.register(Invoice)
admin.site.register(InvoiceRebilling, InvoiceAdmin)

@ -0,0 +1,72 @@
import uuid
import factory
import factory.fuzzy
from functools import partial
from django.utils import timezone
from yandex_money.models import Payment
from courses.factories import CourseFactory, TopicFactory, LessonFactory
from access.factories import UserFactory
from finance.models import Invoice
Faker = partial(factory.Faker, locale='ru_RU')
class BillFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'finance.Bill'
user = factory.SubFactory(UserFactory)
opener = factory.SubFactory(UserFactory)
description = Faker('text')
@factory.lazy_attribute
def course_token(self):
course = CourseFactory()
topic = TopicFactory(
course=course
)
LessonFactory(
topic=topic
)
return course.token
class InvoiceFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'finance.Invoice'
status = factory.fuzzy.FuzzyChoice(
(choice[0] for choice in Invoice.BILL_STATUSES)
)
price = factory.fuzzy.FuzzyDecimal(1000)
real_price = factory.fuzzy.FuzzyDecimal(1000)
method = factory.fuzzy.FuzzyChoice(
(choice[0] for choice in Invoice.BILL_METHOD)
)
key = factory.Faker('pyint')
yandex_pay = factory.SubFactory('factories.finance.PaymentFactory')
comment = Faker('word')
bill = factory.SubFactory(BillFactory)
class PaymentFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'yandex_money.Payment'
user = factory.SubFactory(UserFactory)
pub_date = timezone.now()
order_amount = factory.fuzzy.FuzzyDecimal(1000)
payment_type = factory.fuzzy.FuzzyChoice(
(choice[0] for choice in Payment.PAYMENT_TYPE.CHOICES)
)
order_number = factory.LazyFunction(uuid.uuid4)
status = factory.fuzzy.FuzzyChoice(
(choice[0] for choice in Payment.STATUS.CHOICES)
)
invoice_id = factory.Faker('pyint')
shop_amount = factory.fuzzy.FuzzyDecimal(1000)
performed_datetime = timezone.now()

@ -1,56 +0,0 @@
# -*- coding: utf-8 -*-
import logging
_logger = logging.getLogger('finance_data')
class FinanceLogger:
"""
Все kwargs попадют в %(finance_data)s и логируются
'format': '%(asctime)s - %(levelname)s - %(message)s - %(finance_data)s'
"""
def __init__(self, prefix=None):
self.prefix = prefix
def log(self, level, msg, *args, **kwargs):
_logger.log(level=level, msg=self._get_msg(msg), *args, **self._make_kwargs(kwargs))
def _get_msg(self, msg):
if self.prefix:
msg = '{}: {}'.format(self.prefix, msg)
return msg
def _make_kwargs(self, kwargs):
new_kwargs = {}
for inspected_kwarg in ('exc_info', 'stack_info', 'extra'):
try:
new_kwargs[inspected_kwarg] = kwargs.pop(inspected_kwarg)
except KeyError:
pass
if 'extra' in new_kwargs:
new_kwargs['extra']['finance_data'] = kwargs
else:
new_kwargs['extra'] = dict(finance_data=kwargs)
return new_kwargs
def debug(self, msg, *args, **kwargs):
self.log(level=logging.DEBUG, msg=msg, *args, **kwargs)
def info(self, msg, *args, **kwargs):
self.log(level=logging.INFO, msg=msg, *args, **kwargs)
def warning(self, msg, *args, **kwargs):
self.log(level=logging.WARNING, msg=msg, *args, **kwargs)
# TODO отделить логирование ошибок в другой лог
def error(self, msg, *args, **kwargs):
self.log(level=logging.ERROR, msg=msg, *args, **kwargs)
def critical(self, msg, *args, **kwargs):
self.log(level=logging.CRITICAL, msg=msg, *args, **kwargs)
def exception(self, msg, *args, **kwargs):
kwargs['stack_info'] = True
_logger.exception(self._get_msg(msg), *args, **self._make_kwargs(kwargs))

@ -1,14 +0,0 @@
from django.core.management.base import BaseCommand
from finance.loggers import FinanceLogger
from finance.tasks import periodic_billing
finance_logger = FinanceLogger() # prefix='YandexMoney'
class Command(BaseCommand):
def handle(self, *args, **options):
finance_logger.info("start console repeat payment command")
print('Started')
periodic_billing()

@ -1,22 +0,0 @@
# -*- 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,
),
]

@ -1,35 +0,0 @@
# -*- 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='Дата создания'),
),
]

@ -1,37 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-03-30 11:21
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('finance', '0005_auto_20180329_1346'),
]
operations = [
migrations.RemoveField(
model_name='installmentplan',
name='bill',
),
migrations.AddField(
model_name='invoice',
name='rebilling',
field=models.BooleanField(default=False, editable=False, verbose_name='Повторный платеж'),
),
migrations.AddField(
model_name='invoice',
name='rebilling_on',
field=models.BooleanField(default=False, editable=False, verbose_name='Повторять платеж'),
),
migrations.AlterField(
model_name='invoice',
name='date',
field=models.DateTimeField(auto_now_add=True),
),
migrations.DeleteModel(
name='InstallmentPlan',
),
]

@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-03-30 14:52
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('finance', '0006_auto_20180330_1121'),
]
operations = [
migrations.RemoveField(
model_name='invoice',
name='rebilling',
),
migrations.RemoveField(
model_name='invoice',
name='rebilling_on',
),
]

@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-03-30 14:52
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('finance', '0007_auto_20180330_1452'),
]
operations = [
migrations.CreateModel(
name='InvoiceRebilling',
fields=[
('invoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='finance.Invoice')),
('rebilling_on', models.BooleanField(default=False, editable=False, verbose_name='Повторять платеж')),
],
options={
'verbose_name': 'Повторный платёж',
'verbose_name_plural': 'Повторные платежи',
},
bases=('finance.invoice',),
),
]

@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-03-30 15:03
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('finance', '0008_invoicerebilling'),
]
operations = [
migrations.AddField(
model_name='invoicerebilling',
name='pay_count',
field=models.SmallIntegerField(default=2, editable=False, verbose_name='Всего платежей'),
preserve_default=False,
),
]

@ -1,40 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-04-12 16:28
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('finance', '0009_invoicerebilling_pay_count'),
]
operations = [
migrations.RemoveField(
model_name='invoicerebilling',
name='pay_count',
),
migrations.AddField(
model_name='bill',
name='freeze',
field=models.BooleanField(default=False, verbose_name='Отказ от платежей'),
),
migrations.AddField(
model_name='invoice',
name='date_of_payment',
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата фактической оплаты'),
),
migrations.AddField(
model_name='invoice',
name='expected_date',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Ожидаемая дата платежа'),
),
migrations.AlterField(
model_name='invoice',
name='date',
field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания платежа'),
),
]

@ -1,17 +1,9 @@
# coding=utf-8 # coding=utf-8
from dateutil.relativedelta import relativedelta
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMessage
from django.db import models from django.db import models
from django.utils import timezone
from yandex_money.models import Payment from yandex_money.models import Payment
import logging
from progress.models import Progress
from courses.models import Course, Lesson
logger_business_rules = logging.getLogger('business_rules')
class Bill(models.Model): class Bill(models.Model):
@ -21,85 +13,13 @@ class Bill(models.Model):
comment = models.TextField(verbose_name='Комментарий продавца', help_text='Будет показано пользователю', comment = models.TextField(verbose_name='Комментарий продавца', help_text='Будет показано пользователю',
blank=True, editable=False) blank=True, editable=False)
description = models.TextField(verbose_name='Внутренняя заметка', blank=True) description = models.TextField(verbose_name='Внутренняя заметка', blank=True)
date = models.DateTimeField(verbose_name="Дата выставления", auto_now_add=True)
freeze = models.BooleanField(verbose_name='Отказ от платежей', default=False)
def __str__(self): def __str__(self):
return '%s: %s' % (self.id, self.user) return '%s: %s' % (self.id, self.user)
def freeze_course(self):
if self.invoice_set.exclude(status='F').exists():
log = False
try:
p = Progress.objects.get(user=self.user, course_token=str(self.course_token))
p.is_freeze = True
p.save()
except Progress.DoesNotExist:
log = True
if log:
logger_business_rules.info('Отказ от платежей прошёл успешно', exc_info=True, extra={
'description': 'The privileges were not taken away, as they were not granted',
'user': self.user,
})
msg = EmailMessage(
'Вы откозались от оплаты по счёту',
"""Вы откозались от оплаты по счёту.
Вы сможете продолжить оплату в личном кабинете""",
to=[self.user.email],
bcc=[self.opener.email],
reply_to=[self.opener.email],
)
msg.send()
self.freeze = True
self.save()
else:
logger_business_rules.warning('Попытка нарушения правила отказа от платежей', exc_info=True, extra={
'description': 'All payments already paid',
'user': self.user,
})
def unfreeze_course(self, force=False):
if force or self.invoice_set.exclude(status='F')\
.filter(expected_date__lt=timezone.now() + relativedelta(days=1)).exists():
if self.invoice_set.filter(status='F').exclude(expected_date__lt=timezone.now()).exists():
try:
p = Progress.objects.get(user=self.user, course_token=str(self.course_token))
p.is_freeze = False
p.save()
except Progress.DoesNotExist:
pass
msg = EmailMessage(
'Вы возобновили оплату по счёту',
"""Вы возобновили оплату по счёту.""",
to=[self.user.email],
bcc=[self.opener.email],
reply_to=[self.opener.email],
)
msg.send()
self.freeze = False
self.save()
else:
logger_business_rules.warning('Попытка нарушения правила возобновления платежей', exc_info=True, extra={
'description': 'Excepted date more than one day',
'user': self.user,
})
def get_full_price(self): def get_full_price(self):
return sum([i.price for i in self.invoice_set.all() if not i.price is None]) 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: class Meta:
verbose_name = 'Счет' verbose_name = 'Счет'
verbose_name_plural = 'Счета' verbose_name_plural = 'Счета'
@ -123,7 +43,7 @@ class Invoice(models.Model):
('C', 'Отклонен'), ('C', 'Отклонен'),
) )
status = models.CharField(verbose_name='Статус', max_length=1, default='W', choices=BILL_STATUSES) status = models.CharField(verbose_name='Статус', max_length=1, default='W', choices=BILL_STATUSES)
price = models.IntegerField(verbose_name='Сумма', editable=False, null=True, blank=True) #Todo На самом деле тут не далжно быть значений null price = models.IntegerField(verbose_name='Сумма', editable=False, null=True, blank=True)
real_price = models.FloatField(verbose_name='Полученная сумма', null=True, blank=True, real_price = models.FloatField(verbose_name='Полученная сумма', null=True, blank=True,
help_text='Сумма, минус комиссия', editable=False) help_text='Сумма, минус комиссия', editable=False)
method = models.CharField(verbose_name='Способ оплаты', max_length=2, default='Y', choices=BILL_METHOD) method = models.CharField(verbose_name='Способ оплаты', max_length=2, default='Y', choices=BILL_METHOD)
@ -133,24 +53,7 @@ class Invoice(models.Model):
blank=True, editable=False) blank=True, editable=False)
bill = models.ForeignKey(to=Bill, verbose_name="Связный счёт") bill = models.ForeignKey(to=Bill, verbose_name="Связный счёт")
is_open = models.BooleanField(default=True, verbose_name="Открывает ли платёж курс") is_open = models.BooleanField(default=True, verbose_name="Открывает ли платёж курс")
date = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания платежа") date = models.DateTimeField(auto_now_add=True)
expected_date = models.DateTimeField(default=timezone.now, verbose_name="Ожидаемая дата платежа")
date_of_payment = models.DateTimeField(verbose_name="Дата фактической оплаты", blank=True, null=True)
def get_comment(self):
return '''Вам выставлен счёт,''' if \
self.comment == "" else self.comment
def send_link(self):
msg = EmailMessage(
'Вам выставлен новый счёт',
"""%s для оплаты перейдите по ссылке
%s/api/v1/finance/payment/%s/""" % (self.get_comment(), settings.DOMAIN, self.yandex_pay.id),
to=[self.yandex_pay.cps_email],
bcc=[self.bill.opener.email],
reply_to=[self.bill.opener.email],
)
msg.send()
def __str__(self): def __str__(self):
return '%s:%s %s' % (self.id, self.get_status_display(), self.bill.user) return '%s:%s %s' % (self.id, self.get_status_display(), self.bill.user)
@ -158,26 +61,3 @@ class Invoice(models.Model):
class Meta: class Meta:
verbose_name = 'Платёж' verbose_name = 'Платёж'
verbose_name_plural = 'Платежи' verbose_name_plural = 'Платежи'
class InvoiceRebilling(Invoice):
rebilling_on = models.BooleanField(verbose_name='Повторять платеж', default=False, editable=False)
def create_child_pays(self, count):
# TODO Заменить expected_date
for idx in range(1, int(count)):
InvoiceRebilling.objects.create(
bill=self.bill,
comment=self.comment,
method=self.method,
status='W',
is_open=self.is_open,
price=self.price,
rebilling_on=False,
key=self.key,
expected_date=(timezone.now() + relativedelta(months=idx)),
)
class Meta:
verbose_name = 'Повторный платёж'
verbose_name_plural = 'Повторные платежи'

@ -28,18 +28,18 @@ class BillSerializer(serializers.ModelSerializer):
class InvoiceSerializer(serializers.ModelSerializer): class InvoiceSerializer(serializers.ModelSerializer):
status = serializers.SerializerMethodField() status = serializers.SerializerMethodField()
method = serializers.SerializerMethodField() method = serializers.SerializerMethodField()
yandex_pay = serializers.SerializerMethodField() yandex_pay_id = serializers.SerializerMethodField()
class Meta: class Meta:
model = Invoice model = Invoice
fields = '__all__' exclude = ('bill',)
@staticmethod @staticmethod
def get_status(self): def get_status(self):
return self.get_status_display() return self.get_status_display()
@staticmethod @staticmethod
def get_yandex_pay(self): def get_yandex_pay_id(self):
return None if self.yandex_pay is None else self.yandex_pay.id return None if self.yandex_pay is None else self.yandex_pay.id
@staticmethod @staticmethod

@ -16,6 +16,18 @@ def invoice_signal(instance, **kwargs):
course = Course.objects.get(token=instance.bill.course_token) 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.status == 'F':
if instance.is_open: if instance.is_open:
try: try:
@ -40,7 +52,8 @@ def invoice_signal(instance, **kwargs):
'''Вам открыт доступ к курсу "%s", вы можете перейти по ссылке и '''Вам открыт доступ к курсу "%s", вы можете перейти по ссылке и
ознакомиться с материалами %s/course/%s''' ознакомиться с материалами %s/course/%s'''
% (course.title, settings.DOMAIN, course.slug), % (course.title, settings.DOMAIN, course.slug),
to=[instance.bill.user.email], 'robo@skillbox.ru',
[instance.bill.user.email],
bcc=[instance.bill.opener.email], bcc=[instance.bill.opener.email],
reply_to=[instance.bill.opener.email], reply_to=[instance.bill.opener.email],
) )

@ -1,81 +0,0 @@
import logging
import os
import requests
from django.conf import settings
from django.db import transaction
from django.utils import timezone
from yandex_money.models import Payment
from finance.loggers import FinanceLogger
from finance.models import InvoiceRebilling, Invoice
from lms import celery_app
finance_logger = FinanceLogger()
@celery_app.task
def periodic_billing():
finance_logger.info("start periodic billing task")
# TODO заюзать Invoice.BILL_STATUSES
invoices = InvoiceRebilling.objects.filter(method='Y', status='W')
# TODO тут был exclude('F') то есть все неоплаченные... но это не верно!
for invoice in invoices.filter(expected_date__lt=timezone.now()):
# выбираем все необработанные из прошлого
with transaction.atomic():
try:
_yandex_repeat_card_payment(invoice)
except Exception as exc:
finance_logger.exception('Yandex Money repeatCardPayment Exception', invoice_id=invoice.id)
invoice.comment += 'Yandex Money: Ошибка при попытке повторного платежа, свяжитесь с клиентской службой'
invoice.save()
def _yandex_repeat_card_payment(invoice):
user = invoice.bill.user
yandex_pay = Payment.objects.create(
order_amount=invoice.price,
customer_number=user.id,
user=user,
cps_email=user.email,
shop_id=settings.YANDEX_MONEY_REBILLING_SHOP_ID,
scid=settings.YANDEX_MONEY_REBILLING_SCID
)
invoice.yandex_pay = yandex_pay
finance_logger.info('YandexMoney repeatCardPayment start', invoice_id=invoice.id)
resp = requests.post(
url=settings.YANDEX_MONEY_MWS_URL + 'repeatCardPayment',
data={
'clientOrderId': invoice.id, # уникальное возрастающее целое число
'invoiceId': invoice.key,
'amount': invoice.price,
'orderNumber': invoice.yandex_pay.order_number
},
cert=(
os.path.join(settings.SSL_ROOT, 'skillbox.cer'),
os.path.join(settings.SSL_ROOT, 'skillbox.key')
),
verify=os.path.join(settings.SSL_ROOT, 'yamoney_chain.cer')
)
resp_text = resp.text
finance_logger.info('YandexMoney repeatCardPayment response',
invoice_id=invoice.id, response=resp_text, code=resp.status_code, )
try:
_check_yandex_response_status(invoice, resp_text)
except Exception as exc:
finance_logger.exception("Can't parse yandex response", invoice_id=invoice.id, response=resp_text)
def _check_yandex_response_status(invoice, resp_text):
from xml.dom import minidom
dom = minidom.parseString(resp_text)
dom.normalize()
resp_node = dom.getElementsByTagName("repeatCardPaymentResponse")[0]
status = resp_node.getAttribute('status')
if status != '0':
error = resp_node.getAttribute('error')
processed_dt = resp_node.getAttribute('processedDT')
tech_message = resp_node.getAttribute('techMessage')
invoice.status = Invoice.BILL_STATUSES[-1][0]
invoice.comment += 'Yandex Money: ошибка № {}, сообщение {} от {}'.format(error, tech_message, processed_dt)

@ -0,0 +1,58 @@
import os
import shutil
import pytest
from tests.client import BetterAPIClient
pytest_plugins = [
'access.tests.fixtures',
'finance.tests.fixtures',
'courses.tests.fixtures'
]
def pytest_sessionfinish(session, exitstatus):
""" whole test run finishes. """
print('pytest finish: cleanup')
if os.path.exists('media_qa'):
shutil.rmtree('media_qa')
@pytest.fixture
def api_client():
"""Anonymous client for REST API."""
client = BetterAPIClient()
return client
@pytest.fixture
def admin_client(admin):
"""Authorized as admin(superuser) client for REST API."""
client = BetterAPIClient()
client.force_authenticate(user=admin)
return client
@pytest.fixture
def student_client(student):
"""Authorized as student client for REST API."""
client = BetterAPIClient()
client.force_authenticate(user=student)
return client
@pytest.fixture
def manager_client(manager):
"""Authorized as manager client for REST API."""
client = BetterAPIClient()
client.force_authenticate(user=manager)
return client
@pytest.fixture
def lead_manager_client(lead_manager):
"""Authorized as lead manager client for REST API."""
client = BetterAPIClient()
client.force_authenticate(user=lead_manager)
return client

@ -0,0 +1,53 @@
import pytest
from yandex_money.models import Payment
from finance.factories import BillFactory, InvoiceFactory, PaymentFactory
PRICE = 1000.00
METHOD_CASH = 'C'
METHOD_YANDEX = 'Y'
STATUS_WAIT = 'W'
STATUS_ON_PAYMENT = 'P'
STATUS_PAID = 'F'
@pytest.fixture
def bill_cash(student, manager):
"""
Create bill for the student
"""
_bill = BillFactory(
user=student,
opener=manager,
)
InvoiceFactory(
status=STATUS_ON_PAYMENT,
method=METHOD_CASH,
yandex_pay=None,
bill=_bill
)
return _bill
@pytest.fixture
def bill_yandex(student, manager):
"""
Create bill for the student
"""
_bill = BillFactory(
user=student,
opener=manager,
)
# credit cart
payment = PaymentFactory(
user=student,
payment_type=Payment.PAYMENT_TYPE.AC,
status=Payment.STATUS.PROCESSED
)
InvoiceFactory(
status=STATUS_ON_PAYMENT,
method=METHOD_YANDEX,
yandex_pay=payment,
bill=_bill
)
return _bill

@ -0,0 +1,198 @@
import json
import mock
import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.response import Response
from yandex_money.models import Payment
from finance import models
from progress.models import Progress, ProgressLesson
from finance.tests.fixtures import PRICE, METHOD_CASH, STATUS_WAIT, STATUS_PAID
DUMMY_COMMENT = 'test comment'
DUMMY_DESCRIPTION = 'test description'
mock.patch('lms.global_decorators.transaction_decorator', lambda x: x).start()
@pytest.mark.django_db
def test_list_bills(api_client, student_client, student, manager, admin_client,
manager_client, lead_manager_client, bill_cash):
"""
Test for get list of bills
"""
assert api_client.get(
reverse('finance:bills'),
status=status.HTTP_403_FORBIDDEN
)
response = student_client.get(
reverse('finance:bills'),
status=status.HTTP_200_OK
)
results_student = json.loads(response.content)
assert len(results_student) is 1
assert results_student[0]['opener'] == manager.email
assert results_student[0]['user'] == student.email
response_admin = admin_client.get(
reverse('finance:bills'),
status=status.HTTP_200_OK
)
results_admin = json.loads(response_admin.content)
assert len(results_admin) is 0
response_manager = manager_client.get(
reverse('finance:bills'),
status=status.HTTP_200_OK
)
results_manager = json.loads(response_manager.content)
assert results_manager[0]['opener'] == manager.email
assert results_manager[0]['user'] == student.email
response_lead_manager = lead_manager_client.get(
reverse('finance:bills'),
status=status.HTTP_200_OK
)
results_lead_manager = json.loads(response_lead_manager.content)
assert len(results_lead_manager) is 0
@pytest.mark.django_db
def test_create_bill(api_client, student_client, manager_client,
manager, student, course):
"""
Test create bill
"""
data = {
'bill': {
'course_token': course.token.hex,
'opener': manager.email,
'user': student.email,
'comment': DUMMY_COMMENT,
'description': DUMMY_DESCRIPTION
},
'children': [{
'status': STATUS_WAIT,
'method': METHOD_CASH,
'price': PRICE,
'real_price': PRICE,
}]
}
response = manager_client.post(
reverse('finance:bills'),
data=data,
status=status.HTTP_200_OK
)
results = json.loads(response.content)
assert models.Bill.objects.get(user=student).id == results['bill']['id']
assert models.Invoice.objects.get(
bill=results['bill']['id']).id == results['children'][0]['id']
assert api_client.post(
reverse('finance:bills'),
data=data,
status=status.HTTP_403_FORBIDDEN
)
assert student_client.post(
reverse('finance:bills'),
data=data,
status=status.HTTP_403_FORBIDDEN
)
@pytest.mark.django_db
@mock.patch('django.core.mail.EmailMessage.send')
def test_confirm_pay_manager(mocked_email, manager_client, bill_cash, student):
assert Progress.objects.count() is 0
assert ProgressLesson.objects.count() is 0
data = {
'bill': {
'course_token': bill_cash.course_token.hex,
'opener': bill_cash.opener.email,
'user': bill_cash.user.email,
'comment': bill_cash.comment,
'description': bill_cash.description
},
'children': [{
'status': STATUS_PAID,
'method': METHOD_CASH,
'price': PRICE,
'real_price': PRICE,
}]
}
response = manager_client.post(
reverse('finance:bills'),
data=data,
status=status.HTTP_200_OK
)
results = json.loads(response.content)
assert models.Bill.objects.get(user=student).id == results['bill']['id']
assert Progress.objects.count() is 1
assert ProgressLesson.objects.count() is 1
progress = Progress.objects.filter(user=bill_cash.user).first()
assert Progress.objects.filter(user=bill_cash.user).exists() is True
assert ProgressLesson.objects.filter(progress=progress).exists() is True
assert mocked_email.call_count == 2
def fake_post_success(view, request):
pay = Payment.objects.get(order_number=request.data['orderNumber'])
pay.status = Payment.STATUS.SUCCESS
pay.save()
return Response('ok', status=status.HTTP_200_OK)
@pytest.mark.django_db
@mock.patch('django.core.mail.EmailMessage.send')
@mock.patch('finance.views.YandexCheckView.post', fake_post_success)
def test_confirm_pay_auto_success(mocked_email, api_client, bill_yandex, student):
assert Progress.objects.count() is 0
assert ProgressLesson.objects.count() is 0
invoice = bill_yandex.invoice_set.first()
payment = invoice.yandex_pay
data = {
'orderNumber': payment.order_number,
'shopSumAmount': str(payment.shop_amount)
}
api_client.post(
reverse('yandex_money_check'),
data=data,
status=status.HTTP_200_OK
)
assert Progress.objects.count() is 1
assert ProgressLesson.objects.count() is 1
progress = Progress.objects.filter(user=bill_yandex.user).first()
assert Progress.objects.filter(user=bill_yandex.user).exists() is True
assert ProgressLesson.objects.filter(progress=progress).exists() is True
assert mocked_email.call_count == 1
def fake_post_fail(view, request):
pay = Payment.objects.get(order_number=request.data['orderNumber'])
pay.status = Payment.STATUS.FAIL
pay.save()
return Response('fail', status=status.HTTP_200_OK)
@pytest.mark.django_db
@mock.patch('django.core.mail.EmailMessage.send')
@mock.patch('finance.views.YandexCheckView.post', fake_post_fail)
def test_confirm_pay_auto_fail(mocked_email, api_client, bill_yandex, student):
assert Progress.objects.count() is 0
assert ProgressLesson.objects.count() is 0
invoice = bill_yandex.invoice_set.first()
payment = invoice.yandex_pay
data = {
'orderNumber': payment.order_number,
'shopSumAmount': str(payment.shop_amount)
}
api_client.post(
reverse('yandex_money_check'),
data=data,
status=status.HTTP_200_OK
)
assert Progress.objects.count() is 0
assert ProgressLesson.objects.count() is 0
assert Progress.objects.filter(user=bill_yandex.user).exists() is False
assert mocked_email.call_count == 1

@ -3,13 +3,9 @@ from finance import views
urlpatterns = [ urlpatterns = [
url(r'payment/([0-9]{1,99})/$', views.YandexPay.as_view()), url(r'payment/([0-9]{1,99})/$', views.YandexPay.as_view()),
url(r'bill/([0-9]{1,99})/freeze/$', views.FreezeView.as_view()), url(r'bills/$', views.BillListView.as_view(), name='bills'),
url(r'bill/([0-9]{1,99})/unfreeze/$', views.UnFreezeView.as_view()),
url(r'bills/$', views.BillListView.as_view()),
url(r'bills/([0-9]{1,99})/$', views.BillDetailView.as_view()), url(r'bills/([0-9]{1,99})/$', views.BillDetailView.as_view()),
url(r'bills_find/$', views.FindBillView.as_view()), url(r'bills_find/$', views.FindBillView.as_view()),
url(r'yandex/fail/$', views.YandexFailView.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), url(r'invoices/$', views.get_invoices),
url(r'test/pay/$', views.test_pay),
] ]

@ -1,73 +1,27 @@
import csv import csv
import uuid import logging
import requests import requests
from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.core.mail import EmailMultiAlternatives
from django.db import IntegrityError
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponse, HttpResponseForbidden from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import redirect, render_to_response from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.html import strip_tags
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from yandex_money.models import Payment from yandex_money.models import Payment
from django.conf import settings
from courses.api import CourseParamsApi
from courses.models import Course from courses.models import Course
from finance.loggers import FinanceLogger from courses.api import CourseParamsApi
from finance.models import Bill, Invoice, InvoiceRebilling from finance.models import Bill, Invoice
from finance.serializers import BillSerializer, InvoiceSerializer from finance.serializers import BillSerializer, InvoiceSerializer
from lms.global_decorators import transaction_decorator from lms.global_decorators import transaction_decorator
from lms.tools import get_real_name from lms.tools import get_real_name
from django.utils import timezone
finance_logger = FinanceLogger() # prefix='YandexMoney' logger_yandex = logging.getLogger('yandex_money')
def test_pay(request):
return render_to_response('yandex/test_pay.html', context={
'shop_id': settings.YANDEX_MONEY_SHOP_ID,
'sc_id': settings.YANDEX_MONEY_TEST_SCID,
'order_number': str(uuid.uuid4())[:12],
'user_email': request.user.email if request.user.is_authenticated else 'user@domain.com'
})
class FreezeView(APIView):
renderer_classes = (JSONRenderer,)
@staticmethod
def post(request, pk):
try:
bill = Bill.objects.get(id=pk)
except Bill.DoesNotExist:
return Response("Счёт не найден", status=404)
if request.user.is_authenticated and request.user.email == bill.user.email:
bill.freeze_course()
return Response(status=204)
return Response("Permission denied", status=403)
class UnFreezeView(APIView):
renderer_classes = (JSONRenderer,)
@staticmethod
def post(request, pk):
try:
bill = Bill.objects.get(id=pk)
except Bill.DoesNotExist:
return Response("Счёт не найден", status=404)
if request.user.is_authenticated and request.user.email == bill.user.email:
bill.unfreeze_course()
return Response(status=204)
return Response("Permission denied", status=403)
class BillListView(APIView): class BillListView(APIView):
@ -86,164 +40,84 @@ class BillListView(APIView):
def post(self, request): def post(self, request):
if request.user.is_authenticated and (request.user.groups.filter(name__in=['managers','lead_managers']).exists() if request.user.is_authenticated and (request.user.groups.filter(name__in=['managers','lead_managers']).exists()
or request.user.is_superuser): or request.user.is_superuser):
user = get_user_model().objects.get(email=request.JSON.get('user')) bill = request.JSON.get('bill')
opener = get_user_model().objects.get(email=request.JSON.get('opener')) children = request.JSON.get('children', [])
description = request.JSON.get('description', None)
comment = request.JSON.get('comment', None)
course_token = request.JSON.get('course_token', None)
if course_token is None: if bill:
return Response("Идентификатор курса не передан", status=400) 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: try:
bill_obj = Bill.objects.get(user=user, course_token=course_token) bill_obj = Bill.objects.get(user=user, course_token=course_token)
except Bill.DoesNotExist: except Bill.DoesNotExist:
try:
bill_obj = Bill.objects.create(user=user, course_token=course_token) 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.opener = opener
bill_obj.description = bill_obj.description if description is None else description bill_obj.description = description
bill_obj.comment = bill_obj.comment if comment is None else comment bill_obj.comment = comment
bill_obj.save() bill_obj.save()
return Response(bill_obj.id, status=200) for i in children:
status = get_real_name(elem=i['status'], array=Invoice.BILL_STATUSES)
return Response("Ошибка доступа, возможно вы разлогинились из другой вкладки браузера", status=403)
class InvoiceDetailView(APIView):
renderer_classes = (JSONRenderer,)
@staticmethod
def delete(request, invoice_id):
try: try:
i = Invoice.objects.get(id=invoice_id) invoice_id = i['id']
if not i.status == "F" and not (i.bill.check_pay() and i.is_open): except KeyError:
i.delete() invoice_id = None
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)
pay_count = int(request.JSON.get('pay_count', '1'))
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)
try: try:
bill = Bill.objects.get(id=bill_id) if not invoice_id is None:
except Bill.DoesNotExist: invoice = Invoice.objects.get(id=i['id'])
return Response('Не найден счёт с id=%s' % bill_id, status=404) if invoice.status == "P" or invoice.status == status:
continue
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)
if pay_count > 1:
invoice = InvoiceRebilling.objects.create(
bill=bill,
method=method,
status=status,
is_open=is_open,
rebilling_on=True,
)
else: else:
try: raise Invoice.DoesNotExist
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": except Invoice.DoesNotExist:
# TODO это никогда не выполнится i['method'] = get_real_name(elem=i['method'], array=Invoice.BILL_METHOD)
invoice.real_price = invoice.real_price if real_price is None else real_price i['status'] = status
i['bill'] = bill_obj
if bill.check_pay() and (invoice.price < price): i['yandex_pay'] = None
return Response("""Нельзя менять стоимость по счёту в большую сторону, invoice = Invoice.objects.create(**i)
когда один из платежей оплачен""", status=400)
if i['method'] == 'Y' and invoice.yandex_pay is None:
invoice.price = price yandex_pay = Payment.objects.create(
invoice.is_open = is_open order_amount=i['price'],
invoice.comment = comment
if pay_count > 1:
invoice.create_child_pays(pay_count)
if invoice.method == 'Y' and invoice.yandex_pay is None:
yandex_pay = Payment(
order_amount=invoice.price,
shop_amount=0, shop_amount=0,
customer_number=bill.user.id, customer_number=bill_obj.user.id,
user=bill.user, user=bill_obj.user,
cps_email=bill.user.email, cps_email=bill_obj.user.email,
) )
if pay_count > 1:
yandex_pay.shop_id = settings.YANDEX_MONEY_REBILLING_SHOP_ID
yandex_pay.scid = settings.YANDEX_MONEY_REBILLING_SCID
yandex_pay.save()
invoice.yandex_pay = yandex_pay invoice.yandex_pay = yandex_pay
invoice.send_link() invoice.save()
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,
}
subject, to = 'Выставлен новый счёт', invoice.bill.opener.email
html_content = render_to_string('mail/sales/back_set_bill.html', context) msg = EmailMessage(
text_content = strip_tags(html_content) 'Выставлен новый счёт.',
'''Менеджер %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 = EmailMultiAlternatives(subject, text_content, to=[to], bcc=['dmitry.dolya@skillbox.ru'])
msg.attach_alternative(html_content, "text/html")
msg.send() msg.send()
invoice.save() res = {
"bill": BillSerializer(bill_obj).data,
"children": [InvoiceSerializer(i).data for i in bill_obj.invoice_set.all()],
}
return Response(InvoiceSerializer(invoice).data, status=200) return Response(res, status=200)
return Response("Invoice detail access only for manager users", status=403) return Response("Bill not set", status=400)
return Response("Course detail access only for manager users", status=403)
class BillDetailView(APIView): class BillDetailView(APIView):
@ -284,9 +158,7 @@ class FindBillView(APIView):
if key: if key:
res = Bill.objects.filter( res = Bill.objects.filter(
Q(opener__email__contains=key.lower()) Q(opener__email__contains=key.lower()) | Q(user__email__contains=key.lower())
| Q(user__email__contains=key.lower())
| Q(id__contains=key)
) )
else: else:
@ -309,10 +181,6 @@ class YandexPay(APIView):
def get(request, pk): def get(request, pk):
try: try:
pay = Payment.objects.get(id=pk) pay = Payment.objects.get(id=pk)
try:
inv = InvoiceRebilling.objects.get(yandex_pay=pay)
except InvoiceRebilling.DoesNotExist:
inv = None
r = requests.post('https://money.yandex.ru/eshop.xml', data={ r = requests.post('https://money.yandex.ru/eshop.xml', data={
'shopId': pay.shop_id, 'shopId': pay.shop_id,
'scid': pay.scid, 'scid': pay.scid,
@ -320,12 +188,10 @@ class YandexPay(APIView):
'customerNumber': pay.customer_number, 'customerNumber': pay.customer_number,
'orderNumber': pay.order_number, 'orderNumber': pay.order_number,
'cps_email': pay.cps_email, 'cps_email': pay.cps_email,
'rebillingOn': False if inv is None else inv.rebilling_on,
'shopSuccessURL': settings.YANDEX_MONEY_SUCCESS_URL, 'shopSuccessURL': settings.YANDEX_MONEY_SUCCESS_URL,
'shopFailURL': settings.YANDEX_MONEY_FAIL_URL, 'shopFailURL': settings.YANDEX_MONEY_FAIL_URL,
}) })
try:
msg = EmailMessage( msg = EmailMessage(
'Пользователь перешёл на страницу оплаты.', 'Пользователь перешёл на страницу оплаты.',
'''Пользователь "%s" перешёл на страницу оплаты курса "%s".''' '''Пользователь "%s" перешёл на страницу оплаты курса "%s".'''
@ -336,8 +202,6 @@ class YandexPay(APIView):
) )
msg.send() msg.send()
except Exception as exc:
finance_logger.exception('YandexPay: Email not sended')
return redirect(r.url) return redirect(r.url)
@ -388,41 +252,46 @@ class YandexCheckView(APIView):
@staticmethod @staticmethod
def post(request): def post(request):
data = dict() data = dict()
# TODO заюзать url.parse https://goo.gl/s8Ygos
for i in request.body.decode('utf-8').split('&'): for i in request.body.decode('utf-8').split('&'):
key = i.split('=')[0] key = i.split('=')[0]
val = i.split('=')[1] val = i.split('=')[1]
data[key] = val data[key] = val
finance_logger.info('Проверка платежа запрос', request=data) logger_yandex.info('Проверка платежа запрос', exc_info=True, extra={
'request': data,
})
order_number = data['orderNumber']
try: try:
pay = Payment.objects.get(order_number=order_number) pay = Payment.objects.get(order_number=data['orderNumber'])
except Payment.DoesNotExist: except Payment.DoesNotExist:
finance_logger.error('Ошибка проверки платежа', exc_info=True, logger_yandex.error('Ошибка проверки платежа', exc_info=True, extra={
request="Payment with id=%s not found" % order_number) 'request': "Payment with id=%s not found" % data['orderNumber'],
})
return Response(status=204) return Response(status=204)
if not pay.status == Payment.STATUS.PROCESSED: if not pay.status == Payment.STATUS.PROCESSED:
finance_logger.error('Ошибка проверки платежа', logger_yandex.error('Ошибка проверки платежа', exc_info=True, extra={
order_number=order_number, 'request': "Payment with id=%s have status %s" % (data['orderNumber'], pay.status),
request="Payment with id=%s have status %s" % (order_number, pay.status)) })
return Response(status=204) return Response(status=204)
if not pay.shop_id == int(data['shopId']): if not pay.shop_id == int(data['shopId']):
finance_logger.error('Ошибка проверки платежа', logger_yandex.error('Ошибка проверки платежа', exc_info=True, extra={
request="ShopId=%s not match" % (data['shopId'],)) 'request': "ShopId=%s not match" % (data['shopId'],),
})
return Response(status=204) return Response(status=204)
if not pay.scid == int(data['scid']): if not pay.scid == int(data['scid']):
finance_logger.error('Ошибка проверки платежа', logger_yandex.error('Ошибка проверки платежа', exc_info=True, extra={
request="scid=%s not match" % (data['scid'],)) 'request': "scid=%s not match" % (data['scid'],)
})
return Response(status=204) return Response(status=204)
if not pay.order_amount == float(data['orderSumAmount']): if not pay.order_amount == float(data['orderSumAmount']):
finance_logger.error('Ошибка проверки платежа', logger_yandex.error('Ошибка проверки платежа', exc_info=True, extra={
request="Expected amount is %s received amount is %s" % (pay.order_amount, data['orderSumAmount'])) 'request': "Expected amount is %s received amount is %s"
% (pay.order_amount, data['orderSumAmount']),
})
return Response(status=204) return Response(status=204)
now = timezone.now() now = timezone.now()
@ -432,7 +301,9 @@ class YandexCheckView(APIView):
xml_res = """<checkOrderResponse performedDatetime="%s" code="0" invoiceId="%s" shopId="%s"/> xml_res = """<checkOrderResponse performedDatetime="%s" code="0" invoiceId="%s" shopId="%s"/>
""" % (pay.performed_datetime, str(data['invoiceId']), str(pay.shop_id)) """ % (pay.performed_datetime, str(data['invoiceId']), str(pay.shop_id))
finance_logger.info('Проверка платежа ответ', response=xml_res) logger_yandex.info('Проверка платежа ответ', exc_info=True, extra={
'response': xml_res,
})
return HttpResponse(xml_res, content_type='application/xml') return HttpResponse(xml_res, content_type='application/xml')
@ -448,44 +319,40 @@ class YandexAvisoView(APIView):
val = i.split('=')[1] val = i.split('=')[1]
data[key] = val data[key] = val
finance_logger.info('Подтверждение платежа запрос', request=data)
order_number = data['orderNumber']
try: try:
pay = Payment.objects.get(order_number=order_number) pay = Payment.objects.get(order_number=data['orderNumber'])
except Payment.DoesNotExist: except Payment.DoesNotExist:
finance_logger.error('Ошибка подтверждения платежа', exc_info=True, logger_yandex.error('Ошибка подтверждения платежа', exc_info=True, extra={
request="Payment with invoice_id=%s not found" % order_number) 'request': "Payment with invoice_id=%s not found" % data['orderNumber'],
})
return Response(status=204) return Response(status=204)
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.shop_amount = data['shopSumAmount']
pay.status = Payment.STATUS.SUCCESS pay.status = Payment.STATUS.SUCCESS
now = timezone.now() now = timezone.now()
pay.performed_datetime = now.isoformat() pay.performed_datetime = now.isoformat()
pay.save() pay.save()
InvoiceRebilling.objects.filter(bill=pay.invoice.bill).update(key=data['invoiceId'])
xml_res = """<paymentAvisoResponse performedDatetime="%s" code="0" invoiceId="%s" shopId="%s"/> xml_res = """<paymentAvisoResponse performedDatetime="%s" code="0" invoiceId="%s" shopId="%s"/>
""" % (pay.performed_datetime, str(data['invoiceId']), str(pay.shop_id)) """ % (pay.performed_datetime, str(data['invoiceId']), str(pay.shop_id))
finance_logger.info('Подтверждение платежа ответ', response=xml_res) logger_yandex.info('Подтверждение платежа ответ', exc_info=True, extra={
'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
html_content = render_to_string('mail/sales/pay_access.html', context) msg = EmailMessage(
text_content = strip_tags(html_content) 'Успешная оплата.',
'''Пользователь "%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 = EmailMultiAlternatives(
subject, text_content, to=[to], bcc=['dmitry.dolya@skillbox.ru', 'vera.procenko@skillbox.ru'])
msg.attach_alternative(html_content, "text/html")
msg.send() msg.send()
return HttpResponse(xml_res, content_type='application/xml') return HttpResponse(xml_res, content_type='application/xml')
@ -494,73 +361,23 @@ class YandexAvisoView(APIView):
class YandexFailView(APIView): class YandexFailView(APIView):
renderer_classes = (JSONRenderer,) renderer_classes = (JSONRenderer,)
@staticmethod
def get(request):
data = dict()
for i in request.body.decode('utf-8').split('&'):
key = i.split('=')[0]
val = i.split('=')[1]
data[key] = val
finance_logger.error('YandexFailView', data=data)
return redirect(to=settings.DOMAIN)
class DemoYandexCheckView(APIView):
renderer_classes = (JSONRenderer,)
@staticmethod @staticmethod
def post(request): def post(request):
# Деалем допущение, что customerNumber=abc1111111 иначе всё плохо
data = dict() data = dict()
try:
for i in request.body.decode('utf-8').split('&'): for i in request.body.decode('utf-8').split('&'):
key = i.split('=')[0] key = i.split('=')[0]
val = i.split('=')[1] val = i.split('=')[1]
data[key] = val data[key] = val
finance_logger.info('Проверка демо платежа запрос', request=data) logger_yandex.error(data)
except IndexError:
finance_logger.exception("Непредвиденная ошибка проверки тестогого платежа")
return Response(status=204)
now = timezone.now()
try:
if data['customerNumber'] == "abc000":
xml_res = """<checkOrderResponse performedDatetime="%s" code="0" invoiceId="%s" shopId="%s"/>
""" % (now.isoformat(), str(data['invoiceId']), str(data['shopId']))
else:
xml_res = """<checkOrderResponse performedDatetime="%s" code="100" invoiceId="%s" shopId="%s"
message="Неверный номер ордера"/>""" % (now.isoformat(), str(data['invoiceId']), str(data['shopId']))
finance_logger.warning("Ошибка проверки тестового платежа", response=xml_res)
return HttpResponse(xml_res, content_type='application/xml')
except KeyError:
finance_logger.exception('Ошибка в данных checkYandex')
return Response(status=204)
return redirect(to=settings.DOMAIN)
class DemoYandexAvisoView(APIView):
@staticmethod class DemoYandexCheckView(YandexCheckView):
def post(request): """для тестирования платежей"""
finance_logger.info('Начало тестирования avisoYandex') pass
data = dict()
for i in request.body.decode('utf-8').split('&'):
key = i.split('=')[0]
val = i.split('=')[1]
data[key] = val
now = timezone.now()
if float(data['orderSumAmount']) < 1001:
xml_res = """<paymentAvisoResponse performedDatetime="%s" code="0" invoiceId="%s" shopId="%s"/>
""" % (now.isoformat(), str(data['invoiceId']), str(data['shopId']))
finance_logger.info('Подтверждение демо платежа ответ', response=xml_res)
else: class DemoYandexAvisoView(YandexAvisoView):
xml_res = """<paymentAvisoResponse performedDatetime="%s" code="100" invoiceId="%s" shopId="%s" pass
message="Нам не позволяет совесть принять от вас более 1000 рублей"/
>""" % (now.isoformat(), str(data['invoiceId']), str(data['shopId']))
finance_logger.warning("Ошибка подтверждения тестогого платежа", response=xml_res)
return HttpResponse(xml_res, content_type='application/xml')

@ -3,8 +3,6 @@ import os
from celery import Celery from celery import Celery
from raven import Client from raven import Client
from raven.contrib.celery import register_signal, register_logger_signal from raven.contrib.celery import register_signal, register_logger_signal
from celery.schedules import crontab
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lms.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lms.settings')
@ -15,18 +13,10 @@ app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks() app.autodiscover_tasks()
client = Client('http://caaea487274f4e23a9107862484c79f3:3d463ad4717942508536f7a659921950@sentry.skillbox.ru/7') client = Client('http://caaea487274f4e23a9107862484c79f3:3d463ad4717942508536f7a659921950@sentry.skillbox.ru/3')
register_logger_signal(client) register_logger_signal(client)
register_signal(client) register_signal(client)
app.conf.beat_schedule = {
'periodic_billing': {
# 'schedule': crontab(day_of_month='1', hour='1', minute='1'), # заглушка на время отладки
'schedule': crontab(minute='0', hour='*/3',),
'task': 'finance.tasks.periodic_billing'
}
}
@app.task(bind=True) @app.task(bind=True)
def debug_task(self): def debug_task(self):

@ -5,15 +5,26 @@ import raven
import environ import environ
import socket import socket
root = environ.Path(__file__) - 2 root = environ.Path(__file__) - 2
env = environ.Env() env = environ.Env()
MOD = os.environ.get('MOD', 'Test')
environ.Env.read_env(str(root) + '/config_app/settings/local.env') DEBUG = os.environ.get('DEBUG', 'False')
MASTER_PASSWORD = os.environ.get('MASTER_PASSWORD', '@J*1') 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')
elif MOD == 'Prod':
environ.Env.read_env(str(root) + '/config_app/settings/prod.env')
else:
raise ImportError('no such environ ' + MOD)
EMAIL_CONFIG = env.email_url('EMAIL_URL',) EMAIL_CONFIG = env.email_url('EMAIL_URL',)
vars().update(EMAIL_CONFIG) vars().update(EMAIL_CONFIG)
@ -44,17 +55,10 @@ DOMAIN = 'https://go.skillbox.ru'
YANDEX_MONEY_DEBUG = False YANDEX_MONEY_DEBUG = False
YANDEX_MONEY_SCID = '149639' YANDEX_MONEY_SCID = '149639'
YANDEX_MONEY_TEST_SCID = '558146'
YANDEX_MONEY_SHOP_ID = '157133' YANDEX_MONEY_SHOP_ID = '157133'
#Todo костыль на рассрочку
YANDEX_MONEY_REBILLING_SCID = '723030'
YANDEX_MONEY_REBILLING_SHOP_ID = '199287'
YANDEX_MONEY_SHOP_PASSWORD = 'nu5Xefise' YANDEX_MONEY_SHOP_PASSWORD = 'nu5Xefise'
YANDEX_MONEY_FAIL_URL = '%s/api/v1/finance/yandex/fail/' % DOMAIN YANDEX_MONEY_FAIL_URL = '%s/api/v1/finance/yandex/fail/' % DOMAIN
YANDEX_MONEY_SUCCESS_URL = '%s/' % DOMAIN YANDEX_MONEY_SUCCESS_URL = '%s/' % DOMAIN
YANDEX_MONEY_MWS_URL = 'https://penelope.yamoney.ru/webservice/mws/api/'
# информировать о случаях, когда модуль вернул Яндекс.Кассе ошибку # информировать о случаях, когда модуль вернул Яндекс.Кассе ошибку
YANDEX_MONEY_MAIL_ADMINS_ON_PAYMENT_ERROR = True YANDEX_MONEY_MAIL_ADMINS_ON_PAYMENT_ERROR = True
# Application definition # Application definition
@ -188,8 +192,6 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/' STATIC_URL = '/static/'
SSL_ROOT = os.path.join(BASE_DIR, 'ssl')
RAVEN_CONFIG = { RAVEN_CONFIG = {
'dsn': 'http://1a09557dbd144e52af4b14bea569c114:fbb5dfaa39e64f02a1b4cc7ac665d7d7@sentry.skillbox.ru/7' 'dsn': 'http://1a09557dbd144e52af4b14bea569c114:fbb5dfaa39e64f02a1b4cc7ac665d7d7@sentry.skillbox.ru/7'
} }
@ -206,9 +208,6 @@ LOGGING = {
'format': '%(levelname)s %(asctime)s %(module)s ' 'format': '%(levelname)s %(asctime)s %(module)s '
'%(process)d %(thread)d %(message)s' '%(process)d %(thread)d %(message)s'
}, },
'finance_data_formatter': {
'format': '%(asctime)s - %(levelname)s - %(message)s - %(finance_data)s'
},
}, },
'handlers': { 'handlers': {
'sentry': { 'sentry': {
@ -216,41 +215,24 @@ LOGGING = {
'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler',
'tags': {'custom-tag': 'x'}, 'tags': {'custom-tag': 'x'},
}, },
'finance_data': { 'yandex_money': {
'level': 'DEBUG', 'level': 'DEBUG',
'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler',
'tags': {'custom-tag': 'yandex'}, # TODO переделать на finance_data 'tags': {'custom-tag': 'yandex'},
},
'finance_data_file': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(BASE_DIR, 'logs', 'finance_data.log'),
'maxBytes': 1024 * 1024 * 10,
'formatter': 'finance_data_formatter'
},
'business_rules': {
'level': 'DEBUG',
'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler',
'tags': {'custom-tag': 'business_rules'},
}, },
'console': { 'console': {
'level': 'DEBUG', 'level': 'DEBUG',
'class': 'logging.StreamHandler', 'class': 'logging.StreamHandler',
'formatter': 'verbose' 'formatter': 'verbose'
}, }
}, },
'loggers': { 'loggers': {
'': { '': {
'level': 'WARNING', 'level': 'WARNING',
'handlers': ['sentry'], 'handlers': ['sentry'],
}, },
'finance_data': { 'yandex_money': {
'handlers': ['finance_data', 'finance_data_file', ], 'handlers': ['yandex_money'],
'level': 'DEBUG',
'propagate': False
},
'business_rules': {
'handlers': ['business_rules'],
'level': 'DEBUG', 'level': 'DEBUG',
'propagate': False 'propagate': False
}, },

@ -0,0 +1,21 @@
import logging
from .settings import * # noqa
DEBUG = True
EMAIL_DEBUG = True
TEMPLATES[0]['OPTIONS']['debug'] = DEBUG # noqa
MEDIA_ROOT = os.path.join(BASE_DIR, 'media_qa') # noqa
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
PASSWORD_HASHERS = ('django.contrib.auth.hashers.MD5PasswordHasher', )
CELERY_TASK_ALWAYS_EAGER = True
# Disable cache during testing
CACHE_DISABLED = True
LOGGING = {}
LOCAL_APPS_LOGGERS = {}
# Disable all logging calls with levels less severe than or equal to CRITICAL
logging.disable(logging.CRITICAL)

@ -0,0 +1,520 @@
[2017-10-20 13:12:58,041: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2017-10-20 13:12:58,049: INFO/MainProcess] mingle: searching for neighbors
[2017-10-20 13:12:59,065: INFO/MainProcess] mingle: all alone
[2017-10-20 13:12:59,110: WARNING/MainProcess] /home/andrey/skillbox/lib/python3.6/site-packages/celery/fixups/django.py:202: UserWarning: Using settings.DEBUG leads to a memory leak, never use this setting in production environments!
warnings.warn('Using settings.DEBUG leads to a memory leak, never '
[2017-10-20 13:12:59,113: INFO/MainProcess] celery@andrey-desktop ready.
[2017-10-20 13:13:02,946: INFO/MainProcess] Events of group {task} enabled by remote.
[2017-10-20 13:17:23,104: INFO/MainProcess] Received task: lms.celery.debug_task[e7d9bdc2-a3a8-44b2-b3c6-935acfa092d7]
[2017-10-20 13:17:23,106: WARNING/ForkPoolWorker-2] Request: <Context: {'lang': 'py', 'task': 'lms.celery.debug_task', 'id': 'e7d9bdc2-a3a8-44b2-b3c6-935acfa092d7', 'eta': None, 'expires': None, 'group': None, 'retries': 0, 'timelimit': [None, None], 'root_id': 'e7d9bdc2-a3a8-44b2-b3c6-935acfa092d7', 'parent_id': None, 'argsrepr': '()', 'kwargsrepr': '{}', 'origin': 'gen13641@andrey-desktop', 'reply_to': 'b8c6ce7f-8ee0-35c7-ad70-026ad2f36d4a', 'correlation_id': 'e7d9bdc2-a3a8-44b2-b3c6-935acfa092d7', 'delivery_info': {'exchange': '', 'routing_key': 'celery', 'priority': 0, 'redelivered': False}, 'args': [], 'kwargs': {}, 'hostname': 'celery@andrey-desktop', 'is_eager': False, 'callbacks': None, 'errbacks': None, 'chain': None, 'chord': None, 'called_directly': False, '_protected': 1}>
[2017-10-20 13:17:23,124: INFO/ForkPoolWorker-2] Task lms.celery.debug_task[e7d9bdc2-a3a8-44b2-b3c6-935acfa092d7] succeeded in 0.018874458000027516s: None
[2017-10-20 13:17:29,057: INFO/MainProcess] Received task: lms.celery.debug_task[88f178fc-7222-4de0-b671-57e7472a9be1]
[2017-10-20 13:17:29,059: WARNING/ForkPoolWorker-1] Request: <Context: {'lang': 'py', 'task': 'lms.celery.debug_task', 'id': '88f178fc-7222-4de0-b671-57e7472a9be1', 'eta': None, 'expires': None, 'group': None, 'retries': 0, 'timelimit': [None, None], 'root_id': '88f178fc-7222-4de0-b671-57e7472a9be1', 'parent_id': None, 'argsrepr': '()', 'kwargsrepr': '{}', 'origin': 'gen13641@andrey-desktop', 'reply_to': 'b8c6ce7f-8ee0-35c7-ad70-026ad2f36d4a', 'correlation_id': '88f178fc-7222-4de0-b671-57e7472a9be1', 'delivery_info': {'exchange': '', 'routing_key': 'celery', 'priority': 0, 'redelivered': False}, 'args': [], 'kwargs': {}, 'hostname': 'celery@andrey-desktop', 'is_eager': False, 'callbacks': None, 'errbacks': None, 'chain': None, 'chord': None, 'called_directly': False, '_protected': 1}>
[2017-10-20 13:17:29,081: INFO/ForkPoolWorker-1] Task lms.celery.debug_task[88f178fc-7222-4de0-b671-57e7472a9be1] succeeded in 0.022606980999626103s: None
worker: Warm shutdown (MainProcess)
-------------- celery@andrey-desktop v4.1.0 (latentcall)
---- **** -----
--- * *** * -- Linux-4.10.0-37-generic-x86_64-with-Ubuntu-16.04-xenial 2017-10-20 13:12:57
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: lms:0x7f47cb927be0
- ** ---------- .> transport: amqp://guest:**@localhost:5672//
- ** ---------- .> results:
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. djcelery_email_send_multiple
. lms.celery.debug_task
[2017-10-20 13:32:32,128: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2017-10-20 13:32:32,135: INFO/MainProcess] mingle: searching for neighbors
[2017-10-20 13:32:33,150: INFO/MainProcess] mingle: all alone
[2017-10-20 13:32:33,201: WARNING/MainProcess] /home/andrey/skillbox/lib/python3.6/site-packages/celery/fixups/django.py:202: UserWarning: Using settings.DEBUG leads to a memory leak, never use this setting in production environments!
warnings.warn('Using settings.DEBUG leads to a memory leak, never '
[2017-10-20 13:32:33,202: INFO/MainProcess] celery@andrey-desktop ready.
[2017-10-20 13:32:36,911: INFO/MainProcess] Events of group {task} enabled by remote.
worker: Warm shutdown (MainProcess)
-------------- celery@andrey-desktop v4.1.0 (latentcall)
---- **** -----
--- * *** * -- Linux-4.10.0-37-generic-x86_64-with-Ubuntu-16.04-xenial 2017-10-20 13:32:32
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: lms:0x7f34afe2fbe0
- ** ---------- .> transport: amqp://guest:**@localhost:5672//
- ** ---------- .> results:
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. djcelery_email_send_multiple
. lms.celery.debug_task
[2017-10-20 13:38:00,333: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2017-10-20 13:38:00,341: INFO/MainProcess] mingle: searching for neighbors
[2017-10-20 13:38:01,361: INFO/MainProcess] mingle: all alone
[2017-10-20 13:38:01,408: WARNING/MainProcess] /home/andrey/skillbox/lib/python3.6/site-packages/celery/fixups/django.py:202: UserWarning: Using settings.DEBUG leads to a memory leak, never use this setting in production environments!
warnings.warn('Using settings.DEBUG leads to a memory leak, never '
[2017-10-20 13:38:01,409: INFO/MainProcess] celery@andrey-desktop ready.
[2017-10-20 13:38:05,171: INFO/MainProcess] Events of group {task} enabled by remote.
worker: Warm shutdown (MainProcess)
-------------- celery@andrey-desktop v4.1.0 (latentcall)
---- **** -----
--- * *** * -- Linux-4.10.0-37-generic-x86_64-with-Ubuntu-16.04-xenial 2017-10-20 13:38:00
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: lms:0x7fd8781acbe0
- ** ---------- .> transport: amqp://guest:**@localhost:5672//
- ** ---------- .> results:
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. djcelery_email_send_multiple
. lms.celery.debug_task
[2017-10-20 13:47:50,974: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2017-10-20 13:47:50,985: INFO/MainProcess] mingle: searching for neighbors
[2017-10-20 13:47:52,012: INFO/MainProcess] mingle: all alone
[2017-10-20 13:47:52,069: WARNING/MainProcess] /home/andrey/skillbox/lib/python3.6/site-packages/celery/fixups/django.py:202: UserWarning: Using settings.DEBUG leads to a memory leak, never use this setting in production environments!
warnings.warn('Using settings.DEBUG leads to a memory leak, never '
[2017-10-20 13:47:52,069: INFO/MainProcess] celery@andrey-desktop ready.
[2017-10-20 13:47:56,047: INFO/MainProcess] Events of group {task} enabled by remote.
worker: Warm shutdown (MainProcess)
-------------- celery@andrey-desktop v4.1.0 (latentcall)
---- **** -----
--- * *** * -- Linux-4.10.0-37-generic-x86_64-with-Ubuntu-16.04-xenial 2017-10-20 13:47:50
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: lms:0x7fc6511c1b70
- ** ---------- .> transport: amqp://guest:**@localhost:5672//
- ** ---------- .> results:
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. djcelery_email_send_multiple
. lms.celery.debug_task
[2017-10-20 14:42:47,658: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2017-10-20 14:42:47,664: INFO/MainProcess] mingle: searching for neighbors
[2017-10-20 14:42:48,682: INFO/MainProcess] mingle: all alone
[2017-10-20 14:42:48,727: INFO/MainProcess] celery@andrey-desktop ready.
[2017-10-20 14:42:52,394: INFO/MainProcess] Events of group {task} enabled by remote.
[2017-10-20 14:47:53,063: INFO/MainProcess] Tasks flagged as revoked: ff8246ff-1904-47da-ac57-1a844272c826
worker: Warm shutdown (MainProcess)
-------------- celery@andrey-desktop v4.1.0 (latentcall)
---- **** -----
--- * *** * -- Linux-4.10.0-37-generic-x86_64-with-Ubuntu-16.04-xenial 2017-10-20 14:42:47
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: lms:0x7f89a2228be0
- ** ---------- .> transport: amqp://guest:**@localhost:5672//
- ** ---------- .> results:
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. djcelery_email_send_multiple
. lms.celery.debug_task
[2017-10-20 14:56:43,024: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2017-10-20 14:56:43,031: INFO/MainProcess] mingle: searching for neighbors
[2017-10-20 14:56:44,048: INFO/MainProcess] mingle: all alone
[2017-10-20 14:56:44,112: INFO/MainProcess] celery@andrey-desktop ready.
[2017-10-20 14:56:47,869: INFO/MainProcess] Events of group {task} enabled by remote.
worker: Warm shutdown (MainProcess)
-------------- celery@andrey-desktop v4.1.0 (latentcall)
---- **** -----
--- * *** * -- Linux-4.10.0-37-generic-x86_64-with-Ubuntu-16.04-xenial 2017-10-20 14:56:42
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: lms:0x7f4f0c601be0
- ** ---------- .> transport: amqp://guest:**@localhost:5672//
- ** ---------- .> results:
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. djcelery_email_send_multiple
. lms.celery.debug_task
[2017-10-20 15:14:13,071: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2017-10-20 15:14:13,086: INFO/MainProcess] mingle: searching for neighbors
[2017-10-20 15:14:14,109: INFO/MainProcess] mingle: all alone
[2017-10-20 15:14:14,123: INFO/MainProcess] celery@andrey-desktop ready.
[2017-10-20 15:14:18,076: INFO/MainProcess] Events of group {task} enabled by remote.
worker: Warm shutdown (MainProcess)
-------------- celery@andrey-desktop v4.1.0 (latentcall)
---- **** -----
--- * *** * -- Linux-4.10.0-37-generic-x86_64-with-Ubuntu-16.04-xenial 2017-10-20 15:14:12
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: lms:0x7fd94e575b70
- ** ---------- .> transport: amqp://guest:**@localhost:5672//
- ** ---------- .> results:
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. djcelery_email_send_multiple
. lms.celery.debug_task
[2017-10-20 15:18:26,330: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2017-10-20 15:18:26,337: INFO/MainProcess] mingle: searching for neighbors
[2017-10-20 15:18:27,355: INFO/MainProcess] mingle: all alone
[2017-10-20 15:18:27,417: INFO/MainProcess] celery@andrey-desktop ready.
[2017-10-20 15:18:31,190: INFO/MainProcess] Events of group {task} enabled by remote.
worker: Warm shutdown (MainProcess)
-------------- celery@andrey-desktop v4.1.0 (latentcall)
---- **** -----
--- * *** * -- Linux-4.10.0-37-generic-x86_64-with-Ubuntu-16.04-xenial 2017-10-20 15:18:26
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: lms:0x7f4b2ac05be0
- ** ---------- .> transport: amqp://guest:**@localhost:5672//
- ** ---------- .> results:
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. djcelery_email_send_multiple
. lms.celery.debug_task
[2017-10-20 15:31:17,246: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2017-10-20 15:31:17,253: INFO/MainProcess] mingle: searching for neighbors
[2017-10-20 15:31:18,269: INFO/MainProcess] mingle: all alone
[2017-10-20 15:31:18,311: INFO/MainProcess] celery@andrey-desktop ready.
[2017-10-20 15:31:22,075: INFO/MainProcess] Events of group {task} enabled by remote.
worker: Warm shutdown (MainProcess)
-------------- celery@andrey-desktop v4.1.0 (latentcall)
---- **** -----
--- * *** * -- Linux-4.10.0-37-generic-x86_64-with-Ubuntu-16.04-xenial 2017-10-20 15:31:17
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: lms:0x7f2e6cc96be0
- ** ---------- .> transport: amqp://guest:**@localhost:5672//
- ** ---------- .> results:
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. djcelery_email_send_multiple
. lms.celery.debug_task
[2017-10-20 16:52:27,050: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2017-10-20 16:52:27,057: INFO/MainProcess] mingle: searching for neighbors
[2017-10-20 16:52:28,074: INFO/MainProcess] mingle: all alone
[2017-10-20 16:52:28,128: INFO/MainProcess] celery@andrey-desktop ready.
[2017-10-20 16:52:31,818: INFO/MainProcess] Events of group {task} enabled by remote.
[2017-10-20 17:00:17,556: INFO/MainProcess] Received task: djcelery_email_send[8735440e-9760-43c7-b84c-c6efdac4e64d]
[2017-10-20 17:00:19,202: INFO/ForkPoolWorker-2] Task djcelery_email_send[8735440e-9760-43c7-b84c-c6efdac4e64d] succeeded in 1.4465199199985364s: 1
[2017-10-20 17:39:12,619: INFO/MainProcess] Received task: djcelery_email_send[243eb7a2-b4e0-4221-8ab2-c83c94d8f241]
[2017-10-20 17:39:14,523: INFO/ForkPoolWorker-1] Task djcelery_email_send[243eb7a2-b4e0-4221-8ab2-c83c94d8f241] succeeded in 1.4357735300000058s: 1
[2017-10-20 17:40:33,125: INFO/MainProcess] Received task: djcelery_email_send[863adc65-6bf5-4a1d-a2b9-91ebb9737520]
[2017-10-20 17:40:34,548: INFO/ForkPoolWorker-2] Task djcelery_email_send[863adc65-6bf5-4a1d-a2b9-91ebb9737520] succeeded in 1.3558088320023671s: 1
[2017-10-20 17:42:16,950: INFO/MainProcess] Received task: djcelery_email_send[a60b5727-8d77-49f8-8008-82ae064ed682]
[2017-10-20 17:42:18,493: INFO/ForkPoolWorker-1] Task djcelery_email_send[a60b5727-8d77-49f8-8008-82ae064ed682] succeeded in 1.1552653329999885s: 1
[2017-10-20 17:57:36,541: INFO/MainProcess] Received task: djcelery_email_send[8184ca5f-b2e2-42ee-a096-b3c2ecf152e2]
[2017-10-20 17:57:37,905: INFO/ForkPoolWorker-2] Task djcelery_email_send[8184ca5f-b2e2-42ee-a096-b3c2ecf152e2] succeeded in 1.251813505001337s: 1
[2017-10-20 19:20:51,999: WARNING/MainProcess] consumer: Connection to broker lost. Trying to re-establish the connection...
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/worker/consumer/consumer.py", line 320, in start
blueprint.start(self)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/bootsteps.py", line 119, in start
step.start(parent)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/worker/consumer/consumer.py", line 596, in start
c.loop(*c.loop_args())
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/worker/loops.py", line 88, in asynloop
next(loop)
File "/home/andrey/skillbox/lib/python3.6/site-packages/kombu/async/hub.py", line 354, in create_loop
cb(*cbargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/kombu/transport/base.py", line 236, in on_readable
reader(loop)
File "/home/andrey/skillbox/lib/python3.6/site-packages/kombu/transport/base.py", line 218, in _read
drain_events(timeout=0)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 471, in drain_events
while not self.blocking_read(timeout):
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 477, in blocking_read
return self.on_inbound_frame(frame)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/method_framing.py", line 55, in on_frame
callback(channel, method_sig, buf, None)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 481, in on_inbound_method
method_sig, payload, content,
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/abstract_channel.py", line 128, in dispatch_method
listener(*args)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 603, in _on_close
(class_id, method_id), ConnectionError)
amqp.exceptions.ConnectionForced: (0, 0): (320) CONNECTION_FORCED - broker forced connection closure with reason 'shutdown'
[2017-10-20 19:20:52,012: ERROR/MainProcess] consumer: Cannot connect to amqp://guest:**@127.0.0.1:5672//: [Errno 111] Connection refused.
Trying again in 2.00 seconds...
worker: Warm shutdown (MainProcess)
-------------- celery@andrey-desktop v4.1.0 (latentcall)
---- **** -----
--- * *** * -- Linux-4.10.0-37-generic-x86_64-with-Ubuntu-16.04-xenial 2017-10-20 16:52:26
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: lms:0x7fe25f735be0
- ** ---------- .> transport: amqp://guest:**@localhost:5672//
- ** ---------- .> results:
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. djcelery_email_send
. lms.celery.debug_task
[2017-10-23 08:32:22,988: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2017-10-23 08:32:22,998: INFO/MainProcess] mingle: searching for neighbors
[2017-10-23 08:32:24,013: INFO/MainProcess] mingle: all alone
[2017-10-23 08:32:24,021: INFO/MainProcess] celery@andrey-desktop ready.
[2017-10-23 08:32:27,769: INFO/MainProcess] Events of group {task} enabled by remote.
[2017-10-23 11:42:09,222: INFO/MainProcess] Received task: djcelery_email_send[54f16e2a-a437-4000-850f-0bf29ff758b8]
[2017-10-23 11:42:10,844: INFO/ForkPoolWorker-2] Task djcelery_email_send[54f16e2a-a437-4000-850f-0bf29ff758b8] succeeded in 1.4740454479997425s: 1
[2017-10-23 11:44:36,901: INFO/MainProcess] Received task: djcelery_email_send[2ef60ead-2523-4bf8-b117-366bb120359d]
[2017-10-23 11:44:39,194: INFO/ForkPoolWorker-1] Task djcelery_email_send[2ef60ead-2523-4bf8-b117-366bb120359d] succeeded in 1.4229817790001107s: 1
[2017-10-23 11:45:49,202: INFO/MainProcess] Received task: djcelery_email_send[3e60e35a-c846-4f37-bd88-1d92afc9dcac]
[2017-10-23 11:45:50,624: INFO/ForkPoolWorker-2] Task djcelery_email_send[3e60e35a-c846-4f37-bd88-1d92afc9dcac] succeeded in 1.1725643370009493s: 1
[2017-10-23 11:47:18,371: INFO/MainProcess] Received task: djcelery_email_send[4530e853-0db4-499d-8a65-7128121ee5bc]
[2017-10-23 11:47:20,136: INFO/ForkPoolWorker-1] Task djcelery_email_send[4530e853-0db4-499d-8a65-7128121ee5bc] succeeded in 1.3828427380012727s: 1
[2017-10-23 11:56:05,834: INFO/MainProcess] Received task: djcelery_email_send[9fc201ce-f3ec-40af-9dd1-bf998f34a57d]
[2017-10-23 11:56:08,831: INFO/ForkPoolWorker-2] Task djcelery_email_send[9fc201ce-f3ec-40af-9dd1-bf998f34a57d] succeeded in 1.293271452001136s: 1
[2017-10-23 11:59:57,509: INFO/MainProcess] Received task: djcelery_email_send[7945e9d6-ef7c-4e19-9c59-422100ac9f73]
[2017-10-23 11:59:58,959: INFO/ForkPoolWorker-1] Task djcelery_email_send[7945e9d6-ef7c-4e19-9c59-422100ac9f73] succeeded in 1.1895115009992878s: 1
[2017-10-23 12:08:24,457: INFO/MainProcess] Received task: djcelery_email_send[f0968ae1-8fbd-4d2c-905a-2ecf445aa10e]
[2017-10-23 12:08:25,890: INFO/ForkPoolWorker-2] Task djcelery_email_send[f0968ae1-8fbd-4d2c-905a-2ecf445aa10e] succeeded in 1.3443215190000046s: 1
[2017-10-23 12:20:29,414: INFO/MainProcess] Received task: djcelery_email_send[4438a0d4-228c-40a9-b91d-612f82ea791b]
[2017-10-23 12:20:31,027: INFO/ForkPoolWorker-1] Task djcelery_email_send[4438a0d4-228c-40a9-b91d-612f82ea791b] succeeded in 1.452125650999733s: 1
[2017-10-23 12:21:31,101: INFO/MainProcess] Received task: djcelery_email_send[c3793dcb-e5f9-42bd-83b2-dd94f166c741]
[2017-10-23 12:21:32,804: INFO/ForkPoolWorker-2] Task djcelery_email_send[c3793dcb-e5f9-42bd-83b2-dd94f166c741] succeeded in 1.1413313480006764s: 1
[2017-10-23 12:38:27,027: INFO/MainProcess] Received task: djcelery_email_send[ea55b25a-8e6d-4e88-8b18-52d561bd7c95]
[2017-10-23 12:38:28,225: INFO/ForkPoolWorker-1] Task djcelery_email_send[ea55b25a-8e6d-4e88-8b18-52d561bd7c95] succeeded in 1.150906519000273s: 1
[2017-10-23 13:47:05,338: INFO/MainProcess] Received task: djcelery_email_send[b05bd9eb-848d-49b6-9725-d190e256527e]
[2017-10-23 13:47:08,582: INFO/ForkPoolWorker-2] Task djcelery_email_send[b05bd9eb-848d-49b6-9725-d190e256527e] succeeded in 1.5058496720012045s: 1
[2017-10-23 13:48:09,314: INFO/MainProcess] Received task: djcelery_email_send[fe0bd44a-f23e-476b-a447-c03d8b5bb266]
[2017-10-23 13:48:12,620: INFO/ForkPoolWorker-1] Task djcelery_email_send[fe0bd44a-f23e-476b-a447-c03d8b5bb266] succeeded in 1.340003754998179s: 1
[2017-10-23 14:36:33,382: INFO/MainProcess] Received task: djcelery_email_send[33418ad8-1f87-4a6f-ba11-a2fc0f52374a]
[2017-10-23 14:36:35,323: INFO/ForkPoolWorker-2] Task djcelery_email_send[33418ad8-1f87-4a6f-ba11-a2fc0f52374a] succeeded in 1.3137642000001506s: 1
[2017-10-23 14:36:49,212: INFO/MainProcess] Received task: djcelery_email_send[5f3beb45-7d03-447a-be90-41506efb897b]
[2017-10-23 14:36:51,271: INFO/ForkPoolWorker-1] Task djcelery_email_send[5f3beb45-7d03-447a-be90-41506efb897b] succeeded in 1.23961846200109s: 1
[2017-10-23 14:46:42,199: INFO/MainProcess] Received task: djcelery_email_send[69d6f018-e571-4d65-93a4-151fa78361ae]
[2017-10-23 14:46:44,060: INFO/ForkPoolWorker-2] Task djcelery_email_send[69d6f018-e571-4d65-93a4-151fa78361ae] succeeded in 1.2866839900016203s: 1
[2017-10-23 14:55:56,121: WARNING/MainProcess] consumer: Connection to broker lost. Trying to re-establish the connection...
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/worker/consumer/consumer.py", line 320, in start
blueprint.start(self)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/bootsteps.py", line 119, in start
step.start(parent)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/worker/consumer/consumer.py", line 596, in start
c.loop(*c.loop_args())
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/worker/loops.py", line 88, in asynloop
next(loop)
File "/home/andrey/skillbox/lib/python3.6/site-packages/kombu/async/hub.py", line 354, in create_loop
cb(*cbargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/kombu/transport/base.py", line 236, in on_readable
reader(loop)
File "/home/andrey/skillbox/lib/python3.6/site-packages/kombu/transport/base.py", line 218, in _read
drain_events(timeout=0)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 471, in drain_events
while not self.blocking_read(timeout):
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 477, in blocking_read
return self.on_inbound_frame(frame)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/method_framing.py", line 55, in on_frame
callback(channel, method_sig, buf, None)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 481, in on_inbound_method
method_sig, payload, content,
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/abstract_channel.py", line 128, in dispatch_method
listener(*args)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 601, in _on_close
self._x_close_ok()
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 616, in _x_close_ok
self.send_method(spec.Connection.CloseOk, callback=self._on_close_ok)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/abstract_channel.py", line 50, in send_method
conn.frame_writer(1, self.channel_id, sig, args, content)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/method_framing.py", line 166, in write_frame
write(view[:offset])
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/transport.py", line 258, in write
self._write(s)
ConnectionResetError: [Errno 104] Connection reset by peer
[2017-10-23 14:55:56,137: ERROR/MainProcess] consumer: Cannot connect to amqp://guest:**@127.0.0.1:5672//: [Errno 111] Connection refused.
Trying again in 2.00 seconds...
[2017-10-23 14:55:58,150: ERROR/MainProcess] consumer: Cannot connect to amqp://guest:**@127.0.0.1:5672//: [Errno 111] Connection refused.
Trying again in 4.00 seconds...
[2017-10-23 14:56:02,176: ERROR/MainProcess] consumer: Cannot connect to amqp://guest:**@127.0.0.1:5672//: [Errno 111] Connection refused.
Trying again in 6.00 seconds...
[2017-10-23 14:56:08,193: ERROR/MainProcess] consumer: Cannot connect to amqp://guest:**@127.0.0.1:5672//: [Errno 111] Connection refused.
Trying again in 8.00 seconds...
[2017-10-23 14:56:16,210: ERROR/MainProcess] consumer: Cannot connect to amqp://guest:**@127.0.0.1:5672//: [Errno 111] Connection refused.
Trying again in 10.00 seconds...
[2017-10-23 14:56:26,228: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2017-10-23 14:56:26,237: INFO/MainProcess] mingle: searching for neighbors
[2017-10-23 14:56:26,241: WARNING/MainProcess] consumer: Connection to broker lost. Trying to re-establish the connection...
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/worker/consumer/consumer.py", line 320, in start
blueprint.start(self)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/bootsteps.py", line 119, in start
step.start(parent)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/worker/consumer/mingle.py", line 38, in start
self.sync(c)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/worker/consumer/mingle.py", line 42, in sync
replies = self.send_hello(c)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/worker/consumer/mingle.py", line 55, in send_hello
replies = inspect.hello(c.hostname, our_revoked._data) or {}
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/control.py", line 129, in hello
return self._request('hello', from_node=from_node, revoked=revoked)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/control.py", line 81, in _request
timeout=self.timeout, reply=True,
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/control.py", line 436, in broadcast
limit, callback, channel=channel,
File "/home/andrey/skillbox/lib/python3.6/site-packages/kombu/pidbox.py", line 315, in _broadcast
serializer=serializer)
File "/home/andrey/skillbox/lib/python3.6/site-packages/kombu/pidbox.py", line 290, in _publish
serializer=serializer,
File "/home/andrey/skillbox/lib/python3.6/site-packages/kombu/messaging.py", line 181, in publish
exchange_name, declare,
File "/home/andrey/skillbox/lib/python3.6/site-packages/kombu/messaging.py", line 203, in _publish
mandatory=mandatory, immediate=immediate,
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/channel.py", line 1734, in _basic_publish
(0, exchange, routing_key, mandatory, immediate), msg
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/abstract_channel.py", line 50, in send_method
conn.frame_writer(1, self.channel_id, sig, args, content)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/method_framing.py", line 166, in write_frame
write(view[:offset])
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/transport.py", line 258, in write
self._write(s)
ConnectionResetError: [Errno 104] Connection reset by peer
[2017-10-23 14:56:26,250: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2017-10-23 14:56:26,256: INFO/MainProcess] mingle: searching for neighbors
[2017-10-23 14:56:27,273: INFO/MainProcess] mingle: all alone
[2017-10-23 14:56:27,310: INFO/MainProcess] Received task: djcelery_email_send[455cf8cb-b29f-4159-a81b-b33e5dbee1f3]
[2017-10-23 14:56:28,959: INFO/ForkPoolWorker-2] Task djcelery_email_send[455cf8cb-b29f-4159-a81b-b33e5dbee1f3] succeeded in 1.1903551169998536s: 1
[2017-10-23 18:52:13,477: INFO/MainProcess] Received task: djcelery_email_send[0fb5c171-4123-47fa-8089-823904fde5fd]
[2017-10-23 18:52:16,661: INFO/ForkPoolWorker-1] Task djcelery_email_send[0fb5c171-4123-47fa-8089-823904fde5fd] succeeded in 1.222238110996841s: 1
[2017-10-23 19:05:28,619: WARNING/MainProcess] consumer: Connection to broker lost. Trying to re-establish the connection...
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/worker/consumer/consumer.py", line 320, in start
blueprint.start(self)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/bootsteps.py", line 119, in start
step.start(parent)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/worker/consumer/consumer.py", line 596, in start
c.loop(*c.loop_args())
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/worker/loops.py", line 88, in asynloop
next(loop)
File "/home/andrey/skillbox/lib/python3.6/site-packages/kombu/async/hub.py", line 354, in create_loop
cb(*cbargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/kombu/transport/base.py", line 236, in on_readable
reader(loop)
File "/home/andrey/skillbox/lib/python3.6/site-packages/kombu/transport/base.py", line 218, in _read
drain_events(timeout=0)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 471, in drain_events
while not self.blocking_read(timeout):
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 477, in blocking_read
return self.on_inbound_frame(frame)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/method_framing.py", line 55, in on_frame
callback(channel, method_sig, buf, None)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 481, in on_inbound_method
method_sig, payload, content,
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/abstract_channel.py", line 128, in dispatch_method
listener(*args)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 601, in _on_close
self._x_close_ok()
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/connection.py", line 616, in _x_close_ok
self.send_method(spec.Connection.CloseOk, callback=self._on_close_ok)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/abstract_channel.py", line 50, in send_method
conn.frame_writer(1, self.channel_id, sig, args, content)
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/method_framing.py", line 166, in write_frame
write(view[:offset])
File "/home/andrey/skillbox/lib/python3.6/site-packages/amqp/transport.py", line 258, in write
self._write(s)
ConnectionResetError: [Errno 104] Connection reset by peer
[2017-10-23 19:05:28,624: ERROR/MainProcess] consumer: Cannot connect to amqp://guest:**@127.0.0.1:5672//: [Errno 111] Connection refused.
Trying again in 2.00 seconds...
worker: Warm shutdown (MainProcess)
-------------- celery@andrey-desktop v4.1.0 (latentcall)
---- **** -----
--- * *** * -- Linux-4.10.0-37-generic-x86_64-with-Ubuntu-16.04-xenial 2017-10-23 08:32:22
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: lms:0x7f574ff21ba8
- ** ---------- .> transport: amqp://guest:**@localhost:5672//
- ** ---------- .> results:
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. djcelery_email_send
. lms.celery.debug_task

@ -0,0 +1,623 @@
[E 171020 13:12:57 signal:232] Signal handler <promise@0x7f84e8f79678 --> <bound method Celery._autodiscover_tasks of <Celery lms at 0x7f84e711bbe0>>> raised: ImportError("cannot import name 'TrafSource'",)
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/utils/dispatch/signal.py", line 227, in send
response = receiver(signal=self, sender=sender, **named)
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 143, in __call__
return self.throw()
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 140, in __call__
retval = fun(*final_args, **final_kwargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 671, in _autodiscover_tasks
return self._autodiscover_tasks_from_fixups(related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 684, in _autodiscover_tasks_from_fixups
], related_name=related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 676, in _autodiscover_tasks_from_names
packages() if callable(packages) else packages, related_name,
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 223, in autodiscover_tasks
related_name) if mod)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in autodiscover_tasks
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in <listcomp>
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 274, in find_related_module
return importlib.import_module('{0}.{1}'.format(package, related_name))
File "/usr/lib/python3.6/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 994, in _gcd_import
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 678, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/home/andrey/skillbox/go.skillbox.ru/access/tasks.py", line 4, in <module>
from access.models import User, TrafSource, TrafTokenHistory
ImportError: cannot import name 'TrafSource'
[I 171020 13:12:57 command:139] Visit me at http://localhost:5555
[I 171020 13:12:57 command:144] Broker: amqp://guest:**@localhost:5672//
[I 171020 13:12:57 command:147] Registered tasks:
['celery.accumulate',
'celery.backend_cleanup',
'celery.chain',
'celery.chord',
'celery.chord_unlock',
'celery.chunks',
'celery.group',
'celery.map',
'celery.starmap',
'djcelery_email_send_multiple',
'lms.celery.debug_task']
[I 171020 13:12:57 mixins:224] Connected to amqp://guest:**@127.0.0.1:5672//
[W 171020 13:13:00 control:44] 'stats' inspect method failed
[W 171020 13:13:00 control:44] 'active_queues' inspect method failed
[W 171020 13:13:00 control:44] 'registered' inspect method failed
[W 171020 13:13:00 control:44] 'scheduled' inspect method failed
[W 171020 13:13:00 control:44] 'active' inspect method failed
[W 171020 13:13:00 control:44] 'reserved' inspect method failed
[W 171020 13:13:00 control:44] 'revoked' inspect method failed
[W 171020 13:13:00 control:44] 'conf' inspect method failed
[I 171020 13:32:28 command:47] SIGTERM detected, shutting down
[E 171020 13:32:31 signal:232] Signal handler <promise@0x7f72af4b2678 --> <bound method Celery._autodiscover_tasks of <Celery lms at 0x7f72ad655b70>>> raised: ImportError("cannot import name 'TrafSource'",)
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/utils/dispatch/signal.py", line 227, in send
response = receiver(signal=self, sender=sender, **named)
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 143, in __call__
return self.throw()
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 140, in __call__
retval = fun(*final_args, **final_kwargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 671, in _autodiscover_tasks
return self._autodiscover_tasks_from_fixups(related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 684, in _autodiscover_tasks_from_fixups
], related_name=related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 676, in _autodiscover_tasks_from_names
packages() if callable(packages) else packages, related_name,
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 223, in autodiscover_tasks
related_name) if mod)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in autodiscover_tasks
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in <listcomp>
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 274, in find_related_module
return importlib.import_module('{0}.{1}'.format(package, related_name))
File "/usr/lib/python3.6/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 994, in _gcd_import
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 678, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/home/andrey/skillbox/go.skillbox.ru/access/tasks.py", line 4, in <module>
from access.models import User, TrafSource, TrafTokenHistory
ImportError: cannot import name 'TrafSource'
[I 171020 13:32:31 command:139] Visit me at http://localhost:5555
[I 171020 13:32:31 command:144] Broker: amqp://guest:**@localhost:5672//
[I 171020 13:32:31 command:147] Registered tasks:
['celery.accumulate',
'celery.backend_cleanup',
'celery.chain',
'celery.chord',
'celery.chord_unlock',
'celery.chunks',
'celery.group',
'celery.map',
'celery.starmap',
'djcelery_email_send_multiple',
'lms.celery.debug_task']
[I 171020 13:32:31 mixins:224] Connected to amqp://guest:**@127.0.0.1:5672//
[W 171020 13:32:34 control:44] 'stats' inspect method failed
[W 171020 13:32:34 control:44] 'active_queues' inspect method failed
[W 171020 13:32:34 control:44] 'registered' inspect method failed
[W 171020 13:32:34 control:44] 'scheduled' inspect method failed
[W 171020 13:32:34 control:44] 'active' inspect method failed
[W 171020 13:32:34 control:44] 'reserved' inspect method failed
[W 171020 13:32:34 control:44] 'revoked' inspect method failed
[W 171020 13:32:34 control:44] 'conf' inspect method failed
[I 171020 13:37:56 command:47] SIGTERM detected, shutting down
[E 171020 13:38:00 signal:232] Signal handler <promise@0x7f00bb71c678 --> <bound method Celery._autodiscover_tasks of <Celery lms at 0x7f00b98bebe0>>> raised: ImportError("cannot import name 'TrafSource'",)
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/utils/dispatch/signal.py", line 227, in send
response = receiver(signal=self, sender=sender, **named)
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 143, in __call__
return self.throw()
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 140, in __call__
retval = fun(*final_args, **final_kwargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 671, in _autodiscover_tasks
return self._autodiscover_tasks_from_fixups(related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 684, in _autodiscover_tasks_from_fixups
], related_name=related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 676, in _autodiscover_tasks_from_names
packages() if callable(packages) else packages, related_name,
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 223, in autodiscover_tasks
related_name) if mod)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in autodiscover_tasks
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in <listcomp>
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 274, in find_related_module
return importlib.import_module('{0}.{1}'.format(package, related_name))
File "/usr/lib/python3.6/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 994, in _gcd_import
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 678, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/home/andrey/skillbox/go.skillbox.ru/access/tasks.py", line 4, in <module>
from access.models import User, TrafSource, TrafTokenHistory
ImportError: cannot import name 'TrafSource'
[I 171020 13:38:00 command:139] Visit me at http://localhost:5555
[I 171020 13:38:00 command:144] Broker: amqp://guest:**@localhost:5672//
[I 171020 13:38:00 command:147] Registered tasks:
['celery.accumulate',
'celery.backend_cleanup',
'celery.chain',
'celery.chord',
'celery.chord_unlock',
'celery.chunks',
'celery.group',
'celery.map',
'celery.starmap',
'djcelery_email_send_multiple',
'lms.celery.debug_task']
[I 171020 13:38:00 mixins:224] Connected to amqp://guest:**@127.0.0.1:5672//
[W 171020 13:38:02 control:44] 'stats' inspect method failed
[W 171020 13:38:02 control:44] 'active_queues' inspect method failed
[W 171020 13:38:02 control:44] 'registered' inspect method failed
[W 171020 13:38:02 control:44] 'scheduled' inspect method failed
[W 171020 13:38:02 control:44] 'active' inspect method failed
[W 171020 13:38:02 control:44] 'reserved' inspect method failed
[W 171020 13:38:02 control:44] 'revoked' inspect method failed
[W 171020 13:38:02 control:44] 'conf' inspect method failed
[I 171020 13:47:47 command:47] SIGTERM detected, shutting down
[E 171020 13:47:51 signal:232] Signal handler <promise@0x7f7efbb18678 --> <bound method Celery._autodiscover_tasks of <Celery lms at 0x7f7ef9cbabe0>>> raised: ImportError("cannot import name 'TrafSource'",)
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/utils/dispatch/signal.py", line 227, in send
response = receiver(signal=self, sender=sender, **named)
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 143, in __call__
return self.throw()
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 140, in __call__
retval = fun(*final_args, **final_kwargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 671, in _autodiscover_tasks
return self._autodiscover_tasks_from_fixups(related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 684, in _autodiscover_tasks_from_fixups
], related_name=related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 676, in _autodiscover_tasks_from_names
packages() if callable(packages) else packages, related_name,
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 223, in autodiscover_tasks
related_name) if mod)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in autodiscover_tasks
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in <listcomp>
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 274, in find_related_module
return importlib.import_module('{0}.{1}'.format(package, related_name))
File "/usr/lib/python3.6/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 994, in _gcd_import
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 678, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/home/andrey/skillbox/go.skillbox.ru/access/tasks.py", line 4, in <module>
from access.models import User, TrafSource, TrafTokenHistory
ImportError: cannot import name 'TrafSource'
[I 171020 13:47:51 command:139] Visit me at http://localhost:5555
[I 171020 13:47:51 command:144] Broker: amqp://guest:**@localhost:5672//
[I 171020 13:47:51 command:147] Registered tasks:
['celery.accumulate',
'celery.backend_cleanup',
'celery.chain',
'celery.chord',
'celery.chord_unlock',
'celery.chunks',
'celery.group',
'celery.map',
'celery.starmap',
'djcelery_email_send_multiple',
'lms.celery.debug_task']
[I 171020 13:47:51 mixins:224] Connected to amqp://guest:**@127.0.0.1:5672//
[W 171020 13:47:53 control:44] 'stats' inspect method failed
[W 171020 13:47:53 control:44] 'active_queues' inspect method failed
[W 171020 13:47:53 control:44] 'registered' inspect method failed
[W 171020 13:47:53 control:44] 'scheduled' inspect method failed
[I 171020 14:42:44 command:47] SIGTERM detected, shutting down
[E 171020 14:42:47 signal:232] Signal handler <promise@0x7f9b93353678 --> <bound method Celery._autodiscover_tasks of <Celery lms at 0x7f9b914f5be0>>> raised: ImportError("cannot import name 'TrafSource'",)
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/utils/dispatch/signal.py", line 227, in send
response = receiver(signal=self, sender=sender, **named)
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 143, in __call__
return self.throw()
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 140, in __call__
retval = fun(*final_args, **final_kwargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 671, in _autodiscover_tasks
return self._autodiscover_tasks_from_fixups(related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 684, in _autodiscover_tasks_from_fixups
], related_name=related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 676, in _autodiscover_tasks_from_names
packages() if callable(packages) else packages, related_name,
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 223, in autodiscover_tasks
related_name) if mod)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in autodiscover_tasks
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in <listcomp>
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 274, in find_related_module
return importlib.import_module('{0}.{1}'.format(package, related_name))
File "/usr/lib/python3.6/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 994, in _gcd_import
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 678, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/home/andrey/skillbox/go.skillbox.ru/access/tasks.py", line 4, in <module>
from access.models import User, TrafSource, TrafTokenHistory
ImportError: cannot import name 'TrafSource'
[I 171020 14:42:47 command:139] Visit me at http://localhost:5555
[I 171020 14:42:47 command:144] Broker: amqp://guest:**@localhost:5672//
[I 171020 14:42:47 command:147] Registered tasks:
['celery.accumulate',
'celery.backend_cleanup',
'celery.chain',
'celery.chord',
'celery.chord_unlock',
'celery.chunks',
'celery.group',
'celery.map',
'celery.starmap',
'djcelery_email_send_multiple',
'lms.celery.debug_task']
[I 171020 14:42:47 mixins:224] Connected to amqp://guest:**@127.0.0.1:5672//
[W 171020 14:42:49 control:44] 'stats' inspect method failed
[W 171020 14:42:49 control:44] 'active_queues' inspect method failed
[W 171020 14:42:49 control:44] 'registered' inspect method failed
[W 171020 14:42:49 control:44] 'scheduled' inspect method failed
[W 171020 14:42:49 control:44] 'active' inspect method failed
[W 171020 14:42:49 control:44] 'reserved' inspect method failed
[W 171020 14:42:49 control:44] 'revoked' inspect method failed
[W 171020 14:42:49 control:44] 'conf' inspect method failed
[I 171020 14:56:39 command:47] SIGTERM detected, shutting down
[E 171020 14:56:42 signal:232] Signal handler <promise@0x7f15cad6f678 --> <bound method Celery._autodiscover_tasks of <Celery lms at 0x7f15c8f11be0>>> raised: ImportError("cannot import name 'TrafSource'",)
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/utils/dispatch/signal.py", line 227, in send
response = receiver(signal=self, sender=sender, **named)
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 143, in __call__
return self.throw()
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 140, in __call__
retval = fun(*final_args, **final_kwargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 671, in _autodiscover_tasks
return self._autodiscover_tasks_from_fixups(related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 684, in _autodiscover_tasks_from_fixups
], related_name=related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 676, in _autodiscover_tasks_from_names
packages() if callable(packages) else packages, related_name,
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 223, in autodiscover_tasks
related_name) if mod)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in autodiscover_tasks
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in <listcomp>
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 274, in find_related_module
return importlib.import_module('{0}.{1}'.format(package, related_name))
File "/usr/lib/python3.6/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 994, in _gcd_import
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 678, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/home/andrey/skillbox/go.skillbox.ru/access/tasks.py", line 4, in <module>
from access.models import User, TrafSource, TrafTokenHistory
ImportError: cannot import name 'TrafSource'
[I 171020 14:56:42 command:139] Visit me at http://localhost:5555
[I 171020 14:56:42 command:144] Broker: amqp://guest:**@localhost:5672//
[I 171020 14:56:42 command:147] Registered tasks:
['celery.accumulate',
'celery.backend_cleanup',
'celery.chain',
'celery.chord',
'celery.chord_unlock',
'celery.chunks',
'celery.group',
'celery.map',
'celery.starmap',
'djcelery_email_send_multiple',
'lms.celery.debug_task']
[I 171020 14:56:42 mixins:224] Connected to amqp://guest:**@127.0.0.1:5672//
[W 171020 14:56:45 control:44] 'stats' inspect method failed
[W 171020 14:56:45 control:44] 'active_queues' inspect method failed
[W 171020 14:56:45 control:44] 'registered' inspect method failed
[W 171020 14:56:45 control:44] 'scheduled' inspect method failed
[W 171020 14:56:45 control:44] 'active' inspect method failed
[W 171020 14:56:45 control:44] 'reserved' inspect method failed
[W 171020 14:56:45 control:44] 'revoked' inspect method failed
[W 171020 14:56:45 control:44] 'conf' inspect method failed
[I 171020 15:14:09 command:47] SIGTERM detected, shutting down
[E 171020 15:14:13 signal:232] Signal handler <promise@0x7f60e14ab678 --> <bound method Celery._autodiscover_tasks of <Celery lms at 0x7f60df64dbe0>>> raised: ImportError("cannot import name 'TrafSource'",)
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/utils/dispatch/signal.py", line 227, in send
response = receiver(signal=self, sender=sender, **named)
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 143, in __call__
return self.throw()
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 140, in __call__
retval = fun(*final_args, **final_kwargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 671, in _autodiscover_tasks
return self._autodiscover_tasks_from_fixups(related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 684, in _autodiscover_tasks_from_fixups
], related_name=related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 676, in _autodiscover_tasks_from_names
packages() if callable(packages) else packages, related_name,
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 223, in autodiscover_tasks
related_name) if mod)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in autodiscover_tasks
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in <listcomp>
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 274, in find_related_module
return importlib.import_module('{0}.{1}'.format(package, related_name))
File "/usr/lib/python3.6/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 994, in _gcd_import
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 678, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/home/andrey/skillbox/go.skillbox.ru/access/tasks.py", line 4, in <module>
from access.models import User, TrafSource, TrafTokenHistory
ImportError: cannot import name 'TrafSource'
[I 171020 15:14:13 command:139] Visit me at http://localhost:5555
[I 171020 15:14:13 command:144] Broker: amqp://guest:**@localhost:5672//
[I 171020 15:14:13 command:147] Registered tasks:
['celery.accumulate',
'celery.backend_cleanup',
'celery.chain',
'celery.chord',
'celery.chord_unlock',
'celery.chunks',
'celery.group',
'celery.map',
'celery.starmap',
'djcelery_email_send_multiple',
'lms.celery.debug_task']
[I 171020 15:14:13 mixins:224] Connected to amqp://guest:**@127.0.0.1:5672//
[W 171020 15:14:15 control:44] 'stats' inspect method failed
[W 171020 15:14:15 control:44] 'active_queues' inspect method failed
[W 171020 15:14:15 control:44] 'registered' inspect method failed
[W 171020 15:14:15 control:44] 'scheduled' inspect method failed
[I 171020 15:18:22 command:47] SIGTERM detected, shutting down
[E 171020 15:18:26 signal:232] Signal handler <promise@0x7ff7eb19f678 --> <bound method Celery._autodiscover_tasks of <Celery lms at 0x7ff7e9341b70>>> raised: ImportError("cannot import name 'TrafSource'",)
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/utils/dispatch/signal.py", line 227, in send
response = receiver(signal=self, sender=sender, **named)
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 143, in __call__
return self.throw()
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 140, in __call__
retval = fun(*final_args, **final_kwargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 671, in _autodiscover_tasks
return self._autodiscover_tasks_from_fixups(related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 684, in _autodiscover_tasks_from_fixups
], related_name=related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 676, in _autodiscover_tasks_from_names
packages() if callable(packages) else packages, related_name,
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 223, in autodiscover_tasks
related_name) if mod)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in autodiscover_tasks
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in <listcomp>
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 274, in find_related_module
return importlib.import_module('{0}.{1}'.format(package, related_name))
File "/usr/lib/python3.6/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 994, in _gcd_import
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 678, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/home/andrey/skillbox/go.skillbox.ru/access/tasks.py", line 4, in <module>
from access.models import User, TrafSource, TrafTokenHistory
ImportError: cannot import name 'TrafSource'
[I 171020 15:18:26 command:139] Visit me at http://localhost:5555
[I 171020 15:18:26 command:144] Broker: amqp://guest:**@localhost:5672//
[I 171020 15:18:26 command:147] Registered tasks:
['celery.accumulate',
'celery.backend_cleanup',
'celery.chain',
'celery.chord',
'celery.chord_unlock',
'celery.chunks',
'celery.group',
'celery.map',
'celery.starmap',
'djcelery_email_send_multiple',
'lms.celery.debug_task']
[I 171020 15:18:26 mixins:224] Connected to amqp://guest:**@127.0.0.1:5672//
[W 171020 15:18:28 control:44] 'stats' inspect method failed
[W 171020 15:18:28 control:44] 'active_queues' inspect method failed
[W 171020 15:18:28 control:44] 'registered' inspect method failed
[W 171020 15:18:28 control:44] 'scheduled' inspect method failed
[W 171020 15:18:28 control:44] 'active' inspect method failed
[W 171020 15:18:28 control:44] 'reserved' inspect method failed
[W 171020 15:18:28 control:44] 'revoked' inspect method failed
[W 171020 15:18:28 control:44] 'conf' inspect method failed
[I 171020 15:31:13 command:47] SIGTERM detected, shutting down
[E 171020 15:31:17 signal:232] Signal handler <promise@0x7fcacf94e678 --> <bound method Celery._autodiscover_tasks of <Celery lms at 0x7fcacdaf0be0>>> raised: ImportError("cannot import name 'TrafSource'",)
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/utils/dispatch/signal.py", line 227, in send
response = receiver(signal=self, sender=sender, **named)
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 143, in __call__
return self.throw()
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 140, in __call__
retval = fun(*final_args, **final_kwargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 671, in _autodiscover_tasks
return self._autodiscover_tasks_from_fixups(related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 684, in _autodiscover_tasks_from_fixups
], related_name=related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 676, in _autodiscover_tasks_from_names
packages() if callable(packages) else packages, related_name,
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 223, in autodiscover_tasks
related_name) if mod)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in autodiscover_tasks
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in <listcomp>
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 274, in find_related_module
return importlib.import_module('{0}.{1}'.format(package, related_name))
File "/usr/lib/python3.6/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 994, in _gcd_import
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 678, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/home/andrey/skillbox/go.skillbox.ru/access/tasks.py", line 4, in <module>
from access.models import User, TrafSource, TrafTokenHistory
ImportError: cannot import name 'TrafSource'
[I 171020 15:31:17 command:139] Visit me at http://localhost:5555
[I 171020 15:31:17 command:144] Broker: amqp://guest:**@localhost:5672//
[I 171020 15:31:17 command:147] Registered tasks:
['celery.accumulate',
'celery.backend_cleanup',
'celery.chain',
'celery.chord',
'celery.chord_unlock',
'celery.chunks',
'celery.group',
'celery.map',
'celery.starmap',
'djcelery_email_send_multiple',
'lms.celery.debug_task']
[I 171020 15:31:17 mixins:224] Connected to amqp://guest:**@127.0.0.1:5672//
[W 171020 15:31:19 control:44] 'stats' inspect method failed
[W 171020 15:31:19 control:44] 'active_queues' inspect method failed
[W 171020 15:31:19 control:44] 'registered' inspect method failed
[W 171020 15:31:19 control:44] 'scheduled' inspect method failed
[W 171020 15:31:19 control:44] 'active' inspect method failed
[W 171020 15:31:19 control:44] 'reserved' inspect method failed
[W 171020 15:31:19 control:44] 'revoked' inspect method failed
[W 171020 15:31:19 control:44] 'conf' inspect method failed
[I 171020 16:52:23 command:47] SIGTERM detected, shutting down
[E 171020 16:52:26 signal:232] Signal handler <promise@0x7fb7bea9a678 --> <bound method Celery._autodiscover_tasks of <Celery lms at 0x7fb7bcc3cb70>>> raised: ImportError("cannot import name 'TrafSource'",)
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/utils/dispatch/signal.py", line 227, in send
response = receiver(signal=self, sender=sender, **named)
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 143, in __call__
return self.throw()
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 140, in __call__
retval = fun(*final_args, **final_kwargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 671, in _autodiscover_tasks
return self._autodiscover_tasks_from_fixups(related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 684, in _autodiscover_tasks_from_fixups
], related_name=related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 676, in _autodiscover_tasks_from_names
packages() if callable(packages) else packages, related_name,
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 223, in autodiscover_tasks
related_name) if mod)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in autodiscover_tasks
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in <listcomp>
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 274, in find_related_module
return importlib.import_module('{0}.{1}'.format(package, related_name))
File "/usr/lib/python3.6/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 994, in _gcd_import
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 678, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/home/andrey/skillbox/go.skillbox.ru/access/tasks.py", line 4, in <module>
from access.models import User, TrafSource, TrafTokenHistory
ImportError: cannot import name 'TrafSource'
[I 171020 16:52:26 command:139] Visit me at http://localhost:5555
[I 171020 16:52:26 command:144] Broker: amqp://guest:**@localhost:5672//
[I 171020 16:52:26 command:147] Registered tasks:
['celery.accumulate',
'celery.backend_cleanup',
'celery.chain',
'celery.chord',
'celery.chord_unlock',
'celery.chunks',
'celery.group',
'celery.map',
'celery.starmap',
'djcelery_email_send',
'lms.celery.debug_task']
[I 171020 16:52:26 mixins:224] Connected to amqp://guest:**@127.0.0.1:5672//
[W 171020 16:52:28 control:44] 'stats' inspect method failed
[W 171020 16:52:28 control:44] 'active_queues' inspect method failed
[W 171020 16:52:28 control:44] 'registered' inspect method failed
[W 171020 16:52:28 control:44] 'scheduled' inspect method failed
[W 171020 16:52:28 control:44] 'active' inspect method failed
[W 171020 16:52:28 control:44] 'reserved' inspect method failed
[W 171020 16:52:28 control:44] 'revoked' inspect method failed
[W 171020 16:52:28 control:44] 'conf' inspect method failed
[I 171020 19:20:51 command:47] SIGTERM detected, shutting down
[E 171023 08:32:22 signal:232] Signal handler <promise@0x7f249b502678 --> <bound method Celery._autodiscover_tasks of <Celery lms at 0x7f24996a4be0>>> raised: ImportError("cannot import name 'TrafSource'",)
Traceback (most recent call last):
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/utils/dispatch/signal.py", line 227, in send
response = receiver(signal=self, sender=sender, **named)
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 143, in __call__
return self.throw()
File "/home/andrey/skillbox/lib/python3.6/site-packages/vine/promises.py", line 140, in __call__
retval = fun(*final_args, **final_kwargs)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 671, in _autodiscover_tasks
return self._autodiscover_tasks_from_fixups(related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 684, in _autodiscover_tasks_from_fixups
], related_name=related_name)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/app/base.py", line 676, in _autodiscover_tasks_from_names
packages() if callable(packages) else packages, related_name,
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 223, in autodiscover_tasks
related_name) if mod)
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in autodiscover_tasks
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 248, in <listcomp>
return [find_related_module(pkg, related_name) for pkg in packages]
File "/home/andrey/skillbox/lib/python3.6/site-packages/celery/loaders/base.py", line 274, in find_related_module
return importlib.import_module('{0}.{1}'.format(package, related_name))
File "/usr/lib/python3.6/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 994, in _gcd_import
File "<frozen importlib._bootstrap>", line 971, in _find_and_load
File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 678, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/home/andrey/skillbox/go.skillbox.ru/access/tasks.py", line 4, in <module>
from access.models import User, TrafSource, TrafTokenHistory
ImportError: cannot import name 'TrafSource'
[I 171023 08:32:22 command:139] Visit me at http://localhost:5555
[I 171023 08:32:22 command:144] Broker: amqp://guest:**@localhost:5672//
[I 171023 08:32:22 command:147] Registered tasks:
['celery.accumulate',
'celery.backend_cleanup',
'celery.chain',
'celery.chord',
'celery.chord_unlock',
'celery.chunks',
'celery.group',
'celery.map',
'celery.starmap',
'djcelery_email_send',
'lms.celery.debug_task']
[I 171023 08:32:22 mixins:224] Connected to amqp://guest:**@127.0.0.1:5672//
[W 171023 08:32:24 control:44] 'stats' inspect method failed
[W 171023 08:32:24 control:44] 'active_queues' inspect method failed
[W 171023 08:32:24 control:44] 'registered' inspect method failed
[W 171023 08:32:24 control:44] 'scheduled' inspect method failed
[W 171023 08:32:24 control:44] 'active' inspect method failed
[W 171023 08:32:24 control:44] 'reserved' inspect method failed
[W 171023 08:32:24 control:44] 'revoked' inspect method failed
[W 171023 08:32:24 control:44] 'conf' inspect method failed
[E 171023 14:55:56 events:123] Failed to capture events: '[Errno 104] Connection reset by peer', trying again in 1 seconds.
[I 171023 14:56:17 mixins:224] Connected to amqp://guest:**@127.0.0.1:5672//
[E 171023 14:56:17 events:123] Failed to capture events: '[Errno 104] Connection reset by peer', trying again in 1 seconds.
[I 171023 14:56:18 mixins:224] Connected to amqp://guest:**@127.0.0.1:5672//
[I 171023 19:05:28 command:47] SIGTERM detected, shutting down

@ -0,0 +1,121 @@
[2017-10-20 13:12:57,958: INFO/MainProcess] beat: Starting...
celery beat v4.1.0 (latentcall) is starting.
__ - ... __ - _
LocalTime -> 2017-10-20 13:12:57
Configuration ->
. broker -> amqp://guest:**@localhost:5672//
. loader -> celery.loaders.app.AppLoader
. scheduler -> celery.beat.PersistentScheduler
. db -> celerybeat-schedule
. logfile -> [stderr]@%INFO
. maxinterval -> 5.00 minutes (300s)
[2017-10-20 13:32:31,865: INFO/MainProcess] beat: Starting...
celery beat v4.1.0 (latentcall) is starting.
__ - ... __ - _
LocalTime -> 2017-10-20 13:32:31
Configuration ->
. broker -> amqp://guest:**@localhost:5672//
. loader -> celery.loaders.app.AppLoader
. scheduler -> celery.beat.PersistentScheduler
. db -> celerybeat-schedule
. logfile -> [stderr]@%INFO
. maxinterval -> 5.00 minutes (300s)
[2017-10-20 13:38:00,107: INFO/MainProcess] beat: Starting...
celery beat v4.1.0 (latentcall) is starting.
__ - ... __ - _
LocalTime -> 2017-10-20 13:38:00
Configuration ->
. broker -> amqp://guest:**@localhost:5672//
. loader -> celery.loaders.app.AppLoader
. scheduler -> celery.beat.PersistentScheduler
. db -> celerybeat-schedule
. logfile -> [stderr]@%INFO
. maxinterval -> 5.00 minutes (300s)
[2017-10-20 13:47:51,058: INFO/MainProcess] beat: Starting...
celery beat v4.1.0 (latentcall) is starting.
__ - ... __ - _
LocalTime -> 2017-10-20 13:47:51
Configuration ->
. broker -> amqp://guest:**@localhost:5672//
. loader -> celery.loaders.app.AppLoader
. scheduler -> celery.beat.PersistentScheduler
. db -> celerybeat-schedule
. logfile -> [stderr]@%INFO
. maxinterval -> 5.00 minutes (300s)
[2017-10-20 14:42:47,578: INFO/MainProcess] beat: Starting...
celery beat v4.1.0 (latentcall) is starting.
__ - ... __ - _
LocalTime -> 2017-10-20 14:42:47
Configuration ->
. broker -> amqp://guest:**@localhost:5672//
. loader -> celery.loaders.app.AppLoader
. scheduler -> celery.beat.PersistentScheduler
. db -> celerybeat-schedule
. logfile -> [stderr]@%INFO
. maxinterval -> 5.00 minutes (300s)
[2017-10-20 14:56:42,777: INFO/MainProcess] beat: Starting...
celery beat v4.1.0 (latentcall) is starting.
__ - ... __ - _
LocalTime -> 2017-10-20 14:56:42
Configuration ->
. broker -> amqp://guest:**@localhost:5672//
. loader -> celery.loaders.app.AppLoader
. scheduler -> celery.beat.PersistentScheduler
. db -> celerybeat-schedule
. logfile -> [stderr]@%INFO
. maxinterval -> 5.00 minutes (300s)
[2017-10-20 15:14:13,137: INFO/MainProcess] beat: Starting...
celery beat v4.1.0 (latentcall) is starting.
__ - ... __ - _
LocalTime -> 2017-10-20 15:14:13
Configuration ->
. broker -> amqp://guest:**@localhost:5672//
. loader -> celery.loaders.app.AppLoader
. scheduler -> celery.beat.PersistentScheduler
. db -> celerybeat-schedule
. logfile -> [stderr]@%INFO
. maxinterval -> 5.00 minutes (300s)
[2017-10-20 15:18:26,051: INFO/MainProcess] beat: Starting...
celery beat v4.1.0 (latentcall) is starting.
__ - ... __ - _
LocalTime -> 2017-10-20 15:18:26
Configuration ->
. broker -> amqp://guest:**@localhost:5672//
. loader -> celery.loaders.app.AppLoader
. scheduler -> celery.beat.PersistentScheduler
. db -> celerybeat-schedule
. logfile -> [stderr]@%INFO
. maxinterval -> 5.00 minutes (300s)
[2017-10-20 15:31:17,073: INFO/MainProcess] beat: Starting...
celery beat v4.1.0 (latentcall) is starting.
__ - ... __ - _
LocalTime -> 2017-10-20 15:31:17
Configuration ->
. broker -> amqp://guest:**@localhost:5672//
. loader -> celery.loaders.app.AppLoader
. scheduler -> celery.beat.PersistentScheduler
. db -> celerybeat-schedule
. logfile -> [stderr]@%INFO
. maxinterval -> 5.00 minutes (300s)
[2017-10-20 16:52:26,958: INFO/MainProcess] beat: Starting...
celery beat v4.1.0 (latentcall) is starting.
__ - ... __ - _
LocalTime -> 2017-10-20 16:52:26
Configuration ->
. broker -> amqp://guest:**@localhost:5672//
. loader -> celery.loaders.app.AppLoader
. scheduler -> celery.beat.PersistentScheduler
. db -> celerybeat-schedule
. logfile -> [stderr]@%INFO
. maxinterval -> 5.00 minutes (300s)
[2017-10-23 08:32:22,755: INFO/MainProcess] beat: Starting...
celery beat v4.1.0 (latentcall) is starting.
__ - ... __ - _
LocalTime -> 2017-10-23 08:32:22
Configuration ->
. broker -> amqp://guest:**@localhost:5672//
. loader -> celery.loaders.app.AppLoader
. scheduler -> celery.beat.PersistentScheduler
. db -> celerybeat-schedule
. logfile -> [stderr]@%INFO
. maxinterval -> 5.00 minutes (300s)

@ -1,20 +0,0 @@
# -*- 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='Прохождение было преостановленно'),
),
]

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-04-16 18:50
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('progress', '0009_progress_is_freeze'),
]
operations = [
migrations.AddField(
model_name='progress',
name='exp_date',
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата сгорания'),
),
]

@ -12,9 +12,7 @@ class Progress(models.Model):
user = models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='Студент') user = models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='Студент')
course_token = models.UUIDField(verbose_name="Токен курса", editable=False) course_token = models.UUIDField(verbose_name="Токен курса", editable=False)
is_finish = models.BooleanField(verbose_name="Окончен ли курс", default=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) only_watch = models.BooleanField(verbose_name="Только просмотр", default=False)
exp_date = models.DateTimeField(verbose_name='Дата сгорания', blank=True, null=True)
def progress_status(self, sorted_token_list): def progress_status(self, sorted_token_list):
""" """

@ -17,23 +17,6 @@ class ProgressSerializer(serializers.ModelSerializer):
return [ProgressLessonSerializer(i).data for i in self.progresslesson_set.all()] 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): class SecureProgressSerializer(serializers.ModelSerializer):
jwt_token = serializers.SerializerMethodField() jwt_token = serializers.SerializerMethodField()

@ -3,14 +3,9 @@ from django.conf.urls import url
from progress import views from progress import views
urlpatterns = [ urlpatterns = [
url(r'progress_dynamic/$', views.CourseProgressDynamicView.as_view()),
url(r'students/(?P<teacher_token>[0-9A-Fa-f-]+)/$', views.StudentWorkView.as_view()), url(r'students/(?P<teacher_token>[0-9A-Fa-f-]+)/$', views.StudentWorkView.as_view()),
url(r'student/$', views.StudentUpdateProgress.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'teacher/$', views.TeacherUpdateProgress.as_view()),
url(r'set-progress/$', views.SetProgress.as_view()), url(r'set-progress/$', views.SetProgress.as_view()),
url(r'get_hw_pay/$', views.get_teachers_pay), url(r'get_hw_pay/$', views.get_teachers_pay),
url(r'progress_token/$', views.ProgressToken.as_view()),
] ]

@ -1,11 +1,6 @@
import csv import csv
import datetime import datetime
from django.db import IntegrityError
from django.shortcuts import redirect
from dateutil.relativedelta import relativedelta
from django.contrib import auth
from django.utils import timezone
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
@ -15,12 +10,9 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from django.db.models import Q, Count from django.db.models import Q
import jwt
from django.conf import settings
from access.serializers import UserProgressSearchSerializer from courses.models import Course
from courses.models import Course, Lesson
from progress.models import ProgressLesson, Progress from progress.models import ProgressLesson, Progress
from progress.serializers import ProgressAnalyticSerializer, ProgressLessonSerializer, ProgressSerializer, \ from progress.serializers import ProgressAnalyticSerializer, ProgressLessonSerializer, ProgressSerializer, \
SecureProgressSerializer SecureProgressSerializer
@ -64,31 +56,19 @@ class CourseProgressDynamicView(APIView):
renderer_classes = (JSONRenderer,) renderer_classes = (JSONRenderer,)
@staticmethod @staticmethod
def get(request): def get(request, token):
course_token = request.GET.get('course_token', None)
from_date = int(request.GET.get('from', '7'))
to_date = int(request.GET.get('to', '0'))
if request.user.is_authenticated() and request.user.is_staff: if request.user.is_authenticated() and request.user.is_staff:
progresses = ProgressLesson.objects.filter(status='done')
if not course_token is None:
try: try:
progresses = progresses.filter(progress__course_token=course_token) progresses = Progress.objects.filter(course_token=token)
except ProgressLesson.DoesNotExist:
return Response('incorrect course token', status=404)
progresses = progresses.filter(finish_date__gt=timezone.now() - relativedelta(days=from_date))
progresses = progresses.filter(finish_date__lt=timezone.now() - relativedelta(days=to_date))
progresses = progresses.order_by('finish_date')
res = {} res = {}
progresses = progresses.extra({'finish_date_day': "date(finish_date)"})\ for i in progresses:
.values('progress__course_token', 'finish_date_day')\ key = i.progresslesson_set.filter(status="done").count() + 1
.annotate(total=Count('progress')) res[key] = 1 if not key in res.keys() else res[key] + 1
#TODO Доделать
return Response(res, status=200) return Response(res, status=200)
return Response('Доступно только персоналу', status=403) except ValidationError:
return Response("Bad request", status=400)
return Response(status=403)
class CourseProgressUserView(APIView): class CourseProgressUserView(APIView):
@ -112,7 +92,6 @@ class CourseProgressUserView(APIView):
class TeacherUpdateProgress(APIView): class TeacherUpdateProgress(APIView):
permission_classes = (permissions.IsAuthenticated, )
renderer_classes = (JSONRenderer,) renderer_classes = (JSONRenderer,)
@staticmethod @staticmethod
@ -133,6 +112,7 @@ class TeacherUpdateProgress(APIView):
student = get_user_model().objects.get(out_key=student_out_key) student = get_user_model().objects.get(out_key=student_out_key)
p = Progress.objects.get( p = Progress.objects.get(
user=student, user=student,
teacher=request.user,
course_token=course_token, course_token=course_token,
) )
@ -221,13 +201,6 @@ class StudentUpdateProgress(APIView):
not pv.progress.progresslesson_set.filter(status=ProgressLesson.STATUSES.wait).exists(): not pv.progress.progresslesson_set.filter(status=ProgressLesson.STATUSES.wait).exists():
pv.status = ProgressLesson.STATUSES.wait pv.status = ProgressLesson.STATUSES.wait
pv.comment_tokens.append(comment) 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: elif comment is None:
return Response("Не преложен комментарий", status=400) return Response("Не преложен комментарий", status=400)
@ -275,74 +248,36 @@ class UploadCourseProgressUserView(APIView):
return Response(status=403) return Response(status=403)
class FindProgressView(APIView): class UserGuardView(APIView):
renderer_classes = (JSONRenderer,) renderer_classes = (JSONRenderer,)
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
@staticmethod @staticmethod
def get(request): def get(request, pk, page):
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 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: try:
p = Progress.objects.get(id=key) user = get_user_model().objects.get(out_key=pk)
p.is_freeze = is_freeze except get_user_model().DoesNotExist:
p.save() return Response("User doesn't exist", status=404)
except Progress.DoesNotExist:
return Response("не найден прогресс по заданному id", status=404)
return Response(status=204) is_i = request.user == user
res_403 = Response('Permission denied', status=403)
res_204 = Response(status=204)
if is_i and not request.user.groups.filter(name='teachers').exists() and page == 'homeworks':
return res_403
class ChangeTeacherView(APIView): if is_i and not \
renderer_classes = (JSONRenderer,) request.user.groups.filter(name__in=['students', 'managers', 'lead_managers']).exists() \
permission_classes = (IsAuthenticated,) and page == 'payment':
return res_403
@staticmethod if is_i:
def post(request): return res_204
if not request.user.is_staff:
return Response("Только сотрудники персонала могут вносить изменение в прогресс", status=403)
key = request.JSON.get('id', None)
teacher_email = request.JSON.get('teacher_email', False)
try: if page == 'profile' and (request.user.is_superuser or request.user.is_staff):
p = Progress.objects.get(id=key) return res_204
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 Response(status=204) return res_403
class SetProgress(APIView): class SetProgress(APIView):
@ -467,63 +402,16 @@ def get_teachers_pay(request):
response['Content-Disposition'] = 'attachment; filename="%s.csv"' % file_name response['Content-Disposition'] = 'attachment; filename="%s.csv"' % file_name
writer = csv.writer(response) writer = csv.writer(response)
writer.writerow(['Почта студента', 'Имя студента', 'Тема', 'Курс', 'Дата', 'Время',]) writer.writerow(['Почта студента', 'Имя студента', 'Курс', 'Дата', 'Время',])
for i in progress_lessons.order_by('-finish_date'): for i in progress_lessons.order_by('-finish_date'):
course_api = CourseParamsApi(i.progress.course_token) course_api = CourseParamsApi(i.progress.course_token)
writer.writerow([ writer.writerow([
i.progress.user.email, i.progress.user.email,
i.progress.user.get_full_name(), i.progress.user.get_full_name(),
#TODO очередной костыль
Lesson.objects.get(token=i.lesson_token).topic.title,
course_api.get_slug_and_title()['title'], course_api.get_slug_and_title()['title'],
i.finish_date.date(), i.finish_date.date(),
i.finish_date.time(), i.finish_date.time(),
]) ])
return response return response
class ProgressToken(APIView):
renderer_classes = (JSONRenderer,)
@staticmethod
def get(request):
token = request.GET.get('token', None)
if token is None:
return Response("bad request", 400)
payload = jwt.decode(token, settings.COURSE_PROGRESS_SECRET_KEY, algorithms=['HS256'])
try:
user = get_user_model().objects.get(email=payload['email'].lower())
except get_user_model().DoesNotExist:
user = get_user_model().objects.create_student(email=payload['email'].lower())
user.is_active = True
user.save()
try:
course = Course.objects.get(token=payload['course_token'])
except get_user_model().DoesNotExist:
return Response("Course doesn't exist", status=404)
teacher = get_user_model().objects.get(out_key=course.get_teacher())
try:
p = Progress.objects.create(
user=user,
course_token=payload['course_token'],
teacher=teacher,
exp_date=timezone.now() + relativedelta(days=int(payload['period'])),
)
ProgressLesson.objects.create(
progress=p,
lesson_token=course.get_first_lesson().token,
checker=p.user,
)
except IntegrityError:
pass
auth.login(request, user)
return redirect('/')

@ -1,5 +1,5 @@
[pytest] [pytest]
DJANGO_SETTINGS_MODULE = lms.settings DJANGO_SETTINGS_MODULE = lms.settings_qa
norecursedirs = env/* docs/* misc/* static/* norecursedirs = env/* docs/* misc/* static/*
;addopts = --flake8 -vvs ;addopts = --flake8 -vvs

@ -1,6 +0,0 @@
<div> Выставлен новый счет </div>
<div> Пользователь: {{ user_email }} </div>
<div> Продавец: {{ opener_full_name }} </div>
<div> Курс: {{ course_title }} </div>
<div> Дата продажи: {{ date }} </div>
<div> Сумма: {{ price }} руб. </div>

@ -1,7 +0,0 @@
<div> Успешный платёж </div>
<div> Пользователь: {{ user_email }} </div>
<div> Продавец: {{ opener_full_name }} </div>
<div> Курс: {{ course_title }} </div>
<div> Дата продажи: {{ date }} </div>
<div> Дата оплаты: {{ finish_date }} </div>
<div> Сумма: {{ price }} руб. </div>

@ -1,57 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Тестовый платёж</title>
</head>
<body>
<div>
Номер карты: 4444 4444 4444 4448 <br>
Действует до: любой год и месяц в будущем <br>
Код CVV: 000 <br>
Больше инфы
<a href="https://tech.yandex.ru/money/doc/payment-solution/examples/examples-test-data-docpage/" target="_blank">
В доке
</a><br>
Отправление формы срабатывает только раз затем нужно будет заменить orderNumber
</div>
<br><br><br>
<form action="https://demomoney.yandex.ru/eshop.xml" method="post" target="_blank">
<table>
<tr>
<td>shopId</td>
<td><input name="shopId" value="{{ shop_id }}" type="text"></td>
</tr>
<tr>
<td>scid</td>
<td><input name="scid" value="{{ sc_id }}" type="text"></td>
</tr>
<tr>
<td>sum</td>
<td><input name="sum" value="100.50" type="text"></td>
</tr>
<tr>
<td>customerNumber</td>
<td><input name="customerNumber" value="abc000" type="text"/></td>
</tr>
<tr>
<td>orderNumber</td>
<td><input name="orderNumber" value="{{ order_number }}" type="text"/></td>
</tr>
<tr>
<td>cps_phone</td>
<td><input name="cps_phone" value="79110000000" type="text"/></td>
</tr>
<tr>
<td>cps_email</td>
<td><input name="cps_email" value="{{ user_email }}" type="text"/></td>
</tr>
<tr>
<td>rebillingOn</td>
<td><input name="rebillingOn" value="false" type="text"/></td>
</tr>
</table>
<input type="submit" value="Заплатить"/>
</form>
</body>
</html>

@ -1,28 +0,0 @@
import pytest
from tests.client import BetterAPIClient
pytest_plugins = [
'tests.fixtures.users',
]
@pytest.fixture
def api_client():
"""Anonymous client for REST API."""
client = BetterAPIClient()
return client
@pytest.fixture
def staff_client(user_staff):
"""Authorized as staff client for REST API."""
client = BetterAPIClient()
client.force_authenticate(user=user_staff)
return client
@pytest.fixture
def student_client(user_student):
"""Authorized as staff client for REST API."""
client = BetterAPIClient()
client.force_authenticate(user=user_student)
return client

@ -1,63 +0,0 @@
import pytest
from factories.users import UserFactory, AccountFactory
@pytest.fixture
def user_staff():
"""
Create user as staff with data:
email = 'admin@example.com'
password = 'test'
is_staff=True
is_active = True
is_superuser = True
"""
admin = UserFactory(
last_name='Иванов',
first_name='Иван',
email='admin@example.com',
is_staff=True,
is_active=True,
is_superuser=True
)
AccountFactory(owner=admin)
return admin
@pytest.fixture
def user_student():
"""
Create user as student with data:
email = 'student@example.com'
password = 'test'
is_active = True
"""
student = UserFactory(
last_name='Иванов',
first_name='Иван',
email='student@example.com',
is_staff=False,
is_active=True,
)
AccountFactory(owner=student)
return student
@pytest.fixture
def user_not_active_student():
"""
Create user as student with data:
email = 'notactivestudent@example.com'
password = 'test'
is_active = False
"""
student = UserFactory(
last_name='Иванов',
first_name='Иван',
email='notactivestudent@example.com',
is_staff=False,
is_active=False,
)
return student

@ -1,124 +0,0 @@
import mock
import pytest
from django.contrib.auth import get_user_model
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.EmailMessage.send')
def test_generate_password_by_manager(mocked_send_mail, staff_client,
student_client, user_student):
"""
Test generate new password from admin area by manager
"""
assert staff_client.get(
reverse('users:management-password'),
status=status.HTTP_405_METHOD_NOT_ALLOWED
)
assert student_client.get(
reverse('users:management-password'),
status=status.HTTP_403_FORBIDDEN
)
data = {
'email': user_student.email,
}
assert staff_client.post(
reverse('users:management-password'),
data=data,
status=status.HTTP_201_CREATED
)
test_user = get_object_or_404(get_user_model(), email=user_student.email)
assert not test_user.check_password('test')
assert mocked_send_mail.call_count == 1
assert student_client.post(
reverse('users:management-password'),
data=data,
status=status.HTTP_403_FORBIDDEN
)
wrong_data = {
'email': 'no_user@example.com',
}
assert staff_client.post(
reverse('users:management-password'),
data=wrong_data,
status=status.HTTP_404_NOT_FOUND
)
wrong_email = {
'email': 'no_user@example',
}
assert staff_client.post(
reverse('users:management-password'),
data=wrong_email,
status=status.HTTP_404_NOT_FOUND
)
@pytest.mark.django_db
def test_generate_password_by_manager_for_not_active_student(staff_client,
user_not_active_student):
"""
Test generate new password from admin area by manager for not active student
"""
data = {
'email': user_not_active_student.email,
}
assert staff_client.post(
reverse('users:management-password'),
data=data,
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
)
Loading…
Cancel
Save