начислять лилики с анимацией и попапом

remotes/origin/feature/lilcoin-popup
gzbender 7 years ago
parent 737872be7d
commit 82a953de98
  1. 1
      api/v1/serializers/user.py
  2. 3
      api/v1/urls.py
  3. 15
      api/v1/views.py
  4. 18
      apps/payment/migrations/0035_auto_20190405_1828.py
  5. 18
      apps/payment/models.py
  6. 1
      apps/payment/tests/__init__.py
  7. 0
      apps/payment/tests/tests_models.py
  8. 18
      apps/user/migrations/0031_user_review_url.py
  9. 22
      apps/user/models.py
  10. 28
      apps/user/templates/user/bonus-history.html
  11. 2
      apps/user/templates/user/profile-settings.html
  12. 2
      project/templates/blocks/lil_store_js.html
  13. 8
      project/templates/blocks/popup_bonuses_came.html
  14. 1
      project/templates/blocks/user_menu.html
  15. 80
      project/tests/test_features.py
  16. 1
      web/package.json
  17. BIN
      web/src/img/done2.png
  18. BIN
      web/src/img/lilcoin_salut.gif
  19. BIN
      web/src/img/lilcoin_spin.gif
  20. BIN
      web/src/img/lils.png
  21. 53
      web/src/js/modules/popup.js
  22. 13
      web/src/sass/_common.sass
  23. BIN
      web/src/sounds/Lil_World_Addition.mp3
  24. BIN
      web/src/sounds/Lil_World_Addition.ogg
  25. BIN
      web/src/sounds/Lil_World_Jingle_08.mp3
  26. BIN
      web/src/sounds/Lil_World_Jingle_08.ogg
  27. 5
      web/webpack.config.js

@ -45,6 +45,7 @@ class UserSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
'balance',
'show_in_mainpage',
'trial_lesson',
'review_url',
)
read_only_fields = (

@ -18,7 +18,7 @@ from .views import (
UserViewSet, LessonViewSet, ImageObjectViewSet,
SchoolScheduleViewSet, LiveLessonViewSet,
PaymentViewSet, ObjectCommentsViewSet,
ContestViewSet, ContestWorkViewSet,
ContestViewSet, ContestWorkViewSet, NotifiedAboutBonuses,
AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet, UserGalleryViewSet, BonusesViewSet)
router = DefaultRouter()
@ -64,6 +64,7 @@ schema_view = get_schema_view(
)
urlpatterns = [
path('notified-about-bonuses/', NotifiedAboutBonuses.as_view(), name='notified-about-bonuses'),
path('capture-email/', CaptureEmail.as_view(), name='capture-email'),
path('author-balance-users/', AuthorBalanceUsersViewSet.as_view(), name='author-balance-users'),
path('api-token-auth/', ObtainToken.as_view(), name='api-token-auth'),

@ -9,9 +9,9 @@ from rest_framework.decorators import (detail_route, list_route, action,
permission_classes, authentication_classes as auth_classes)
from rest_framework.response import Response
from rest_framework.settings import api_settings
from django.utils.timezone import now
from . import ExtendedModelViewSet, BothListFormatMixin
from .serializers.config import ConfigSerializer
from .serializers.course import (
CategorySerializer, LikeSerializer,
@ -738,3 +738,16 @@ class BonusesViewSet(BothListFormatMixin, ExtendedModelViewSet):
'referral__referral__first_name',
'referral__referral__last_name',
)
class NotifiedAboutBonuses(views.APIView):
authentication_classes = ()
def post(self, request):
#FIXME
user = User.objects.get(pk=request.data.get('user'))
for b in user.bonuses.filter(notified_at__isnull=True):
b.notified_at = now()
b.save()
return Response({'status': 'ok'})

@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2019-04-05 18:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payment', '0034_auto_20190327_2055'),
]
operations = [
migrations.AlterField(
model_name='userbonus',
name='action_name',
field=models.CharField(blank=True, default='', max_length=20),
),
]

@ -1,4 +1,3 @@
from decimal import Decimal
import arrow
import short_url
from django.db.models import Func, F
@ -14,7 +13,6 @@ from django.core.validators import RegexValidator
from django.utils.timezone import now
from django.conf import settings
from apps.content.models import ImageObject
from project.utils import weekdays_in_date_range
from apps.course.models import Course
@ -249,6 +247,11 @@ class Payment(PolymorphicModel):
if created:
from apps.notification.tasks import send_gift_certificate
send_gift_certificate(ugs.id)
# Если это не первая покупка, - отправляем бонусы юзеру
if self.user.paid_one_more and not self.user.bonuses.filter(
is_service=True, action_name=UserBonus.ACTION_PAID_ONE_MORE).count():
UserBonus.objects.create(user=self.user, amount=UserBonus.AMOUNT_PAID_ONE_MORE, is_service=True,
action_name=UserBonus.ACTION_PAID_ONE_MORE)
# Если юзер реферал и нет платежа, где применялась скидка
if hasattr(self.user, 'referral') and not self.user.referral.payment:
# Платеж - как сигнал, что скидка применилась
@ -299,13 +302,22 @@ class GiftCertificatePayment(Payment):
class UserBonus(models.Model):
ACTION_FILL_PROFILE = 'fill_profile'
ACTION_PAID_ONE_MORE = 'paid_one_more'
ACTION_HAVE_REVIEW = 'have_review'
ACTION_CHILD_BIRTHDAY = 'child_birthday'
AMOUNT_FILL_PROFILE = 50
AMOUNT_PAID_ONE_MORE = 100
AMOUNT_HAVE_REVIEW = 300
AMOUNT_CHILD_BIRTHDAY = 200
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='bonuses')
amount = models.DecimalField(max_digits=8, decimal_places=2, default=0)
payment = models.ForeignKey(Payment, on_delete=models.SET_NULL, null=True)
referral = models.ForeignKey('user.Referral', on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)
is_service = models.BooleanField(default=False)
action_name = models.CharField(max_length=10, blank=True, default='')
action_name = models.CharField(max_length=20, blank=True, default='')
notified_at = models.DateTimeField(blank=True, null=True)
class Meta:

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2019-04-04 18:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0030_auto_20190318_1320'),
]
operations = [
migrations.AddField(
model_name='user',
name='review_url',
field=models.URLField(blank=True, default='', verbose_name='Ссылка на видеоотзыв'),
),
]

