Merge branch 'master' of gitlab.com:lilcity/backend into feature/drawing-camp

remotes/origin/feature/drawing-camp
gzbender 7 years ago
commit d770acb648
  1. 6
      api/v1/serializers/config.py
  2. 9
      api/v1/views.py
  3. 28
      apps/config/migrations/0014_auto_20190605_1338.py
  4. 6
      apps/config/models.py
  5. 10
      apps/content/models.py
  6. 14
      apps/content/templates/content/blocks/video.html
  7. 1
      apps/course/templates/course/course.html
  8. 4
      apps/course/templates/course/lesson.html
  9. 19
      apps/course/templatetags/plural.py
  10. 80
      apps/notification/tasks.py
  11. 22
      apps/notification/templates/notification/email/buy_email.html
  12. 9
      apps/notification/templates/notification/email/camp_certificate.html
  13. 25
      apps/notification/templates/notification/email/certificate.html
  14. 2
      apps/payment/models.py
  15. 15
      apps/payment/views.py
  16. 14
      apps/school/templates/blocks/schedule_item.html
  17. 35
      apps/school/templates/school/livelesson_detail.html
  18. 8
      apps/school/views.py
  19. 20
      apps/user/forms.py
  20. 26
      apps/user/migrations/0032_child.py
  21. 21
      apps/user/migrations/0033_add_childs.py
  22. 29
      apps/user/migrations/0034_auto_20190612_1852.py
  23. 34
      apps/user/models.py
  24. 2
      apps/user/templates/user/bonus-history.html
  25. 99
      apps/user/templates/user/profile-settings.html
  26. 4
      apps/user/templates/user/profile.html
  27. 78
      apps/user/views.py
  28. 15
      project/templates/blocks/about.html
  29. 6
      project/templates/blocks/video.html
  30. 3
      project/tests/test_features.py
  31. 120
      web/src/components/Childs.vue
  32. 4
      web/src/components/blocks/BlockImages.vue
  33. BIN
      web/src/img/heart.png
  34. BIN
      web/src/img/video-ended-popup-like.jpg
  35. BIN
      web/src/img/video-ended-popup-logo.png
  36. BIN
      web/src/img/video-ended-popup-ref.jpg
  37. 1
      web/src/js/app.js
  38. 7
      web/src/js/modules/api.js
  39. 62
      web/src/js/modules/content.js
  40. 2
      web/src/js/modules/courses.js
  41. 2
      web/src/js/modules/popup.js
  42. 1
      web/src/js/pages/course.js
  43. 1
      web/src/js/pages/live-lesson.js
  44. 6
      web/src/js/pages/profile.js
  45. 31
      web/src/js/utils.js
  46. 90
      web/src/sass/_common.sass
  47. 2
      web/webpack.config.js

@ -27,6 +27,9 @@ class ConfigSerializer(serializers.ModelSerializer):
REFERRAL_BONUS = serializers.IntegerField()
MAIN_PAGE_VIDEO_URL = serializers.CharField(required=False)
MAIN_PAGE_VIDEO_PREVIEW_IMG = serializers.SerializerMethodField()
NUMBER_OF_STUDENTS = serializers.IntegerField()
NUMBER_OF_COUNTRIES = serializers.IntegerField()
NUMBER_OF_CITIES = serializers.IntegerField()
class Meta:
model = Config
@ -50,6 +53,9 @@ class ConfigSerializer(serializers.ModelSerializer):
'REFERRAL_BONUS',
'MAIN_PAGE_VIDEO_URL',
'MAIN_PAGE_VIDEO_PREVIEW_IMG',
'NUMBER_OF_STUDENTS',
'NUMBER_OF_COUNTRIES',
'NUMBER_OF_CITIES',
)
def get_SCHOOL_LOGO_IMAGE(self, config):

@ -212,6 +212,15 @@ class LikeViewSet(ExtendedModelViewSet):
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
@action(methods=['get'], detail=False, url_path='course-liked')
def course_liked(self, request):
user_id = request.query_params.get('user_id')
course_id = request.query_params.get('course_id')
course = Course.objects.get(id=course_id)
return Response({
'is_liked': course.likes.filter(user_id=user_id).exists()
})
class CategoryViewSet(BothListFormatMixin, ExtendedModelViewSet):
queryset = Category.objects.order_by('-id')

@ -0,0 +1,28 @@
# Generated by Django 2.0.7 on 2019-06-05 13:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0013_config_main_page_video_preview_img'),
]
operations = [
migrations.AddField(
model_name='config',
name='NUMBER_OF_CITIES',
field=models.IntegerField(default=107),
),
migrations.AddField(
model_name='config',
name='NUMBER_OF_COUNTRIES',
field=models.IntegerField(default=81),
),
migrations.AddField(
model_name='config',
name='NUMBER_OF_STUDENTS',
field=models.IntegerField(default=17000),
),
]

@ -23,6 +23,9 @@ class Config(models.Model):
REFERRAL_BONUS = models.IntegerField(default=10)
MAIN_PAGE_VIDEO_URL = models.URLField(max_length=126, default='https://www.youtube.com/embed/1w3amQGtQyc')
MAIN_PAGE_VIDEO_PREVIEW_IMG = models.ImageField(null=True, blank=True)
NUMBER_OF_STUDENTS = models.IntegerField(default=17000)
NUMBER_OF_COUNTRIES = models.IntegerField(default=81)
NUMBER_OF_CITIES = models.IntegerField(default=107)
def save(self, *args, **kwargs):
self.pk = 1
@ -57,5 +60,8 @@ class Config(models.Model):
'REFERRAL_BONUS': '',
'MAIN_PAGE_VIDEO_URL': '',
'MAIN_PAGE_VIDEO_PREVIEW_IMG': '',
'NUMBER_OF_STUDENTS': '',
'NUMBER_OF_COUNTRIES': '',
'NUMBER_OF_CITIES': '',
}
return obj

@ -99,7 +99,7 @@ class Video(Content):
url = models.URLField('Ссылка')
def video_index(self):
if 'youtu.be' in self.url or 'youtube.com' in self.url and 'watch' in self.url:
if self.is_youtube_video:
url = urlparse(self.url)
query = url.query.split('&')
for q in query:
@ -107,6 +107,14 @@ class Video(Content):
return q.split('=')[-1]
return self.url.split('/')[-1]
@property
def is_youtube_video(self):
return 'youtu.be' in self.url or 'youtube.com' in self.url and 'watch' in self.url
@property
def is_vimeo_video(self):
return 'vimeo.com' in self.url
class Gallery(Content):
pass

@ -4,12 +4,14 @@
{{ content.title }}
</div>
<div class="content-block__video iframe__container">
{% if 'youtube.com' in content.url or 'youtu.be' in content.url %}
<iframe src="https://www.youtube.com/embed/{{ content.video_index }}" frameborder="0" allow="autoplay; encrypted-media"
webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
{% elif 'vimeo.com' in content.url %}
<iframe src="https://player.vimeo.com/video/{{ content.video_index }}" frameborder="0" webkitallowfullscreen
mozallowfullscreen allowfullscreen>
{% if content.is_youtube_video %}
<iframe class="js-video" data-is-youtube="1" src="https://www.youtube.com/embed/{{ content.video_index }}"
data-course-id="{% if content.lesson_id %}{{ content.lesson.course_id }}{% else %}{{ content.course_id|default:'' }}{% endif %}"
frameborder="0" allow="autoplay; encrypted-media" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
{% elif content.is_vimeo_video %}
<iframe class="js-video" data-is-vimeo="1" src="https://player.vimeo.com/video/{{ content.video_index }}"
data-course-id="{% if content.lesson_id %}{{ content.lesson.course_id }}{% else %}{{ content.course_id|default:'' }}{% endif %}"
frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen>
</iframe>
{% endif %}
</div>

