Разрешение конфликтов

remotes/origin/feature/test_courses_
Andrey 8 years ago
commit 5306b025ce
  1. 3
      .gitignore
  2. 9
      Envoy.blade.php
  3. 7
      access/models/other.py
  4. 30
      access/serializers.py
  5. 1
      access/urls.py
  6. 5
      access/views.py
  7. 5
      config_app/settings/prod.env
  8. 5
      config_app/settings/prod.env.skeleton
  9. 23
      courses/migrations/0006_auto_20180323_1743.py
  10. 8
      courses/models.py
  11. 14
      courses/serializers.py
  12. 4
      courses/urls.py
  13. 165
      courses/views.py
  14. 7
      finance/admin.py
  15. 25
      finance/migrations/0004_auto_20180321_1653.py
  16. 22
      finance/migrations/0004_bill_date.py
  17. 35
      finance/migrations/0005_auto_20180329_1346.py
  18. 27
      finance/models.py
  19. 6
      finance/serializers.py
  20. 16
      finance/signals.py
  21. 86
      finance/tasks.py
  22. 1
      finance/urls.py
  23. 235
      finance/views.py
  24. 18
      lms/settings.py
  25. 2
      logs/finance_data.log
  26. 20
      progress/migrations/0009_progress_is_freeze.py
  27. 1
      progress/models.py
  28. 17
      progress/serializers.py
  29. 3
      progress/urls.py
  30. 84
      progress/views.py
  31. 6
      templates/mail/sales/back_set_bill.html
  32. 7
      templates/mail/sales/pay_access.html

3
.gitignore vendored

@ -36,6 +36,5 @@ coverage.xml
# Celery
celerybeat-schedule
/config_app/settings/dev.env
/config_app/settings/test.env
/media_qa/
/config_app/settings/local.env

@ -29,9 +29,12 @@ echo '{{ $new_release_dir }}'
@task('create_symlinks', ['on' => 'localhost'])
echo '>> Создание симлинков'
@if ($branch)
ln -nfs {{ $app_dir }}/configs/master/prod.env {{ $new_release_dir }}/config_app/settings/prod.env
ln -nfs {{ $app_dir }}/configs/dev/dev.env {{ $new_release_dir }}/config_app/settings/dev.env
@if ($branch == 'master')
ln -nfs {{ $app_dir }}/configs/master/prod.env {{ $new_release_dir }}/config_app/settings/local.env
ln -nfs {{ $app_dir }}/media/master {{ $new_release_dir }}/media
@endif
@if ($branch == 'dev')
ln -nfs {{ $app_dir }}/configs/dev/dev.env {{ $new_release_dir }}/config_app/settings/local.env
ln -nfs {{ $app_dir }}/media/master {{ $new_release_dir }}/media
@endif
@endtask

@ -53,6 +53,13 @@ class Account(models.Model):
def __str__(self):
return self.owner.email
def get_phone(self):
try:
return '' if self.phone is None else ('' if self.phone.national_number else \
(self.phone.national_number if self.phone.country_code is None else str(self.phone)))
except AttributeError:
return self.phone
class Meta:
verbose_name = 'Дополнительная информация о пользователе'
verbose_name_plural = 'Дополнительная информация о пользователе'

@ -3,11 +3,12 @@ from rest_framework import serializers
from access.models.other import Account
from achievements.serialers import DiplomaSerializer, AchievementsSerializer
from progress.serializers import SecureProgressSerializer
from progress.serializers import SecureProgressSerializer, SupportProgressSerializer
class AccountSerializer(serializers.ModelSerializer):
gender = serializers.SerializerMethodField()
phone = serializers.SerializerMethodField()
class Meta:
model = Account
@ -17,6 +18,10 @@ class AccountSerializer(serializers.ModelSerializer):
def get_gender(self):
return self.get_gender_display()
@staticmethod
def get_phone(self):
return self.get_phone()
class UserSelfSerializer(serializers.ModelSerializer):
account = serializers.SerializerMethodField()
@ -48,7 +53,7 @@ class UserSelfSerializer(serializers.ModelSerializer):
@staticmethod
def get_progresses(self):
return [SecureProgressSerializer(i).data for i in self.progress_set.all()]
return [SecureProgressSerializer(i).data for i in self.progress_set.filter(is_freeze=False)]
class UserProfileSerializer(serializers.ModelSerializer):
@ -84,7 +89,7 @@ class UserSearchSerializer(serializers.ModelSerializer):
@staticmethod
def get_phone(self):
return None if self.account.phone is None else self.account.phone.raw_input
return self.account.get_phone()
@staticmethod
def get_pay(self):
@ -101,3 +106,22 @@ class UserSearchSerializer(serializers.ModelSerializer):
@staticmethod
def get_last_request(self):
return self.useractivity.last_request
class UserProgressSearchSerializer(serializers.ModelSerializer):
phone = serializers.SerializerMethodField()
progresses = serializers.SerializerMethodField()
class Meta:
model = get_user_model()
fields = ('out_key', 'email', 'first_name',
'last_name', 'phone', 'progresses')
@staticmethod
def get_phone(self):
return self.account.get_phone()
@staticmethod
def get_progresses(self):
return [SupportProgressSerializer(i).data for i in self.progress_set.all()]