@ -94,6 +94,7 @@ class User(AbstractUser):
child_gender = models.CharField(
'Пол ребенка', max_length=1, default='n', choices=GENDER_CHOICES)
child_birthday = models.DateField('День рождения ребенка', null=True, blank=True)
review_url = models.URLField('Ссылка на видеоотзыв', blank=True, default='')
objects = UserManager()
@ -146,6 +147,27 @@ class User(AbstractUser):
).order_by('-created_at').first()
return bool(last_bonus) and not last_bonus.notified_at
@property
def paid_one_more(self):
from apps.payment.models import SchoolPayment, CoursePayment, Payment
school_payments_cnt = SchoolPayment.objects.filter(status__in=Payment.PW_PAID_STATUSES, user=self,
add_days=False).count()
course_payment_cnt = CoursePayment.objects.filter(status__in=Payment.PW_PAID_STATUSES, user=self).count()
return school_payments_cnt > 1 or course_payment_cnt > 1 or (school_payments_cnt and course_payment_cnt)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
from apps.payment.models import UserBonus
# начисляемм бонусы, если заполнил профиль
if self.phone and self.first_name and self.last_name and not self.bonuses.filter(
is_service=True, action_name=UserBonus.ACTION_FILL_PROFILE).count():
UserBonus.objects.create(user=self, amount=UserBonus.AMOUNT_FILL_PROFILE, is_service=True,
action_name=UserBonus.ACTION_FILL_PROFILE)
if self.review_url and not self.bonuses.filter(is_service=True,
action_name=UserBonus.ACTION_HAVE_REVIEW).count():
UserBonus.objects.create(user=self, amount=UserBonus.AMOUNT_HAVE_REVIEW, is_service=True,
action_name=UserBonus.ACTION_HAVE_REVIEW)
@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):

