Merge branch 'master' of gitlab.com:lilcity/backend into feature/testing_courses_30-01-19

remotes/origin/feature/testing_courses_30-01-19
gzbender 7 years ago
commit 45427829f8
  1. 17
      api/v1/__init__.py
  2. 4
      api/v1/serializers/course.py
  3. 2
      api/v1/serializers/mixins.py
  4. 4
      api/v1/serializers/payment.py
  5. 4
      api/v1/serializers/user.py
  6. 37
      api/v1/views.py
  7. 40
      apps/course/management/commands/fix_access_expire.py
  8. 2
      apps/course/management/commands/update_courses_slug.py
  9. 18
      apps/course/migrations/0048_auto_20190206_1710.py
  10. 18
      apps/course/migrations/0049_auto_20190207_1551.py
  11. 2
      apps/course/models.py
  12. 13
      apps/course/templates/course/_items.html
  13. 168
      apps/course/templates/course/course.html
  14. 48
      apps/course/templates/course/course_only_lessons.html
  15. 8
      apps/course/tests/__init__.py
  16. 28
      apps/course/views.py
  17. 18
      apps/payment/management/commands/fill_access_expired.py
  18. 18
      apps/payment/migrations/0031_coursepayment_access_expired.py
  19. 18
      apps/payment/migrations/0032_auto_20190207_1233.py
  20. 66
      apps/payment/models.py
  21. 6
      apps/payment/views.py
  22. 3
      apps/school/views.py
  23. 6
      apps/user/models.py
  24. 5
      project/templates/blocks/popup_course_buy.html
  25. 5
      project/templates/lilcity/index.html
  26. 57
      web/src/components/CourseRedactor.vue
  27. 4
      web/src/js/modules/api.js

@ -43,3 +43,20 @@ class ExtendViewSet(object):
class ExtendedModelViewSet(ExtendViewSet, viewsets.ModelViewSet): class ExtendedModelViewSet(ExtendViewSet, viewsets.ModelViewSet):
pass pass
class BothListFormatMixin:
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
if request.query_params.get('page'):
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
else:
return Response({'results': []})
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

@ -133,7 +133,7 @@ class CourseCreateSerializer(DispatchContentMixin,
'is_infinite', 'is_infinite',
'deferred_start_at', 'deferred_start_at',
'category', 'category',
'duration', 'access_duration',
'is_featured', 'is_featured',
'url', 'url',
'status', 'status',
@ -286,7 +286,7 @@ class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
'is_infinite', 'is_infinite',
'deferred_start_at', 'deferred_start_at',
'category', 'category',
'duration', 'access_duration',
'is_featured', 'is_featured',
'url', 'url',
'status', 'status',

@ -145,6 +145,8 @@ class DispatchGalleryMixin(object):
) )
if 'images' in gallery: if 'images' in gallery:
for image in gallery['images']: for image in gallery['images']:
if not image.get('img'):
continue
if isinstance(image['img'], ImageObject): if isinstance(image['img'], ImageObject):
img = image['img'] img = image['img']
else: else:

@ -122,7 +122,7 @@ class CoursePaymentCreateSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CoursePayment model = CoursePayment
fields = BASE_PAYMENT_FIELDS + ('course',) fields = BASE_PAYMENT_FIELDS + ('course', 'access_expire')
read_only_fields = ( read_only_fields = (
'id', 'id',
'user', 'user',
@ -138,7 +138,7 @@ class CoursePaymentSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CoursePayment model = CoursePayment
fields = BASE_PAYMENT_FIELDS + ('course',) fields = BASE_PAYMENT_FIELDS + ('course', 'access_expire')
read_only_fields = ( read_only_fields = (
'id', 'id',
'user', 'user',

@ -1,6 +1,6 @@
from phonenumber_field.serializerfields import PhoneNumberField from phonenumber_field.serializerfields import PhoneNumberField
from rest_framework import serializers from rest_framework import serializers
from drf_dynamic_fields import DynamicFieldsMixin
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from api.v1.serializers.content import GallerySerializer, GalleryImageSerializer, GalleryImageCreateSerializer from api.v1.serializers.content import GallerySerializer, GalleryImageSerializer, GalleryImageCreateSerializer
@ -11,7 +11,7 @@ from .mixins import DispatchGalleryMixin
User = get_user_model() User = get_user_model()
class UserSerializer(serializers.ModelSerializer): class UserSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
phone = PhoneNumberField(required=False, allow_null=True, allow_blank=True) phone = PhoneNumberField(required=False, allow_null=True, allow_blank=True)
class Meta: class Meta:

@ -10,7 +10,7 @@ from rest_framework.decorators import (detail_route, list_route, action,
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from . import ExtendedModelViewSet from . import ExtendedModelViewSet, BothListFormatMixin
from .serializers.config import ConfigSerializer from .serializers.config import ConfigSerializer
from .serializers.course import ( from .serializers.course import (
@ -213,7 +213,7 @@ class LikeViewSet(ExtendedModelViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class CategoryViewSet(ExtendedModelViewSet): class CategoryViewSet(BothListFormatMixin, ExtendedModelViewSet):
queryset = Category.objects.order_by('-id') queryset = Category.objects.order_by('-id')
serializer_class = CategorySerializer serializer_class = CategorySerializer
search_fields = ('title',) search_fields = ('title',)
@ -221,7 +221,7 @@ class CategoryViewSet(ExtendedModelViewSet):
# permission_classes = (IsAdmin,) # permission_classes = (IsAdmin,)
class CourseViewSet(ExtendedModelViewSet): class CourseViewSet(BothListFormatMixin, ExtendedModelViewSet):
queryset = Course.objects.select_related( queryset = Course.objects.select_related(
'author', 'category', 'cover', 'gallery', 'author', 'category', 'cover', 'gallery',
).prefetch_related( ).prefetch_related(
@ -243,20 +243,6 @@ class CourseViewSet(ExtendedModelViewSet):
# 'delete': IsAdmin, # 'delete': IsAdmin,
# } # }
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
if request.query_params.get('page'):
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
else:
return Response({'results': []})
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@list_route(methods=['get']) @list_route(methods=['get'])
def draft(self, request): def draft(self, request):
drafts = Course.objects.filter(author=request.user, status=Course.DRAFT) drafts = Course.objects.filter(author=request.user, status=Course.DRAFT)
@ -455,8 +441,7 @@ class UserViewSet(ExtendedModelViewSet):
serializer = self.get_serializer(page, many=True) serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data) return self.get_paginated_response(serializer.data)
# FIXME queryset = queryset[:3000]
queryset = queryset[:2000]
serializer = self.get_serializer(queryset, many=True) serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data) return Response(serializer.data)
@ -736,7 +721,7 @@ class FAQViewSet(ExtendedModelViewSet):
serializer_class = FAQSerializer serializer_class = FAQSerializer
class BonusesViewSet(ExtendedModelViewSet): class BonusesViewSet(BothListFormatMixin, ExtendedModelViewSet):
queryset = UserBonus.objects.all() queryset = UserBonus.objects.all()
serializer_class = UserBonusCreateSerializer serializer_class = UserBonusCreateSerializer
serializer_class_map = { serializer_class_map = {
@ -753,15 +738,3 @@ class BonusesViewSet(ExtendedModelViewSet):
'referral__referral__first_name', 'referral__referral__first_name',
'referral__referral__last_name', 'referral__referral__last_name',
) )
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
if request.query_params.get('page'):
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

@ -0,0 +1,40 @@
from datetime import timedelta
from django.core.management.base import BaseCommand
from apps.payment.models import CoursePayment
from apps.course.models import Course
class Command(BaseCommand):
help = 'Fix access duration in all paid courses'
def add_arguments(self, parser):
parser.add_argument('access_duration', type=int, help='New access duration',)
def handle(self, *args, **options):
access_duration = options.get('access_duration')
for course in Course.objects.filter(price__gt=0):
course.access_duration = access_duration
course.save()
for payment in CoursePayment.objects.filter(status__in=CoursePayment.PW_PAID_STATUSES):
payment.access_expire = payment.created_at.date() + timedelta(days=payment.course.access_duration)
payment.save()
'''
TEST
select c.id, c.title, cp.access_duration
from
course_course c,
(select cp.course_id, count(*), cp.access_expire - date_trunc('day', p.created_at) access_duration
from payment_coursepayment cp,
payment_payment p
where p.id = cp.payment_ptr_id and p.status = 0
group by cp.course_id, cp.access_expire - date_trunc('day', p.created_at)) cp
where cp.course_id = c.id
order by c.title
'''

@ -7,7 +7,7 @@ from apps.course.models import Course
class Command(BaseCommand): class Command(BaseCommand):
help = 'Upload users to Roistat' help = 'Update courses slug'
def handle(self, *args, **options): def handle(self, *args, **options):
courses = Course.objects.filter(Q(slug__isnull=True) | Q(slug='')) courses = Course.objects.filter(Q(slug__isnull=True) | Q(slug=''))

@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2019-02-06 17:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0047_course_old_price'),
]
operations = [
migrations.AlterField(
model_name='course',
name='duration',
field=models.IntegerField(default=0, verbose_name='Продолжительность доступа к курсу'),
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2019-02-07 15:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('course', '0048_auto_20190206_1710'),
]
operations = [
migrations.RenameField(
model_name='course',
old_name='duration',
new_name='access_duration',
),
]

@ -98,7 +98,7 @@ class Course(BaseModel, DeactivatedMixin):
null=True, blank=True, validators=[deferred_start_at_validator], null=True, blank=True, validators=[deferred_start_at_validator],
) )
category = models.ForeignKey('Category', null=True, blank=True, on_delete=models.PROTECT, related_name='courses') category = models.ForeignKey('Category', null=True, blank=True, on_delete=models.PROTECT, related_name='courses')
duration = models.IntegerField('Продолжительность курса', default=0) access_duration = models.IntegerField('Продолжительность доступа к курсу', default=0)
is_featured = models.BooleanField(default=False) is_featured = models.BooleanField(default=False)
status = models.PositiveSmallIntegerField( status = models.PositiveSmallIntegerField(
'Статус', default=DRAFT, choices=STATUS_CHOICES 'Статус', default=DRAFT, choices=STATUS_CHOICES

@ -50,11 +50,16 @@
<a class="courses__theme theme {{ theme_color }}" <a class="courses__theme theme {{ theme_color }}"
href="{% url 'courses' %}?category={{ course.category.id }}">{{ course.category | upper }}</a> href="{% url 'courses' %}?category={{ course.category.id }}">{{ course.category | upper }}</a>
{% if not course.is_free %} {% if not course.is_free %}
{% if course.old_price %} {% if course.buy_again_price %}
<div class="courses__old-price"><s>{{ course.old_price|floatformat:"-2" }}₽</s></div> <div class="courses__old-price"><s>{{ course.price|floatformat:"-2" }}₽</s></div>
<div class="courses__price" style="color: red;">{{ course.buy_again_price|floatformat:"-2" }}₽</div>
{% else %}
{% if course.old_price %}
<div class="courses__old-price"><s>{{ course.old_price|floatformat:"-2" }}₽</s></div>
{% endif %}
<div class="courses__price"
{% if course.old_price %}style="color: red;"{% endif %}>{{ course.price|floatformat:"-2" }}₽</div>
{% endif %} {% endif %}
<div class="courses__price"
{% if course.old_price %}style="color: red;"{% endif %}>{{ course.price|floatformat:"-2" }}₽</div>
{% endif %} {% endif %}
</div> </div>
<a class="courses__title" href="{{ course.url }}">{{ course.title }}</a> <a class="courses__title" href="{{ course.url }}">{{ course.title }}</a>

@ -37,23 +37,27 @@
{% if has_full_access %} {% if has_full_access %}
<a class="btn btn_light-gray" href="{% url 'course_edit' course.id %}">Редактировать</a> <a class="btn btn_light-gray" href="{% url 'course_edit' course.id %}">Редактировать</a>
{% endif %} {% endif %}
{% if course.author != request.user and not paid and course.price %} {% if not is_owner and course.price %}
<div> {% if not paid or can_buy_again %}
<a href="#" <div>
class="btn{% if pending %} btn_gray{% endif %} btn_md" <a href="#"
{% if user.is_authenticated %} class="btn{% if pending %} btn_gray{% endif %} btn_md"
{% if not pending %} {% if user.is_authenticated %}
data-course-buy {% if not pending %}
data-popup=".js-popup-course-buy" data-course-buy
{% endif %} data-popup=".js-popup-course-buy"
{% else %} {% endif %}
data-popup=".js-popup-auth" {% else %}
data-popup=".js-popup-auth"
{% endif %}
>{% if pending %}ОЖИДАЕТСЯ ПОДТВЕРЖДЕНИЕ ОПЛАТЫ{% else %}
{% if paid and can_buy_again %}ПРОДЛИТЬ ДОСТУП{% else %}КУПИТЬ КУРС{% endif %}
{% endif %}</a>
{% if not paid %}
<a class="main__btn btn btn_stroke-black" href="{% url 'gift-certificates' %}">Подарить другу</a>
{% endif %} {% endif %}
>{% if pending %}ОЖИДАЕТСЯ ПОДТВЕРЖДЕНИЕ ОПЛАТЫ{% else %}КУПИТЬ КУРС{% endif %}</a> </div>
{% if not paid %}
<a class="main__btn btn btn_stroke-black" href="{% url 'gift-certificates' %}">Подарить другу</a>
{% endif %} {% endif %}
</div>
{% endif %} {% endif %}
</div> </div>
<div <div
@ -105,26 +109,41 @@
</a> </a>
<div class="course__metas"> <div class="course__metas">
<div class="course__meta meta"> <div class="course__meta meta">
<a class="meta__item" title="Продолжительность курса">
<div class="meta__icon">
<svg class="icon icon-time">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-time"></use>
</svg>
</div>
<div class="meta__title">{{ course.duration | rupluralize:"день,дня,дней" }}</div>
</a>
{% if course.price %} {% if course.price %}
<div class="meta__item" title="Цена"> {% if paid %}
<div class="meta__icon"> <a class="meta__item" title="Осталось {{ access_duration | rupluralize:'день,дня,дней' }} доступа к курсу">
<svg class="icon icon-money"> <div class="meta__icon">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-money"></use> <svg class="icon icon-time">
</svg> <use xlink:href="{% static 'img/sprite.svg' %}#icon-time"></use>
</div> </svg>
<div class="meta__title"> </div>
{% if course.old_price %}<s>{{ course.old_price|floatformat:"-2" }}₽</s>{% endif %} <div class="meta__title">{{ access_duration | rupluralize:"день,дня,дней" }}</div>
{{ course.price|floatformat:"-2" }}₽ </a>
{% else %}
<a class="meta__item" title="Продолжительность доступа к курсу">
<div class="meta__icon">
<svg class="icon icon-time">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-time"></use>
</svg>
</div>
<div class="meta__title">{{ course.access_duration | rupluralize:"день,дня,дней" }}</div>
</a>
{% endif %}
<div class="meta__item" title="Цена{% if can_buy_again %} повторной покупки{% endif %}">
<div class="meta__icon">
<svg class="icon icon-money">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-money"></use>
</svg>
</div>
<div class="meta__title">
{% if can_buy_again %}
<s>{{ course.price|floatformat:"-2" }}₽</s>
{% else %}
{% if course.old_price %}<s>{{ course.old_price|floatformat:"-2" }}₽</s>{% endif %}
{% endif %}
<span {% if can_buy_again or course.old_price %}style="color: red;"{% endif %}>{{ course_price|floatformat:"-2" }}₽</span>
</div>
</div> </div>
</div>
{% endif %} {% endif %}
<div class="meta__item"> <div class="meta__item">
<div class="meta__icon"> <div class="meta__icon">
@ -298,37 +317,64 @@
</a> </a>
<div class="course__info"> <div class="course__info">
<div class="course__meta meta meta_white"> <div class="course__meta meta meta_white">
<a class="meta__item" title="Продолжительность курса">
<div class="meta__icon">
<svg class="icon icon-time">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-time"></use>
</svg>
</div>
<div class="meta__title">{{ course.duration | rupluralize:"день,дня,дней" }}</div>
</a>
{% if course.price %} {% if course.price %}
<div class="meta__item"> {% if paid %}
<div class="meta__icon"> <a class="meta__item" title="Осталось {{ access_duration | rupluralize:'день,дня,дней' }} доступа к курсу">
<svg class="icon icon-money"> <div class="meta__icon">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-money"></use> <svg class="icon icon-time">
</svg> <use xlink:href="{% static 'img/sprite.svg' %}#icon-time"></use>
</svg>
</div>
<div class="meta__title">{{ access_duration | rupluralize:"день,дня,дней" }}</div>
</a>
{% else %}
<a class="meta__item" title="Продолжительность доступа к курсу">
<div class="meta__icon">
<svg class="icon icon-time">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-time"></use>
</svg>
</div>
<div class="meta__title">{{ course.access_duration | rupluralize:"день,дня,дней" }}</div>
</a>
{% endif %}
<div class="meta__item" title="Цена{% if can_buy_again %} повторной покупки{% endif %}">
<div class="meta__icon">
<svg class="icon icon-money">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-money"></use>
</svg>
</div>
<div class="meta__title">
{% if can_buy_again %}
<s>{{ course.price|floatformat:"-2" }}₽</s>
{% else %}
{% if course.old_price %}<s>{{ course.old_price|floatformat:"-2" }}₽</s>{% endif %}
{% endif %}
<span {% if can_buy_again or course.old_price %}style="color: red;"{% endif %}>{{ course_price|floatformat:"-2" }}₽</span>
</div>
</div> </div>
<div class="meta__title">{{ course.price|floatformat:"-2" }}₽</div>
</div>
{% endif %} {% endif %}
</div> </div>
{% if course.author != request.user and not paid and course.price %} {% if not is_owner and course.price %}
<a href="#" {% if not paid or can_buy_again %}
class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md" <div>
{% if user.is_authenticated %} <a href="#"
{% if not pending %} class="btn{% if pending %} btn_gray{% endif %} btn_md"
data-course-buy {% if user.is_authenticated %}
data-popup=".js-popup-course-buy" {% if not pending %}
{% endif %} data-course-buy
{% else %} data-popup=".js-popup-course-buy"
data-popup=".js-popup-auth" {% endif %}
{% else %}
data-popup=".js-popup-auth"
{% endif %}
>{% if pending %}ОЖИДАЕТСЯ ПОДТВЕРЖДЕНИЕ ОПЛАТЫ{% else %}
{% if paid and can_buy_again %}ПРОДЛИТЬ ДОСТУП{% else %}КУПИТЬ КУРС{% endif %}
{% endif %}</a>
{% if not paid %}
<a class="main__btn btn btn_stroke-black" href="{% url 'gift-certificates' %}">Подарить другу</a>
{% endif %} {% endif %}
>{% if pending %}ОЖИДАЕТСЯ ПОДТВЕРЖДЕНИЕ ОПЛАТЫ{% else %}КУПИТЬ КУРС{% endif %}</a> </div>
{% endif %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -345,3 +391,7 @@
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}
{% block foot %}
{% include "templates/blocks/popup_course_buy.html" %}
{% endblock foot %}

@ -95,23 +95,41 @@
</a> </a>
<div class="course__metas"> <div class="course__metas">
<div class="course__meta meta"> <div class="course__meta meta">
<a class="meta__item" title="Продолжительность курса">
<div class="meta__icon">
<svg class="icon icon-time">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-time"></use>
</svg>
</div>
<div class="meta__title">{{ course.duration | rupluralize:"день,дня,дней" }}</div>
</a>
{% if course.price %} {% if course.price %}
<div class="meta__item"> {% if paid %}
<div class="meta__icon"> <a class="meta__item" title="Осталось {{ access_duration | rupluralize:'день,дня,дней' }} доступа к курсу">
<svg class="icon icon-money"> <div class="meta__icon">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-money"></use> <svg class="icon icon-time">
</svg> <use xlink:href="{% static 'img/sprite.svg' %}#icon-time"></use>
</svg>
</div>
<div class="meta__title">{{ access_duration | rupluralize:"день,дня,дней" }}</div>
</a>
{% else %}
<a class="meta__item" title="Продолжительность доступа к курсу">
<div class="meta__icon">
<svg class="icon icon-time">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-time"></use>
</svg>
</div>
<div class="meta__title">{{ course.access_duration | rupluralize:"день,дня,дней" }}</div>
</a>
{% endif %}
<div class="meta__item" title="Цена{% if can_buy_again %} повторной покупки{% endif %}">
<div class="meta__icon">
<svg class="icon icon-money">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-money"></use>
</svg>
</div>
<div class="meta__title">
{% if can_buy_again %}
<s>{{ course.price|floatformat:"-2" }}₽</s>
{% else %}
{% if course.old_price %}<s>{{ course.old_price|floatformat:"-2" }}₽</s>{% endif %}
{% endif %}
<span {% if can_buy_again or course.old_price %}style="color: red;"{% endif %}>{{ course_price|floatformat:"-2" }}₽</span>
</div>
</div> </div>
<div class="meta__title">{{ course.price|floatformat:"-2" }}₽</div>
</div>
{% endif %} {% endif %}
<div class="meta__item"> <div class="meta__item">
<div class="meta__icon"> <div class="meta__icon">

@ -0,0 +1,8 @@
# TODO: test_course_access_duration
'''
сделать поле "продолжительность доступа" у курсов: автор указывает продолжительность,
после покупки по истечению указанного кол-ва дней у пользователя нет доступа к курсу,
но он может его купить с 50% скидкой, при покупке курса появляется надпись "осталось Х дней доступа",
так же за неделю до окончания можно купить курс еще раз с 50% скидкой, при этом доступ продлевается
на указанное кол-во дней в продолжительности
'''

@ -1,5 +1,6 @@
from paymentwall import Pingback from datetime import timedelta
from paymentwall import Pingback
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Q from django.db.models import Q
@ -12,8 +13,9 @@ from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.utils.timezone import now
from apps.payment.models import AuthorBalance from apps.payment.models import AuthorBalance, CoursePayment
from .models import Course, Like, Lesson, CourseComment, LessonComment from .models import Course, Like, Lesson, CourseComment, LessonComment
from .filters import CourseFilter from .filters import CourseFilter
@ -231,14 +233,20 @@ class CourseView(DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
context['next'] = self.request.GET.get('next', None) context['next'] = self.request.GET.get('next', None)
context['paid'] = self.object.payments.filter( # берем последнюю оплату курса
payments = self.object.payments.filter(
user=self.request.user, user=self.request.user,
status__in=[ status__in=[
Pingback.PINGBACK_TYPE_REGULAR, Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL, Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
], ])
).exists() payment = payments.filter(access_expire__gte=now().date()).order_by('-access_expire').first()
context['payment'] = payment
context['access_duration'] = ((payment.access_expire - now().date()).days + 1) if payment else self.object.access_duration
context['paid'] = bool(payment)
context['can_buy_again'] = bool(self.object.price) and (context['access_duration'] <= 7 if payment else
payments.filter(access_expire__lt=now().date()).exists())
context['pending'] = self.object.payments.filter( context['pending'] = self.object.payments.filter(
user=self.request.user, user=self.request.user,
status=Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW, status=Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW,
@ -249,6 +257,8 @@ class CourseView(DetailView):
context['is_owner'] = self.object.author == self.request.user context['is_owner'] = self.object.author == self.request.user
context['is_admin'] = self.request.user.role == User.ADMIN_ROLE context['is_admin'] = self.request.user.role == User.ADMIN_ROLE
context['has_full_access'] = context['is_owner'] or context['is_admin'] context['has_full_access'] = context['is_owner'] or context['is_admin']
context['course_price'] = self.object.price / 2 if context.get('can_buy_again') else self.object.price
return context return context
def get_queryset(self): def get_queryset(self):
@ -317,6 +327,13 @@ class CoursesView(ListView):
context['age_name'] = dict(Course.AGE_CHOICES).get(age, '') context['age_name'] = dict(Course.AGE_CHOICES).get(age, '')
else: else:
context['age_name'] = '' context['age_name'] = ''
if self.request.user.is_authenticated:
can_buy_again_courses = list(CoursePayment.objects.filter(user=self.request.user,
status__in=CoursePayment.PW_PAID_STATUSES,
access_expire__lte=now().date() + timedelta(7)).values_list('course_id', flat=True))
for course in context['course_items']:
if course.id in can_buy_again_courses:
course.buy_again_price = course.price / 2
return context return context
def get_template_names(self): def get_template_names(self):
@ -340,6 +357,7 @@ class LessonView(DetailView):
Pingback.PINGBACK_TYPE_GOODWILL, Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
], ],
access_expire__gte=now().date(),
).exists() ).exists()
# если это не автор или админ # если это не автор или админ
if not (request.user.is_authenticated and if not (request.user.is_authenticated and

@ -0,0 +1,18 @@
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.db.models import F
from apps.payment.models import CoursePayment
class Command(BaseCommand):
help = 'Fill payment.access_expire where it is not filled'
def handle(self, *args, **options):
for payment in CoursePayment.objects.filter(access_expire__isnull=True):
payment.access_expire = payment.created_at.date() + timedelta(days=payment.course.access_duration)
payment.save()

@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2019-02-06 17:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payment', '0030_auto_20190114_1649'),
]
operations = [
migrations.AddField(
model_name='coursepayment',
name='access_expired',
field=models.DateField(null=True, verbose_name='Доступ к курсу до даты'),
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2019-02-07 12:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('payment', '0031_coursepayment_access_expired'),
]
operations = [
migrations.RenameField(
model_name='coursepayment',
old_name='access_expired',
new_name='access_expire',
),
]

@ -63,7 +63,7 @@ class AuthorBalance(models.Model):
verbose_name_plural = 'Балансы' verbose_name_plural = 'Балансы'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.status != self.ACCEPTED: if self.type == self.IN and not self.id:
self.commission = self.calc_commission() self.commission = self.calc_commission()
if self.type == self.OUT: if self.type == self.OUT:
if self.status == self.DECLINED and not self.declined_send_at: if self.status == self.DECLINED and not self.declined_send_at:
@ -152,18 +152,15 @@ class Payment(PolymorphicModel):
elif isinstance(payment, GiftCertificatePayment): elif isinstance(payment, GiftCertificatePayment):
price = payment.gift_certificate.price price = payment.gift_certificate.price
elif course: elif course:
price = course.price paid_before = CoursePayment.objects.filter(user=user, course=course, status__in=Payment.PW_PAID_STATUSES).exists()
price = course.price / 2 if paid_before else course.price
else: else:
if user: if user:
school_payments = SchoolPayment.objects.filter( school_payments = SchoolPayment.objects.filter(
user=user, user=user,
date_start__lte=date_start, date_start__lte=date_start,
date_end__gte=date_start, date_end__gte=date_start,
status__in=[ status__in=Payment.PW_PAID_STATUSES,
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
) )
school_schedules_purchased = school_payments.annotate( school_schedules_purchased = school_payments.annotate(
joined_weekdays=Func(F('weekdays'), function='unnest', ) joined_weekdays=Func(F('weekdays'), function='unnest', )
@ -232,39 +229,38 @@ class Payment(PolymorphicModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
amount_data = Payment.calc_amount(payment=self) amount_data = Payment.calc_amount(payment=self)
if self.status is None and not self.bonus: if not self.is_paid():
self.amount = amount_data.get('amount') if not self.bonus:
if isinstance(self, SchoolPayment): self.amount = amount_data.get('amount')
self.weekdays = amount_data.get('weekdays') if isinstance(self, SchoolPayment):
self.weekdays = amount_data.get('weekdays')
super().save(*args, **kwargs) super().save(*args, **kwargs)
if isinstance(self, CoursePayment) and self.is_paid(): if self.is_paid():
author_balance = getattr(self, 'authorbalance', None) if isinstance(self, CoursePayment):
if not author_balance: if not getattr(self, 'authorbalance', None):
AuthorBalance.objects.create( AuthorBalance.objects.create(
author=self.course.author, author=self.course.author,
amount=self.amount, amount=self.amount,
payment=self, payment=self,
) )
else: if isinstance(self, GiftCertificatePayment):
author_balance.amount = self.amount ugs, created = UserGiftCertificate.objects.get_or_create(user=self.user, gift_certificate=self.gift_certificate,
author_balance.save() payment=self)
if isinstance(self, GiftCertificatePayment) and self.is_paid(): if created:
ugs, created = UserGiftCertificate.objects.get_or_create(user=self.user, gift_certificate=self.gift_certificate, from apps.notification.tasks import send_gift_certificate
payment=self) send_gift_certificate(ugs.id)
if created: # Если юзер реферал и нет платежа, где применялась скидка
from apps.notification.tasks import send_gift_certificate if hasattr(self.user, 'referral') and not self.user.referral.payment:
send_gift_certificate(ugs.id) # Платеж - как сигнал, что скидка применилась
# Если юзер реферал и нет платежа, где применялась скидка self.user.referral.payment = self
if hasattr(self.user, 'referral') and not self.user.referral.payment and self.is_paid(): self.user.referral.save()
# Платеж - как сигнал, что скидка применилась # Отправляем кэшбэк
self.user.referral.payment = self self.user.referral.send_bonuses(amount_data.get('referral_bonus'), amount_data.get('referrer_bonus'))
self.user.referral.save()
# Отправляем кэшбэк
self.user.referral.send_bonuses(amount_data.get('referral_bonus'), amount_data.get('referrer_bonus'))
class CoursePayment(Payment): class CoursePayment(Payment):
course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name='Курс', related_name='payments') course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name='Курс', related_name='payments')
access_expire = models.DateField('Доступ к курсу до даты', null=True)
class Meta: class Meta:
verbose_name = 'Платеж за курс' verbose_name = 'Платеж за курс'

@ -67,9 +67,15 @@ class CourseBuyView(TemplateView):
if request.user == course.author: if request.user == course.author:
messages.error(request, 'Вы не можете приобрести свой курс.') messages.error(request, 'Вы не можете приобрести свой курс.')
return redirect(reverse_lazy('course', args=[course.id])) return redirect(reverse_lazy('course', args=[course.id]))
prev_payment = CoursePayment.objects.filter(user=request.user, course=course,
status__in=Payment.PW_PAID_STATUSES).order_by('-access_expire').first()
access_duration = course.access_duration or 90
access_expire = prev_payment.access_expire + timedelta(days=access_duration) if prev_payment \
else now().date() + timedelta(days=access_duration - 1)
course_payment = CoursePayment.objects.create( course_payment = CoursePayment.objects.create(
user=request.user, user=request.user,
course=course, course=course,
access_expire=access_expire,
roistat_visit=roistat_visit, roistat_visit=roistat_visit,
) )
if use_bonuses: if use_bonuses:

@ -187,7 +187,7 @@ class SchoolView(TemplateView):
date__range=date_range, date__range=date_range,
deactivated_at__isnull=True, deactivated_at__isnull=True,
date__week_day__in=list(map(lambda x: 1 if x == 7 else x+1, sp.weekdays)), date__week_day__in=list(map(lambda x: 1 if x == 7 else x+1, sp.weekdays)),
).values_list('id', flat=True)) ).exclude(title='').values_list('id', flat=True))
prev_live_lessons = LiveLesson.objects.filter(id__in=set(prev_live_lessons)).order_by('-date') prev_live_lessons = LiveLesson.objects.filter(id__in=set(prev_live_lessons)).order_by('-date')
prev_live_lessons_exists = prev_live_lessons.exists() prev_live_lessons_exists = prev_live_lessons.exists()
if prev_live_lessons_exists: if prev_live_lessons_exists:
@ -195,7 +195,6 @@ class SchoolView(TemplateView):
school_schedules_dict[0] = school_schedules_dict.get(7) school_schedules_dict[0] = school_schedules_dict.get(7)
for ll in prev_live_lessons: for ll in prev_live_lessons:
ll.school_schedule = school_schedules_dict.get(ll.date.isoweekday()) ll.school_schedule = school_schedules_dict.get(ll.date.isoweekday())
context.update({ context.update({
'online': online, 'online': online,
'prev_live_lessons': prev_live_lessons, 'prev_live_lessons': prev_live_lessons,

@ -108,9 +108,9 @@ class User(AbstractUser):
@cached_property @cached_property
def balance(self): def balance(self):
from apps.payment.models import Payment from apps.payment.models import Payment, AuthorBalance
income = self.balances.filter( income = self.balances.filter(
type=0, type=AuthorBalance.IN,
payment__isnull=False, payment__isnull=False,
payment__status__in=Payment.PW_PAID_STATUSES, payment__status__in=Payment.PW_PAID_STATUSES,
).aggregate( ).aggregate(
@ -120,7 +120,7 @@ class User(AbstractUser):
income_amount = income.get('amount__sum') or 0 income_amount = income.get('amount__sum') or 0
income_commission = income.get('commission__sum') or 0 income_commission = income.get('commission__sum') or 0
payout = self.balances.filter(type=1, status=1).aggregate(models.Sum('amount')) payout = self.balances.filter(type=AuthorBalance.OUT, status=AuthorBalance.ACCEPTED).aggregate(models.Sum('amount'))
payout_amount = payout.get('amount__sum') or 0 payout_amount = payout.get('amount__sum') or 0
return income_amount - income_commission - payout_amount return income_amount - income_commission - payout_amount

@ -37,7 +37,7 @@
<div class="order__subtitle">Итого:</div> <div class="order__subtitle">Итого:</div>
<div class="order__total"> <div class="order__total">
<div class="loading-loader"></div> <div class="loading-loader"></div>
<span class="order_price_text">{{ course.price }}р.</span> <span class="order_price_text">{{ course_price }}р.</span>
</div> </div>
</div> </div>
</div> </div>
@ -45,7 +45,8 @@
</div> </div>
</div> </div>
<div class="buy__foot"> <div class="buy__foot">
<a class="buy__btn btn btn_md but_btn_popup" data-link="{% url 'course-checkout' course.id %}" data-price="{{ course.price }}">КУПИТЬ КУРС</a> <a class="buy__btn btn btn_md but_btn_popup" data-link="{% url 'course-checkout' course.id %}"
data-price="{{ course_price }}">{% if paid and can_buy_again %}ПРОДЛИТЬ ДОСТУП{% else %}КУПИТЬ КУРС{% endif %}</a>
</div> </div>
</div> </div>
</div> </div>

@ -27,6 +27,8 @@
{% comment %} <meta property="fb:admins" content="Facebook numeric ID"> {% endcomment %} {% comment %} <meta property="fb:admins" content="Facebook numeric ID"> {% endcomment %}
<meta name="csrf-token" content="{{ csrf_token }}"> <meta name="csrf-token" content="{{ csrf_token }}">
<meta name="yandex-verification" content="bb471d5abd9fdec7" />
<meta name="google-site-verification" content="3ULNxGYLRXUpDpKuZgMLTTrXAJx7UEzwAXseCcfdm1s" />
{% compress css %} {% compress css %}
<link rel="stylesheet" media="all" href={% static "app.css" %}> <link rel="stylesheet" media="all" href={% static "app.css" %}>
@ -137,9 +139,6 @@
{% include "templates/blocks/footer.html" %} {% include "templates/blocks/footer.html" %}
{% include "templates/blocks/popup_auth.html" %} {% include "templates/blocks/popup_auth.html" %}
{% include "templates/blocks/popup_school_buy.html" %} {% include "templates/blocks/popup_school_buy.html" %}
{% if course %}
{% include "templates/blocks/popup_course_buy.html" %}
{% endif %}
{% if is_gift_certificate_url %} {% if is_gift_certificate_url %}
{% include "templates/blocks/popup_gift_certificate.html" %} {% include "templates/blocks/popup_gift_certificate.html" %}
{% endif %} {% endif %}

@ -68,16 +68,6 @@
</div> </div>
</div> --> </div> -->
<div class="info__field field field_info" v-if="!live"
v-bind:class="{ error: ($v.course.duration.$dirty || showErrors) && $v.course.duration.$invalid }">
<div class="field__label field__label_gray">ПРОДОЛЖИТЕЛЬНОСТЬ</div>
<div class="field__wrap field__wrap__appended">
<input type="text" class="field__input field__input__appended" name="course-duration"
v-model.number="course.duration" @input="$v.course.duration.$touch()">
<button disabled class="field__append">{{pluralize(course.duration, ['день', 'дня', 'дней'])}}</button>
</div>
</div>
<div v-if="!live" class="info__field field"> <div v-if="!live" class="info__field field">
<div class="field__label field__label_gray">ДОСТУП</div> <div class="field__label field__label_gray">ДОСТУП</div>
<div class="field__wrap"> <div class="field__wrap">
@ -94,20 +84,31 @@
</div> </div>
</div> </div>
<div v-if="course.is_paid" class="info__field field"> <div class="info__field field field_info" v-if="!live && course.is_paid"
<div class="field__label field__label_gray">СТОИМОСТЬ</div> v-bind:class="{ error: ($v.course.access_duration.$dirty || showErrors)
<div class="field__wrap field__wrap__appended field__wrap__100px"> && $v.course.access_duration.$invalid }">
<input type="text" class="field__input field__input__appended" v-model.number.lazy="displayPrice" <div class="field__label field__label_gray">ПРОДОЛЖИТЕЛЬНОСТЬ ДОСТУПА</div>
name="course-price"> <div class="field__wrap field__wrap__appended">
<button disabled class="field__append">руб.</button> <input type="text" class="field__input field__input__appended" v-model.number="course.access_duration"
@input="$v.course.access_duration.$touch()">
<button disabled class="field__append">{{pluralize(course.access_duration, ['день', 'дня', 'дней'])}}</button>
</div> </div>
</div> </div>
<div v-if="course.is_paid" class="info__field field">
<div class="field__label field__label_gray">СТОИМОСТЬ БЕЗ СКИДКИ</div> <div style="display: flex;">
<div class="field__wrap field__wrap__appended field__wrap__100px"> <div v-if="course.is_paid" class="info__field field">
<input type="text" class="field__input field__input__appended" v-model.number.lazy="displayOldPrice" <div class="field__label field__label_gray">СТОИМОСТЬ</div>
name="course-old-price"> <div class="field__wrap field__wrap__appended" style="width: 120px;">
<button disabled class="field__append">руб.</button> <input type="text" class="field__input field__input__appended" v-model.number.lazy="displayPrice">
<button disabled class="field__append">руб.</button>
</div>
</div>
<div v-if="course.is_paid" class="info__field field" style="margin-left: 10px;">
<div class="field__label field__label_gray">СТОИМОСТЬ БЕЗ СКИДКИ</div>
<div class="field__wrap field__wrap__appended">
<input type="text" class="field__input field__input__appended" v-model.number.lazy="displayOldPrice">
<button disabled class="field__append">руб.</button>
</div>
</div> </div>
</div> </div>
<div v-if="!live" class="info__field field"> <div v-if="!live" class="info__field field">
@ -280,7 +281,7 @@
status: null, status: null,
category: null, category: null,
categorySelect: null, categorySelect: null,
duration: null, access_duration: null,
author: null, author: null,
price: null, price: null,
old_price: null, old_price: null,
@ -388,7 +389,7 @@
short_description: "Краткое описание", short_description: "Краткое описание",
stream: "Ссылка на Vimeo", stream: "Ссылка на Vimeo",
date: "Дата", date: "Дата",
duration: "Продолжительность", access_duration: "Продолжительность доступа",
category: "Категория", category: "Категория",
}, },
lessonFields: { lessonFields: {
@ -431,8 +432,8 @@
short_description: { short_description: {
required required
}, },
duration: { access_duration: {
required, required: this.course.is_paid ? required : false,
numeric, numeric,
minValue: minValue(1) minValue: minValue(1)
}, },
@ -611,7 +612,7 @@
this.lessons = data.lessons.map((lessonJson) => { this.lessons = data.lessons.map((lessonJson) => {
return api.convertLessonJson(lessonJson); return api.convertLessonJson(lessonJson);
}); });
this.course.duration = this.course.duration || ''; this.course.access_duration = this.course.access_duration || '';
}, },
loadCourseDraft() { loadCourseDraft() {
//console.log('loadCourseDraft'); //console.log('loadCourseDraft');
@ -900,7 +901,7 @@
promises.push(cats); promises.push(cats);
cats.then((response) => { cats.then((response) => {
if (response.data) { if (response.data) {
this.categoryOptions = response.data.results; this.categoryOptions = response.data;
} }
}); });

@ -111,7 +111,7 @@ export const api = {
old_price: courseObject.is_paid && courseObject.old_price || 0, old_price: courseObject.is_paid && courseObject.old_price || 0,
age: courseObject.age, age: courseObject.age,
deferred_start_at: deferredStart, deferred_start_at: deferredStart,
duration: courseObject.duration || 0, access_duration: courseObject.is_paid && courseObject.access_duration || 0,
is_featured: courseObject.is_featured, is_featured: courseObject.is_featured,
slug: (courseObject.slug || '').toLowerCase(), slug: (courseObject.slug || '').toLowerCase(),
date: (courseObject.date) ? courseObject.date.value:null, date: (courseObject.date) ? courseObject.date.value:null,
@ -192,7 +192,7 @@ export const api = {
is_deferred: isDeferred, is_deferred: isDeferred,
date: deferredDate || courseJSON.date, date: deferredDate || courseJSON.date,
time: deferredTime, time: deferredTime,
duration: courseJSON.duration, access_duration: courseJSON.access_duration,
is_featured: courseJSON.is_featured, is_featured: courseJSON.is_featured,
slug: courseJSON.slug, slug: courseJSON.slug,
stream: courseJSON.stream, stream: courseJSON.stream,

Loading…
Cancel
Save