@ -10,7 +10,6 @@ urlpatterns = [
url(r'detail/(?P<out_key>[0-9A-Fa-f-]+)/$', 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'guard/(?P<pk>[0-9]{1,99})/(?P<page>.+)/$', progress.views.UserGuardView.as_view()),
url(r'find/$', views.FindUserView.as_view()),
url(r'registration/$', views.RegistrationView.as_view()),
url(r'change_password/$', views.ChangePasswordView.as_view()),

@ -28,7 +28,10 @@ class TeacherListView(APIView):
status_code = 200
def get(self, request):
return Response([i.email for i in get_user_model().objects.filter(groups__name='teachers')], self.status_code)
return Response([{
'email': i.email,
'token': i.out_key,
} for i in get_user_model().objects.filter(groups__name='teachers')], self.status_code)
class ResetPasswordView(APIView):

@ -1,5 +0,0 @@
DEBUG=False
SECRET_KEY='!eiquy7_+2#vn3z%zfp51$m-=tmvtcv*cj*@x$!v(_9btq0w=$'
DATABASE_URL='psql://team:nu5Xefise@127.0.0.1:5432/new_lms'
EMAIL_URL='smtp+tls://robo@skillbox.ru:nu5Xefise@smtp.gmail.com:587'
CACHE_URL=rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient

@ -0,0 +1,5 @@
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,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-03-23 17:43
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('courses', '0005_auto_20180222_1911'),
]
operations = [
migrations.RemoveField(
model_name='topic',
name='description',
),
migrations.RemoveField(
model_name='topic',
name='icon',
),
]

@ -58,8 +58,6 @@ class Lesson(models.Model):
class Topic(models.Model):
course = models.ForeignKey(to="Course", verbose_name='курс')
title = models.CharField(verbose_name='Название', max_length=255)
description = models.TextField(verbose_name='Описание', blank=True, null=True)
icon = models.ImageField(verbose_name='Иконка темы', null=True, blank=True)
sort = models.SmallIntegerField(verbose_name='Поле сортировки')
def __str__(self):
@ -75,12 +73,12 @@ class Topic(models.Model):
class CourseManager(models.Manager):
def update_or_create_course(self, image=None, big_image=None, statistic=None,
big_mobile_image=None, slug=None, teachers=None,
big_mobile_image=None, slug=None, teacher_tokens=None,
level=None, direction=None, **kwargs):
slug = slug if slug else slugify(unidecode.unidecode(kwargs['title']))
kwargs['teacher_tokens'] = teachers
kwargs['teacher_tokens'] = teacher_tokens
if image:
path = 'course/image%s.png' % slug
@ -101,7 +99,7 @@ class CourseManager(models.Manager):
kwargs['level'] = get_real_name(COURSE_LEVEL, level)
if direction:
kwargs['direction'] = get_real_name(COURSE_DIRECTION, direction)
kwargs['direction'] = get_real_name(COURSE_DIRECTION, direction[0])
try:
course = self.get(slug=slug)

@ -1,6 +1,8 @@
from rest_framework import serializers
from django.conf import settings
from courses.models import Course, Lesson, Topic
import os
class TopicSerializer(serializers.ModelSerializer):
@ -19,11 +21,12 @@ class MiniLessonSerializer(serializers.ModelSerializer):
class Meta:
model = Lesson
fields = ('title', 'free', 'token', 'is_hm')
fields = ('title', 'free', 'token', 'is_hm', 'sort')
class LessonSerializer(MiniLessonSerializer):
course_slug = serializers.SerializerMethodField()
materials = serializers.SerializerMethodField()
class Meta:
model = Lesson
@ -33,6 +36,15 @@ class LessonSerializer(MiniLessonSerializer):
def get_course_slug(self):
return self.topic.course.slug
@staticmethod
def get_materials(self):
try:
prefix = 'materials/%s/%s/%s' % (self.topic.course.token, self.topic.id, self.token)
name_list = os.listdir('%s/%s/' % (settings.MEDIA_ROOT, prefix))
return ["%s%s/%s" % (settings.MEDIA_URL, prefix, i) for i in name_list]
except FileNotFoundError:
return []
class TeacherLessonSerializer(MiniLessonSerializer):
topic_sort = serializers.SerializerMethodField()

@ -3,9 +3,13 @@ from django.conf.urls import url
from courses import views as views
urlpatterns = [
url(r'lesson/update/$', views.UpdateLessonView.as_view()),
url(r'lesson/delete/(?P<lesson_token>.+)/$', views.DeleteLessonView.as_view()),
url(r'lesson/teacher/(?P<token>.+)/$', views.LessonInfoView.as_view()),
url(r'lesson/(?P<token>.+)/$', views.LessonDetail.as_view()),
url(r'tree/(?P<slug>.+)/$', views.TreeView.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()),
]