@ -394,4 +394,5 @@
{% block foot %}
{% include "templates/blocks/popup_course_buy.html" %}
<script type="text/javascript" src="{% static 'course.js' %}"></script>
{% endblock foot %}

@ -123,3 +123,7 @@
</div>
</div>
{% endblock content %}
{% block foot %}
<script type="text/javascript" src="{% static 'course.js' %}"></script>
{% endblock foot %}

@ -4,14 +4,23 @@ from django.template.defaultfilters import stringfilter
register = template.Library()
@register.filter(is_safe=False)
@stringfilter
def rupluralize(value, arg):
def _ruplural(value, arg):
args = arg.split(',')
try:
_value = str(0 if not value or int(value) <= 0 else value)[-1:]
value = value + ' ' + args[0 if _value == '1' else (1 if _value in '234' else 2)]
return value
return args[0 if _value == '1' else (1 if _value in '234' else 2)]
except:
raise template.TemplateSyntaxError
return ''
@register.filter(is_safe=False)
@stringfilter
def rupluralize(value, arg):
return value + ' ' + _ruplural(value, arg)
@register.filter(is_safe=False)
@stringfilter
def ruplural(value, arg):
return _ruplural(value, arg)

@ -3,12 +3,14 @@ from datetime import datetime, date, timedelta
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
from unidecode import unidecode
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.contrib.staticfiles.storage import staticfiles_storage
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.utils import send_email
@ -16,6 +18,7 @@ from apps.payment.models import SchoolPayment, CoursePayment, Payment, UserGiftC
from project.celery import app
from project.utils.db import format_sql, execute_sql
from project.sengrid import get_sendgrid_client
from apps.user.models import Child
User = get_user_model()
@ -36,7 +39,11 @@ def draw_cert(path, email, first_name, last_name, x=None, y=900, color=(29, 115,
os.mkdir(fn)
except:
pass
fn = os.path.join(fn, '%scertificate-for-%s.jpg' % (fn_prefix, email))
if first_name:
name = '-'.join(filter(None, [first_name, last_name]))
fn = os.path.join(fn, '%scertificate-for-%s-%s.jpg' % (fn_prefix, email, slugify(unidecode(name))))
else:
fn = os.path.join(fn, '%scertificate-for-%s.jpg' % (fn_prefix, email))
img.save(fn)
img.close()
return fn
@ -76,25 +83,30 @@ def send_certificates(email=None, date_end=None, dry_run=False):
if dry_run:
continue
file_names = []
un.certificate_number = un.certificate_number + 1 \
if un.certificate_number and staticfiles_storage.exists(path_pattern % (un.certificate_number + 1)) \
else 1
if un.user.child_first_name:
if un.user.child_filled:
fn = staticfiles_storage.path(signed_path_pattern % un.certificate_number)
fn = draw_cert(fn, un.user.email, un.user.child_first_name, un.user.child_last_name)
for child in un.user.childs.all():
file_names.append(draw_cert(fn, un.user.email, child.first_name, child.last_name))
else:
fn = staticfiles_storage.path(path_pattern % un.certificate_number)
file = open(fn, 'rb')
file_names.append(staticfiles_storage.path(path_pattern % un.certificate_number))
files = [open(fn, 'rb') for fn in file_names]
try:
send_email('Грамота от Lil School', un.user.email, 'notification/email/certificate.html',
attachments=[(file.name, file.read(), 'image/jpeg')], user_notification=un)
attachments=[(f.name, f.read(), 'image/jpeg') for f in files], user_notification=un,
many_childs=un.user.childs.all().count() > 1)
except:
print('Not OK')
continue
finally:
file.close()
if un.user.child_first_name:
os.remove(fn)
for f in files:
f.close()
if un.user.child_filled:
for fn in file_names:
os.remove(fn)
un.certificate_last_email = now()
un.save()
@ -148,19 +160,20 @@ def send_gift_certificate(user_gift_certificate):
@app.task
def send_child_birthday_email_and_bonuses():
for u in User.objects.exclude(child_first_name='', child_last_name='',).filter(child_birthday=now().date()):
if not UserBonus.objects.filter(user=u, is_service=True, action_name=UserBonus.ACTION_CHILD_BIRTHDAY).count():
print('user', u.email)
UserBonus.objects.create(user=u, amount=UserBonus.AMOUNT_CHILD_BIRTHDAY, is_service=True,
for user_id, email in set(Child.objects.filter(birthday=now().date()).select_related('user')
.values_list('user_id', 'user__email')):
print('user', email)
if not UserBonus.objects.filter(user=user_id, is_service=True, action_name=UserBonus.ACTION_CHILD_BIRTHDAY).count():
UserBonus.objects.create(user=user_id, amount=UserBonus.AMOUNT_CHILD_BIRTHDAY, is_service=True,
action_name=UserBonus.ACTION_CHILD_BIRTHDAY)
try:
fn = staticfiles_storage.path('img/birthday_postcard.jpg')
file = open(fn, 'rb')
send_email('С Днем Рождения!', u.email, 'notification/email/birthday_postcard.html',
attachments=[(file.name, file.read(), 'image/jpeg')],)
print('email has been sent')
finally:
file.close()
try:
fn = staticfiles_storage.path('img/birthday_postcard.jpg')
file = open(fn, 'rb')
send_email('С Днем Рождения!', email, 'notification/email/birthday_postcard.html',
attachments=[(file.name, file.read(), 'image/jpeg')],)
print('email has been sent')
finally:
file.close()
@app.task
def send_camp_certificates(email=None, dry_run=False, certificate_number=None):
@ -178,7 +191,6 @@ def send_camp_certificates(email=None, dry_run=False, certificate_number=None):
font_size=120, y=1000, color=color)
file = open(fn, 'rb')
try:
pass
send_email('Грамота от Lil School', email, 'notification/email/camp_certificate.html',
attachments=[(file.name, file.read(), 'image/jpeg')], certificate_number=certificate_number)
except:
@ -205,23 +217,27 @@ def send_camp_certificates(email=None, dry_run=False, certificate_number=None):
if dry_run:
continue
if un.user.child_first_name:
file_names = []
if un.user.child_filled:
fn = staticfiles_storage.path(signed_path_pattern % certificate_number)
fn = draw_cert(fn, un.user.email, un.user.child_first_name, un.user.child_last_name,
font_size=120, y=1000, color=color)
for child in un.user.childs.all():
file_names.append(draw_cert(fn, un.user.email, child.first_name, child.last_name,
font_size=120, y=1000, color=color))
else:
fn = staticfiles_storage.path(path_pattern % certificate_number)
file = open(fn, 'rb')
file_names.append(staticfiles_storage.path(path_pattern % certificate_number))
files = [open(fn, 'rb') for fn in file_names]
try:
send_email('Грамота от Lil School', un.user.email, 'notification/email/camp_certificate.html',
attachments=[(file.name, file.read(), 'image/jpeg')], user_notification=un,
certificate_number=certificate_number)
attachments=[(f.name, f.read(), 'image/jpeg') for f in files], user_notification=un,
certificate_number=certificate_number, many_childs=un.user.childs.all().count() > 1)
except:
print('Not OK')
continue
finally:
file.close()
if un.user.child_first_name:
os.remove(fn)
for f in files:
f.close()
if un.user.child_filled:
for fn in file_names:
os.remove(fn)
un.camp_certificate_last_email = date_end
un.save()

