Уведомление об окончании доступа к курсу

remotes/origin/feature/course-access-expire-notification
gzbender 6 years ago
parent 7980b911f1
commit 86214d8e27
  1. 24
      apps/content/migrations/0031_auto_20190925_1723.py
  2. 6
      apps/course/templates/course/course.html
  3. 3
      apps/course/views.py
  4. 19
      apps/notification/management/commands/send_course_access_expire_email.py
  5. 26
      apps/notification/migrations/0003_coursenotification.py
  6. 7
      apps/notification/models.py
  7. 29
      apps/notification/tasks.py
  8. 22
      apps/notification/templates/notification/email/buy_email.html
  9. 8
      apps/notification/templates/notification/email/course_access_expire.html
  10. 24
      apps/payment/migrations/0039_auto_20190925_1723.py
  11. 4
      apps/payment/models.py
  12. 2
      apps/payment/templates/payment/gift_certificate_item.html
  13. 4
      apps/payment/views.py
  14. 23
      apps/user/migrations/0035_auto_20190925_1723.py
  15. 5
      project/settings.py
  16. 47
      web/src/sass/_common.sass

@ -0,0 +1,24 @@
# Generated by Django 2.0.7 on 2019-09-25 17:23
from django.db import migrations
import project.utils.db
class Migration(migrations.Migration):
dependencies = [
('content', '0030_auto_20190809_0133'),
]
operations = [
migrations.AlterField(
model_name='banner',
name='image',
field=project.utils.db.SafeImageField(upload_to=''),
),
migrations.AlterField(
model_name='imageobject',
name='image',
field=project.utils.db.SafeImageField(upload_to='content/imageobject', verbose_name='Изображение'),
),
]

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