@ -1,10 +1,9 @@
from jwt import DecodeError
from courses.models import Course, Lesson
from courses.models import Course, Lesson, Topic
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView
from django.contrib.auth import get_user_model
from courses.serializers import CourseDetailSerializer, CourseTreeSerializer, LessonSerializer, TeacherLessonSerializer
import jwt
@ -31,27 +30,6 @@ class CourseListView(APIView):
status_code = 200
def post(self, request):
"""
This API endpoint create/update course.
---
parameters:
- name: level
type: string
required: true
location: form
- name: direction
type: string
required: true
location: form
- name: statistic
type: string
required: true
location: form
...
"""
# TODO: Костыль
teachers_emails = request.JSON.get('teachers', [])
request.JSON['teachers'] = [get_user_model().objects.get(email=i).out_key for i in teachers_emails]
course = Course.objects.update_or_create_course(**request.JSON.dict())
return Response(CourseDetailSerializer(course).data, status=self.status_code)
@ -80,6 +58,141 @@ class CourseDetailView(APIView):
return Response(CourseDetailSerializer(Course.objects.get(slug=slug)).data, self.status_code)
class DeleteTopicView(APIView):
renderer_classes = (JSONRenderer,)
@staticmethod
def delete(request, topic_id):
if request.user and request.user.is_staff:
try:
t = Topic.objects.get(id=topic_id)
except Topic.DoesNotExist:
return Response("Темы не существует", status=404)
t.delete()
return Response(CourseTreeSerializer(t.course).data, status=200)
class DeleteLessonView(APIView):
renderer_classes = (JSONRenderer,)
@staticmethod
def delete(request, lesson_token):
if request.user and request.user.is_staff:
try:
l = Lesson.objects.get(token=lesson_token)
except Lesson.DoesNotExist:
return Response("Темы не существует", status=404)
l.delete()
return Response(CourseTreeSerializer(l.topic.course).data, status=200)
class UpdateLessonView(APIView):
renderer_classes = (JSONRenderer,)
@staticmethod
def post(request):
lesson_token = request.JSON.get('token', None)
sort = request.JSON.get('sort', None)
topic_id = request.JSON.get('topic', None)
title = request.JSON.get('title', None)
description = request.JSON.get('description', None)
video = request.JSON.get('video', None)
free = request.JSON.get('free', None)
is_hm = request.JSON.get('is_hm', None)
if topic_id is None:
return Response("topic не передан", status=400)
if sort is None:
return Response("sort не передан", status=400)
try:
topic = Topic.objects.get(id=topic_id)
except Topic.DoesNotExist:
return Response("Тема не найдена", status=404)
if lesson_token is None:
if title is None:
return Response("Название урока не переданно", status=400)
for lesson in reversed(topic.lesson_set.filter(sort__gte=sort)):
lesson.sort = lesson.sort + 1
lesson.save()
l = Lesson.objects.create(
title=title,
topic=topic,
sort=sort,
)
else:
try:
l = Lesson.objects.get(token=lesson_token)
except Lesson.DoesNotExist:
return Response("Урок не найден", status=404)
l.title = l.title if title is None else title
l.video = l.video if video is None else video
l.free = l.free if free is None else free
l.is_hm = l.is_hm if is_hm is None else is_hm
l.description = l.description if description is None else description
if not l.sort == sort:
for lesson in reversed(topic.lesson_set.filter(sort__gte=sort)):
lesson.sort = lesson.sort + 1
lesson.save()
l.sort = sort
l.save()
return Response(CourseTreeSerializer(topic.course).data, status=200)
class UpdateTopicView(APIView):
renderer_classes = (JSONRenderer,)
@staticmethod
def post(request):
topic_id = request.JSON.get('id', None)
sort = request.JSON.get('sort', None)
course_token = request.JSON.get('course_token', None)
title = request.JSON.get('title', None)
if course_token is None:
return Response("Не передан course_token", status=400)
if sort is None:
return Response("Не передан sort", status=400)
try:
course = Course.objects.get(token=course_token)
except Course.DoesNotExist:
return Response("Курс не найден", status=404)
try:
if topic_id:
t = Topic.objects.get(id=topic_id)
if not t.sort == sort:
for topic in reversed(course.topic_set.filter(sort__gte=sort)):
topic.sort = topic.sort + 1
topic.save()
t.sort = sort
t.title = t.title if title is None else title
t.save()
else:
raise Topic.DoesNotExist()
except Topic.DoesNotExist:
if title is None:
return Response("Не передан title", status=400)
for topic in reversed(course.topic_set.filter(sort__gte=sort)):
topic.sort = topic.sort + 1
topic.save()
Topic.objects.create(
course=course,
title=title,
sort=sort,
)
return Response(CourseTreeSerializer(course).data, status=200)
class LessonInfoView(APIView):
renderer_classes = (JSONRenderer,)
status_code = 200
@ -109,7 +222,7 @@ class LessonDetail(APIView):
l = LessonSerializer(lesson).data
try:
payload = None if jwt_token is None\
payload = None if jwt_token is None \
else jwt.decode(jwt_token, settings.COURSE_PROGRESS_SECRET_KEY, algorithms=['HS256'])
except DecodeError:
payload = None
@ -117,7 +230,7 @@ class LessonDetail(APIView):
course = lesson.topic.course
if payload is None:
if not lesson.free:
if not (lesson.free or request.user.is_authenticated and request.user.is_staff):
return Response("Bad token", status=400)
else:
@ -144,7 +257,7 @@ class LessonDetail(APIView):
if not new_lesson:
return Response("Permission denied", status=403)
#TODO Задача для селери
# TODO Задача для селери
add_lesson(request.user.out_key, course.token, lesson.token, course.get_teacher(), lesson.is_hm)
return Response(l, status=200)