@ -0,0 +1,22 @@
{% extends "notification/email/_base.html" %}
{% load settings %}
{% block content %}
{% if product_type == 'course' %}
<p>Курс ждет вас по ссылке <a href="https://{% setting 'MAIN_HOST' %}{{ url }}">
https://{% setting 'MAIN_HOST' %}{{ url }}</a></p>
{% endif %}
{% if product_type == 'school' %}
<p>Школа ждет вас по ссылке <a href="https://{% setting 'MAIN_HOST' %}{% url 'school:school' %}">
https://{% setting 'MAIN_HOST' %}{% url 'school:school' %}</a></p>
{% endif %}
{% if product_type == 'drawing_camp' %}
<p>Рисовальный лагерь ждет вас по ссылке <a href="https://{% setting 'MAIN_HOST' %}{% url 'school:drawing-camp' %}">
https://{% setting 'MAIN_HOST' %}{% url 'school:drawing-camp' %}</a></p>
{% endif %}
<p>Так же вы можете найти ссылку в личном кабинете в разделе «Мои покупки».</p>
<p>Занимайтесь с удовольствием!</p>
<p>Команда «Lil School».</p>
{% endblock content %}

@ -6,20 +6,21 @@
{% if certificate_number == 1 %}
<p>
Поздравляем! Вы прошли месяц обучения в лагере Lil School.<br>
К письму прикреплена грамота. Распечатайте её и вручите вашим детям.<br>
К письму {% if many_childs %}прикреплены грамоты. Распечатайте их{% else %}прикреплена грамота. Распечатайте её{% endif %} и вручите вашим детям.<br>
Ждём вас в следующем месяце на наших творческих занятиях!
</p>
{% endif %}
{% if certificate_number == 2 %}
<p>
Вы помните, что каждый месяц вам приходит грамота за прекрасную учебу в рисовальном лагере?<br>
Вы помните, что каждый месяц вам {% if many_childs %}приходят грамоты{% else %}приходит грамота{% endif %} за прекрасную учебу в рисовальном лагере?<br>
Скачивайте. Распечатывайте. И соберите свою коллекцию!
</p>
{% endif %}
{% if certificate_number == 3 %}
<p>
Вот и закончился лагерь, пора переходить к основным урокам.<br>
Спасибо, что были с нами, успехов вам в дальнейшем обучении!
Поздравляем с успешным окончаниемм рисовального лагеря!<br>
В письме вы найдёте {% if many_childs %}грамоты, они{% else %}грамоту, она{% endif %} для вашей семьи.<br>
До встречи в онлайн школе!
</p>
{% endif %}
</div>

@ -6,20 +6,20 @@
{% if not user_notification or user_notification.certificate_number == 1 %}
<p>
Поздравляем! Вы прошли месяц обучения в Lil School.<br>
К письму прикреплена грамота. Распечатайте её и вручите вашим детям.<br>
К письму {% if many_childs %}прикреплены грамоты. Распечатайте их{% else %}прикреплена грамота. Распечатайте её{% endif %} и вручите вашим детям.<br>
Ждём вас в следующем месяце на наших творческих занятиях!
</p>
{% endif %}
{% if user_notification and user_notification.certificate_number == 2 %}
<p>
Вы помните, что каждый месяц вам приходит грамота за прекрасную учебу в нашей творческой школе?<br>
Вы помните, что каждый месяц вам {% if many_childs %}приходят грамоты{% else %}приходит грамота{% endif %} за прекрасную учебу в нашей творческой школе?<br>
Скачивайте. Распечатывайте. И соберите свою коллекцию!
</p>
{% endif %}
{% if user_notification and user_notification.certificate_number == 3 %}
<p>
Вам понравился наш творческий месяц?<br>
В письме вы найдёте грамоту, она для вашей семьи.<br>
В письме вы найдёте {% if many_childs %}грамоты, они{% else %}грамоту, она{% endif %} для вашей семьи.<br>
Как здорово, что у нас есть такие ученики!<br>
Ждём вас в следующем месяце.
</p>
@ -27,22 +27,27 @@
{% if user_notification and user_notification.certificate_number == 4 %}
<p>
Прошёл целый месяц обучения на платформе Lil School - месяц творчества, креатива и невероятных идей.<br>
Во вложении вас ждёт грамота.<br>
Во вложении вас {% if many_childs %}ждут грамоты{% else %}ждёт грамота{% endif %}.<br>
До встречи на занятиях!
</p>
{% endif %}
{% if user_notification and user_notification.certificate_number == 5 %}
<p>
{% if many_childs %}
Ваши грамоты за успехи в учебе в Lil School ждут вас во вложении.<br>
Скорее распечатайте их!<br>
{% else %}
Ваша грамота за успехи в учебе в Lil School ждёт вас во вложении.<br>
Скорее распечатайте её!<br>
Вам есть чем гордится!<br>
До встречи в следующем месяце!
Скорее распечатайте её!<br>
{% endif %}
Вам есть чем гордится!<br>
До встречи в следующем месяце!
</p>
{% endif %}
{% if user_notification and user_notification.certificate_number == 6 %}
<p>
Как здорово вы потрудились на занятиях в этом месяце!<br>
И наша грамота уже ждёт вас!<br>
И {% if many_childs %}наши грамоты уже ждут{% else %}наша грамота уже ждёт{% endif %} вас!<br>
Спасибо за творчество и креатив.<br>
Ждём вас в следующем месяце!
</p>
@ -50,7 +55,7 @@
{% if user_notification and user_notification.certificate_number == 7 %}
<p>
Какой классный месяц у нас был! Вместе мы очень здорово и креативно потрудились.<br>
Во вложении вас ждёт заслуженная грамота!<br>
Во вложении вас {% if many_childs %}ждут заслуженные грамоты{% else %}ждёт заслуженная грамота{% endif %}!<br>
До встречи на уроках!
</p>
{% endif %}
@ -58,7 +63,7 @@
<p>
Месяц творчества и креатива пролетел как один миг! А как много работ мы вместе сделали!<br>
Вы - большие молодцы.<br>
Во вложении ваш ждёт грамота!<br>
Во вложении вас {% if many_childs %}ждут грамоты{% else %}ждёт грамота{% endif %}!<br>
До встречи на занятиях.
</p>
{% endif %}

@ -356,7 +356,7 @@ class SchoolPayment(Payment):
class DrawingCampPayment(Payment):
MONTH_PRICE = Decimal(1490)
MONTH_PRICE = Decimal(1990)
WEEKDAYS = {1, 3, 5}
date_start = models.DateField('Дата начала подписки', null=True, blank=True)

@ -1,6 +1,5 @@
from decimal import Decimal
import short_url
import arrow
import json
import logging
@ -8,24 +7,22 @@ from datetime import timedelta
from urllib.parse import urlsplit
import datetime
import calendar
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, Http404
from django.shortcuts import redirect, get_object_or_404
from django.views.generic import View, TemplateView, DetailView
from django.views.generic import View, TemplateView
from django.views.decorators.csrf import csrf_exempt
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.timezone import now
from django.conf import settings
from paymentwall import Pingback, Product, Widget
from apps.course.models import Course
from apps.school.models import SchoolSchedule
from apps.payment.tasks import transaction_to_mixpanel, product_payment_to_mixpanel, transaction_to_roistat
from apps.notification.utils import send_email
from .models import AuthorBalance, CoursePayment, SchoolPayment, Payment, UserBonus, GiftCertificate, \
GiftCertificatePayment, UserGiftCertificate, DrawingCampPayment
@ -325,7 +322,6 @@ class PaymentwallCallbackView(View):
product_type_name,
)
if product_type_name == 'course':
properties = {
'payment_id': payment.id,
@ -386,6 +382,13 @@ class PaymentwallCallbackView(View):
payment.roistat_visit,
)
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)
elif product_type_name != 'gift_certificate':
send_email.delay('Спасибо за покупку!', payment.user.email, 'notification/email/buy_email.html',
product_type=product_type_name)
author_balance = getattr(payment, 'author_balance', None)
if author_balance and author_balance.type == AuthorBalance.IN:
if pingback.is_deliverable():

