From 86214d8e273a0e63cf1a73182e746786c0e87310 Mon Sep 17 00:00:00 2001 From: gzbender Date: Thu, 26 Sep 2019 09:20:06 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=B1=20=D0=BE=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=87=D0=B0=D0=BD=D0=B8=D0=B8=20=D0=B4=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=83=D0=BF=D0=B0=20=D0=BA=20=D0=BA=D1=83=D1=80=D1=81=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0031_auto_20190925_1723.py | 24 ++++++++++ apps/course/templates/course/course.html | 6 +-- apps/course/views.py | 3 +- .../send_course_access_expire_email.py | 19 ++++++++ .../migrations/0003_coursenotification.py | 26 ++++++++++ apps/notification/models.py | 7 ++- apps/notification/tasks.py | 29 +++++++++++- .../notification/email/buy_email.html | 22 +++++---- .../email/course_access_expire.html | 8 ++++ .../migrations/0039_auto_20190925_1723.py | 24 ++++++++++ apps/payment/models.py | 4 ++ .../payment/gift_certificate_item.html | 2 +- apps/payment/views.py | 4 +- .../migrations/0035_auto_20190925_1723.py | 23 +++++++++ project/settings.py | 5 ++ web/src/sass/_common.sass | 47 ++++++++++--------- 16 files changed, 213 insertions(+), 40 deletions(-) create mode 100644 apps/content/migrations/0031_auto_20190925_1723.py create mode 100644 apps/notification/management/commands/send_course_access_expire_email.py create mode 100644 apps/notification/migrations/0003_coursenotification.py create mode 100644 apps/notification/templates/notification/email/course_access_expire.html create mode 100644 apps/payment/migrations/0039_auto_20190925_1723.py create mode 100644 apps/user/migrations/0035_auto_20190925_1723.py diff --git a/apps/content/migrations/0031_auto_20190925_1723.py b/apps/content/migrations/0031_auto_20190925_1723.py new file mode 100644 index 00000000..71067608 --- /dev/null +++ b/apps/content/migrations/0031_auto_20190925_1723.py @@ -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='Изображение'), + ), + ] diff --git a/apps/course/templates/course/course.html b/apps/course/templates/course/course.html index db228e96..adc2de97 100644 --- a/apps/course/templates/course/course.html +++ b/apps/course/templates/course/course.html @@ -111,13 +111,13 @@
{% if course.price %} {% if paid %} - +
-
{{ access_duration | rupluralize:"день,дня,дней" }}
+
Доступ закончится через {{ payment.access_duration | rupluralize:"день,дня,дней" }}
{% else %} @@ -139,7 +139,7 @@ {% if can_buy_again %} {{ course.price|floatformat:"-2" }}₽ {% else %} - {% if course.old_price %}{{ course.old_price|floatformat:"-2" }}₽{% endif %} + {% if course.old_price and not paid %}{{ course.old_price|floatformat:"-2" }}₽{% endif %} {% endif %} {{ course_price|floatformat:"-2" }}₽
diff --git a/apps/course/views.py b/apps/course/views.py index d4bd0298..aed63b75 100644 --- a/apps/course/views.py +++ b/apps/course/views.py @@ -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, diff --git a/apps/notification/management/commands/send_course_access_expire_email.py b/apps/notification/management/commands/send_course_access_expire_email.py new file mode 100644 index 00000000..3e358383 --- /dev/null +++ b/apps/notification/management/commands/send_course_access_expire_email.py @@ -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')) + diff --git a/apps/notification/migrations/0003_coursenotification.py b/apps/notification/migrations/0003_coursenotification.py new file mode 100644 index 00000000..e48a63c7 --- /dev/null +++ b/apps/notification/migrations/0003_coursenotification.py @@ -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)), + ], + ), + ] diff --git a/apps/notification/models.py b/apps/notification/models.py index c60c91cc..6770745c 100644 --- a/apps/notification/models.py +++ b/apps/notification/models.py @@ -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) diff --git a/apps/notification/tasks.py b/apps/notification/tasks.py index 791ed307..759ba298 100644 --- a/apps/notification/tasks.py +++ b/apps/notification/tasks.py @@ -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() diff --git a/apps/notification/templates/notification/email/buy_email.html b/apps/notification/templates/notification/email/buy_email.html index 5733ed7a..fb4393df 100644 --- a/apps/notification/templates/notification/email/buy_email.html +++ b/apps/notification/templates/notification/email/buy_email.html @@ -1,21 +1,23 @@ {% extends "notification/email/_base.html" %} -{% load settings %} +{% load rupluralize from plural %} {% block content %} {% if product_type == 'course' %} -

