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

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">
{% if course.price %}
{% if paid %}
<a class="meta__item" title="Осталось {{ access_duration | rupluralize:'день,дня,дней' }} доступа к курсу">
<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">{{ access_duration | rupluralize:"день,дня,дней" }}</div>
<div class="meta__title">Доступ закончится через {{ payment.access_duration | rupluralize:"день,дня,дней" }}</div>
</a>
{% else %}
<a class="meta__item" title="Продолжительность доступа к курсу">
@ -139,7 +139,7 @@
{% if can_buy_again %}
<s>{{ course.price|floatformat:"-2" }}₽</s>
{% 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 %}
<span {% if can_buy_again or course.old_price %}style="color: red;"{% endif %}>{{ course_price|floatformat:"-2" }}₽</span>
</div>

@ -246,9 +246,8 @@ class CourseView(DetailView):
])
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
context['can_buy_again'] = bool(self.object.price) and (payment.access_duration <= 7 if payment else
payments.filter(access_expire__lt=now().date()).exists())
context['pending'] = self.object.payments.filter(
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.contrib.auth import get_user_model
User = get_user_model()
@ -10,3 +9,9 @@ class UserNotification(models.Model):
certificate_number = models.SmallIntegerField(blank=True, null=True)
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.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.payment.models import SchoolPayment, CoursePayment, Payment, UserGiftCertificate, UserBonus, DrawingCampPayment
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:
if os.path.isfile(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" %}
{% load settings %}
{% load rupluralize from plural %}
{% block content %}
{% if product_type == 'course' %}
<p>Добрый день! Спасибо за покупку знаний в «Lil School»!</p>
<p>Где искать уроки?</p>
<p>После оплаты курс появится в вашем личном кабинете на платформе.</p>
<p><a href="https://{% setting 'MAIN_HOST' %}{{ url }}">
https://{% setting 'MAIN_HOST' %}{{ url }}</a></p>
<p>Добрый день, {{ username }}!</p>
<p>Вы приобрели видеокурс «{{ course_title }}». Спасибо за покупку!</p>
<p>Обратите внимание, что доступ к курсу действует {{ access_duration | rupluralize:'день,дня,дней' }}. Этого должно хватить, чтобы пройти его.
Если вы не успеете, то по окончании этого срока система автоматически предложит вам продлить доступ к курсу за 50% от его стоимости./p>
<p>Найти курс можно в вашем личном кабинете на платформе.</p>
<p><a href="https://{{ settings.MAIN_HOST }}{{ url }}">
https://{{ settings.MAIN_HOST }}{{ url }}</a></p>
<p>Все ваши покупки будут храниться там в рамках срока доступа к курсу.</p>
{% endif %}
{% if product_type == 'school' %}
<p>Добрый день! Спасибо за покупку знаний в «Lil School»!</p>
<p>Где искать уроки?</p>
<p>После оплаты уроки появятся в вашем личном кабинете на платформе.</p>
<p><a href="https://{% setting 'MAIN_HOST' %}{% url 'school:school' %}">
https://{% setting 'MAIN_HOST' %}{% url 'school:school' %}</a></p>
<p><a href="https://{{ settings.MAIN_HOST }}{% url 'school:school' %}">
https://{{ settings.MAIN_HOST }}{% url 'school:school' %}</a></p>
<p>В онлайн-школе урок хранится неделю. Ровно до следующего урока.</p>
{% endif %}
{% if product_type == 'drawing_camp' %}
@ -76,8 +78,8 @@
Обязательно делитесь своими впечатлениями и работами, отмечая их хэштегом #lil_summer. Спасибо, что вы с нами!
</p>
{% else %}
<p>Рисовальный лагерь ждет вас по ссылке <a href="https://{% setting 'MAIN_HOST' %}{% url 'school:drawing-camp' %}">
https://{% setting 'MAIN_HOST' %}{% url 'school:drawing-camp' %}</a></p>
<p>Рисовальный лагерь ждет вас по ссылке <a href="https://{{ settings.MAIN_HOST }}{% url 'school:drawing-camp' %}">
https://{{ settings.MAIN_HOST }}{% url 'school:drawing-camp' %}</a></p>
{% 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_plural = 'Платежи за курсы'
@property
def access_duration(self):
return (self.access_expire - now().date()).days + 1
class SchoolPayment(Payment):
weekdays = ArrayField(models.IntegerField(), size=7, verbose_name='Дни недели')

@ -4,7 +4,7 @@
<div class="gift-certificates__item">
<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 class="gift-certificates__details">
<span class="gift-certificates__title theme {{ theme_color }}">

@ -385,7 +385,9 @@ class PaymentwallCallbackView(View):
if product_type_name == 'course':
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':
send_email.delay('Спасибо за покупку!', payment.user.email, 'notification/email/buy_email.html',
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'),
'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:

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

Loading…
Cancel
Save