@ -3,5 +3,10 @@ from django.contrib import admin
from finance.models import Bill, Invoice
class InvoiceAdmin(admin.ModelAdmin):
list_display = ('__str__', 'rebilling_on', 'rebilling')
admin.site.register(Bill)
admin.site.register(Invoice)
admin.site.register(Invoice, InvoiceAdmin)

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-03-21 16:53
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('finance', '0003_auto_20180315_1358'),
]
operations = [
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='Повторять платеж'),
),
]

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-03-23 17:43
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('finance', '0003_auto_20180315_1358'),
]
operations = [
migrations.AddField(
model_name='bill',
name='date',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Дата выставления'),
preserve_default=False,
),
]

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-03-29 13:46
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('finance', '0004_bill_date'),
]
operations = [
migrations.CreateModel(
name='InstallmentPlan',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('invoice_amount', models.IntegerField(verbose_name='Количество платежей')),
('price', models.IntegerField(verbose_name='Цена одного платежа')),
('bill', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='finance.Bill', verbose_name='Связный счёт')),
],
options={
'verbose_name': 'Рассрочка',
'verbose_name_plural': 'Рассрочки',
},
),
migrations.AlterField(
model_name='invoice',
name='date',
field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'),
),
]

@ -3,8 +3,6 @@ from django.conf import settings
from django.db import models
from yandex_money.models import Payment
from courses.models import Course, Lesson
class Bill(models.Model):
course_token = models.UUIDField(verbose_name="Токен курса", editable=False)
@ -13,6 +11,7 @@ class Bill(models.Model):
comment = models.TextField(verbose_name='Комментарий продавца', help_text='Будет показано пользователю',
blank=True, editable=False)
description = models.TextField(verbose_name='Внутренняя заметка', blank=True)
date = models.DateTimeField(verbose_name="Дата выставления", auto_now_add=True)
def __str__(self):
return '%s: %s' % (self.id, self.user)
@ -20,6 +19,12 @@ class Bill(models.Model):
def get_full_price(self):
return sum([i.price for i in self.invoice_set.all() if not i.price is None])
def check_validate(self, invoice_id):
return self.invoice_set.filter(is_open=True).exclude(id=invoice_id).count() == 1
def check_pay(self):
return self.invoice_set.filter(status="F").exists()
class Meta:
verbose_name = 'Счет'
verbose_name_plural = 'Счета'
@ -43,7 +48,7 @@ class Invoice(models.Model):
('C', 'Отклонен'),
)
status = models.CharField(verbose_name='Статус', max_length=1, default='W', choices=BILL_STATUSES)
price = models.IntegerField(verbose_name='Сумма', editable=False, null=True, blank=True)
price = models.IntegerField(verbose_name='Сумма', editable=False, null=True, blank=True) #Todo На самом деле тут не далжно быть значений null
real_price = models.FloatField(verbose_name='Полученная сумма', null=True, blank=True,
help_text='Сумма, минус комиссия', editable=False)
method = models.CharField(verbose_name='Способ оплаты', max_length=2, default='Y', choices=BILL_METHOD)
@ -54,6 +59,8 @@ class Invoice(models.Model):
bill = models.ForeignKey(to=Bill, verbose_name="Связный счёт")
is_open = models.BooleanField(default=True, verbose_name="Открывает ли платёж курс")
date = models.DateTimeField(auto_now_add=True)
rebilling_on = models.BooleanField(verbose_name='Повторять платеж', default=False, editable=False)
rebilling = models.BooleanField(verbose_name='Повторный платеж', default=False, editable=False)
def __str__(self):
return '%s:%s %s' % (self.id, self.get_status_display(), self.bill.user)
@ -61,3 +68,17 @@ class Invoice(models.Model):
class Meta:
verbose_name = 'Платёж'
verbose_name_plural = 'Платежи'
class InstallmentPlan(models.Model):
bill = models.OneToOneField(to=Bill, verbose_name="Связный счёт")
date = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
invoice_amount = models.IntegerField(verbose_name="Количество платежей")
price = models.IntegerField(verbose_name="Цена одного платежа")
def __str__(self):
return '%s' % self.bill.user.email
class Meta:
verbose_name = 'Рассрочка'
verbose_name_plural = 'Рассрочки'

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