@ -246,9 +246,8 @@ class CourseView(DetailView):
]) ])
payment = payments.filter(access_expire__gte=now().date()).order_by('-access_expire').first() payment = payments.filter(access_expire__gte=now().date()).order_by('-access_expire').first()
context['payment'] = payment 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['paid'] = bool(payment)
context['can_buy_again'] = bool(self.object.price) and (context['access_duration'] <= 7 if payment else context['can_buy_again'] = bool(self.object.price) and (payment.access_duration <= 7 if payment else
payments.filter(access_expire__lt=now().date()).exists()) payments.filter(access_expire__lt=now().date()).exists())
context['pending'] = self.object.payments.filter( context['pending'] = self.object.payments.filter(
user=self.request.user, user=self.request.user,

@ -0,0 +1,19 @@
from django.core.management.base import BaseCommand
from apps.notification.tasks import send_course_access_expire_email
class Command(BaseCommand):
help = 'Send course access expire email'
def add_arguments(self, parser):
parser.add_argument(
'--days',
dest='days',
type=int,
help='Days before access expired',
)
def handle(self, *args, **options):
send_course_access_expire_email(options.get('days'))

@ -0,0 +1,26 @@
# Generated by Django 2.0.7 on 2019-09-25 17:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('course', '0050_auto_20190818_1043'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('notification', '0002_usernotification_camp_certificate_last_email'),
]
operations = [
migrations.CreateModel(
name='CourseNotification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('access_expire_last_email', models.DateTimeField(blank=True, null=True)),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.Course')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

@ -1,7 +1,6 @@
from django.db import models from django.db import models
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
User = get_user_model() User = get_user_model()
@ -10,3 +9,9 @@ class UserNotification(models.Model):
certificate_number = models.SmallIntegerField(blank=True, null=True) certificate_number = models.SmallIntegerField(blank=True, null=True)
certificate_last_email = models.DateTimeField(blank=True, null=True) certificate_last_email = models.DateTimeField(blank=True, null=True)
camp_certificate_last_email = models.DateTimeField(blank=True, null=True) camp_certificate_last_email = models.DateTimeField(blank=True, null=True)
class CourseNotification(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
course = models.ForeignKey('course.Course', on_delete=models.CASCADE)
access_expire_last_email = models.DateTimeField(blank=True, null=True)

@ -12,7 +12,7 @@ from django.utils.timezone import now
from django.conf import settings from django.conf import settings
from django.utils.text import slugify from django.utils.text import slugify
from apps.notification.models import UserNotification from apps.notification.models import UserNotification, CourseNotification
from apps.notification.utils import send_email from apps.notification.utils import send_email
from apps.payment.models import SchoolPayment, CoursePayment, Payment, UserGiftCertificate, UserBonus, DrawingCampPayment from apps.payment.models import SchoolPayment, CoursePayment, Payment, UserGiftCertificate, UserBonus, DrawingCampPayment
from project.celery import app from project.celery import app
@ -245,3 +245,30 @@ def send_camp_certificates(email=None, dry_run=False, certificate_number=None):
for fn in file_names: for fn in file_names:
if os.path.isfile(fn): if os.path.isfile(fn):
os.remove(fn) os.remove(fn)
@app.task
def send_course_access_expire_email(days=None):
if days is None:
days = 30
payments = CoursePayment.objects.paid().filter(access_expire=now() + timedelta(days - 1), created_at__lte=now() - timedelta(10))
for payment in payments:
cn, created = CourseNotification.objects.get_or_create(course=payment.course, user=payment.user)
if not created and cn.access_expire_last_email and cn.access_expire_last_email.date() >= now().date() - timedelta(days):
continue
print(payment.user.email)
try:
#payment.user.email
send_email('Доступ к курсу скоро закончится', 'gzbender74@gmail.com', 'notification/email/course_access_expire.html',
username=payment.user.get_full_name(), course_title=payment.course.title,
access_duration=payment.access_duration)
except:
print('Not OK')
continue
else:
cn.access_expire_last_email = now()
cn.save()
user_courses = dict(CoursePayment.objects.paid().filter(access_expire__lt=now().date()).values_list('user_id', 'course_id'))
for cn in CourseNotification.objects.filter(course_id__in=user_courses.values(), user_id__in=user_courses.keys()):
if user_courses.get(cn.user_id) == cn.course_id:
cn.delete()

@ -1,21 +1,23 @@
{% extends "notification/email/_base.html" %} {% extends "notification/email/_base.html" %}
{% load settings %} {% load rupluralize from plural %}
{% block content %} {% block content %}
{% if product_type == 'course' %} {% if product_type == 'course' %}
<p>Добрый день! Спасибо за покупку знаний в «Lil School»!</p> <p>Добрый день, {{ username }}!</p>
<p>Где искать уроки?</p> <p>Вы приобрели видеокурс «{{ course_title }}». Спасибо за покупку!</p>
<p>После оплаты курс появится в вашем личном кабинете на платформе.</p> <p>Обратите внимание, что доступ к курсу действует {{ access_duration | rupluralize:'день,дня,дней' }}. Этого должно хватить, чтобы пройти его.
<p><a href="https://{% setting 'MAIN_HOST' %}{{ url }}"> Если вы не успеете, то по окончании этого срока система автоматически предложит вам продлить доступ к курсу за 50% от его стоимости./p>
https://{% setting 'MAIN_HOST' %}{{ url }}</a></p> <p>Найти курс можно в вашем личном кабинете на платформе.</p>
<p><a href="https://{{ settings.MAIN_HOST }}{{ url }}">
https://{{ settings.MAIN_HOST }}{{ url }}</a></p>
<p>Все ваши покупки будут храниться там в рамках срока доступа к курсу.</p> <p>Все ваши покупки будут храниться там в рамках срока доступа к курсу.</p>
{% endif %} {% endif %}
{% if product_type == 'school' %} {% if product_type == 'school' %}
<p>Добрый день! Спасибо за покупку знаний в «Lil School»!</p> <p>Добрый день! Спасибо за покупку знаний в «Lil School»!</p>
<p>Где искать уроки?</p> <p>Где искать уроки?</p>
<p>После оплаты уроки появятся в вашем личном кабинете на платформе.</p> <p>После оплаты уроки появятся в вашем личном кабинете на платформе.</p>
<p><a href="https://{% setting 'MAIN_HOST' %}{% url 'school:school' %}"> <p><a href="https://{{ settings.MAIN_HOST }}{% url 'school:school' %}">
https://{% setting 'MAIN_HOST' %}{% url 'school:school' %}</a></p> https://{{ settings.MAIN_HOST }}{% url 'school:school' %}</a></p>
<p>В онлайн-школе урок хранится неделю. Ровно до следующего урока.</p> <p>В онлайн-школе урок хранится неделю. Ровно до следующего урока.</p>
{% endif %} {% endif %}
{% if product_type == 'drawing_camp' %} {% if product_type == 'drawing_camp' %}
@ -76,8 +78,8 @@
Обязательно делитесь своими впечатлениями и работами, отмечая их хэштегом #lil_summer. Спасибо, что вы с нами! Обязательно делитесь своими впечатлениями и работами, отмечая их хэштегом #lil_summer. Спасибо, что вы с нами!
</p> </p>
{% else %} {% else %}
<p>Рисовальный лагерь ждет вас по ссылке <a href="https://{% setting 'MAIN_HOST' %}{% url 'school:drawing-camp' %}"> <p>Рисовальный лагерь ждет вас по ссылке <a href="https://{{ settings.MAIN_HOST }}{% url 'school:drawing-camp' %}">
https://{% setting 'MAIN_HOST' %}{% url 'school:drawing-camp' %}</a></p> https://{{ settings.MAIN_HOST }}{% url 'school:drawing-camp' %}</a></p>
{% endif %} {% endif %}
{% endif %} {% endif %}

@ -0,0 +1,8 @@
{% load rupluralize from plural %}
<p>Добрый день, {{ username }}!</p>
<p>Доступ к видеокурсу «{{ course_title }}», который вы приобретали на платформе lil.school,
заканчивается через {{ access_duration|rupluralize:'день,дня,дней' }}. Если вы ещё не успели пройти этот курс, самое время это сделать.
По окончании этого срока система автоматически предложит вам продлить доступ к курсу за 50% от его стоимости.</p>
<p>Команда «Lil School».</p>

@ -0,0 +1,24 @@
# Generated by Django 2.0.7 on 2019-09-25 17:23
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('payment', '0038_auto_20190814_1506'),
]
operations = [
migrations.AlterField(
model_name='userbonus',
name='payment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='payment.Payment'),
),
migrations.AlterField(
model_name='userbonus',
name='referral',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='user.Referral'),
),
]

@ -325,6 +325,10 @@ class CoursePayment(Payment):
verbose_name = 'Платеж за курс' verbose_name = 'Платеж за курс'
verbose_name_plural = 'Платежи за курсы' verbose_name_plural = 'Платежи за курсы'
@property
def access_duration(self):
return (self.access_expire - now().date()).days + 1
class SchoolPayment(Payment): class SchoolPayment(Payment):
weekdays = ArrayField(models.IntegerField(), size=7, verbose_name='Дни недели') weekdays = ArrayField(models.IntegerField(), size=7, verbose_name='Дни недели')

@ -4,7 +4,7 @@
<div class="gift-certificates__item"> <div class="gift-certificates__item">
<div class="gift-certificates__preview {{ theme_color }}"> <div class="gift-certificates__preview {{ theme_color }}">
<img class="gift-certificates__cover" src="{{ gift_certificate.cover }}" /> <img class="gift-certificates__cover" src="{{ gift_certificate.cover|default:'' }}" />
</div> </div>
<div class="gift-certificates__details"> <div class="gift-certificates__details">
<span class="gift-certificates__title theme {{ theme_color }}"> <span class="gift-certificates__title theme {{ theme_color }}">

@ -385,7 +385,9 @@ class PaymentwallCallbackView(View):
if product_type_name == 'course': if product_type_name == 'course':
send_email.delay('Спасибо за покупку!', payment.user.email, 'notification/email/buy_email.html', send_email.delay('Спасибо за покупку!', payment.user.email, 'notification/email/buy_email.html',
product_type=product_type_name, url=payment.course.url) product_type=product_type_name, url=payment.course.url,
username=payment.user.get_full_name(), course_title=payment.course.title,
access_duration=payment.access_duration)
elif product_type_name != 'school': elif product_type_name != 'school':
send_email.delay('Спасибо за покупку!', payment.user.email, 'notification/email/buy_email.html', send_email.delay('Спасибо за покупку!', payment.user.email, 'notification/email/buy_email.html',
product_type=product_type_name) product_type=product_type_name)

@ -0,0 +1,23 @@
# Generated by Django 2.0.7 on 2019-09-25 17:23
from django.db import migrations
import project.utils.db
class Migration(migrations.Migration):
dependencies = [
('user', '0034_auto_20190612_1852'),
]
operations = [
migrations.AlterModelOptions(
name='child',
options={'ordering': ('id',)},
),
migrations.AlterField(
model_name='user',
name='photo',
field=project.utils.db.SafeImageField(blank=True, null=True, upload_to='users', verbose_name='Фото'),
),
]

@ -357,6 +357,11 @@ CELERY_BEAT_SCHEDULE = {
'schedule': crontab(0, 9, day_of_month='1', month_of_year='9'), 'schedule': crontab(0, 9, day_of_month='1', month_of_year='9'),
'kwargs': {'certificate_number': 3}, 'kwargs': {'certificate_number': 3},
}, },
'send_course_access_expire_email': {
'task': 'apps.notification.tasks.send_course_access_expire_email',
'schedule': crontab(minute=0, hour=10),
'args': (),
},
} }
try: try:

@ -2831,11 +2831,10 @@ a.grey-link
display: flex display: flex
align-items: center align-items: center
color: inherit color: inherit
&__item
//&:not(:last-child)
margin-right: 40px margin-right: 40px
+t +t
margin-right: 20px margin-right: 20px
margin-bottom: 10px
&__icon &__icon
margin-right: 10px margin-right: 10px
font-size: 0 font-size: 0
@ -2923,6 +2922,7 @@ a.grey-link
margin-bottom: 15px margin-bottom: 15px
&__metas &__meta &__metas &__meta
+m +m
flex-wrap: wrap
margin-bottom: 20px margin-bottom: 20px
&__actions &__actions
display: flex display: flex
@ -4857,37 +4857,40 @@ a
.gift-certificates .gift-certificates
display: flex display: flex
margin: 0 -10px margin: 0 -20px
flex-wrap: wrap flex-wrap: wrap
+m +m
display: block display: block
margin: 0 margin: 0
&__item &__item
display: block display: block
margin: 0 10px 60px margin: 0 20px 75px
color: $cl color: $cl
flex: 0 0 calc(33.33% - 20px) flex: 0 0 288px
+t +t
margin-bottom: 50px !important margin-bottom: 50px !important
+m +m
margin: 0 0 30px margin: 0 5px 30px
flex: 0 0 calc(50% - 15px)
&__preview &__preview
display: block display: block
position: relative position: relative
margin-bottom: 15px margin-bottom: 15px
border-radius: 2px border-radius: 10px
color: $cl color: $cl
overflow: hidden overflow: hidden
width: 300px width: 288px
height: 200px height: 200px
+t +t
margin-bottom: 10px margin-bottom: 10px
&:hover
box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 0.15)
+m +m
margin-left: auto height: 120px
margin-right: auto width: auto
&__cover &__cover
object-fit: cover; object-fit: cover
width: 100%; width: 100%
&__details &__details
display: flex display: flex
margin-bottom: 10px margin-bottom: 10px
@ -4899,16 +4902,18 @@ a
color: $cl color: $cl
&__title &__title
text-transform: uppercase text-transform: uppercase
flex: 1
+m
font-size: 10px
&__status &__status
font-family: 'ProximaNova-Bold', serif display: block
font-size: 12px margin-bottom: 10px
letter-spacing: 2px font-size: 18px
text-transform: uppercase color: $cl
& .icon +t
width: 16px line-height: 1.33
display: inline-block +m
height: 16px font-size: 13px
margin-bottom: -4px
&__buy-btn &__buy-btn
width: 100% width: 100%
&__preview.theme_pink2 &__preview.theme_pink2

Loading…
Cancel
Save