Merge branch 'dev' into 'pm_payments_repeat'

# Conflicts:
#   finance/models.py
remotes/origin/yandex_rebiling
Andrey 8 years ago
commit 79f9b02260
  1. 4
      .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. 22
      finance/migrations/0004_bill_date.py
  15. 35
      finance/migrations/0005_auto_20180329_1346.py
  16. 25
      finance/models.py
  17. 6
      finance/serializers.py
  18. 12
      finance/signals.py
  19. 1
      finance/urls.py
  20. 229
      finance/views.py
  21. 16
      lms/settings.py
  22. 20
      progress/migrations/0009_progress_is_freeze.py
  23. 1
      progress/models.py
  24. 17
      progress/serializers.py
  25. 3
      progress/urls.py
  26. 84
      progress/views.py
  27. 6
      templates/mail/sales/back_set_bill.html
  28. 7
      templates/mail/sales/pay_access.html

4
.gitignore vendored

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

@ -29,9 +29,12 @@ echo '{{ $new_release_dir }}'
@task('create_symlinks', ['on' => 'localhost']) @task('create_symlinks', ['on' => 'localhost'])
echo '>> Создание симлинков' echo '>> Создание симлинков'
@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
@endtask @endtask

@ -53,6 +53,13 @@ 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,11 +3,12 @@ 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 from progress.serializers import SecureProgressSerializer, SupportProgressSerializer
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
@ -17,6 +18,10 @@ 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()
@ -48,7 +53,7 @@ class UserSelfSerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_progresses(self): 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): class UserProfileSerializer(serializers.ModelSerializer):
@ -84,7 +89,7 @@ class UserSearchSerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_phone(self): def get_phone(self):
return None if self.account.phone is None else self.account.phone.raw_input return self.account.get_phone()
@staticmethod @staticmethod
def get_pay(self): def get_pay(self):
@ -101,3 +106,22 @@ 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()]

@ -10,7 +10,6 @@ urlpatterns = [
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()),

@ -28,7 +28,10 @@ class TeacherListView(APIView):
status_code = 200 status_code = 200
def get(self, request): 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): 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): 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):
@ -75,12 +73,12 @@ 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, 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): 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'] = teachers kwargs['teacher_tokens'] = teacher_tokens
if image: if image:
path = 'course/image%s.png' % slug path = 'course/image%s.png' % slug
@ -101,7 +99,7 @@ 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) kwargs['direction'] = get_real_name(COURSE_DIRECTION, direction[0])
try: try:
course = self.get(slug=slug) course = self.get(slug=slug)

@ -1,6 +1,8 @@
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):
@ -19,11 +21,12 @@ class MiniLessonSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Lesson model = Lesson
fields = ('title', 'free', 'token', 'is_hm') fields = ('title', 'free', 'token', 'is_hm', 'sort')
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
@ -33,6 +36,15 @@ 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()

@ -3,9 +3,13 @@ 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,10 +1,9 @@
from jwt import DecodeError 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.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
@ -31,27 +30,6 @@ 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)
@ -80,6 +58,141 @@ 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
@ -109,7 +222,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
@ -117,7 +230,7 @@ class LessonDetail(APIView):
course = lesson.topic.course course = lesson.topic.course
if payload is None: 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) return Response("Bad token", status=400)
else: else:
@ -144,7 +257,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)