@ -16,19 +16,18 @@ def invoice_signal(instance, **kwargs):
course = Course.objects.get(token=instance.bill.course_token)
if instance.yandex_pay and instance.method == 'Y' and instance.status == 'P':
if instance.yandex_pay and instance.method == 'Y' and instance.status == 'P' and not instance.rebilling:
msg = EmailMessage(
'Вам выставлен новый счёт',
'''Вам выставлен счёт, для оплаты перейдите по ссылке
%s/api/v1/finance/payment/%s/''' % (settings.DOMAIN, instance.yandex_pay.id,),
'robo@skillbox.ru',
[instance.yandex_pay.cps_email],
[instance.bill.opener.email],
"""%s для оплаты перейдите по ссылке
%s/api/v1/finance/payment/%s/""" % (instance.get_comment(), settings.DOMAIN, instance.yandex_pay.id),
to=[instance.yandex_pay.cps_email],
bcc=[instance.bill.opener.email],
reply_to=[instance.bill.opener.email],
)
msg.send()
if instance.status == 'F':
if instance.status == 'F' and not instance.rebilling:
if instance.is_open:
try:
Progress.objects.get(
@ -52,8 +51,7 @@ def invoice_signal(instance, **kwargs):
'''Вам открыт доступ к курсу "%s", вы можете перейти по ссылке и
ознакомиться с материалами %s/course/%s'''
% (course.title, settings.DOMAIN, course.slug),
'robo@skillbox.ru',
[instance.bill.user.email],
to=[instance.bill.user.email],
bcc=[instance.bill.opener.email],
reply_to=[instance.bill.opener.email],
)

@ -0,0 +1,86 @@
import json
from datetime import datetime, timedelta
import logging
import os
import requests
from django_celery_beat.models import CrontabSchedule, PeriodicTask
from yandex_money.models import Payment
from finance.models import Invoice
from lms import celery_app
from django.conf import settings
logger_yandex = logging.getLogger('yandex_money')
def setup_periodic_billing(order_number):
# TODO: настроить периодичность и срок окончания
# 12:00 первого числа каждого месяца
schedule, _ = CrontabSchedule.objects.get_or_create(
minute='0',
hour='12',
day_of_week='*',
day_of_month='1',
month_of_year='*'
)
PeriodicTask.objects.create(
crontab=schedule,
name='Periodic billing (order_number={})'.format(order_number),
task='finance.tasks.periodic_billing',
kwargs=json.dumps({
'order_number': order_number
}),
expires=datetime.utcnow() + timedelta(days=180) # в течение полугода
)
@celery_app.task
def periodic_billing(order_number):
try:
sample = Invoice.objects.get(yandex_pay__order_number=order_number)
except Invoice.DoesNotExist:
raise ValueError('Номер заказа {} не найден'.format(order_number))
bill = sample.bill
invoice = Invoice.objects.create(
status='P',
price=sample.price,
method=sample.method,
rebilling=True,
bill=bill
)
if invoice.method == 'Y':
user = bill.user
yandex_pay = Payment.objects.create(
invoice_id=sample.yandex_pay.invoice_id,
order_amount=invoice.price,
customer_number=user.id,
user=user,
cps_email=user.email
)
invoice.yandex_pay = yandex_pay
invoice.save()
repeat_card_payment(invoice)
def repeat_card_payment(invoice):
resp = requests.post(settings.YANDEX_MONEY_MWS_URL + 'repeatCardPayment',
data={
'clientOrderId': invoice.id, # уникальное возрастающее целое число
'invoiceId': invoice.yandex_pay.invoice_id,
'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'))
logger_yandex.info(resp.text)

@ -7,5 +7,6 @@ urlpatterns = [
url(r'bills/([0-9]{1,99})/$', views.BillDetailView.as_view()),
url(r'bills_find/$', views.FindBillView.as_view()),
url(r'yandex/fail/$', views.YandexFailView.as_view()),
url(r'invoice/([0-9]{1,99})/$', views.InvoiceDetailView.as_view()),
url(r'invoices/$', views.get_invoices),
]

