diff --git a/api/v1/__init__.py b/api/v1/__init__.py index 45967a33..a8299561 100644 --- a/api/v1/__init__.py +++ b/api/v1/__init__.py @@ -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) diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index e393aac9..718dad8c 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -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', diff --git a/api/v1/serializers/mixins.py b/api/v1/serializers/mixins.py index a79ff8e5..451b362a 100644 --- a/api/v1/serializers/mixins.py +++ b/api/v1/serializers/mixins.py @@ -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: diff --git a/api/v1/serializers/payment.py b/api/v1/serializers/payment.py index 7c48cf46..4e9e8709 100644 --- a/api/v1/serializers/payment.py +++ b/api/v1/serializers/payment.py @@ -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', diff --git a/api/v1/serializers/user.py b/api/v1/serializers/user.py index 79f17d32..3903a66d 100644 --- a/api/v1/serializers/user.py +++ b/api/v1/serializers/user.py @@ -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: diff --git a/api/v1/views.py b/api/v1/views.py index 6f475b55..f43ca18a 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -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) diff --git a/apps/course/management/commands/fix_access_expire.py b/apps/course/management/commands/fix_access_expire.py new file mode 100644 index 00000000..2570e8e0 --- /dev/null +++ b/apps/course/management/commands/fix_access_expire.py @@ -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 +''' + diff --git a/apps/course/management/commands/update_courses_slug.py b/apps/course/management/commands/update_courses_slug.py index 24999a2e..f154e8f9 100644 --- a/apps/course/management/commands/update_courses_slug.py +++ b/apps/course/management/commands/update_courses_slug.py @@ -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='')) diff --git a/apps/course/migrations/0048_auto_20190206_1710.py b/apps/course/migrations/0048_auto_20190206_1710.py new file mode 100644 index 00000000..9a1bbbe1 --- /dev/null +++ b/apps/course/migrations/0048_auto_20190206_1710.py @@ -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='Продолжительность доступа к курсу'), + ), + ] diff --git a/apps/course/migrations/0049_auto_20190207_1551.py b/apps/course/migrations/0049_auto_20190207_1551.py new file mode 100644 index 00000000..24f5128e --- /dev/null +++ b/apps/course/migrations/0049_auto_20190207_1551.py @@ -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', + ), + ] diff --git a/apps/course/models.py b/apps/course/models.py index e680e643..f7ef5f20 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -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 diff --git a/apps/course/templates/course/_items.html b/apps/course/templates/course/_items.html index acbccf69..070f5dbc 100644 --- a/apps/course/templates/course/_items.html +++ b/apps/course/templates/course/_items.html @@ -50,11 +50,16 @@ {{ course.category | upper }} {% if not course.is_free %} - {% if course.old_price %} -
{{ course.old_price|floatformat:"-2" }}₽
+ {% if course.buy_again_price %} +
{{ course.price|floatformat:"-2" }}₽
+
{{ course.buy_again_price|floatformat:"-2" }}₽
+ {% else %} + {% if course.old_price %} +
{{ course.old_price|floatformat:"-2" }}₽
+ {% endif %} +
{{ course.price|floatformat:"-2" }}₽
{% endif %} -
{{ course.price|floatformat:"-2" }}₽
{% endif %} {{ course.title }} diff --git a/apps/course/templates/course/course.html b/apps/course/templates/course/course.html index 9711811a..fb9c351f 100644 --- a/apps/course/templates/course/course.html +++ b/apps/course/templates/course/course.html @@ -37,23 +37,27 @@ {% if has_full_access %} Редактировать {% endif %} - {% if course.author != request.user and not paid and course.price %} -
- + {% if pending %}ОЖИДАЕТСЯ ПОДТВЕРЖДЕНИЕ ОПЛАТЫ{% else %} + {% if paid and can_buy_again %}ПРОДЛИТЬ ДОСТУП{% else %}КУПИТЬ КУРС{% endif %} + {% endif %} + {% if not paid %} + Подарить другу {% endif %} - >{% if pending %}ОЖИДАЕТСЯ ПОДТВЕРЖДЕНИЕ ОПЛАТЫ{% else %}КУПИТЬ КУРС{% endif %} - {% if not paid %} - Подарить другу +
{% endif %} - {% endif %}
- -
- - - -
-
{{ course.duration | rupluralize:"день,дня,дней" }}
-
{% if course.price %} -
-
- - - -
-
- {% if course.old_price %}{{ course.old_price|floatformat:"-2" }}₽{% endif %} - {{ course.price|floatformat:"-2" }}₽ + {% if paid %} + +
+ + + +
+
{{ access_duration | rupluralize:"день,дня,дней" }}
+
+ {% else %} + +
+ + + +
+
{{ course.access_duration | rupluralize:"день,дня,дней" }}
+
+ {% endif %} +
+
+ + + +
+
+ {% if can_buy_again %} + {{ course.price|floatformat:"-2" }}₽ + {% else %} + {% if course.old_price %}{{ course.old_price|floatformat:"-2" }}₽{% endif %} + {% endif %} + {{ course_price|floatformat:"-2" }}₽ +
-
{% endif %}
@@ -298,37 +317,64 @@
- -
- - - -
-
{{ course.duration | rupluralize:"день,дня,дней" }}
-
{% if course.price %} -
-
- - - + {% if paid %} + +
+ + + +
+
{{ access_duration | rupluralize:"день,дня,дней" }}
+
+ {% else %} + +
+ + + +
+
{{ course.access_duration | rupluralize:"день,дня,дней" }}
+
+ {% endif %} +
+
+ + + +
+
+ {% if can_buy_again %} + {{ course.price|floatformat:"-2" }}₽ + {% else %} + {% if course.old_price %}{{ course.old_price|floatformat:"-2" }}₽{% endif %} + {% endif %} + {{ course_price|floatformat:"-2" }}₽ +
-
{{ course.price|floatformat:"-2" }}₽
-
{% endif %}
- {% if course.author != request.user and not paid and course.price %} - + {% if pending %}ОЖИДАЕТСЯ ПОДТВЕРЖДЕНИЕ ОПЛАТЫ{% else %} + {% if paid and can_buy_again %}ПРОДЛИТЬ ДОСТУП{% else %}КУПИТЬ КУРС{% endif %} + {% endif %} + {% if not paid %} + Подарить другу {% endif %} - >{% if pending %}ОЖИДАЕТСЯ ПОДТВЕРЖДЕНИЕ ОПЛАТЫ{% else %}КУПИТЬ КУРС{% endif %} +
+ {% endif %} {% endif %}
@@ -345,3 +391,7 @@
{% endblock content %} + +{% block foot %} +{% include "templates/blocks/popup_course_buy.html" %} +{% endblock foot %} diff --git a/apps/course/templates/course/course_only_lessons.html b/apps/course/templates/course/course_only_lessons.html index 58a3e1a4..5942dd74 100644 --- a/apps/course/templates/course/course_only_lessons.html +++ b/apps/course/templates/course/course_only_lessons.html @@ -95,23 +95,41 @@
- -
- - - -
-
{{ course.duration | rupluralize:"день,дня,дней" }}
-
{% if course.price %} -
-
- - - + {% if paid %} + +
+ + + +
+
{{ access_duration | rupluralize:"день,дня,дней" }}
+
+ {% else %} + +
+ + + +
+
{{ course.access_duration | rupluralize:"день,дня,дней" }}
+
+ {% endif %} +
+
+ + + +
+
+ {% if can_buy_again %} + {{ course.price|floatformat:"-2" }}₽ + {% else %} + {% if course.old_price %}{{ course.old_price|floatformat:"-2" }}₽{% endif %} + {% endif %} + {{ course_price|floatformat:"-2" }}₽ +
-
{{ course.price|floatformat:"-2" }}₽
-
{% endif %}
diff --git a/apps/course/tests/__init__.py b/apps/course/tests/__init__.py index e69de29b..7230f40d 100644 --- a/apps/course/tests/__init__.py +++ b/apps/course/tests/__init__.py @@ -0,0 +1,8 @@ +# TODO: test_course_access_duration +''' +сделать поле "продолжительность доступа" у курсов: автор указывает продолжительность, +после покупки по истечению указанного кол-ва дней у пользователя нет доступа к курсу, +но он может его купить с 50% скидкой, при покупке курса появляется надпись "осталось Х дней доступа", +так же за неделю до окончания можно купить курс еще раз с 50% скидкой, при этом доступ продлевается +на указанное кол-во дней в продолжительности +''' diff --git a/apps/course/views.py b/apps/course/views.py index 7ac3297d..1f0a101f 100644 --- a/apps/course/views.py +++ b/apps/course/views.py @@ -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 diff --git a/apps/payment/management/commands/fill_access_expired.py b/apps/payment/management/commands/fill_access_expired.py new file mode 100644 index 00000000..b9fd134a --- /dev/null +++ b/apps/payment/management/commands/fill_access_expired.py @@ -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() + + diff --git a/apps/payment/migrations/0031_coursepayment_access_expired.py b/apps/payment/migrations/0031_coursepayment_access_expired.py new file mode 100644 index 00000000..bf472d32 --- /dev/null +++ b/apps/payment/migrations/0031_coursepayment_access_expired.py @@ -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='Доступ к курсу до даты'), + ), + ] diff --git a/apps/payment/migrations/0032_auto_20190207_1233.py b/apps/payment/migrations/0032_auto_20190207_1233.py new file mode 100644 index 00000000..764c08a2 --- /dev/null +++ b/apps/payment/migrations/0032_auto_20190207_1233.py @@ -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', + ), + ] diff --git a/apps/payment/models.py b/apps/payment/models.py index 5b4e7615..4e157d4a 100644 --- a/apps/payment/models.py +++ b/apps/payment/models.py @@ -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 = 'Платеж за курс' diff --git a/apps/payment/views.py b/apps/payment/views.py index b9491321..f1399ff2 100644 --- a/apps/payment/views.py +++ b/apps/payment/views.py @@ -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: diff --git a/apps/school/views.py b/apps/school/views.py index 7387c129..3dc0f01a 100644 --- a/apps/school/views.py +++ b/apps/school/views.py @@ -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, diff --git a/apps/user/models.py b/apps/user/models.py index 5a7337bd..5713e6db 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -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 diff --git a/project/templates/blocks/popup_course_buy.html b/project/templates/blocks/popup_course_buy.html index 0498564d..6158a551 100644 --- a/project/templates/blocks/popup_course_buy.html +++ b/project/templates/blocks/popup_course_buy.html @@ -37,7 +37,7 @@
Итого:
- {{ course.price }}р. + {{ course_price }}р.
@@ -45,7 +45,8 @@
diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index f62d48ee..acb8efc4 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -27,6 +27,8 @@ {% comment %} {% endcomment %} + + {% compress 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 %} diff --git a/web/src/components/CourseRedactor.vue b/web/src/components/CourseRedactor.vue index 79a1ed67..4e1ec796 100644 --- a/web/src/components/CourseRedactor.vue +++ b/web/src/components/CourseRedactor.vue @@ -68,16 +68,6 @@
--> -
-
ПРОДОЛЖИТЕЛЬНОСТЬ
-
- - -
-
-
ДОСТУП
@@ -94,20 +84,31 @@
-
-
СТОИМОСТЬ
-
- - +
+
ПРОДОЛЖИТЕЛЬНОСТЬ ДОСТУПА
+
+ +
-
-
СТОИМОСТЬ БЕЗ СКИДКИ
-
- - + +
+
+
СТОИМОСТЬ
+
+ + +
+
+
+
СТОИМОСТЬ БЕЗ СКИДКИ
+
+ + +
@@ -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; } }); diff --git a/web/src/js/modules/api.js b/web/src/js/modules/api.js index 93b3281c..086b932b 100644 --- a/web/src/js/modules/api.js +++ b/web/src/js/modules/api.js @@ -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,