From 9641c0deac1d763882dab6d081eac2211278fb98 Mon Sep 17 00:00:00 2001 From: gzbender Date: Thu, 7 Feb 2019 21:05:46 +0500 Subject: [PATCH] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=B5=20"=D0=BF=D1=80=D0=BE=D0=B4=D0=BE?= =?UTF-8?q?=D0=BB=D0=B6=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=B0"=20?= =?UTF-8?q?=D1=83=20=D0=BA=D1=83=D1=80=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/serializers/course.py | 4 +- .../migrations/0048_auto_20190206_1710.py | 18 ++++ .../migrations/0049_auto_20190207_1551.py | 18 ++++ apps/course/models.py | 2 +- apps/course/templates/course/course.html | 89 ++++++++++++------- apps/course/tests/__init__.py | 8 ++ apps/course/views.py | 15 +++- .../commands/fill_access_expired.py | 18 ++++ .../0031_coursepayment_access_expired.py | 18 ++++ .../migrations/0032_auto_20190207_1233.py | 18 ++++ apps/payment/models.py | 10 +-- apps/payment/views.py | 5 ++ .../templates/blocks/popup_course_buy.html | 5 +- project/templates/lilcity/index.html | 3 - web/src/components/CourseRedactor.vue | 52 +++++------ web/src/js/modules/api.js | 4 +- 16 files changed, 210 insertions(+), 77 deletions(-) create mode 100644 apps/course/migrations/0048_auto_20190206_1710.py create mode 100644 apps/course/migrations/0049_auto_20190207_1551.py create mode 100644 apps/course/tests/__init__.py create mode 100644 apps/payment/management/commands/fill_access_expired.py create mode 100644 apps/payment/migrations/0031_coursepayment_access_expired.py create mode 100644 apps/payment/migrations/0032_auto_20190207_1233.py 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/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/course.html b/apps/course/templates/course/course.html index 9711811a..ca13e0f3 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.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 %}
@@ -345,3 +364,7 @@
{% endblock content %} + +{% block foot %} +{% include "templates/blocks/popup_course_buy.html" %} +{% endblock foot %} diff --git a/apps/course/tests/__init__.py b/apps/course/tests/__init__.py new file mode 100644 index 00000000..7230f40d --- /dev/null +++ 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..fe9d5810 100644 --- a/apps/course/views.py +++ b/apps/course/views.py @@ -12,6 +12,7 @@ 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 .models import Course, Like, Lesson, CourseComment, LessonComment @@ -231,14 +232,21 @@ 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['course_price'] = self.object.price / 2 if context['can_buy_again'] else self.object.price context['pending'] = self.object.payments.filter( user=self.request.user, status=Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW, @@ -340,6 +348,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..07547e19 100644 --- a/apps/payment/models.py +++ b/apps/payment/models.py @@ -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', ) @@ -265,6 +262,7 @@ class Payment(PolymorphicModel): 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..31a0f389 100644 --- a/apps/payment/views.py +++ b/apps/payment/views.py @@ -67,9 +67,14 @@ 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_expire = prev_payment.access_expire + timedelta(days=course.access_duration) if prev_payment \ + else now().date() + timedelta(days=course.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/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 @@
- КУПИТЬ КУРС + {% if paid and can_buy_again %}ПРОДЛИТЬ ДОСТУП{% else %}КУПИТЬ КУРС{% endif %}
diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index f62d48ee..ce40f2bc 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -137,9 +137,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 073e9f49..bf4bc7d7 100644 --- a/web/src/components/CourseRedactor.vue +++ b/web/src/components/CourseRedactor.vue @@ -67,16 +67,6 @@ --> -
-
ПРОДОЛЖИТЕЛЬНОСТЬ
-
- - -
-
-
ДОСТУП
@@ -91,18 +81,31 @@
-
-
СТОИМОСТЬ
-
- - +
+
ПРОДОЛЖИТЕЛЬНОСТЬ ДОСТУПА
+
+ +
-
-
СТОИМОСТЬ БЕЗ СКИДКИ
-
- - + +
+
+
СТОИМОСТЬ
+
+ + +
+
+
+
СТОИМОСТЬ БЕЗ СКИДКИ
+
+ + +
@@ -269,7 +272,7 @@ status: null, category: null, categorySelect: null, - duration: null, + access_duration: null, author: null, price: null, old_price: null, @@ -377,7 +380,7 @@ short_description: "Краткое описание", stream: "Ссылка на Vimeo", date: "Дата", - duration: "Продолжительность", + access_duration: "Продолжительность доступа", category: "Категория", }, lessonFields: { @@ -420,8 +423,7 @@ short_description: { required }, - duration: { - required, + access_duration: { numeric, minValue: minValue(1) }, @@ -600,7 +602,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'); 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,