@ -4,6 +4,7 @@ import logging
import requests
from django.contrib.auth import get_user_model
from django.core.mail import EmailMessage
from django.db import IntegrityError
from django.db.models import Q
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import redirect
@ -12,11 +13,15 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from yandex_money.models import Payment
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from courses.models import Course
from courses.api import CourseParamsApi
from finance.models import Bill, Invoice
from finance.serializers import BillSerializer, InvoiceSerializer
from finance.tasks import setup_periodic_billing
from lms.global_decorators import transaction_decorator
from lms.tools import get_real_name
from django.utils import timezone
@ -40,84 +45,144 @@ class BillListView(APIView):
def post(self, request):
if request.user.is_authenticated and (request.user.groups.filter(name__in=['managers','lead_managers']).exists()
or request.user.is_superuser):
bill = request.JSON.get('bill')
children = request.JSON.get('children', [])
user = get_user_model().objects.get(email=request.JSON.get('user'))
opener = get_user_model().objects.get(email=request.JSON.get('opener'))
description = request.JSON.get('description', None)
comment = request.JSON.get('comment', None)
course_token = request.JSON.get('course_token', None)
if bill:
user = get_user_model().objects.get(email=bill['user'])
opener = get_user_model().objects.get(email=bill['opener'])
description = bill['description']
comment = bill['comment']
course_token = bill['course_token']
if course_token is None:
return Response("Идентификатор курса не передан", status=400)
try:
bill_obj = Bill.objects.get(user=user, course_token=course_token)
except Bill.DoesNotExist:
try:
bill_obj = Bill.objects.get(user=user, course_token=course_token)
except Bill.DoesNotExist:
bill_obj = Bill.objects.create(user=user, course_token=course_token)
except IntegrityError:
return Response("У пользователя уже есть счёт на этот курс", status=400)
bill_obj.opener = bill_obj.opener if opener is None else opener
bill_obj.description = bill_obj.description if description is None else description
bill_obj.comment = bill_obj.comment if comment is None else comment
bill_obj.save()
return Response(bill_obj.id, status=200)
return Response("Ошибка доступа, возможно вы разлогинились из другой вкладки браузера", status=403)
class InvoiceDetailView(APIView):
renderer_classes = (JSONRenderer,)
@staticmethod
def delete(request, invoice_id):
try:
i = Invoice.objects.get(id=invoice_id)
if not i.status == "F" and not (i.bill.check_pay() and i.is_open):
i.delete()
except Invoice.DoesNotExist:
pass
return Response(status=204)
@staticmethod
def post(request, invoice_id):
if request.user.is_authenticated and (request.user.groups.filter(name__in=['managers','lead_managers']).exists()
or request.user.is_superuser):
invoice_id = int(invoice_id)
bill_id = request.JSON.get('bill', None)
is_open = request.JSON.get('is_open', None)
method = request.JSON.get('method', None)
status = request.JSON.get('status', None)
price = request.JSON.get('price', None)
comment = request.JSON.get('comment', None)
real_price = request.JSON.get('real_price', None)
if bill_id is None:
return Response("Не передан id счёта", status=400)
if is_open is None or method is None or status is None or price is None:
return Response("Не передан один из пораметров is_open, method, status, price", status=400)
bill_obj.opener = opener
bill_obj.description = description
bill_obj.comment = comment
bill_obj.save()
for i in children:
status = get_real_name(elem=i['status'], array=Invoice.BILL_STATUSES)
try:
invoice_id = i['id']
except KeyError:
invoice_id = None
try:
if not invoice_id is None:
invoice = Invoice.objects.get(id=i['id'])
if invoice.status == "P" or invoice.status == status:
continue
else:
raise Invoice.DoesNotExist
except Invoice.DoesNotExist:
i['method'] = get_real_name(elem=i['method'], array=Invoice.BILL_METHOD)
i['status'] = status
i['bill'] = bill_obj
i['yandex_pay'] = None
invoice = Invoice.objects.create(**i)
if i['method'] == 'Y' and invoice.yandex_pay is None:
yandex_pay = Payment.objects.create(
order_amount=i['price'],
shop_amount=0,
customer_number=bill_obj.user.id,
user=bill_obj.user,
cps_email=bill_obj.user.email,
)
invoice.yandex_pay = yandex_pay
invoice.save()
msg = EmailMessage(
'Выставлен новый счёт.',
'''Менеджер %s выставил счёт пользователю %s на курс "%s".'''
% (
invoice.bill.opener.get_full_name(),
invoice.bill.user.email,
Course.objects.get(token=invoice.bill.course_token).title,
),
'robo@skillbox.ru',
[invoice.bill.opener.email],
bcc=['dmitry.dolya@skillbox.ru'],
)
msg.send()
res = {
"bill": BillSerializer(bill_obj).data,
"children": [InvoiceSerializer(i).data for i in bill_obj.invoice_set.all()],
try:
bill = Bill.objects.get(id=bill_id)
except Bill.DoesNotExist:
return Response('Не найден счёт с id=%s' % bill_id, status=404)
method = get_real_name(elem=method[0], array=Invoice.BILL_METHOD)
status = get_real_name(elem=status[0], array=Invoice.BILL_STATUSES)
if bill.check_validate(invoice_id) and is_open:
return Response("Уже есть платёж открывающий курс", status=400)
try:
invoice = Invoice.objects.get(id=invoice_id)
except Invoice.DoesNotExist:
if not invoice_id == 0:
return Response("Платёж не найден", status=404)
if bill.check_pay():
return Response(
"Нельзя добавить новый платёж, так как один из платежей по счёту уже оплачен", status=400)
invoice = Invoice.objects.create(
bill=bill,
method=method,
status=status,
is_open=is_open,
)
if invoice.status == "F":
return Response(InvoiceSerializer(invoice).data, status=200)
invoice.real_price = None
invoice.method = method
invoice.status = status
if invoice.status == "F":
invoice.real_price = invoice.real_price if real_price is None else real_price
if bill.check_pay() and (invoice.price < price):
return Response("""Нельзя менять стоимость по счёту в большую сторону,
когда один из платежей оплачен""", status=400)
invoice.price = price
invoice.is_open = is_open
invoice.comment = comment
if invoice.method == 'Y' and invoice.yandex_pay is None:
yandex_pay = Payment.objects.create(
order_amount=invoice.price,
shop_amount=0,
customer_number=bill.user.id,
user=bill.user,
cps_email=bill.user.email,
)
invoice.yandex_pay = yandex_pay
context = {
'user_email': invoice.bill.user.email,
'opener_full_name': invoice.bill.opener.get_full_name(),
'course_title': Course.objects.get(token=invoice.bill.course_token).title,
'date': str(invoice.date),
'price': invoice.price,
}
return Response(res, status=200)
subject, to = 'Выставлен новый счёт', invoice.bill.opener.email
html_content = render_to_string('mail/sales/back_set_bill.html', context)
text_content = strip_tags(html_content)
return Response("Bill not set", status=400)
msg = EmailMultiAlternatives(subject, text_content, to=[to], bcc=['dmitry.dolya@skillbox.ru'])
msg.attach_alternative(html_content, "text/html")
msg.send()
return Response("Course detail access only for manager users", status=403)
invoice.save()
return Response(InvoiceSerializer(invoice).data, status=200)
return Response("Invoice detail access only for manager users", status=403)
class BillDetailView(APIView):
@ -158,7 +223,9 @@ class FindBillView(APIView):
if key:
res = Bill.objects.filter(
Q(opener__email__contains=key.lower()) | Q(user__email__contains=key.lower())
Q(opener__email__contains=key.lower())
| Q(user__email__contains=key.lower())
| Q(id__contains=key)
)
else:
@ -188,6 +255,7 @@ class YandexPay(APIView):
'customerNumber': pay.customer_number,
'orderNumber': pay.order_number,
'cps_email': pay.cps_email,
'rebillingOn': pay.invoice.rebilling_on,
'shopSuccessURL': settings.YANDEX_MONEY_SUCCESS_URL,
'shopFailURL': settings.YANDEX_MONEY_FAIL_URL,
})
@ -333,6 +401,7 @@ class YandexAvisoView(APIView):
pay.shop_amount = data['shopSumAmount']
pay.status = Payment.STATUS.SUCCESS
pay.invoice_id = data['invoiceId']
now = timezone.now()
pay.performed_datetime = now.isoformat()
pay.save()
@ -343,18 +412,28 @@ class YandexAvisoView(APIView):
'response': xml_res,
})
context = {
'user_email': pay.invoice.bill.user.email,
'opener_full_name': pay.invoice.bill.opener.get_full_name(),
'course_title': Course.objects.get(token=pay.invoice.bill.course_token).title,
'date': str(pay.invoice.date),
'price': pay.invoice.price,
'finish_date': pay.performed_datetime,
}
msg = EmailMessage(
'Успешная оплата.',
'''Пользователь "%s", перевёл %s рублей. Номер платежа в яндекс кассе %s'''
% (pay.invoice.bill.user.email, str(pay.invoice.price), str(data['invoiceId'])),
'robo@skillbox.ru',
[pay.invoice.bill.opener.email],
bcc=['dmitry.dolya@skillbox.ru', 'vera.procenko@skillbox.ru'],
)
subject, to = 'Счёт оплачен', pay.invoice.bill.opener.email
html_content = render_to_string('mail/sales/pay_access.html', context)
text_content = strip_tags(html_content)
msg = EmailMultiAlternatives(
subject, text_content, to=[to], bcc=['dmitry.dolya@skillbox.ru', 'vera.procenko@skillbox.ru'])
msg.attach_alternative(html_content, "text/html")
msg.send()
if pay.invoice.rebilling_on:
setup_periodic_billing(pay.order_number)
return HttpResponse(xml_res, content_type='application/xml')