@ -28,9 +28,11 @@
</div>
</div>
<div class="section section_gray">
<div class="section section_gray" style="padding-bottom: 0;">
<div class="section__center center center_sm">
<img src="{% static 'img/lils.png' %}" />
<div style="text-align: center; border-top: 1px solid #ddd; padding-top: 40px;">
<img src="{% static 'img/lils.png' %}" />
</div>
</div>
</div>
@ -50,27 +52,28 @@
</div>
<div>+50 LIL</div>
<div><a class="btn btn_stroke" href="{% url 'user-profile' %}">ЗАПОЛНИТЬ</a></div>
<div></div>
<div>{% if request.user.phone and request.user.first_name and request.user.last_name %}
<img src="{% static 'img/done2.png' %}" />{% endif %}</div>
</div>
<div class="bonuses-table__row">
<div>2</div>
<div class="bonuses-table__title">
<div>Оплата второго месяца или курса</div>
<div>
<div class="bonuses-table__title">Оплата второго месяца или курса</div>
Получайте бонусы при оплате последующий месяцев обучения или курсов. Чем большевы с нами, чем больше Лиликов вы получаете.
</div>
<div>+100 LIL</div>
<div><a class="btn btn_stroke" href="{% url 'user-profile' %}">ВЫБРАТЬ КУРС</a></div>
<div><img src="{% static 'img/done2.png' %}" /></div>
<div><a class="btn btn_stroke" href="{% url 'courses' %}">ВЫБРАТЬ КУРС</a></div>
<div>{% if request.user.paid_one_more %}<img src="{% static 'img/done2.png' %}" />{% endif %}</div>
</div>
<div class="bonuses-table__row">
<div>3</div>
<div class="bonuses-table__title">
<div>Видео-отзыв о Lil School</div>
<div>
<div class="bonuses-table__title">Видео-отзыв о Lil School</div>
Выкладывайте свои видео-обзоры и видео-отзывы в Инстаграм с тегом @Lil.School #LilSchool и высылаете отчет на почту school@lil.city
</div>
<div>+100 LIL</div>
<div><a class="btn btn_stroke" href="{% url 'user-profile' %}">ПОСМОТРЕТЬ ПРИМЕР</a></div>
<div></div>
<div><a class="btn btn_stroke" target="_blank" href="https://youtu.be/QrlR5sL_eGI">ПОСМОТРЕТЬ ПРИМЕР</a></div>
<div>{% if request.user.review_url %}<img src="{% static 'img/done2.png' %}" />{% endif %}</div>
</div>
<div class="bonuses-table__row">
<div>4</div>
@ -80,7 +83,8 @@
</div>
<div>+200 LIL</div>
<div><a class="btn btn_stroke" href="{% url 'user-profile' %}">ЗАПОЛНИТЬ КАРТОЧКУ РЕБЕНКА</a></div>
<div></div>
<div>{% if request.user.child_first_name and request.user.child_last_name and request.user.child_birthday %}
<img src="{% static 'img/done2.png' %}" />{% endif %}</div>
</div>
</div>
</div>

@ -152,7 +152,7 @@
</div>
</div>
<div class="form__field field{% if form.phone.errors %} error{% endif %}">
<div class="field__label">Телефон</div>
<div class="field__label">НОМЕР ТЕЛЕФОНА РОДИТЕЛЯ</div>
<div class="field__wrap">
<input name='phone' class="field__input" type="phone" placeholder="+7 (999) 999-99-99" value="{{ user.phone }}">
</div>

@ -17,7 +17,7 @@
id: '{{ request.user.id|default:'' }}',
role: +'{{ request.user.role }}',
photo: '{% if request.user.photo %}{{ request.user.photo.url }}{% else %}{% static 'img/user_default.jpg' %}{% endif %}',
hasNewBonuses: '{{ request.user.has_new_bonuses|yesno:"true,false" }}',
hasNewBonuses: {{ request.user.has_new_bonuses|yesno:"true,false" }},
bonus: +'{{ request.user.bonus }}',
},
components: {},

@ -1,5 +1,5 @@
{% load static %}
<div class="popup bonuses-came js-popup-bonuses-came">
<div class="popup bonuses-came js-popup-bonuses-came" data-manual-close="1">
<div class="popup__wrap popup__wrap_md js-popup-wrap">
<button class="popup__close js-popup-close">
<svg class="icon icon-close">
@ -7,13 +7,13 @@
</svg>
</button>
<div class="popup__body bonuses-came__body">
<img src="{% static 'img/lilcoin48x54.png' %}" style="width: 60px;" />
<img class="bonuses-came__img" src="{% static 'img/lilcoin48x54.png' %}" style="width: 60px;" />
<div class="bonuses-came__title">Вам начислено</div>
<div class="bonuses-came__bonuses">300</div>
<div class="bonuses-came__bonuses"></div>
<div>лиликов</div>
<div class="bonuses-came__text">Приглашайте друзей в Lil School и получайте 30% от суммы их первой покупки.
Накапливайте монеты и тратьте их на оплату школы и курсов.</div>
<a href="#" class="bonuses-came__link">Узнать больше про Лилики</a>
<a href="{% url 'user-bonuses' %}" class="bonuses-came__link">Узнать больше про Лилики</a>
</div>
</div>
</div>