@ -7,7 +7,7 @@
<div class="timing__day{% if school_schedule.is_online %} active{% endif %}">
{{ school_schedule }}
{% if not is_purchased and request.user_agent.is_mobile and school_schedule.trial_lesson %}
<a class="timing__trial-lesson js-video-modal" href="#" data-trial-lesson="1" data-video-url="{{ school_schedule.trial_lesson }}">Пробный урок</a>
<a class="timing__trial-lesson js-video-modal" href="#" data-trial-lesson="1" data-video-url="{{ school_schedule.trial_lesson|safe }}">Пробный урок</a>
{% endif %}
</div>
{% if is_purchased and live_lesson %}
@ -24,8 +24,8 @@
{% include './day_pay_btn.html' %}
{% endif %}
{% endif %}
{% if not is_purchased and not request.user_agent.is_mobile and school_schedule.trial_lesson and not is_drawing_camp %}
<a class="timing__trial-lesson js-video-modal" href="#" data-trial-lesson="1" data-video-url="{{ school_schedule.trial_lesson }}">Пробный урок</a>
{% if not is_purchased and not request.user_agent.is_mobile and school_schedule.trial_lesson %}
<a class="timing__trial-lesson js-video-modal" href="#" data-trial-lesson="1" data-video-url="{{ school_schedule.trial_lesson|safe }}">Пробный урок</a>
{% endif %}
</div>
</div>
@ -56,13 +56,11 @@
{% endif %}
</div>
<div class="timing__content">
{% comment %}
{% if live_lesson and live_lesson.short_description %}
{{ live_lesson.short_description }}
{{ live_lesson.short_description|safe }}
{% else %}
{% endcomment %}
{{ school_schedule.description }}
{% comment %}{% endif %}{% endcomment %}
{{ school_schedule.description|safe }}
{% endif %}
</div>
<div class="timing__more">
<div class="timing__head">Материалы</div>

@ -15,12 +15,6 @@
{% if livelesson.cover and livelesson.cover.image %}{{ livelesson.cover.image.height }}{% else %}{{ block.super }}{% endif %}
{% endblock ogimage-height %}
{% block head %}
{% if livelesson.stream_index %}
<script src="https://player.vimeo.com/api/player.js"></script>
{% endif %}
{% endblock head %}
{% block content %}
<div class="section" style="margin-bottom:0;padding-bottom:0">
<div class="section__center center center_sm">
@ -29,12 +23,11 @@
<div class="lesson__content">{{ livelesson.short_description | safe | linebreaks }}</div>
<div class="lesson__video video">
{% if livelesson.stream_index %}
<iframe class="lesson__video_frame" src="https://player.vimeo.com/video/{{ livelesson.stream_index }}" frameborder="0"
<iframe class="lesson__video_frame js-video" data-is-vimeo="1" src="https://player.vimeo.com/video/{{ livelesson.stream_index }}" frameborder="0"
width="720" height="400"
webkitallowfullscreen mozallowfullscreen allowfullscreen>
</iframe>
<span>Если видео не загрузилось, - уменьшите качество видео или <a href="#" onclick="location.reload();">обновите страницу</a></span>
<!--<iframe class="lesson__chat_frame" src="https://vimeo.com/live-chat/{{ livelesson.stream_index }}" frameborder="0"></iframe>-->
<comments obj-type="live-lesson" obj-id="{{ livelesson.id }}" :is-chat="true"></comments>
{% else %}
{% if livelesson.cover %}
@ -60,29 +53,5 @@
{% endblock content %}
{% block foot %}
{% if livelesson.stream_index %}
<script>
var iframe = document.querySelector('iframe.lesson__video_frame');
var player = new Vimeo.Player(iframe);
player.on('ended', function() {
console.log('vimeo player: ended', arguments[0]);
});
player.on('error', function() {
console.log('vimeo player: error', arguments[0]);
});
player.on('loaded', function() {
console.log('vimeo player: loaded', arguments[0]);
});
player.on('ready', function() {
console.log('vimeo player: ready', arguments[0]);
});
player.on('play', function() {
console.log('vimeo player: play', arguments[0]);
});
player.on('pause', function() {
console.log('vimeo player: pause', arguments[0]);
});
player.play().catch(e => console.log(e));
</script>
{% endif %}
<script type="text/javascript" src="{% static 'liveLesson.js' %}"></script>
{% endblock foot %}

@ -38,12 +38,12 @@ class LiveLessonsView(ListView):
def get_queryset(self):
date_start = (now() - timedelta(days=7)).date()
date_start, date_end = SchoolPayment.get_date_range(date_start, days=17, is_camp=False)
date_start, date_end = SchoolPayment.get_date_range(date_start, days=16, is_camp=False)
date_range = Q(
date__range=[date_start, date_end]
)
exist_dates = LiveLesson.objects.filter(date_range, is_camp=False).values_list('date', flat=True)
for i in range((date_end - date_start).days):
for i in range((date_end - date_start).days + 1):
d = date_start + timedelta(days=i)
if d not in exist_dates:
try:
@ -74,7 +74,7 @@ class DrawingCampLessonsView(ListView):
date__range=[date_start, date_end]
)
exist_dates = LiveLesson.objects.filter(date_range, is_camp=True).values_list('date', flat=True)
for i in range((date_end - date_start).days):
for i in range((date_end - date_start).days + 1):
d = date_start + timedelta(days=i)
if d.isoweekday() in DrawingCampPayment.WEEKDAYS and d not in exist_dates:
try:
@ -289,7 +289,7 @@ class DrawingCampView(TemplateView):
next_schedule = school_schedule
next_lesson_start = next_schedule.start_at_humanize
if not next_schedule:
next_camp_lesson = LiveLesson.objects.filter(date__gt=date_now, is_camp=True).first()
next_camp_lesson = LiveLesson.objects.filter(date__gt=date_now, is_camp=True).order_by('date').first()
if next_camp_lesson and next_camp_lesson.school_schedule:
next_schedule = next_camp_lesson.school_schedule
next_lesson_start = arrow.get(datetime.combine(next_camp_lesson.date, next_schedule.start_at),

@ -2,7 +2,8 @@ from django import forms
from django.contrib.auth import get_user_model
from phonenumber_field.formfields import PhoneNumberField
from .fields import CreditCardField
from apps.user.fields import CreditCardField
from apps.user.models import Child
User = get_user_model()
@ -31,11 +32,6 @@ class UserEditForm(forms.ModelForm):
site = forms.URLField(required=False)
photo = forms.ImageField(required=False)
child_gender = forms.CharField(required=False)
child_birthday = forms.DateField(input_formats=['%d/%m/%Y'], required=False)
child_first_name = forms.CharField(required=False)
child_last_name = forms.CharField(required=False)
class Meta:
model = User
fields = (
@ -61,10 +57,6 @@ class UserEditForm(forms.ModelForm):
'vkontakte',
'site',
'photo',
'child_gender',
'child_birthday',
'child_first_name',
'child_last_name',
)
@ -79,3 +71,11 @@ class AuthorRequesForm(forms.Form):
email = forms.CharField()
about = forms.CharField()
facebook = forms.URLField(required=False)
class ChildForm(forms.ModelForm):
birthday = forms.DateField(input_formats=['%d/%m/%Y'], required=False)
class Meta:
model = Child
fields = '__all__'

@ -0,0 +1,26 @@
# Generated by Django 2.0.7 on 2019-06-06 21:05
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('user', '0031_user_review_url'),
]
operations = [
migrations.CreateModel(
name='Child',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=30, verbose_name='Имя ребенка')),
('last_name', models.CharField(blank=True, default='', max_length=150, verbose_name='Фамилия ребенка')),
('gender', models.CharField(choices=[('n', 'не указан'), ('m', 'Мужчина'), ('f', 'Женщина')], default='n', max_length=1, verbose_name='Пол ребенка')),
('birthday', models.DateField(blank=True, null=True, verbose_name='День рождения ребенка')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='childs', to=settings.AUTH_USER_MODEL)),
],
),
]