@ -8,23 +8,10 @@ import socket
root = environ.Path(__file__) - 2
env = environ.Env()
MOD = os.environ.get('MOD', 'Test')
DEBUG = os.environ.get('DEBUG', 'False')
environ.Env.read_env(str(root) + '/config_app/settings/local.env')
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',)
vars().update(EMAIL_CONFIG)
@ -59,6 +46,7 @@ YANDEX_MONEY_SHOP_ID = '157133'
YANDEX_MONEY_SHOP_PASSWORD = 'nu5Xefise'
YANDEX_MONEY_FAIL_URL = '%s/api/v1/finance/yandex/fail/' % 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
# Application definition
@ -192,6 +180,8 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/'
SSL_ROOT = os.path.join(BASE_DIR, 'ssl')
RAVEN_CONFIG = {
'dsn': 'http://1a09557dbd144e52af4b14bea569c114:fbb5dfaa39e64f02a1b4cc7ac665d7d7@sentry.skillbox.ru/7'
}

@ -0,0 +1,2 @@
2018-04-24 11:40:39,510 - INFO - start console repeat payment command - {}
2018-04-24 11:40:39,674 - INFO - start periodic billing task - {}

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-03-27 13:29
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('progress', '0008_auto_20180227_1803'),
]
operations = [
migrations.AddField(
model_name='progress',
name='is_freeze',
field=models.BooleanField(default=False, verbose_name='Прохождение было преостановленно'),
),
]

@ -12,6 +12,7 @@ class Progress(models.Model):
user = models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='Студент')
course_token = models.UUIDField(verbose_name="Токен курса", editable=False)
is_finish = models.BooleanField(verbose_name="Окончен ли курс", default=False)
is_freeze = models.BooleanField(verbose_name="Прохождение было преостановленно", default=False)
only_watch = models.BooleanField(verbose_name="Только просмотр", default=False)
def progress_status(self, sorted_token_list):