@ -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 django.db import models
from yandex_money.models import Payment from yandex_money.models import Payment
from courses.models import Course, Lesson
class Bill(models.Model): class Bill(models.Model):
course_token = models.UUIDField(verbose_name="Токен курса", editable=False) course_token = models.UUIDField(verbose_name="Токен курса", editable=False)
@ -13,6 +11,7 @@ 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)
def __str__(self): def __str__(self):
return '%s: %s' % (self.id, self.user) return '%s: %s' % (self.id, self.user)
@ -20,6 +19,12 @@ class Bill(models.Model):
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 = 'Счета'
@ -43,7 +48,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) price = models.IntegerField(verbose_name='Сумма', editable=False, null=True, blank=True) #Todo На самом деле тут не далжно быть значений null
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)
@ -63,3 +68,17 @@ class Invoice(models.Model):
class Meta: class Meta:
verbose_name = 'Платёж' verbose_name = 'Платёж'
verbose_name_plural = 'Платежи' 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): class InvoiceSerializer(serializers.ModelSerializer):
status = serializers.SerializerMethodField() status = serializers.SerializerMethodField()
method = serializers.SerializerMethodField() method = serializers.SerializerMethodField()
yandex_pay_id = serializers.SerializerMethodField() yandex_pay = serializers.SerializerMethodField()
class Meta: class Meta:
model = Invoice model = Invoice
exclude = ('bill',) fields = '__all__'
@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_id(self): def get_yandex_pay(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

@ -19,11 +19,10 @@ def invoice_signal(instance, **kwargs):
if instance.yandex_pay and instance.method == 'Y' and instance.status == 'P' and not instance.rebilling: if instance.yandex_pay and instance.method == 'Y' and instance.status == 'P' and not instance.rebilling:
msg = EmailMessage( msg = EmailMessage(
'Вам выставлен новый счёт', 'Вам выставлен новый счёт',
'''Вам выставлен счёт, для оплаты перейдите по ссылке """%s для оплаты перейдите по ссылке
%s/api/v1/finance/payment/%s/''' % (settings.DOMAIN, instance.yandex_pay.id,), %s/api/v1/finance/payment/%s/""" % (instance.get_comment(), settings.DOMAIN, instance.yandex_pay.id),
'robo@skillbox.ru', to=[instance.yandex_pay.cps_email],
[instance.yandex_pay.cps_email], bcc=[instance.bill.opener.email],
[instance.bill.opener.email],
reply_to=[instance.bill.opener.email], reply_to=[instance.bill.opener.email],
) )
msg.send() msg.send()
@ -52,8 +51,7 @@ 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),
'robo@skillbox.ru', to=[instance.bill.user.email],
[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],
) )

@ -7,5 +7,6 @@ urlpatterns = [
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),
] ]

@ -4,6 +4,7 @@ import logging
import requests import requests
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.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 from django.shortcuts import redirect
@ -12,6 +13,9 @@ 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 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.models import Course
from courses.api import CourseParamsApi from courses.api import CourseParamsApi
@ -41,84 +45,144 @@ 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):
bill = request.JSON.get('bill') user = get_user_model().objects.get(email=request.JSON.get('user'))
children = request.JSON.get('children', []) 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: if course_token is None:
user = get_user_model().objects.get(email=bill['user']) return Response("Идентификатор курса не передан", status=400)
opener = get_user_model().objects.get(email=bill['opener'])
description = bill['description']
comment = bill['comment']
course_token = bill['course_token']
try:
bill_obj = Bill.objects.get(user=user, course_token=course_token)
except Bill.DoesNotExist:
try: try:
bill_obj = Bill.objects.get(user=user, course_token=course_token)
except Bill.DoesNotExist:
bill_obj = Bill.objects.create(user=user, course_token=course_token) bill_obj = 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 try:
bill_obj.description = description bill = Bill.objects.get(id=bill_id)
bill_obj.comment = comment except Bill.DoesNotExist:
bill_obj.save() return Response('Не найден счёт с id=%s' % bill_id, status=404)
for i in children: method = get_real_name(elem=method[0], array=Invoice.BILL_METHOD)
status = get_real_name(elem=i['status'], array=Invoice.BILL_STATUSES) status = get_real_name(elem=status[0], array=Invoice.BILL_STATUSES)
try:
invoice_id = i['id'] if bill.check_validate(invoice_id) and is_open:
except KeyError: return Response("Уже есть платёж открывающий курс", status=400)
invoice_id = None
try:
try: invoice = Invoice.objects.get(id=invoice_id)
if not invoice_id is None: except Invoice.DoesNotExist:
invoice = Invoice.objects.get(id=i['id']) if not invoice_id == 0:
if invoice.status == "P" or invoice.status == status: return Response("Платёж не найден", status=404)
continue
else: if bill.check_pay():
raise Invoice.DoesNotExist return Response(
"Нельзя добавить новый платёж, так как один из платежей по счёту уже оплачен", status=400)
except Invoice.DoesNotExist: invoice = Invoice.objects.create(
i['method'] = get_real_name(elem=i['method'], array=Invoice.BILL_METHOD) bill=bill,
i['status'] = status method=method,
i['bill'] = bill_obj status=status,
i['yandex_pay'] = None is_open=is_open,
invoice = Invoice.objects.create(**i) )
if i['method'] == 'Y' and invoice.yandex_pay is None: if invoice.status == "F":
yandex_pay = Payment.objects.create( return Response(InvoiceSerializer(invoice).data, status=200)
order_amount=i['price'],
shop_amount=0, invoice.real_price = None
customer_number=bill_obj.user.id, invoice.method = method
user=bill_obj.user, invoice.status = status
cps_email=bill_obj.user.email,
) if invoice.status == "F":
invoice.yandex_pay = yandex_pay invoice.real_price = invoice.real_price if real_price is None else real_price
invoice.save()
if bill.check_pay() and (invoice.price < price):
msg = EmailMessage( return Response("""Нельзя менять стоимость по счёту в большую сторону,
'Выставлен новый счёт.', когда один из платежей оплачен""", status=400)
'''Менеджер %s выставил счёт пользователю %s на курс "%s".'''
% ( invoice.price = price
invoice.bill.opener.get_full_name(), invoice.is_open = is_open
invoice.bill.user.email, invoice.comment = comment
Course.objects.get(token=invoice.bill.course_token).title,
), if invoice.method == 'Y' and invoice.yandex_pay is None:
'robo@skillbox.ru', yandex_pay = Payment.objects.create(
[invoice.bill.opener.email], order_amount=invoice.price,
bcc=['dmitry.dolya@skillbox.ru'], shop_amount=0,
) customer_number=bill.user.id,
user=bill.user,
msg.send() cps_email=bill.user.email,
)
res = { invoice.yandex_pay = yandex_pay
"bill": BillSerializer(bill_obj).data,
"children": [InvoiceSerializer(i).data for i in bill_obj.invoice_set.all()], context = {
'user_email': invoice.bill.user.email,
'opener_full_name': invoice.bill.opener.get_full_name(),
'course_title': Course.objects.get(token=invoice.bill.course_token).title,
'date': str(invoice.date),
'price': invoice.price,
} }
return Response(res, status=200) subject, to = 'Выставлен новый счёт', invoice.bill.opener.email
return Response("Bill not set", status=400) html_content = render_to_string('mail/sales/back_set_bill.html', context)
text_content = strip_tags(html_content)
return Response("Course detail access only for manager users", status=403) msg = EmailMultiAlternatives(subject, text_content, to=[to], bcc=['dmitry.dolya@skillbox.ru'])
msg.attach_alternative(html_content, "text/html")
msg.send()
invoice.save()
return Response(InvoiceSerializer(invoice).data, status=200)
return Response("Invoice detail access only for manager users", status=403)
class BillDetailView(APIView): class BillDetailView(APIView):
@ -159,7 +223,9 @@ class FindBillView(APIView):
if key: if key:
res = Bill.objects.filter( 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: else:
@ -346,16 +412,23 @@ class YandexAvisoView(APIView):
'response': xml_res, 'response': xml_res,
}) })
context = {
'user_email': pay.invoice.bill.user.email,
'opener_full_name': pay.invoice.bill.opener.get_full_name(),
'course_title': Course.objects.get(token=pay.invoice.bill.course_token).title,
'date': str(pay.invoice.date),
'price': pay.invoice.price,
'finish_date': pay.performed_datetime,
}
subject, to = 'Счёт оплачен', pay.invoice.bill.opener.email
msg = EmailMessage( html_content = render_to_string('mail/sales/pay_access.html', context)
'Успешная оплата.', 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()
if pay.invoice.rebilling_on: if pay.invoice.rebilling_on:

@ -8,22 +8,10 @@ import socket
root = environ.Path(__file__) - 2 root = environ.Path(__file__) - 2
env = environ.Env() env = environ.Env()
MOD = os.environ.get('MOD', 'Prod')
DEBUG = os.environ.get('DEBUG', 'False')
MASTER_PASSWORD = os.environ.get('MASTER_PASSWORD', '@J*1') environ.Env.read_env(str(root) + '/config_app/settings/local.env')
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': MASTER_PASSWORD = os.environ.get('MASTER_PASSWORD', '@J*1')
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)

@ -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='Студент') 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)
def progress_status(self, sorted_token_list): 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()] 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()

@ -5,6 +5,9 @@ from progress import views
urlpatterns = [ urlpatterns = [
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),

@ -12,6 +12,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from django.db.models import Q from django.db.models import Q
from access.serializers import UserProgressSearchSerializer
from courses.models import Course from courses.models import Course
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, \
@ -201,6 +202,13 @@ 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)
@ -248,36 +256,74 @@ class UploadCourseProgressUserView(APIView):
return Response(status=403) 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,) renderer_classes = (JSONRenderer,)
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
@staticmethod @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: try:
user = get_user_model().objects.get(out_key=pk) p = Progress.objects.get(id=key)
except get_user_model().DoesNotExist: p.is_freeze = is_freeze
return Response("User doesn't exist", status=404) p.save()
except Progress.DoesNotExist:
return Response("не найден прогресс по заданному id", status=404)
is_i = request.user == user return Response(status=204)
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
if is_i and not \ class ChangeTeacherView(APIView):
request.user.groups.filter(name__in=['students', 'managers', 'lead_managers']).exists() \ renderer_classes = (JSONRenderer,)
and page == 'payment': permission_classes = (IsAuthenticated,)
return res_403
if is_i: @staticmethod
return res_204 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): try:
return res_204 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): 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