сделать поле "продолжительность доступа" у курсов

remotes/origin/hotfix/users_in_admin_bonuses_9-02-19
gzbender 7 years ago
parent 5d792681f5
commit 9641c0deac
  1. 4
      api/v1/serializers/course.py
  2. 18
      apps/course/migrations/0048_auto_20190206_1710.py
  3. 18
      apps/course/migrations/0049_auto_20190207_1551.py
  4. 2
      apps/course/models.py
  5. 89
      apps/course/templates/course/course.html
  6. 8
      apps/course/tests/__init__.py
  7. 15
      apps/course/views.py
  8. 18
      apps/payment/management/commands/fill_access_expired.py
  9. 18
      apps/payment/migrations/0031_coursepayment_access_expired.py
  10. 18
      apps/payment/migrations/0032_auto_20190207_1233.py
  11. 10
      apps/payment/models.py
  12. 5
      apps/payment/views.py
  13. 5
      project/templates/blocks/popup_course_buy.html
  14. 3
      project/templates/lilcity/index.html
  15. 52
      web/src/components/CourseRedactor.vue
  16. 4
      web/src/js/modules/api.js

@ -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',

@ -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

@ -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.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">
@ -345,3 +364,7 @@
</div>
</div>
{% endblock content %}
{% block foot %}
{% include "templates/blocks/popup_course_buy.html" %}
{% endblock foot %}

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

@ -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

@ -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',
),
]

@ -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 = 'Платеж за курс'

@ -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:

@ -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>

@ -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 %}

@ -67,16 +67,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" 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">
@ -91,18 +81,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">
<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">
<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">
@ -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');

@ -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