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):
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',
'deferred_start_at',
'category',
'duration',
'access_duration',
'is_featured',
'url',
'status',
@ -286,7 +286,7 @@ class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
'is_infinite',
'deferred_start_at',
'category',
'duration',
'access_duration',
'is_featured',
'url',
'status',

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

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

@ -1,6 +1,6 @@
from phonenumber_field.serializerfields import PhoneNumberField
from rest_framework import serializers
from drf_dynamic_fields import DynamicFieldsMixin
from django.contrib.auth import get_user_model
from api.v1.serializers.content import GallerySerializer, GalleryImageSerializer, GalleryImageCreateSerializer
@ -11,7 +11,7 @@ from .mixins import DispatchGalleryMixin
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
class UserSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
phone = PhoneNumberField(required=False, allow_null=True, allow_blank=True)
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.settings import api_settings
from . import ExtendedModelViewSet
from . import ExtendedModelViewSet, BothListFormatMixin
from .serializers.config import ConfigSerializer
from .serializers.course import (
@ -213,7 +213,7 @@ class LikeViewSet(ExtendedModelViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class CategoryViewSet(ExtendedModelViewSet):
class CategoryViewSet(BothListFormatMixin, ExtendedModelViewSet):
queryset = Category.objects.order_by('-id')
serializer_class = CategorySerializer
search_fields = ('title',)
@ -221,7 +221,7 @@ class CategoryViewSet(ExtendedModelViewSet):
# permission_classes = (IsAdmin,)
class CourseViewSet(ExtendedModelViewSet):
class CourseViewSet(BothListFormatMixin, ExtendedModelViewSet):
queryset = Course.objects.select_related(
'author', 'category', 'cover', 'gallery',
).prefetch_related(
@ -243,20 +243,6 @@ class CourseViewSet(ExtendedModelViewSet):
# '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'])
def draft(self, request):
drafts = Course.objects.filter(author=request.user, status=Course.DRAFT)
@ -455,8 +441,7 @@ class UserViewSet(ExtendedModelViewSet):
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
# FIXME
queryset = queryset[:2000]
queryset = queryset[:3000]
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@ -736,7 +721,7 @@ class FAQViewSet(ExtendedModelViewSet):
serializer_class = FAQSerializer
class BonusesViewSet(ExtendedModelViewSet):
class BonusesViewSet(BothListFormatMixin, ExtendedModelViewSet):
queryset = UserBonus.objects.all()
serializer_class = UserBonusCreateSerializer
serializer_class_map = {
@ -753,15 +738,3 @@ class BonusesViewSet(ExtendedModelViewSet):
'referral__referral__first_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):
help = 'Upload users to Roistat'
help = 'Update courses slug'
def handle(self, *args, **options):
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],
)
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)
status = models.PositiveSmallIntegerField(
'Статус', default=DRAFT, choices=STATUS_CHOICES

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

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

@ -95,23 +95,41 @@
</a>
<div class="course__metas">
<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 %}
<div class="meta__item">
<div class="meta__icon">
<svg class="icon icon-money">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-money"></use>
</svg>
{% if paid %}
<a class="meta__item" title="Осталось {{ access_duration | rupluralize:'день,дня,дней' }} доступа к курсу">
<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">{{ 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 class="meta__title">{{ course.price|floatformat:"-2" }}₽</div>
</div>
{% endif %}
<div class="meta__item">
<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.decorators import login_required
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.http import require_http_methods
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 .filters import CourseFilter
@ -231,14 +233,20 @@ class CourseView(DetailView):
context = super().get_context_data(**kwargs)
if self.request.user.is_authenticated:
context['next'] = self.request.GET.get('next', None)
context['paid'] = self.object.payments.filter(
# берем последнюю оплату курса
payments = self.object.payments.filter(
user=self.request.user,
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
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(
user=self.request.user,
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_admin'] = self.request.user.role == User.ADMIN_ROLE
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
def get_queryset(self):
@ -317,6 +327,13 @@ class CoursesView(ListView):
context['age_name'] = dict(Course.AGE_CHOICES).get(age, '')
else:
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
def get_template_names(self):
@ -340,6 +357,7 @@ class LessonView(DetailView):
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
access_expire__gte=now().date(),
).exists()
# если это не автор или админ
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 = 'Балансы'
def save(self, *args, **kwargs):
if self.status != self.ACCEPTED:
if self.type == self.IN and not self.id:
self.commission = self.calc_commission()
if self.type == self.OUT:
if self.status == self.DECLINED and not self.declined_send_at:
@ -152,18 +152,15 @@ class Payment(PolymorphicModel):
elif isinstance(payment, GiftCertificatePayment):
price = payment.gift_certificate.price
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:
if user:
school_payments = SchoolPayment.objects.filter(
user=user,
date_start__lte=date_start,
date_end__gte=date_start,
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
status__in=Payment.PW_PAID_STATUSES,
)
school_schedules_purchased = school_payments.annotate(
joined_weekdays=Func(F('weekdays'), function='unnest', )
@ -232,39 +229,38 @@ class Payment(PolymorphicModel):
def save(self, *args, **kwargs):
amount_data = Payment.calc_amount(payment=self)
if self.status is None and not self.bonus:
self.amount = amount_data.get('amount')
if isinstance(self, SchoolPayment):
self.weekdays = amount_data.get('weekdays')
if not self.is_paid():
if not self.bonus:
self.amount = amount_data.get('amount')
if isinstance(self, SchoolPayment):
self.weekdays = amount_data.get('weekdays')
super().save(*args, **kwargs)
if isinstance(self, CoursePayment) and self.is_paid():
author_balance = getattr(self, 'authorbalance', None)
if not author_balance:
AuthorBalance.objects.create(
author=self.course.author,
amount=self.amount,
payment=self,
)
else:
author_balance.amount = self.amount
author_balance.save()
if isinstance(self, GiftCertificatePayment) and self.is_paid():
ugs, created = UserGiftCertificate.objects.get_or_create(user=self.user, gift_certificate=self.gift_certificate,
payment=self)
if created:
from apps.notification.tasks import send_gift_certificate
send_gift_certificate(ugs.id)
# Если юзер реферал и нет платежа, где применялась скидка
if hasattr(self.user, 'referral') and not self.user.referral.payment and self.is_paid():
# Платеж - как сигнал, что скидка применилась
self.user.referral.payment = self
self.user.referral.save()
# Отправляем кэшбэк
self.user.referral.send_bonuses(amount_data.get('referral_bonus'), amount_data.get('referrer_bonus'))
if self.is_paid():
if isinstance(self, CoursePayment):
if not getattr(self, 'authorbalance', None):
AuthorBalance.objects.create(
author=self.course.author,
amount=self.amount,
payment=self,
)
if isinstance(self, GiftCertificatePayment):
ugs, created = UserGiftCertificate.objects.get_or_create(user=self.user, gift_certificate=self.gift_certificate,
payment=self)
if created:
from apps.notification.tasks import send_gift_certificate
send_gift_certificate(ugs.id)
# Если юзер реферал и нет платежа, где применялась скидка
if hasattr(self.user, 'referral') and not self.user.referral.payment:
# Платеж - как сигнал, что скидка применилась
self.user.referral.payment = self
self.user.referral.save()
# Отправляем кэшбэк
self.user.referral.send_bonuses(amount_data.get('referral_bonus'), amount_data.get('referrer_bonus'))
class CoursePayment(Payment):
course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name='Курс', related_name='payments')
access_expire = models.DateField('Доступ к курсу до даты', null=True)
class Meta:
verbose_name = 'Платеж за курс'

@ -67,9 +67,15 @@ class CourseBuyView(TemplateView):
if request.user == course.author:
messages.error(request, 'Вы не можете приобрести свой курс.')
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(
user=request.user,
course=course,
access_expire=access_expire,
roistat_visit=roistat_visit,
)
if use_bonuses:

@ -187,7 +187,7 @@ class SchoolView(TemplateView):
date__range=date_range,
deactivated_at__isnull=True,
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_exists = 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)
for ll in prev_live_lessons:
ll.school_schedule = school_schedules_dict.get(ll.date.isoweekday())
context.update({
'online': online,
'prev_live_lessons': prev_live_lessons,

@ -108,9 +108,9 @@ class User(AbstractUser):
@cached_property
def balance(self):
from apps.payment.models import Payment
from apps.payment.models import Payment, AuthorBalance
income = self.balances.filter(
type=0,
type=AuthorBalance.IN,
payment__isnull=False,
payment__status__in=Payment.PW_PAID_STATUSES,
).aggregate(
@ -120,7 +120,7 @@ class User(AbstractUser):
income_amount = income.get('amount__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
return income_amount - income_commission - payout_amount

@ -37,7 +37,7 @@
<div class="order__subtitle">Итого:</div>
<div class="order__total">
<div class="loading-loader"></div>
<span class="order_price_text">{{ course.price }}р.</span>
<span class="order_price_text">{{ course_price }}р.</span>
</div>
</div>
</div>
@ -45,7 +45,8 @@
</div>
</div>
<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>

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

@ -68,16 +68,6 @@
</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 class="field__label field__label_gray">ДОСТУП</div>
<div class="field__wrap">
@ -94,20 +84,31 @@
</div>
</div>
<div v-if="course.is_paid" class="info__field field">
<div class="field__label field__label_gray">СТОИМОСТЬ</div>
<div class="field__wrap field__wrap__appended field__wrap__100px">
<input type="text" class="field__input field__input__appended" v-model.number.lazy="displayPrice"
name="course-price">
<button disabled class="field__append">руб.</button>
<div class="info__field field field_info" v-if="!live && course.is_paid"
v-bind:class="{ error: ($v.course.access_duration.$dirty || showErrors)
&& $v.course.access_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" 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 v-if="course.is_paid" class="info__field field">
<div class="field__label field__label_gray">СТОИМОСТЬ БЕЗ СКИДКИ</div>
<div class="field__wrap field__wrap__appended field__wrap__100px">
<input type="text" class="field__input field__input__appended" v-model.number.lazy="displayOldPrice"
name="course-old-price">
<button disabled class="field__append">руб.</button>
<div style="display: flex;">
<div v-if="course.is_paid" class="info__field field">
<div class="field__label field__label_gray">СТОИМОСТЬ</div>
<div class="field__wrap field__wrap__appended" style="width: 120px;">
<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 v-if="!live" class="info__field field">
@ -280,7 +281,7 @@
status: null,
category: null,
categorySelect: null,
duration: null,
access_duration: null,
author: null,
price: null,
old_price: null,
@ -388,7 +389,7 @@
short_description: "Краткое описание",
stream: "Ссылка на Vimeo",
date: "Дата",
duration: "Продолжительность",
access_duration: "Продолжительность доступа",
category: "Категория",
},
lessonFields: {
@ -431,8 +432,8 @@
short_description: {
required
},
duration: {
required,
access_duration: {
required: this.course.is_paid ? required : false,
numeric,
minValue: minValue(1)
},
@ -611,7 +612,7 @@
this.lessons = data.lessons.map((lessonJson) => {
return api.convertLessonJson(lessonJson);
});
this.course.duration = this.course.duration || '';
this.course.access_duration = this.course.access_duration || '';
},
loadCourseDraft() {
//console.log('loadCourseDraft');
@ -900,7 +901,7 @@
promises.push(cats);
cats.then((response) => {
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,
age: courseObject.age,
deferred_start_at: deferredStart,
duration: courseObject.duration || 0,
access_duration: courseObject.is_paid && courseObject.access_duration || 0,
is_featured: courseObject.is_featured,
slug: (courseObject.slug || '').toLowerCase(),
date: (courseObject.date) ? courseObject.date.value:null,
@ -192,7 +192,7 @@ export const api = {
is_deferred: isDeferred,
date: deferredDate || courseJSON.date,
time: deferredTime,
duration: courseJSON.duration,
access_duration: courseJSON.access_duration,
is_featured: courseJSON.is_featured,
slug: courseJSON.slug,
stream: courseJSON.stream,

Loading…
Cancel
Save