@ -0,0 +1,21 @@
# Generated by Django 2.0.7 on 2019-06-07 20:48
from django.db import migrations
def add_childs(apps, schema_editor):
User = apps.get_model('user', 'User')
Child = apps.get_model('user', 'Child')
for user in User.objects.exclude(child_first_name=''):
Child.objects.get_or_create(user=user, first_name=user.child_first_name, last_name=user.child_last_name,
gender=user.child_gender, birthday=user.child_birthday)
class Migration(migrations.Migration):
dependencies = [
('user', '0032_child'),
]
operations = [
migrations.RunPython(add_childs),
]

@ -0,0 +1,29 @@
# Generated by Django 2.0.7 on 2019-06-12 18:52
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('user', '0033_add_childs'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='child_birthday',
),
migrations.RemoveField(
model_name='user',
name='child_first_name',
),
migrations.RemoveField(
model_name='user',
name='child_gender',
),
migrations.RemoveField(
model_name='user',
name='child_last_name',
),
]

@ -88,12 +88,6 @@ class User(AbstractUser):
'content.Gallery', on_delete=models.CASCADE,
verbose_name='Галерея', null=True, blank=True,
)
child_first_name = models.CharField('Имя ребенка', max_length=30, blank=True)
child_last_name = models.CharField('Фамилия ребенка', max_length=150, blank=True)
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()
@ -104,6 +98,14 @@ class User(AbstractUser):
class Meta(AbstractUser.Meta):
ordering = ('-date_joined',)
@cached_property
def has_child(self):
return self.childs.all().count() > 0
def child_filled(self):
child = self.childs.all().first()
return child and child.first_name and child.last_name and child.birthday
@property
def url(self):
return reverse('user', args=[self.slug or self.id])
@ -338,3 +340,23 @@ class EmailLog(models.Model):
email = models.EmailField(_('email address'))
created_at = models.DateTimeField(auto_now_add=True)
source = models.PositiveSmallIntegerField(choices=SOURCE_CHOICES)
class Child(models.Model):
NOT_DEFINED = 'n'
MALE = 'm'
FEMALE = 'f'
GENDER_CHOICES = (
(NOT_DEFINED, 'не указан'),
(MALE, 'Мужчина'),
(FEMALE, 'Женщина'),
)
user = models.ForeignKey(User, related_name='childs', on_delete=models.CASCADE)
first_name = models.CharField('Имя ребенка', max_length=30)
last_name = models.CharField('Фамилия ребенка', max_length=150, blank=True, default='')
gender = models.CharField(
'Пол ребенка', max_length=1, default='n', choices=GENDER_CHOICES)
birthday = models.DateField('День рождения ребенка', null=True, blank=True)
class Meta:
ordering = ('id',)

@ -83,7 +83,7 @@
</div>
<div>+{{ amount_child_birthday }} LIL</div>
<div><a class="btn btn_stroke" href="{% url 'user-profile' %}">ЗАПОЛНИТЬ КАРТОЧКУ РЕБЕНКА</a></div>
<div>{% if request.user.child_first_name and request.user.child_last_name and request.user.child_birthday %}
<div>{% if request.user.child_filled %}
<img src="{% static 'img/done2.png' %}" />{% endif %}</div>
</div>
</div>

@ -67,101 +67,41 @@
{% endfor %}
</div>
</div>
<div class="form__field field{% if form.email.errors %} error{% endif %}">
<div class="field__label">Почта</div>
<div class="field__wrap">
<input name='email' class="field__input" type="email" placeholder="Почта" value="{{ user.email }}">
</div>
{% for error in form.email.errors %}
<div class="field__error">{{ error }}</div>
{% endfor %}
</div>
<div class="form__field field{% if form.slug.errors %} error{% endif %}">
<div class="field__label">Ссылка</div>
<div class="field__wrap field__wrap__appended">
<div class="field__text">{% setting 'MAIN_HOST' %}/user/</div>
<input name='slug' id="user-slug" class="field__input field__input__appended" type="text"
value="{{ user.slug|default:'' }}" data-current-slug="{{ user.slug|default:'' }}">
</div>
{% for error in form.slug.errors %}
<div class="field__error">{{ error }}</div>
{% endfor %}
</div>
</div>
<!-- child form -->
<div class="user-child-form form__group">
<div class="form__title">Карточка ребёнка</div>
<div class="user-child-form__description">
Вся информация конфиденциальна и не передается третьим лицам. Необходима только для персонализации наград,
поздравлений с Днем Рождения и других персонализированных акций.
</div>
<div class="form__fieldset">
<div class="form__field field{% if form.child_first_name.errors %} error{% endif %}">
<div class="field__label">ИМЯ РЕБЕНКА</div>
<div class="field__wrap">
<input name='child_first_name' id="child-first-name" class="field__input" type="text" placeholder="Имя"
value="{{ user.child_first_name }}">
</div>
{% for error in form.child_first_name.errors %}
<div class="field__error">{{ error }}</div>
{% endfor %}
</div>
<div class="form__field field{% if form.child_last_name.errors %} error{% endif %}">
<div class="field__label">ФАМИЛИЯ РЕБЕНКА</div>
<div class="field__wrap">
<input name='child_last_name' id="child-last-name" class="field__input" type="text" placeholder="Фамилия"
value="{{ user.child_last_name }}">
</div>
{% for error in form.child_last_name.errors %}
<div class="field__error">{{ error }}</div>
{% endfor %}
</div>
</div>
<div class="form__fieldset">
<div class="form__field field{% if form.child_birthday.errors %} error{% endif %}">
<div class="field__label">ДАТА РОЖДЕНИЯ</div>
<div class="form__field field{% if form.email.errors %} error{% endif %}">
<div class="field__label">Почта</div>
<div class="field__wrap">
<vue-datepicker input-class="field__input" name="child_birthday" language="ru" format="dd/MM/yyyy"
value="{% if user.child_birthday %}{{ user.child_birthday | date:'Y-m-d' }}{% endif %}" placeholder="dd/mm/yyyy"/>
<input name='email' class="field__input" type="email" placeholder="Почта" value="{{ user.email }}">
</div>
{% for error in form.child_birthday.errors %}
{% for error in form.email.errors %}
<div class="field__error">{{ error }}</div>
{% endfor %}
</div>
<div class="form__field field{% if form.child_gender.errors %} error{% endif %}">
<div class="field__label">ПОЛ</div>
<div class="form__field field{% if form.phone.errors %} error{% endif %}">
<div class="field__label">НОМЕР ТЕЛЕФОНА</div>
<div class="field__wrap">
<div class="field__select select js-select{% if user.child_gender and user.child_gender != 'n' %} selected{% endif %}">
<div class="select__head js-select-head">
{% if user.child_gender == 'f' %}Ж{% elif user.child_gender == 'm' %}M{% else %}М / Ж{% endif %}
</div>
<div class="select__drop js-select-drop" data-gender-select data-input="#child-gender">
<div class="select__option js-select-option" data-gender-option data-gender="m">
<div class="select__title">М</div>
</div>
<div class="select__option js-select-option" data-gender-option data-gender="f">
<div class="select__title">Ж</div>
</div>
</div>
<input id="child-gender" name='child_gender' class="select__input" type="hidden">
</div>
<input name='phone' class="field__input" type="phone" placeholder="+7 (999) 999-99-99" value="{{ user.phone|default:'' }}">
</div>
{% for error in form.child_gender.errors %}
{% for error in form.phone.errors %}
<div class="field__error">{{ error }}</div>
{% endfor %}
</div>
</div>
<div class="form__field field{% if form.phone.errors %} error{% endif %}">
<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|default:'' }}">
<div class="form__field field{% if form.slug.errors %} error{% endif %}">
<div class="field__label">Ссылка</div>
<div class="field__wrap field__wrap__appended">
<div class="field__text">{% setting 'MAIN_HOST' %}/user/</div>
<input name='slug' id="user-slug" class="field__input field__input__appended" type="text"
value="{{ user.slug|default:'' }}" data-current-slug="{{ user.slug|default:'' }}">
</div>
{% for error in form.phone.errors %}
{% for error in form.slug.errors %}
<div class="field__error">{{ error }}</div>
{% endfor %}
</div>
</div>
<!-- end child form -->
<childs :childs="$root.store.data.childs"></childs>
<div class="form__group">
<div class="form__fieldset">
<div class="form__field field{% if form.city.errors %} error{% endif %}">
@ -393,5 +333,8 @@
{% endblock foot %}
{% block pre_app_js %}
<script>
window.LIL_STORE.data.childs = {{ childs|safe }};
</script>
<script type="text/javascript" src="{% static 'profile.js' %}"></script>
{% endblock pre_app_js %}