Добрый день! Спасибо за покупку знаний в «Lil School»!

-

Где искать уроки?

-

После оплаты курс появится в вашем личном кабинете на платформе.

-

- https://{% setting 'MAIN_HOST' %}{{ url }}

+

Добрый день, {{ username }}!

+

Вы приобрели видеокурс «{{ course_title }}». Спасибо за покупку!

+

Обратите внимание, что доступ к курсу действует {{ access_duration | rupluralize:'день,дня,дней' }}. Этого должно хватить, чтобы пройти его. + Если вы не успеете, то по окончании этого срока система автоматически предложит вам продлить доступ к курсу за 50% от его стоимости./p> +

Найти курс можно в вашем личном кабинете на платформе.

+

+ https://{{ settings.MAIN_HOST }}{{ url }}

Все ваши покупки будут храниться там в рамках срока доступа к курсу.

{% endif %} {% if product_type == 'school' %}

Добрый день! Спасибо за покупку знаний в «Lil School»!

Где искать уроки?

После оплаты уроки появятся в вашем личном кабинете на платформе.

-

- https://{% setting 'MAIN_HOST' %}{% url 'school:school' %}

+

+ https://{{ settings.MAIN_HOST }}{% url 'school:school' %}

В онлайн-школе урок хранится неделю. Ровно до следующего урока.

{% endif %} {% if product_type == 'drawing_camp' %} @@ -76,8 +78,8 @@ Обязательно делитесь своими впечатлениями и работами, отмечая их хэштегом #lil_summer. Спасибо, что вы с нами!

{% else %} -

Рисовальный лагерь ждет вас по ссылке - https://{% setting 'MAIN_HOST' %}{% url 'school:drawing-camp' %}

+

Рисовальный лагерь ждет вас по ссылке + https://{{ settings.MAIN_HOST }}{% url 'school:drawing-camp' %}

{% endif %} {% endif %} diff --git a/apps/notification/templates/notification/email/course_access_expire.html b/apps/notification/templates/notification/email/course_access_expire.html new file mode 100644 index 00000000..0c8f22f3 --- /dev/null +++ b/apps/notification/templates/notification/email/course_access_expire.html @@ -0,0 +1,8 @@ +{% load rupluralize from plural %} +

Добрый день, {{ username }}!

+ +

Доступ к видеокурсу «{{ course_title }}», который вы приобретали на платформе lil.school, + заканчивается через {{ access_duration|rupluralize:'день,дня,дней' }}. Если вы ещё не успели пройти этот курс, самое время это сделать. + По окончании этого срока система автоматически предложит вам продлить доступ к курсу за 50% от его стоимости.

+ +

Команда «Lil School».

diff --git a/apps/payment/migrations/0039_auto_20190925_1723.py b/apps/payment/migrations/0039_auto_20190925_1723.py new file mode 100644 index 00000000..d04deda6 --- /dev/null +++ b/apps/payment/migrations/0039_auto_20190925_1723.py @@ -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'), + ), + ] diff --git a/apps/payment/models.py b/apps/payment/models.py index 47af6711..86f40dfc 100644 --- a/apps/payment/models.py +++ b/apps/payment/models.py @@ -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='Дни недели') diff --git a/apps/payment/templates/payment/gift_certificate_item.html b/apps/payment/templates/payment/gift_certificate_item.html index 7d4d3d8f..f9e92e8d 100644 --- a/apps/payment/templates/payment/gift_certificate_item.html +++ b/apps/payment/templates/payment/gift_certificate_item.html @@ -4,7 +4,7 @@
- +
diff --git a/apps/payment/views.py b/apps/payment/views.py index d9a4edc3..c57c005f 100644 --- a/apps/payment/views.py +++ b/apps/payment/views.py @@ -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) diff --git a/apps/user/migrations/0035_auto_20190925_1723.py b/apps/user/migrations/0035_auto_20190925_1723.py new file mode 100644 index 00000000..063c816f --- /dev/null +++ b/apps/user/migrations/0035_auto_20190925_1723.py @@ -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='Фото'), + ), + ] diff --git a/project/settings.py b/project/settings.py index a56c9463..c06e3a55 100644 --- a/project/settings.py +++ b/project/settings.py @@ -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: diff --git a/web/src/sass/_common.sass b/web/src/sass/_common.sass index bb6a4d07..82a81309 100755 --- a/web/src/sass/_common.sass +++ b/web/src/sass/_common.sass @@ -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