@ -17,6 +17,23 @@ class ProgressSerializer(serializers.ModelSerializer):
return [ProgressLessonSerializer(i).data for i in self.progresslesson_set.all()]
class SupportProgressSerializer(serializers.ModelSerializer):
teacher_email = serializers.SerializerMethodField()
mod = serializers.SerializerMethodField()
class Meta:
model = Progress
fields = ('id', 'course_token', 'mod', 'teacher_email', "is_freeze")
@staticmethod
def get_teacher_email(self):
return self.teacher.email
@staticmethod
def get_mod(self):
return "Без дз" if self.only_watch else "Стандарт"
class SecureProgressSerializer(serializers.ModelSerializer):
jwt_token = serializers.SerializerMethodField()

@ -5,6 +5,9 @@ from progress import views
urlpatterns = [
url(r'students/(?P<teacher_token>[0-9A-Fa-f-]+)/$', views.StudentWorkView.as_view()),
url(r'student/$', views.StudentUpdateProgress.as_view()),
url(r'find/$', views.FindProgressView.as_view()),
url(r'freeze/$', views.FreezeProgressView.as_view()),
url(r'change_teacher/$', views.ChangeTeacherView.as_view()),
url(r'teacher/$', views.TeacherUpdateProgress.as_view()),
url(r'set-progress/$', views.SetProgress.as_view()),
url(r'get_hw_pay/$', views.get_teachers_pay),

@ -12,6 +12,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from django.db.models import Q
from access.serializers import UserProgressSearchSerializer
from courses.models import Course
from progress.models import ProgressLesson, Progress
from progress.serializers import ProgressAnalyticSerializer, ProgressLessonSerializer, ProgressSerializer, \
@ -201,6 +202,13 @@ class StudentUpdateProgress(APIView):
not pv.progress.progresslesson_set.filter(status=ProgressLesson.STATUSES.wait).exists():
pv.status = ProgressLesson.STATUSES.wait
pv.comment_tokens.append(comment)
msg = EmailMessage(
'Студент оставил комментарий',
'''Студент "%s" оставил вам комментарий.''' % request.user.get_full_name(),
'robo@skillbox.ru',
[pv.checker.email],
)
msg.send()
elif comment is None:
return Response("Не преложен комментарий", status=400)
@ -248,36 +256,74 @@ class UploadCourseProgressUserView(APIView):
return Response(status=403)
class UserGuardView(APIView):
class FindProgressView(APIView):
renderer_classes = (JSONRenderer,)
permission_classes = (IsAuthenticated,)
@staticmethod
def get(request):
if not request.user.is_staff:
return Response("Только сотрудники персонала могут запрашивать инфо по прогрессу", status=403)
key = request.GET.get('key', None)
count = int(request.GET.get('count', '10'))
if key:
res = get_user_model().objects.filter(
Q(id__contains=key) | Q(email__contains=key.lower()) | Q(first_name__contains=key) |
Q(last_name__contains=key) | Q(account__phone__contains=key)
)
else:
res = get_user_model().objects.all()
res = res[:(count if len(res) > count else len(res))]
return Response([UserProgressSearchSerializer(i).data for i in res], status=200)
class FreezeProgressView(APIView):
renderer_classes = (JSONRenderer,)
permission_classes = (IsAuthenticated,)
@staticmethod
def get(request, pk, page):
def post(request):
if not request.user.is_staff:
return Response("Только сотрудники персонала могут вносить изменение в прогресс", status=403)
key = request.JSON.get('id', None)
is_freeze = request.JSON.get('is_freeze', False)
try:
user = get_user_model().objects.get(out_key=pk)
except get_user_model().DoesNotExist:
return Response("User doesn't exist", status=404)
p = Progress.objects.get(id=key)
p.is_freeze = is_freeze
p.save()
except Progress.DoesNotExist:
return Response("не найден прогресс по заданному id", status=404)
is_i = request.user == user
res_403 = Response('Permission denied', status=403)
res_204 = Response(status=204)
return Response(status=204)
if is_i and not request.user.groups.filter(name='teachers').exists() and page == 'homeworks':
return res_403
if is_i and not \
request.user.groups.filter(name__in=['students', 'managers', 'lead_managers']).exists() \
and page == 'payment':
return res_403
class ChangeTeacherView(APIView):
renderer_classes = (JSONRenderer,)
permission_classes = (IsAuthenticated,)
if is_i:
return res_204
@staticmethod
def post(request):
if not request.user.is_staff:
return Response("Только сотрудники персонала могут вносить изменение в прогресс", status=403)
key = request.JSON.get('id', None)
teacher_email = request.JSON.get('teacher_email', False)
if page == 'profile' and (request.user.is_superuser or request.user.is_staff):
return res_204
try:
p = Progress.objects.get(id=key)
try:
teacher = get_user_model().objects.get(email=teacher_email.lower())
except get_user_model().DoesNotExist:
return Response("Нет пользователя c таким email", status=404)
p.teacher = teacher
p.progresslesson_set.filter(status=ProgressLesson.STATUSES.wait).update(checker=teacher)
p.save()
except Progress.DoesNotExist:
return Response("не найден прогресс по заданному id", status=404)
return res_403
return Response(status=204)
class SetProgress(APIView):

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

@ -0,0 +1,7 @@
<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>
Loading…
Cancel
Save