@ -1,6 +1,6 @@
{% extends "templates/lilcity/index.html" %} {% load static %} {% load thumbnail %} {% block content %}
{% if not user.child_first_name or not user.child_birthday %}
{% if not user.has_child %}
<div class="section">
<div class="section__center center">
<div class="fill-profile">
@ -142,7 +142,7 @@
{% if paid.exists %}
{% include "course/course_items.html" with course_items=paid %}
{% endif %}
{% if not is_school_purchased and not paid.exists and not user_gift_certificates.exists %}
{% if not is_school_purchased and not is_camp_purchased and not paid.exists and not user_gift_certificates.exists %}
<div class="center center_xs">
<div class="done">
<div class="done__title">Вы пока ничего не приобрели...</div>

@ -26,9 +26,9 @@ from apps.notification.utils import send_email
from apps.school.models import SchoolSchedule
from apps.payment.models import AuthorBalance, CoursePayment, SchoolPayment, Payment, UserGiftCertificate, UserBonus, \
DrawingCampPayment
from apps.user.models import AuthorRequest, EmailSubscription, SubscriptionCategory
from apps.user.models import AuthorRequest, EmailSubscription, SubscriptionCategory, Child
from .forms import AuthorRequesForm, UserEditForm, WithdrawalForm
from .forms import AuthorRequesForm, UserEditForm, WithdrawalForm, ChildForm
User = get_user_model()
@ -73,7 +73,7 @@ class ProfileView(TemplateView):
).distinct()
school_payment = SchoolPayment.objects.filter(
user=self.object,
date_end__gte=now(),
date_end__gte=now() - timedelta(7),
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
@ -93,7 +93,7 @@ class ProfileView(TemplateView):
camp_payment = DrawingCampPayment.objects.filter(
user=self.object,
date_end__gte=now(),
date_end__gte=now() - timedelta(7),
status__in=DrawingCampPayment.PW_PAID_STATUSES,
)
context['is_camp_purchased'] = camp_payment.exists()
@ -217,6 +217,26 @@ class ProfileEditView(UpdateView):
context['is_teacher'] = self.object.role == User.TEACHER_ROLE
return context
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
if self.object.childs.all().count():
context['childs'] = [{
'id': c.id,
'first_name': c.first_name,
'last_name': c.last_name,
'gender': c.gender or 'n',
'birthday': c.birthday.strftime('%Y-%m-%d') if c.birthday else '',
} for c in self.object.childs.all()]
else:
context['childs'] = [{
'id': '',
'first_name': '',
'last_name': '',
'gender': 'n',
'birthday': '',
}]
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
# it's magic *-*-*-*-*
if 'photo' in request.FILES:
@ -256,15 +276,47 @@ class ProfileEditView(UpdateView):
login(request, request.user)
else:
messages.error(request, 'Неверный пароль.')
messages.info(request, 'Данные сохранены.')
response = super().post(request, *args, **kwargs)
user = User.objects.get(pk=request.user.id)
# начисляемм бонусы, если заполнил профиль
if user.phone and user.first_name and user.last_name and not user.bonuses.filter(
is_service=True, action_name=UserBonus.ACTION_FILL_PROFILE).count():
UserBonus.objects.create(user=user, amount=UserBonus.AMOUNT_FILL_PROFILE, is_service=True,
action_name=UserBonus.ACTION_FILL_PROFILE)
return response
child_ids = request.POST.getlist('child_id')
child_first_names = request.POST.getlist('child_first_name')
child_last_names = request.POST.getlist('child_last_name')
child_genders = request.POST.getlist('child_gender')
child_birthdays = request.POST.getlist('child_birthday')
ziped = zip(child_ids, child_first_names, child_last_names, child_genders, child_birthdays)
childs = [dict(zip(['id', 'first_name', 'last_name', 'gender', 'birthday'], z)) for z in ziped]
childs_saved = []
child_errors = False
for child in childs:
child_instance = None
if child.get('id'):
child_instance = Child.objects.get(pk=child.get('id'))
child['user'] = request.user.id
child['gender'] = child['gender'] or 'n'
child_form = ChildForm(data=child, instance=child_instance)
if child_form.is_valid():
childs_saved.append(child_form.save().id)
elif not(len(childs) == 1 and not child.get('id') and not child.get('first_name')):
child_errors = True
child['errors'] = {f: e[0] for f, e in child_form.errors.items()}
if not child_errors:
request.user.childs.exclude(id__in=childs_saved).delete()
form = self.get_form()
if form.is_valid() and not child_errors:
messages.info(request, 'Данные сохранены.')
response = self.form_valid(form)
# начисляем бонусы, если заполнил профиль
if self.object.phone and self.object.first_name and self.object.last_name and not self.object.bonuses.filter(
is_service=True, action_name=UserBonus.ACTION_FILL_PROFILE).count():
UserBonus.objects.create(user=self.object, amount=UserBonus.AMOUNT_FILL_PROFILE, is_service=True,
action_name=UserBonus.ACTION_FILL_PROFILE)
return response
else:
context = self.get_context_data(**kwargs)
context['childs'] = childs
return self.render_to_response(context)
def get_success_url(self):
return reverse('user-edit-profile')

@ -1,4 +1,5 @@
{% load static %}
{% load ruplural from plural %}
<div class="anchor" id="about"></div>
<div class="section section_school" style="padding-top: 0;">
<div class="section__center center">
@ -20,26 +21,26 @@
</div>
<div class="school school_main">
<div class="school__col">
<div class="school__preview">> 12000</div>
<div class="school__title">учеников</div>
<div class="school__preview">> {{ config.NUMBER_OF_STUDENTS }}</div>
<div class="school__title">{{ config.NUMBER_OF_STUDENTS|ruplural:'ученик,ученика,учеников' }}</div>
<div class="school__text">прошли обучение в&nbsp;Lil&nbsp;School</div>
</div>
<div class="school__col">
<div class="school__preview">{{ works_count }}</div>
<div class="school__title">работ</div>
<div class="school__title">{{ works_count|ruplural:'работа,работы,работ' }}</div>
<div class="school__text">
создано учениками Lil&nbsp;School.<br>
Большую часть из&nbsp;них легко найти в&nbsp;<a href="{{ config.SERVICE_INSTAGRAM_URL }}">инстаграм</a>
</div>
</div>
<div class="school__col">
<div class="school__preview">32</div>
<div class="school__title">страны</div>
<div class="school__preview">{{ config.NUMBER_OF_COUNTRIES }}</div>
<div class="school__title">{{ config.NUMBER_OF_COUNTRIES|ruplural:'страна,страны,стран' }}</div>
<div class="school__text">где живут талантливые ученики Lil&nbsp;School</div>
</div>
<div class="school__col">
<div class="school__preview">107</div>
<div class="school__title">городов</div>
<div class="school__preview">{{ config.NUMBER_OF_CITIES }}</div>
<div class="school__title">{{ config.NUMBER_OF_CITIES|ruplural:'город,города,городов' }}</div>
<div class="school__text">со&nbsp;всего мира со&nbsp;счастливыми учениками Lil&nbsp;School</div>
</div>
</div>

@ -1,10 +1,10 @@
<div class="section section_video">
<div class="section__center center center_sm" style="text-align: center;">
<div class="title">Пробный урок</div>
<img class="main-video-preview js-video-modal" data-video-url="{{ config.MAIN_PAGE_VIDEO_URL }}" data-trial-lesson="1"
<img class="main-video-preview js-video-modal" data-video-url="{{ config.MAIN_PAGE_VIDEO_URL|safe }}" data-trial-lesson="1"
src="{{ config.MAIN_PAGE_VIDEO_PREVIEW_IMG.url }}"/>
<a href="#" class="btn js-video-modal btn_stroke-black" style="margin: 20px;"
data-video-url="{{ config.MAIN_PAGE_VIDEO_URL }}" data-trial-lesson="1">Смотреть бесплатно</a>
<div>Много развивающих видео на&nbsp;нашем <a href="{{ config.SERVICE_YOUTUBE_URL }}">YouTube&nbsp;канале</a></div>
data-video-url="{{ config.MAIN_PAGE_VIDEO_URL|safe }}" data-trial-lesson="1">Смотреть бесплатно</a>
<div>Много развивающих видео на&nbsp;нашем <a href="{{ config.SERVICE_YOUTUBE_URL|safe }}">YouTube&nbsp;канале</a></div>
</div>
</div>

@ -59,8 +59,7 @@ class AutoAddingUserBonusTestCase(TestCase):
user = UserFactory.create(role=User.USER_ROLE, first_name='', last_name='', phone='')
self.assertEqual(user.bonus, 0, 'При создании есть бонусы')
self.client.force_login(user)
data = {k: user.__dict__[k] for k in ['email', 'slug', 'child_first_name',
'child_last_name', 'child_birthday', 'child_gender', 'city',
data = {k: user.__dict__[k] for k in ['email', 'slug', 'city',
'country', 'birthday', 'gender', 'about', 'instagram', 'facebook',
'twitter', 'pinterest', 'youtube', 'vkontakte', 'site']}

@ -0,0 +1,120 @@
<template>
<div class="user-child-form form__group">
<div class="form__title">Карточка ребёнка</div>
<div class="user-child-form__description">
Вся информация конфиденциальна и не передается третьим лицам. Необходима только для персонализации наград,
поздравлений с Днем Рождения и других персонализированных акций.
</div>
<div class="tabs">
<div class="user-child-form__tabs tabs__nav">
<a class="user-child-form__tab tabs__btn" v-for="(child, index) in childs" :key="index" href="#"
v-bind:class="{active: selectedIndex == index}" @click.prevent="select(index)">
{{ child.first_name ? (child.first_name + '&nbsp;' + child.last_name) : ('Ребенок ' + (index || '')) }}</a>
<a href="#" class="tabs__btn" @click.prevent="add()">Добавить ребенка</a>
</div>
</div>
<div class="tabs__container">
<div v-for="(child, index) in childs" :key="index" class="tabs__item js-tabs-item"
v-bind:style="{display: selectedIndex == index ? 'block' : ''}">
<div class="form__fieldset">
<input type="hidden" name="child_id" :value="child.id" />
<div class="form__field field" v-bind:class="{error: child.errors && child.errors.gender}">
<div class="field__label">ИМЯ РЕБЕНКА</div>
<div class="field__wrap">
<input name='child_first_name' class="field__input" type="text" placeholder="Имя"
v-model="child.first_name">
</div>
<div v-if="child.errors && child.errors.first_name" class="field__error">{{ child.errors.first_name }}</div>
</div>
<div class="form__field field" v-bind:class="{error: child.errors && child.errors.last_name}">
<div class="field__label">ФАМИЛИЯ РЕБЕНКА</div>
<div class="field__wrap">
<input name='child_last_name' id="child-last-name" class="field__input" type="text" placeholder="Фамилия"
v-model="child.last_name">
</div>
<div v-if="child.errors && child.errors.last_name" class="field__error">{{ child.errors.last_name }}</div>
</div>
</div>
<div class="form__fieldset">
<div class="form__field field" v-bind:class="{error: child.errors && child.errors.birthday}">
<div class="field__label">ДАТА РОЖДЕНИЯ</div>
<div class="field__wrap">
<vue-datepicker input-class="field__input" name="child_birthday" language="ru" format="dd/MM/yyyy"
v-model="child.birthday" placeholder="dd/mm/yyyy"/>
</div>
<div v-if="child.errors && child.errors.birthday" class="field__error">{{ child.errors.birthday }}</div>
</div>
<div class="form__field field" v-bind:class="{error: child.errors && child.errors.gender}">
<div class="field__label">ПОЛ</div>
<div class="field__wrap">
<lil-select :value.sync="child.gender" :options="genders" value-key="0" title-key="1"/>
<input name='child_gender' type="hidden" v-model="child.gender">
</div>
<div v-if="child.errors && child.errors.gender" class="field__error">{{ child.errors.gender }}</div>
</div>
</div>
<a href="#" @click.prevent="remove(index)">Удалить</a>
</div>
</div>
</div>
</template>
<script>
import DatePicker from 'vuejs-datepicker';
import LilSelect from './inputs/LilSelect';
export default {
name: "childs",
props: ['childs'],
data() {
return {
selectedIndex: 0,
selectedChild: null,
genders: [
['n', 'М / Ж'],
['f', 'Ж'],
['m', 'М'],
]
}
},
methods: {
add(){
this.childs.push({
first_name: '',
last_name: '',
birthday: '',
gender: 'n',
errors: {},
});
this.select(this.childs.length - 1);
},
remove(index){
if(! confirm('Вы действительно хотите удалить данные о ребенке?')){
return;
}
this.childs.splice(index, 1);
if(! this.childs.length){
this.add();
}
else if(this.selectedIndex == index){
this.select(0);
}
},
select(index){
this.selectedIndex = index;
this.selectedChild = this.childs[index];
}
},
mounted(){
for(let i in this.childs){
if(this.childs[i].errors){
this.select(i);
}
}
},
components: {
'vue-datepicker': DatePicker,
LilSelect,
}
}
</script>

@ -72,11 +72,11 @@
},
onRemoveImage(index) {
let images = this.images;
let id = images[index].image_id;
const id = images[index].image_id;
api.removeImage(id, this.accessToken)
.then(response => {
images.splice(index, 1);
images = images.filter(image => image.image_id != id);
this.$emit('update:images', images);
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

@ -19,7 +19,6 @@ import "./modules/mixpanel";
import "../sass/app.sass";
import $ from 'jquery';
import Vue from 'vue';
import Vuelidate from 'vuelidate';
import VueAutosize from '../components/directives/autosize'

@ -75,6 +75,13 @@ export const api = {
}
});
},
removeGalleryImage: (galleryImageId, accessToken) => {
return api.delete(`/api/v1/gallery-images/${galleryImageId}/`, {
headers: {
'Authorization': `Token ${accessToken}`,
}
});
},
loadCourse: (courseId, accessToken) => {
return api.get(`/api/v1/courses/${courseId}/`, {
headers: {

@ -0,0 +1,62 @@
import $ from 'jquery';
import {loadScript} from '../utils';
function getVideoPopup($videoEl){
const $container = $videoEl.parent();
let $popup = $container.children('.video-ended-popup');
if(! $popup[0]){
$popup = $('<div class="video-ended-popup">' +
'<div class="loading-loader"></div>' +
'<div class="video-ended-popup__like-btn"></div></div>');
$container.append($popup);
}
return $popup;
}
$(document).ready(function () {
$('.js-video').each(function (){
const $iframe = $(this);
if($iframe.data('isYoutube')){
}
if($iframe.data('isVimeo')){
loadScript('https://player.vimeo.com/api/player.js').then(() => {
const player = new Vimeo.Player(this);
player.on('pause', function(data) {
if(data.percent == 1){
const $popup = getVideoPopup($iframe);
$popup.show().animate({opacity: 1}, 200);
if (document.fullscreenElement){
document.exitFullscreen();
}
const courseId = $iframe.data('courseId');
if(courseId && window.LIL_STORE.user.id && !window.LIL_STORE.data.courseLiked){
$popup.addClass('video-ended-popup_loading');
$.get('/api/v1/likes/course-liked/', {
course_id: courseId,
user_id: window.LIL_STORE.user.id
}).then(response => {
if(! response.is_liked){
$popup.addClass('video-ended-popup_like');
$popup.find('.video-ended-popup__like-btn').click(() => {
$.post(`/course/${courseId}/like`).then(response => {
$popup.removeClass('video-ended-popup_like');
window.LIL_STORE.data.courseLiked = response.success;
});
});
}
$popup.removeClass('video-ended-popup_loading');
});
}
}
});
player.on('play', function() {
const $p = getVideoPopup($iframe);
if($p.is(':visible')){
$p.animate({opacity: 0}, 800).then(() => {$p.hide().attr('class', 'video-ended-popup');});
}
});
});
}
});
});

@ -91,9 +91,11 @@ $(document).ready(function () {
if (data.is_liked) {
likedCourseElement.addClass('active');
likedCourseElement.attr('data-liked', '1');
window.LIL_STORE.data.courseLiked = true;
} else {
likedCourseElement.removeClass('active');
likedCourseElement.attr('data-liked', '0');
window.LIL_STORE.data.courseLiked = false;
}
}
})

@ -81,7 +81,7 @@ $(document).ready(function () {
channel = 'youtube';
}
if(url.indexOf('youtube.com') > -1 && url.indexOf('watch') > -1){
const m = /[?&]v=([a-zA-Z]+)(&.)?/.exec(url);
const m = /[?&]v=([^&]+)(&.+)?$/.exec(url);
channel = 'youtube';
videoId = m && m[1];
}

@ -0,0 +1 @@
import "../modules/content";

@ -0,0 +1 @@
import "../modules/content";

@ -2,6 +2,10 @@ import $ from 'jquery';
import slugify from 'slugify';
import ClipboardJS from 'clipboard';
import {showNotification} from '../modules/notification';
import Childs from '../../components/Childs.vue';
window.LIL_STORE.components['childs'] = Childs;
$(document).ready(function () {
if(window.LIL_STORE.urlIs('userBonuses')){
@ -25,7 +29,7 @@ $(document).ready(function () {
$(`[data-gender=${currentGender}]`).addClass('active');
$input.val(currentGender)
});
})
});
$('#user-photo-upload').change(file => {

@ -1,14 +1,21 @@
export const rupluralize = (value, args, addValue=true) => {
let digit = Math.trunc(value) + '';
digit = digit[digit.length - 1];
return (addValue ? value + ' ' : '') +
args[(+value > 10 && +value < 20)
? 2
: (digit == '1' ? 0 : ('234'.search(digit) > -1 ? 1 : 2))];
}
export const rupluralize = (value, args, addValue = true) => {
let digit = Math.trunc(value) + '';
digit = digit[digit.length - 1];
return (addValue ? value + ' ' : '') +
args[(+value > 10 && +value < 20)
? 2
: (digit == '1' ? 0 : ('234'.search(digit) > -1 ? 1 : 2))];
};
export const loadScript = (url, onload) => {
const tag = document.createElement('script');
tag.url = url;
document.getElementsByTagName('body');
}
return new Promise(resolve => {
const script = document.createElement('script');
script.async = true;
document.body.appendChild(script);
script.onload = function () {
onload && onload.apply(this, arguments);
resolve.apply(this, arguments);
};
script.src = url;
});
};

@ -4130,11 +4130,6 @@ a.grey-link
letter-spacing: 1px
+m
margin-right: 0
&__item a.timing__btn
opacity: 0;
transition: 0.5s ease;
&__item:hover a.timing__btn
opacity: 1;
&__pic
display: block
width: 100%
@ -4358,11 +4353,13 @@ a
&.pic
position: relative
width: 100%
&__video
position: relative
&__video iframe
height: 360px;
width: 640px;
max-width: 100%;
max-height: 60%;
height: 360px
width: 640px
max-width: 100%
max-height: 60%
@media only screen and (max-width: 639px)
.content-block__video iframe
@ -4433,6 +4430,8 @@ a
margin: 0 auto
display: flex
padding: 0 40px
+t
width: 100%
+m
width: 100%
padding: 0 10px
@ -4706,12 +4705,24 @@ a
border-radius: 20px
box-shadow: 0 10px 24px 0 rgba(0, 0, 0, 0.05)
+t
margin: 30px -80px 30px -60px
+m
background: white
margin: 30px -80px 30px -60px
padding: 30px 80px 40px 60px
&__description
margin-bottom: 40px
margin-top: -20px
color: #333333
font-size: 12px
&__tabs
justify-content: left
overflow-x: auto
overflow-y: hidden
.bonuses-table
margin-left: -120px
@ -4771,3 +4782,64 @@ a
&__text
margin: 40px 0
.video-ended-popup
display: none
position: absolute
top: 90px
height: 200px
background: url(../img/video-ended-popup-ref.jpg) no-repeat center white
left: 50%
width: 600px
margin-left: -300px
border-radius: 5px
padding: 30px
box-shadow: 0 2px 20px rgba(0,0,0,0.10)
opacity: 0
+m
width: 100%
left: 0
top: 0
height: 153px
background-position: center bottom
background-size: auto 130px
border-radius: 0
background-color: #151a1e
margin-left: 0
& .loading-loader
display: none
&_like
background-image: url(../img/video-ended-popup-like.jpg)
&_black
background: url(../img/video-ended-popup-logo.png) no-repeat center #141a1d
&_loading
background: #141a1d
&_loading .loading-loader
display: block
&__like-btn
background: url(../img/heart.png) no-repeat center
position: absolute
top: 90px
display: none
width: 91px
height: 52px
cursor: pointer
left: 50%
margin-left: -45px
+m
background-size: contain
width: 54px
margin-left: -27px
bottom: 27px
top: auto
&_like &__like-btn
display: block

@ -9,10 +9,12 @@ const SpriteLoaderPlugin = require('svg-sprite-loader/plugin');
module.exports = {
entry: {
app: "./src/js/app.js",
course: "./src/js/pages/course.js",
courseEdit: "./src/js/pages/course-edit.js",
contest: "./src/js/pages/contest.js",
contestEdit: "./src/js/pages/contest-edit.js",
profile: "./src/js/pages/profile.js",
liveLesson: "./src/js/pages/live-lesson.js",
userGalleryEdit: "./src/js/pages/user-gallery-edit.js",
mixpanel: "./src/js/third_party/mixpanel-2-latest.js",
sprite: glob('./src/icons/*.svg'),

Loading…
Cancel
Save