@ -4,6 +4,7 @@
<div class="header__bonuses">
<a class="header__bonuses-link" href="{% url 'user-bonuses' %}">
<img class="header__lil-coin-img" src="{% static 'img/lilcoin.png' %}" />
<img class="header__lil-coin-img-salut" />
{{ request.user.bonus }}
</a>
</div>

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from django.test import TestCase
from django.utils.timezone import now
from factory.faker import Faker
from project.tests.factories import UserFactory, CourseFactory
from apps.payment.models import CoursePayment, SchoolPayment, UserBonus, Payment
from apps.user.models import User
class AutoAddingUserBonusTestCase(TestCase):
def test_adding_user_bonuses_after_paid_more_one(self):
user = UserFactory.create(role=User.USER_ROLE, first_name='', last_name='', phone='')
course = CourseFactory.create(price=1000)
course2 = CourseFactory.create(price=1000)
self.assertEqual(user.bonus, 0, 'При создании есть бонусы')
cp = CoursePayment.objects.create(user=user, course=course, access_expire=now() + timedelta(100))
cp.status = Payment.PW_PAID_STATUSES[0]
cp.save()
del user.__dict__['bonus']
self.assertEqual(user.bonus, 0, 'Есть бонусы после покупки 1 курса')
cp2 = CoursePayment.objects.create(user=user, course=course2, access_expire=now() + timedelta(100))
cp2.status = Payment.PW_PAID_STATUSES[0]
cp2.save()
del user.__dict__['bonus']
self.assertEqual(user.bonus, UserBonus.AMOUNT_PAID_ONE_MORE, 'Неверное кол-во бонусов после покупки 2 курсов')
cp.delete()
del user.__dict__['bonus']
self.assertEqual(user.bonus, UserBonus.AMOUNT_PAID_ONE_MORE, 'Изменились бонусы после удаления оплаты 1 курса')
user.bonuses.all().delete()
sp = SchoolPayment.objects.create(user=user, weekdays=[1, 2, 3], add_days=False, date_start=now().date(),
date_end=(now() + timedelta(30)).date(), amount=1000, status=Payment.PW_PAID_STATUSES[0])
del user.__dict__['bonus']
self.assertEqual(user.bonus, UserBonus.AMOUNT_PAID_ONE_MORE, 'Неверное кол-во бонусов после покупки курса и школы')
user.bonuses.all().delete()
cp2.delete()
sp2 = SchoolPayment.objects.create(user=user, weekdays=[1, 2, 3], add_days=False, date_start=(now() + timedelta(31)).date(),
date_end=(now() + timedelta(60)).date(), amount=1000, status=Payment.PW_PAID_STATUSES[0])
del user.__dict__['bonus']
self.assertEqual(user.bonus, UserBonus.AMOUNT_PAID_ONE_MORE, 'Неверное кол-во бонусов после покупки 2 месяцев школы')
User.objects.all().delete()
CoursePayment.objects.all().delete()
SchoolPayment.objects.all().delete()
UserBonus.objects.all().delete()
def test_adding_user_bonuses_after_fill_profile(self):
user = UserFactory.create(role=User.USER_ROLE, first_name='', last_name='', phone='')
self.assertEqual(user.bonus, 0, 'При создании есть бонусы')
user.first_name = Faker('first_name').generate({})
user.last_name = Faker('last_name').generate({})
user.phone = '+7123456789'
user.save()
del user.__dict__['bonus']
self.assertEqual(user.bonus, UserBonus.AMOUNT_FILL_PROFILE, 'Неверное кол-во бонусов после заполнения профиля')
User.objects.all().delete()
UserBonus.objects.all().delete()
def test_adding_user_bonuses_after_fill_review_url(self):
user = UserFactory.create(role=User.USER_ROLE, first_name='', last_name='', phone='')
self.assertEqual(user.bonus, 0, 'При создании есть бонусы')
user.review_url = 'http://youtube.com/review_url'
user.save()
del user.__dict__['bonus']
self.assertEqual(user.bonus, UserBonus.AMOUNT_HAVE_REVIEW, 'Неверное кол-во бонусов после заполнения ссылки на отзыв')
User.objects.all().delete()
UserBonus.objects.all().delete()

@ -39,6 +39,7 @@
"axios": "^0.17.1",
"babel-polyfill": "^6.26.0",
"baguettebox.js": "^1.10.0",
"bowser": "^2.1.2",
"clipboard": "^2.0.1",
"downscale": "^1.0.4",
"extract-loader": "^3.1.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 471 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@ -4,7 +4,8 @@ import {api} from './api';
import "modal-video/js/jquery-modal-video.js";
import "modal-video/css/modal-video.min.css";
import {email as emailValid} from 'vuelidate/lib/validators';
import Cookies from 'js-cookie'
import Cookies from 'js-cookie';
import Bowser from "bowser";
moment.locale('ru');
@ -303,9 +304,53 @@ $(document).ready(function () {
showPopup();
}
if(window.LIL_STORE.user.id && window.LIL_STORE.user.hasNewBonuses){
popup = $('.js-popup-bonuses-came');
showPopup();
if(window.LIL_STORE.user.id && window.LIL_STORE.user.hasNewBonuses && window.LIL_STORE.user.bonus > 0){
$(document).ready(() => {
popup = $('.js-popup-bonuses-came');
const $bonusesImg = popup.find('.bonuses-came__img');
const $bonusesValue = popup.find('.bonuses-came__bonuses');
showPopup();
const audioAddition = new Audio();
const audioSalut = new Audio();
audioAddition.preload = 'auto';
audioSalut.preload = 'auto';
const browser = Bowser.parse(window.navigator.userAgent).browser.name.toLowerCase();
if(browser.indexOf("firefox") > -1 || browser.indexOf("opera") > -1){
audioAddition.src = window.LIL_STORE.staticUrl + 'sounds/Lil_World_Addition.ogg';
audioSalut.src = window.LIL_STORE.staticUrl + 'sounds/Lil_World_Jingle_08.ogg';
}
else{
audioAddition.src = window.LIL_STORE.staticUrl + 'sounds/Lil_World_Addition.mp3';
audioSalut.src = window.LIL_STORE.staticUrl + 'sounds/Lil_World_Jingle_08.mp3';
}
audioAddition.play().catch(error => console.log(error));
$bonusesImg.attr('src', window.LIL_STORE.staticUrl + 'img/lilcoin48x54.png')
.attr('src', window.LIL_STORE.staticUrl + 'img/lilcoin_spin.gif');
$({bonuses: 0}).animate({bonuses: window.LIL_STORE.user.bonus}, {
duration: 2900,
easing: "linear",
step: function(val) {
$bonusesValue.html(Math.ceil(val));
}
});
api.post('/api/v1/notified-about-bonuses/', {
user: window.LIL_STORE.user.id
});
popup.find('.js-popup-close').click(() => {
hidePopup().then(() => {
const $salut = $('.header__lil-coin-img-salut');
const $lilcoin = $('.header__lil-coin-img');
$lilcoin.animate({opacity: 0}, 200);
$salut.attr('src', '').attr('src', window.LIL_STORE.staticUrl + 'img/lilcoin_salut.gif')
.animate({opacity: 100}, 200);
audioSalut.play();
setTimeout(() => {
$salut.animate({opacity: 0}, 500);
$lilcoin.animate({opacity: 100}, 500);
}, 2000);
});
})
});
}
function showPopup(){

@ -653,6 +653,12 @@ a.btn
color: rgba(25, 25, 25, 0.3)
&__lil-coin-img
margin-bottom: -5px
&__lil-coin-img-salut
position: absolute
height: 65px
left: -9px
top: -15px
opacity: 0
.main
display: flex
@ -4577,13 +4583,14 @@ a
.bonuses-table
margin-left: -110px
margin-left: -120px
margin-top: 50px
margin-right: -46px
&__row
display: flex
width: 100%
margin-bottom: 20px
margin-bottom: 35px
& > div
font-size: 14px
@ -4608,6 +4615,8 @@ a
& > div:nth-child(5)
flex: 40px
padding-top: 13px
padding-left: 10px
text-align: center
&__title

@ -22,6 +22,7 @@ module.exports = {
imagesGiftCertificates: glob('./src/img/gift-certificates/*'),
imagesReviews: glob('./src/img/reviews/*'),
fonts: glob('./src/fonts/*'),
sounds: glob('./src/sounds/*'),
},
output: {
path: path.join(__dirname, "build"),
@ -118,6 +119,10 @@ module.exports = {
{
test: /\.(ttf|otf|eot|woff(2)?)(\?[a-z0-9]+)?$/,
loader: 'file-loader?name=[name].[ext]'
},
{
test: /\.(mp3|wav|ogg)$/,
loader: 'file-loader?name=[name].[ext]&outputPath=./sounds/'
}
]
},

Loading…
Cancel
Save