Merge branch 'features/course-search-16-08-19' into 'master'

Features/course search 16 08 19

See merge request lilschool/site!376
remotes/origin/hotfix/bonuses-text
Danil 7 years ago
commit 393742c521
  1. 9
      api/v1/serializers/content.py
  2. 19
      api/v1/serializers/course.py
  3. 29
      api/v1/serializers/mixins.py
  4. 4
      api/v1/urls.py
  5. 39
      api/v1/views.py
  6. 7
      apps/content/admin.py
  7. 38
      apps/content/migrations/0029_auto_20190730_2032.py
  8. 18
      apps/content/migrations/0030_auto_20190809_0133.py
  9. 27
      apps/content/models.py
  10. 12
      apps/course/admin.py
  11. 38
      apps/course/migrations/0050_auto_20190818_1043.py
  12. 24
      apps/course/models.py
  13. 13
      apps/course/templates/course/_items.html
  14. 14
      apps/course/templates/course/course.html
  15. 29
      apps/course/templates/course/course_items.html
  16. 40
      apps/course/templates/course/courses.html
  17. 27
      apps/course/views.py
  18. 13
      apps/notification/templates/notification/email/buy_email.html
  19. 25
      apps/payment/migrations/0038_auto_20190814_1506.py
  20. 87
      apps/payment/models.py
  21. 12
      apps/payment/templates/payment/package_payment_success.html
  22. 5
      apps/payment/templates/payment/payment_success.html
  23. 66
      apps/payment/views.py
  24. 19
      apps/school/views.py
  25. 3
      apps/user/admin.py
  26. 25
      project/templates/blocks/about.html
  27. 9
      project/templates/blocks/banner.html
  28. 4
      project/templates/blocks/banners.html
  29. 6
      project/templates/blocks/counters.html
  30. 28
      project/templates/blocks/footer.html
  31. 10
      project/templates/blocks/header.html
  32. 19
      project/templates/blocks/online_school.html
  33. 2
      project/templates/blocks/partners.html
  34. 12
      project/templates/blocks/reviews.html
  35. 32
      project/templates/blocks/students.html
  36. 156
      project/templates/blocks/teachers.html
  37. 31
      project/templates/blocks/video.html
  38. 4
      project/templates/lilcity/home.html
  39. 1
      project/templates/lilcity/index.html
  40. 2
      project/templates/lilcity/layer.html
  41. 144
      project/templates/lilcity/packages.html
  42. 3
      project/urls.py
  43. 4
      project/utils/__init__.py
  44. 46
      project/views.py
  45. 11
      web/package.json
  46. 103
      web/src/components/CourseRedactor.vue
  47. 17
      web/src/img/bubble-icon.svg
  48. BIN
      web/src/img/emoji-cat.png
  49. BIN
      web/src/img/emoji-holiday.png
  50. BIN
      web/src/img/emoji-present.png
  51. BIN
      web/src/img/emoji-students.png
  52. BIN
      web/src/img/emoji-winners.png
  53. BIN
      web/src/img/emoji_present.png
  54. BIN
      web/src/img/girl-umbrela.png
  55. BIN
      web/src/img/medal.png
  56. BIN
      web/src/img/zlata.png
  57. 22
      web/src/js/modules/api.js
  58. 25
      web/src/js/modules/common.js
  59. 15
      web/src/js/modules/courses.js
  60. 644
      web/src/sass/_common.sass

@ -4,7 +4,7 @@ from django.conf import settings
from apps.content.models import ( from apps.content.models import (
Banner, Content, Image, Text, ImageText, Video, Banner, Content, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject, FAQ) Gallery, GalleryImage, ImageObject, FAQ, Package)
from . import Base64ImageField from . import Base64ImageField
@ -268,3 +268,10 @@ class FAQSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = FAQ model = FAQ
fields = '__all__' fields = '__all__'
class PackageSerializer(serializers.ModelSerializer):
class Meta:
model = Package
fields = '__all__'

@ -9,16 +9,22 @@ from apps.course.models import (
Comment, CourseComment, LessonComment, Comment, CourseComment, LessonComment,
Material, Lesson, Material, Lesson,
Like, Like,
LiveLessonComment) LiveLessonComment, Tag)
from .content import ( from .content import (
ImageObjectSerializer, ContentSerializer, ContentCreateSerializer, ImageObjectSerializer, ContentSerializer, ContentCreateSerializer,
GallerySerializer, ) GallerySerializer, )
from .mixins import DispatchContentMixin, DispatchGalleryMixin, DispatchMaterialMixin from .mixins import DispatchContentMixin, DispatchGalleryMixin, DispatchMaterialMixin, DispatchTagsMixin
from .user import UserSerializer from .user import UserSerializer
User = get_user_model() User = get_user_model()
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ('__all__')
class MaterialCreateSerializer(serializers.ModelSerializer): class MaterialCreateSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -103,6 +109,7 @@ class CourseBulkChangeCategorySerializer(serializers.Serializer):
class CourseCreateSerializer(DispatchContentMixin, class CourseCreateSerializer(DispatchContentMixin,
DispatchGalleryMixin, DispatchGalleryMixin,
DispatchMaterialMixin, DispatchMaterialMixin,
DispatchTagsMixin,
serializers.ModelSerializer serializers.ModelSerializer
): ):
title = serializers.CharField(allow_blank=True) title = serializers.CharField(allow_blank=True)
@ -116,6 +123,7 @@ class CourseCreateSerializer(DispatchContentMixin,
) )
materials = MaterialSerializer(many=True, required=False) materials = MaterialSerializer(many=True, required=False)
gallery = GallerySerializer() gallery = GallerySerializer()
tags = TagSerializer(many=True, required=False)
class Meta: class Meta:
model = Course model = Course
@ -145,6 +153,7 @@ class CourseCreateSerializer(DispatchContentMixin,
'content', 'content',
'gallery', 'gallery',
'lessons', 'lessons',
'tags',
) )
read_only_fields = ( read_only_fields = (
@ -161,12 +170,14 @@ class CourseCreateSerializer(DispatchContentMixin,
materials = validated_data.pop('materials', []) materials = validated_data.pop('materials', [])
gallery = validated_data.pop('gallery', {}) gallery = validated_data.pop('gallery', {})
author = validated_data.get('author', None) author = validated_data.get('author', None)
tags = validated_data.pop('tags', [])
if not author: if not author:
validated_data['author'] = self.context['request'].user validated_data['author'] = self.context['request'].user
course = super().create(validated_data) course = super().create(validated_data)
self.dispatch_content(course, content) self.dispatch_content(course, content)
self.dispatch_materials(course, materials) self.dispatch_materials(course, materials)
self.dispatch_gallery(course, gallery) self.dispatch_gallery(course, gallery)
self.dispatch_tags(course, tags)
return course return course
def update(self, instance, validated_data): def update(self, instance, validated_data):
@ -174,12 +185,14 @@ class CourseCreateSerializer(DispatchContentMixin,
materials = validated_data.pop('materials', []) materials = validated_data.pop('materials', [])
gallery = validated_data.pop('gallery', {}) gallery = validated_data.pop('gallery', {})
author = validated_data.get('author', None) author = validated_data.get('author', None)
tags = validated_data.pop('tags', [])
if not instance.author or author and instance.author != author: if not instance.author or author and instance.author != author:
validated_data['author'] = self.context['request'].user validated_data['author'] = self.context['request'].user
course = super().update(instance, validated_data) course = super().update(instance, validated_data)
self.dispatch_materials(course, materials) self.dispatch_materials(course, materials)
self.dispatch_content(course, content) self.dispatch_content(course, content)
self.dispatch_gallery(course, gallery) self.dispatch_gallery(course, gallery)
self.dispatch_tags(course, tags)
return course return course
def to_representation(self, instance): def to_representation(self, instance):
@ -269,6 +282,7 @@ class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
gallery = GallerySerializer() gallery = GallerySerializer()
content = ContentSerializer(many=True) content = ContentSerializer(many=True)
lessons = LessonSerializer(many=True) lessons = LessonSerializer(many=True)
tags = TagSerializer(many=True, required=False)
class Meta: class Meta:
model = Course model = Course
@ -298,6 +312,7 @@ class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
'content', 'content',
'gallery', 'gallery',
'lessons', 'lessons',
'tags',
) )
read_only_fields = ( read_only_fields = (

@ -1,4 +1,4 @@
from apps.course.models import Category, Course, Material, Lesson, Like from apps.course.models import Category, Course, Material, Lesson, Like, Tag, CourseTags
from apps.school.models import LiveLesson from apps.school.models import LiveLesson
from apps.content.models import ( from apps.content.models import (
@ -163,3 +163,30 @@ class DispatchGalleryMixin(object):
) )
obj.gallery = g obj.gallery = g
obj.save() obj.save()
class DispatchTagsMixin(object):
def dispatch_tags(self, obj, tags):
current_tags = list(obj.tags.all())
new_tags = []
if tags:
for tag in tags:
id = tag.get('id')
tag_text = tag.get('tag')
if not id and tag_text:
t, created = Tag.objects.get_or_create(tag=tag_text)
id = t.id
new_tags.append(id)
if isinstance(obj, Course):
CourseTags.objects.filter(course=obj, tag__in=current_tags).delete()
for tag in Tag.objects.filter(id__in=new_tags):
CourseTags.objects.get_or_create(course=obj, tag=tag)
else:
obj.tags.clear(current_tags)
obj.tags.add(Tag.objects.filter(id__in=set(new_tags)))
for tag in current_tags:
if not tag.course_set.all().count():
tag.delete()

@ -19,7 +19,7 @@ from .views import (
SchoolScheduleViewSet, LiveLessonViewSet, SchoolScheduleViewSet, LiveLessonViewSet,
PaymentViewSet, ObjectCommentsViewSet, PaymentViewSet, ObjectCommentsViewSet,
ContestViewSet, ContestWorkViewSet, NotifiedAboutBonuses, ContestViewSet, ContestWorkViewSet, NotifiedAboutBonuses,
AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet, UserGalleryViewSet, BonusesViewSet) AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet, UserGalleryViewSet, BonusesViewSet, PackageViewSet, TagViewSet)
router = DefaultRouter() router = DefaultRouter()
router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests') router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests')
@ -48,6 +48,8 @@ router.register(r'users', UserViewSet, base_name='users')
router.register(r'user-gallery', UserGalleryViewSet, base_name='user-gallery') router.register(r'user-gallery', UserGalleryViewSet, base_name='user-gallery')
router.register(r'contests', ContestViewSet, base_name='contests') router.register(r'contests', ContestViewSet, base_name='contests')
router.register(r'contest-works', ContestWorkViewSet, base_name='contest_works') router.register(r'contest-works', ContestWorkViewSet, base_name='contest_works')
router.register(r'packages', PackageViewSet, base_name='packages')
router.register(r'tags', TagViewSet, base_name='tags')
# router.register(r'configs', ConfigViewSet, base_name='configs') # router.register(r'configs', ConfigViewSet, base_name='configs')

@ -11,6 +11,7 @@ from rest_framework.response import Response
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from django.utils.timezone import now from django.utils.timezone import now
from project.utils.db import format_sql, execute_sql
from . import ExtendedModelViewSet, BothListFormatMixin from . import ExtendedModelViewSet, BothListFormatMixin
from .serializers.config import ConfigSerializer from .serializers.config import ConfigSerializer
from .serializers.course import ( from .serializers.course import (
@ -21,7 +22,7 @@ from .serializers.course import (
MaterialSerializer, MaterialCreateSerializer, MaterialSerializer, MaterialCreateSerializer,
LessonSerializer, LessonCreateSerializer, LessonSerializer, LessonCreateSerializer,
LikeCreateSerializer, CourseCommentSerializer, LessonCommentSerializer, LikeCreateSerializer, CourseCommentSerializer, LessonCommentSerializer,
LiveLessonCommentSerializer,) LiveLessonCommentSerializer, TagSerializer)
from .serializers.content import ( from .serializers.content import (
BannerSerializer, BannerSerializer,
ImageSerializer, ImageCreateSerializer, ImageSerializer, ImageCreateSerializer,
@ -30,7 +31,7 @@ from .serializers.content import (
VideoSerializer, VideoCreateSerializer, VideoSerializer, VideoCreateSerializer,
GallerySerializer, GallerySerializer,
GalleryImageSerializer, GalleryImageCreateSerializer, GalleryImageSerializer, GalleryImageCreateSerializer,
ImageObjectSerializer, FAQSerializer, ImageObjectSerializer, FAQSerializer, PackageSerializer,
) )
from .serializers.school import ( from .serializers.school import (
SchoolScheduleSerializer, SchoolScheduleSerializer,
@ -60,12 +61,12 @@ from apps.course.models import (
Comment, CourseComment, LessonComment, Comment, CourseComment, LessonComment,
Material, Lesson, Material, Lesson,
Like, Like,
LiveLessonComment) LiveLessonComment, Tag, CourseTags)
from apps.config.models import Config from apps.config.models import Config
from apps.content.models import ( from apps.content.models import (
Banner, Image, Text, ImageText, Video, Banner, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject, Gallery, GalleryImage, ImageObject,
Contest, ContestWork, FAQ) Contest, ContestWork, FAQ, Package)
from apps.payment.models import ( from apps.payment.models import (
AuthorBalance, Payment, AuthorBalance, Payment,
CoursePayment, SchoolPayment, UserBonus, CoursePayment, SchoolPayment, UserBonus,
@ -652,14 +653,13 @@ class PaymentViewSet(viewsets.ModelViewSet):
def calc_amount(self, request, pk=None): def calc_amount(self, request, pk=None):
user = request.query_params.get('user') user = request.query_params.get('user')
course = request.query_params.get('course') course = request.query_params.get('course')
weekdays = request.query_params.getlist('weekdays[]')
date_start = request.query_params.get('date_start') date_start = request.query_params.get('date_start')
is_camp = bool(request.query_params.get('is_camp')) is_camp = bool(request.query_params.get('is_camp'))
user = user and User.objects.get(pk=user) user = user and User.objects.get(pk=user)
course = course and Course.objects.get(pk=course) course = course and Course.objects.get(pk=course)
date_start = date_start and datetime.strptime(date_start, '%Y-%m-%d') date_start = date_start and datetime.strptime(date_start, '%Y-%m-%d')
return Response(Payment.calc_amount(user=user, course=course, date_start=date_start, weekdays=weekdays, return Response(Payment.calc_amount(user=user, course=course, date_start=date_start,
is_camp=is_camp)) is_camp=is_camp))
@ -774,3 +774,30 @@ class NotifiedAboutBonuses(views.APIView):
b.save() b.save()
return Response({'status': 'ok'}) return Response({'status': 'ok'})
class PackageViewSet(ExtendedModelViewSet):
queryset = Package.objects.all()
serializer_class = PackageSerializer
permission_classes = (IsAdmin,)
class TagViewSet(ExtendedModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
search_fields = ('tag',)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(methods=['get'], detail=False, url_path='popular', authentication_classes=[], permission_classes=[])
def popular(self, *args, **kwargs):
sql = format_sql('''
select {ct.tag_id}
from {ct}
group by {ct.tag_id}
order by count(*) desc
limit 15''', ct=CourseTags)
tags = [t[0] for t in execute_sql(sql)]
tags = Tag.objects.filter(id__in=tags).order_by('tag')
serializer = self.get_serializer(tags, many=True)
return Response(serializer.data)

@ -8,7 +8,7 @@ from polymorphic.admin import (
from apps.content.models import ( from apps.content.models import (
Banner, Content, Image, Text, ImageText, Video, Banner, Content, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject, Gallery, GalleryImage, ImageObject,
Contest, ContestWork, FAQ, Contest, ContestWork, FAQ, Package,
) )
@ -99,3 +99,8 @@ class ContestWorkAdmin(admin.ModelAdmin):
@admin.register(FAQ) @admin.register(FAQ)
class FAQAdmin(admin.ModelAdmin): class FAQAdmin(admin.ModelAdmin):
base_model = FAQ base_model = FAQ
@admin.register(Package)
class PackageAdmin(admin.ModelAdmin):
base_model = Package

@ -0,0 +1,38 @@
# Generated by Django 2.0.7 on 2019-07-30 20:32
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content', '0028_auto_20190726_0106'),
]
operations = [
migrations.CreateModel(
name='Package',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('high_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('description', models.TextField(db_index=True, default='', verbose_name='Описание')),
('duration', models.PositiveSmallIntegerField()),
('options', models.TextField(db_index=True, default='', verbose_name='Описание')),
],
options={
'ordering': ('duration',),
},
),
migrations.AlterField(
model_name='banner',
name='main_banner',
field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(choices=[(1, 'Главная'), (2, 'Курсы'), (3, 'Школа'), (4, 'Пакеты')]), blank=True, default=[], size=None),
),
migrations.AlterField(
model_name='banner',
name='pages',
field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(choices=[(1, 'Главная'), (2, 'Курсы'), (3, 'Школа'), (4, 'Пакеты')]), blank=True, default=[], size=None),
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2019-08-09 01:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content', '0029_auto_20190730_2032'),
]
operations = [
migrations.AlterField(
model_name='package',
name='options',
field=models.TextField(db_index=True, default='', verbose_name='Опции'),
),
]

@ -146,11 +146,13 @@ class Banner(models.Model):
PAGE_INDEX = 1 PAGE_INDEX = 1
PAGE_COURSES = 2 PAGE_COURSES = 2
PAGE_SCHOOL = 3 PAGE_SCHOOL = 3
PAGE_PACKAGES = 4
PAGE_CHOICES = ( PAGE_CHOICES = (
(PAGE_INDEX, 'Главная'), (PAGE_INDEX, 'Главная'),
(PAGE_COURSES, 'Курсы'), (PAGE_COURSES, 'Курсы'),
(PAGE_SCHOOL, 'Школа'), (PAGE_SCHOOL, 'Школа'),
(PAGE_PACKAGES, 'Пакеты')
) )
text = models.TextField(blank=True, default='') text = models.TextField(blank=True, default='')
@ -178,6 +180,12 @@ class Banner(models.Model):
is_main=RawSQL('main_banner @> %s', ([page],)) is_main=RawSQL('main_banner @> %s', ([page],))
).order_by('-is_main') ).order_by('-is_main')
@property
def is_video_url(self):
return self.url and ('vimeo.com' in self.url
or 'youtube.com' in self.url and 'watch' in self.url
or 'youto.be' in self.url)
class Contest(models.Model): class Contest(models.Model):
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
@ -241,3 +249,22 @@ class ContestWork(models.Model):
class FAQ(models.Model): class FAQ(models.Model):
question = models.TextField(max_length=1000,) question = models.TextField(max_length=1000,)
answer = models.TextField(max_length=1000,) answer = models.TextField(max_length=1000,)
class Package(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2,)
high_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
description = models.TextField(
'Описание', default='', db_index=True
)
duration = models.PositiveSmallIntegerField()
options = models.TextField(
'Опции', default='', db_index=True
)
class Meta:
ordering = ('duration',)
@property
def options_html(self):
return ''.join(map(lambda x: '<p>%s</p>' % x, self.options.split('\n')))

@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from mptt.admin import MPTTModelAdmin, DraggableMPTTAdmin from mptt.admin import MPTTModelAdmin, DraggableMPTTAdmin
from .models import Course, Category, Lesson, Material, CourseComment, LessonComment from .models import Course, Category, Lesson, Material, CourseComment, LessonComment, Tag, CourseTags
@admin.register(Course) @admin.register(Course)
@ -47,3 +47,13 @@ class CourseCommentAdmin(DraggableMPTTAdmin):
@admin.register(LessonComment) @admin.register(LessonComment)
class LessonCommentAdmin(DraggableMPTTAdmin): class LessonCommentAdmin(DraggableMPTTAdmin):
pass pass
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
pass
@admin.register(CourseTags)
class CourseTagAdmin(admin.ModelAdmin):
pass

@ -0,0 +1,38 @@
# Generated by Django 2.0.7 on 2019-08-18 10:43
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('course', '0049_auto_20190207_1551'),
]
operations = [
migrations.CreateModel(
name='CourseTags',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.Course')),
],
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tag', models.CharField(max_length=20)),
],
),
migrations.AddField(
model_name='coursetags',
name='tag',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.Tag'),
),
migrations.AddField(
model_name='course',
name='tags',
field=models.ManyToManyField(blank=True, through='course.CourseTags', to='course.Tag'),
),
]

@ -21,6 +21,17 @@ from apps.content.models import ImageObject, Gallery, Video, ContestWork
User = get_user_model() User = get_user_model()
def default_slug():
return str(uuid4())
def deferred_start_at_validator(value):
if value < now():
raise ValidationError(
'Дата и время начала курса не может быть меньше текущих.',
)
class Like(models.Model): class Like(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
ip = models.GenericIPAddressField(blank=True, null=True) ip = models.GenericIPAddressField(blank=True, null=True)
@ -29,15 +40,13 @@ class Like(models.Model):
update_at = models.DateTimeField(auto_now=True) update_at = models.DateTimeField(auto_now=True)
def default_slug(): class Tag(models.Model):
return str(uuid4()) tag = models.CharField(max_length=20,)
def deferred_start_at_validator(value): class CourseTags(models.Model):
if value < now(): tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
raise ValidationError( course = models.ForeignKey('Course', on_delete=models.CASCADE)
'Дата и время начала курса не может быть меньше текущих.',
)
class Course(BaseModel, DeactivatedMixin): class Course(BaseModel, DeactivatedMixin):
@ -110,6 +119,7 @@ class Course(BaseModel, DeactivatedMixin):
on_delete=models.CASCADE, null=True, blank=True, on_delete=models.CASCADE, null=True, blank=True,
related_name='results_gallery', related_name='results_gallery',
) )
tags = models.ManyToManyField('Tag', through=CourseTags, blank=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True) update_at = models.DateTimeField(auto_now=True)

@ -8,12 +8,11 @@
{% if course.is_deferred_start and course.status == 2 %}data-future-course data-future-course-time={{ course.deferred_start_at.timestamp }}{% endif %} {% if course.is_deferred_start and course.status == 2 %}data-future-course data-future-course-time={{ course.deferred_start_at.timestamp }}{% endif %}
> >
<a class="courses__preview" href="{% if course.status <= 1 %}{% url 'course_edit' course.id %}{% else %}{{ course.url }}{% endif %}"> <a class="courses__preview" href="{% if course.status <= 1 %}{% url 'course_edit' course.id %}{% else %}{{ course.url }}{% endif %}">
{% thumbnail course.cover.image "300x200" crop="center" as im %} {% if course.cover %}
<img class="courses__pic" src="{{ im.url }}" width="{{ im.width }}" /> <img class="courses__pic" src="{{ course.cover.image_thumbnail.url }}" width="{{ course.cover.image_thumbnail.width }}" />
{% empty %} {% else %}
<img class="courses__pic" src="{% static 'img/no_cover.png' %}" width="300px" /> <img class="courses__pic" src="{% static 'img/no_cover.png' %}" width="300px" />
{% endthumbnail %} {% endif %}
<div class="courses__view">Подробнее</div>
{% if course.is_featured %} {% if course.is_featured %}
<div class="courses__label courses__label_fav"></div> <div class="courses__label courses__label_fav"></div>
{% endif %} {% endif %}
@ -51,11 +50,11 @@
href="{% url 'courses' %}?category={{ course.category.id }}">{{ course.category | upper }}</a> href="{% url 'courses' %}?category={{ course.category.id }}">{{ course.category | upper }}</a>
{% if not course.is_free %} {% if not course.is_free %}
{% if course.buy_again_price %} {% if course.buy_again_price %}
<div class="courses__old-price"><s>{{ course.price|floatformat:"-2" }}₽</s></div> <div class="courses__old-price mobile-hide"><s>{{ course.price|floatformat:"-2" }}₽</s></div>
<div class="courses__price" style="color: red;">{{ course.buy_again_price|floatformat:"-2" }}₽</div> <div class="courses__price" style="color: red;">{{ course.buy_again_price|floatformat:"-2" }}₽</div>
{% else %} {% else %}
{% if course.old_price %} {% if course.old_price %}
<div class="courses__old-price"><s>{{ course.old_price|floatformat:"-2" }}₽</s></div> <div class="courses__old-price mobile-hide"><s>{{ course.old_price|floatformat:"-2" }}₽</s></div>
{% endif %} {% endif %}
<div class="courses__price" <div class="courses__price"
{% if course.old_price %}style="color: red;"{% endif %}>{{ course.price|floatformat:"-2" }}₽</div> {% if course.old_price %}style="color: red;"{% endif %}>{{ course.price|floatformat:"-2" }}₽</div>

@ -23,7 +23,7 @@
{% block ogdescription %}{{ course.short_description | striptags }}{% endblock ogdescription %} {% block ogdescription %}{{ course.short_description | striptags }}{% endblock ogdescription %}
{% block content %} {% block content %}
<div class="section section_border course"> <div class="section section_course section_border course">
<div class="section__center center center_sm"> <div class="section__center center center_sm">
<div class="go"> <div class="go">
<a class="go__item" href="{% if next %}{{next}}{% else %}{% url 'courses' %}{% endif %}"> <a class="go__item" href="{% if next %}{{next}}{% else %}{% url 'courses' %}{% endif %}">
@ -39,9 +39,9 @@
{% endif %} {% endif %}
{% if not is_owner and course.price %} {% if not is_owner and course.price %}
{% if not paid or can_buy_again %} {% if not paid or can_buy_again %}
<div> <div class="section__course-buttons">
<a href="#" <a href="#"
class="btn{% if pending %} btn_gray{% endif %} btn_md" class="btn{% if pending %} btn_gray{% endif %}"
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if not pending %} {% if not pending %}
data-course-buy data-course-buy
@ -54,7 +54,7 @@
{% if paid and can_buy_again %}ПРОДЛИТЬ ДОСТУП{% else %}КУПИТЬ КУРС{% endif %} {% if paid and can_buy_again %}ПРОДЛИТЬ ДОСТУП{% else %}КУПИТЬ КУРС{% endif %}
{% endif %}</a> {% endif %}</a>
{% if not paid %} {% if not paid %}
<a class="main__btn btn btn_stroke-black" href="{% url 'gift-certificates' %}">Подарить другу</a> <a class="btn btn_stroke-black" href="{% url 'gift-certificates' %}">Подарить другу</a>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
@ -153,6 +153,7 @@
</div> </div>
<div class="meta__title">{{ course.lessons.count | rupluralize:"урок,урока,уроков" }}</div> <div class="meta__title">{{ course.lessons.count | rupluralize:"урок,урока,уроков" }}</div>
</div> </div>
{% if not request.user_agent.is_mobile %}
<div class="meta__item"> <div class="meta__item">
<div class="meta__icon"> <div class="meta__icon">
<svg class="icon icon-video"> <svg class="icon icon-video">
@ -161,6 +162,7 @@
</div> </div>
<div class="meta__title">{{ course.count_videos_in_lessons }} видео</div> <div class="meta__title">{{ course.count_videos_in_lessons }} видео</div>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
<div class="course__actions"> <div class="course__actions">
@ -356,9 +358,9 @@
</div> </div>
{% if not is_owner and course.price %} {% if not is_owner and course.price %}
{% if not paid or can_buy_again %} {% if not paid or can_buy_again %}
<div> <div {% if request.user_agent.is_mobile %}style="margin-top: 20px;"{% endif %}>
<a href="#" <a href="#"
class="btn{% if pending %} btn_gray{% endif %} btn_md" class="btn{% if pending %} btn_gray{% endif %}"
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if not pending %} {% if not pending %}
data-course-buy data-course-buy

@ -1,4 +1,29 @@
{% for course in course_items %} {% for course in course_items %} {% load static %}
{% cycle '' 'theme_green' 'theme_violet' as theme_color silent %} {% cycle '' 'theme_green' 'theme_violet' as theme_color silent %}
{% if forloop.counter == 6 and search_query and new_courses.count >= 4 %}
<div class="courses__item new-courses">
<div class="new-courses__block-title">Новые курсы</div>
<div>
{% for new_course in new_courses %}
<div class="new-courses__item">
<a class="new-courses__image" href="{{ course.url }}">
{% if new_course.cover %}
<img src="{{ new_course.cover.image_thumbnail.url }}" width="80px" />
{% else %}
<img src="{% static 'img/no_cover.png' %}" width="80px" />
{% endif %}
</a>
<div class="new-courses__details">
<a class="new-courses__title" href="{{ course.url }}">
{{ new_course.title }}
</a>
<a class="new-courses__author"
href="{{ new_course.author.url }}">{{ new_course.author.get_full_name }}</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% include "course/_items.html" %} {% include "course/_items.html" %}
{% endfor %} {% endfor %}

@ -12,17 +12,41 @@
</div> </div>
{% endif %} {% endif %}
<div class="section"> <div class="section">
<div class="section__center center course-search">
<form class="course-search__form" action="." method="get">
{% if cat %}<input type="hidden" name="cat" value="{{ cat.0|default:'' }}" />
{% else %}
<input type="hidden" name="category" value="{% if category %}{{ category.id }}{% endif %}" />
{% endif %}
<input type="hidden" name="age" value="{{ age.0|default:'' }}" />
<input name="q" class="course-search__query" type="text" value="{{ search_query|default:'' }}" />
<button class="btn course-search__search" type="submit">
<svg class="icon icon-search">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-search"></use>
</svg>
</button>
</form>
{% if tags|length %}
<div class="course-search__text">
ИСКАТЬ ПО ТЕГАМ
</div>
<div class="course-search__tags">
{% for tag in tags %}
<a href="{% url 'courses' %}?cat={{ cat.0|default:'' }}&age={{ age.0|default:'' }}&q=%23{{ tag.tag }}"
class="course-search__tag tag">#{{ tag.tag }}</a>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="section" style="padding-top: 5px">
<div class="section__center center"> <div class="section__center center">
<div class="text text_lg">
<p>Учите и развивайте креативное мышление когда и где угодно. Если вам не совсем удобно заниматься с нами в прямом эфире каждый день, как в
нашей онлайн-школе, специально для вас мы делаем отдельные уроки в записи, которые вы можете проходить,
когда вам будет удобно.</p><img class="text__curve text__curve_four" src="{% static 'img/curve-4.svg' %}" width="155"></div>
<div class="head"> <div class="head">
<div class="head__title title title_center">Курсы</div> <div class="head__title title">Курсы</div>
<div class="head__right"> <div class="head__right courses-filter">
<div class="head__field field"> <div class="head__field field">
<div class="field__wrap"> <div class="field__wrap">
<div class="field__select select js-select{% if category %} selected{% endif %}" data-category-select> <div class="courses-filter__select field__select select js-select{% if category %} selected{% endif %}" data-category-select>
<div class="select__head js-select-head">Категории</div> <div class="select__head js-select-head">Категории</div>
<div class="select__drop js-select-drop"> <div class="select__drop js-select-drop">
<div class="select__option js-select-option{% if not category.0 %} active{% endif %}" data-category-option> <div class="select__option js-select-option{% if not category.0 %} active{% endif %}" data-category-option>
@ -35,7 +59,7 @@
</div> </div>
<div class="head__field field"> <div class="head__field field">
<div class="field__wrap"> <div class="field__wrap">
<div class="field__select select js-select{% if age.0 %} selected{% endif %}"> <div class="courses-filter__select field__select select js-select{% if age.0 %} selected{% endif %}">
<div class="select__head js-select-head">{% if age_name %}{{ age_name }}{% else %}Возраст{% endif %}</div> <div class="select__head js-select-head">{% if age_name %}{{ age_name }}{% else %}Возраст{% endif %}</div>
<div class="select__drop js-select-drop"> <div class="select__drop js-select-drop">
<div class="select__option js-select-option{% if not age.0 %} active{% endif %}" data-age-option> <div class="select__option js-select-option{% if not age.0 %} active{% endif %}" data-age-option>

@ -1,4 +1,5 @@
from datetime import timedelta from datetime import timedelta
from itertools import groupby
from paymentwall import Pingback from paymentwall import Pingback
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -17,8 +18,9 @@ from django.utils.timezone import now
from apps.content.models import Banner from apps.content.models import Banner
from apps.payment.models import AuthorBalance, CoursePayment from apps.payment.models import AuthorBalance, CoursePayment
from .models import Course, Like, Lesson, CourseComment, LessonComment, Category from .models import Course, Like, Lesson, CourseComment, LessonComment, Category, CourseTags, Tag
from .filters import CourseFilter from .filters import CourseFilter
from project.utils.db import ModelFieldsNames, format_sql, execute_sql
User = get_user_model() User = get_user_model()
@ -228,6 +230,7 @@ class CourseView(DetailView):
except queryset.model.DoesNotExist: except queryset.model.DoesNotExist:
raise Http404(_("No %(verbose_name)s found matching the query") % raise Http404(_("No %(verbose_name)s found matching the query") %
{'verbose_name': queryset.model._meta.verbose_name}) {'verbose_name': queryset.model._meta.verbose_name})
obj.cover = None
return obj return obj
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -312,6 +315,14 @@ class CoursesView(ListView):
).prefetch_related( ).prefetch_related(
'likes', 'materials', 'content', 'likes', 'materials', 'content',
).filter(status=Course.PUBLISHED) ).filter(status=Course.PUBLISHED)
q = self.request.GET.get('q')
if q:
if q.startswith('#'):
queryset = queryset.filter(tags__tag__istartswith=q[1:]).distinct()
else:
queryset = queryset.filter(Q(tags__tag__icontains=q) | Q(title__icontains=q) | Q(short_description__icontains=q)
| Q(author__first_name__icontains=q) | Q(author__last_name__icontains=q)
| Q(author__email__icontains=q)).distinct()
filtered = CourseFilter(self.request.GET, queryset=queryset) filtered = CourseFilter(self.request.GET, queryset=queryset)
return filtered.qs return filtered.qs
@ -319,8 +330,18 @@ class CoursesView(ListView):
context = super().get_context_data() context = super().get_context_data()
filtered = CourseFilter(self.request.GET) filtered = CourseFilter(self.request.GET)
context.update(filtered.data) context.update(filtered.data)
sql = format_sql('''
select {ct.tag_id}
from {ct}
group by {ct.tag_id}
order by count(*) desc
limit 15''', ct=CourseTags)
tags = [t[0] for t in execute_sql(sql)]
context['tags'] = Tag.objects.filter(id__in=tags).order_by('tag')
context['search_query'] = self.request.GET.get('q', '')
context['banners'] = Banner.get_for_page(Banner.PAGE_COURSES) context['banners'] = Banner.get_for_page(Banner.PAGE_COURSES)
context['course_items'] = Course.shuffle(context.get('course_items')) context['course_items'] = Course.shuffle(context.get('course_items'))
context['new_courses'] = Course.objects.filter(status=Course.PUBLISHED).order_by('-created_at')[:4]
context['ages'] = Course.AGE_CHOICES[1:] context['ages'] = Course.AGE_CHOICES[1:]
age = context.get('age') age = context.get('age')
if age and age[0]: if age and age[0]:
@ -339,6 +360,10 @@ class CoursesView(ListView):
for course in context['course_items']: for course in context['course_items']:
if course.id in can_buy_again_courses: if course.id in can_buy_again_courses:
course.buy_again_price = course.price / 2 course.buy_again_price = course.price / 2
for course in context['course_items']:
course.cover = None
for course in context['new_courses']:
course.cover = None
return context return context
def get_template_names(self): def get_template_names(self):

@ -3,12 +3,20 @@
{% block content %} {% block content %}
{% if product_type == 'course' %} {% if product_type == 'course' %}
<p>Курс ждет вас по ссылке <a href="https://{% setting 'MAIN_HOST' %}{{ url }}"> <p>Добрый день! Спасибо за покупку знаний в «Lil School»!</p>
<p>Где искать уроки?</p>
<p>После оплаты курс появится в вашем личном кабинете на платформе.</p>
<p><a href="https://{% setting 'MAIN_HOST' %}{{ url }}">
https://{% setting 'MAIN_HOST' %}{{ url }}</a></p> https://{% setting 'MAIN_HOST' %}{{ url }}</a></p>
<p>Все ваши покупки будут храниться там в рамках срока доступа к курсу.</p>
{% endif %} {% endif %}
{% if product_type == 'school' %} {% if product_type == 'school' %}
<p>Школа ждет вас по ссылке <a href="https://{% setting 'MAIN_HOST' %}{% url 'school:school' %}"> <p>Добрый день! Спасибо за покупку знаний в «Lil School»!</p>
<p>Где искать уроки?</p>
<p>После оплаты уроки появятся в вашем личном кабинете на платформе.</p>
<p><a href="https://{% setting 'MAIN_HOST' %}{% url 'school:school' %}">
https://{% setting 'MAIN_HOST' %}{% url 'school:school' %}</a></p> https://{% setting 'MAIN_HOST' %}{% url 'school:school' %}</a></p>
<p>В онлайн-школе урок хранится неделю. Ровно до следующего урока.</p>
{% endif %} {% endif %}
{% if product_type == 'drawing_camp' %} {% if product_type == 'drawing_camp' %}
{% if date_start.month == 7 and date_start.day == 1 and date_end.day == 31 %} {% if date_start.month == 7 and date_start.day == 1 and date_end.day == 31 %}
@ -72,7 +80,6 @@
https://{% setting 'MAIN_HOST' %}{% url 'school:drawing-camp' %}</a></p> https://{% setting 'MAIN_HOST' %}{% url 'school:drawing-camp' %}</a></p>
{% endif %} {% endif %}
{% endif %} {% endif %}
<p>Так же вы можете найти ссылку в личном кабинете в разделе «Мои покупки».</p>
<p>Занимайтесь с удовольствием!</p> <p>Занимайтесь с удовольствием!</p>

@ -0,0 +1,25 @@
# Generated by Django 2.0.7 on 2019-08-14 15:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('content', '0030_auto_20190809_0133'),
('payment', '0037_add_paid_one_more_bonuses'),
]
operations = [
migrations.AddField(
model_name='schoolpayment',
name='package',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='content.Package'),
),
migrations.AlterField(
model_name='payment',
name='bonus',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_payments', to='payment.UserBonus'),
),
]

@ -16,12 +16,14 @@ from django.core.validators import RegexValidator
from django.utils.timezone import now from django.utils.timezone import now
from django.conf import settings from django.conf import settings
from apps.content.models import Package
from project.utils import weekdays_in_date_range from project.utils import weekdays_in_date_range
from apps.course.models import Course from apps.course.models import Course
from apps.config.models import Config from apps.config.models import Config
from apps.school.models import SchoolSchedule from apps.school.models import SchoolSchedule
from apps.notification.utils import send_email from apps.notification.utils import send_email
from project.utils import dates_overlap
config = Config.load() config = Config.load()
@ -126,11 +128,11 @@ class Payment(PolymorphicModel):
ordering = ('created_at',) ordering = ('created_at',)
@classmethod @classmethod
def ajust_date_bounds(cls, date_start=None, date_end=None, is_camp=False): def adjust_date_bounds(cls, date_start=None, date_end=None, is_camp=False):
school_start = date(now().year, 9, 1) school_start = date((date_start or date_end).year, 9, 1)
school_end = date(now().year, 5, 31) school_end = date((date_end or date_start).year, 5, 31)
camp_start = date(now().year, 6, 1) camp_start = date((date_start or date_end).year, 6, 1)
camp_end = date(now().year, 8, 31) camp_end = date((date_end or date_start).year, 8, 31)
if date_start: if date_start:
if is_camp: if is_camp:
@ -151,24 +153,32 @@ class Payment(PolymorphicModel):
else: else:
return date_start or date_end return date_start or date_end
@classmethod
def date_add(cls, date_start, days=0, months=0, is_camp=False):
date_end = arrow.get(date_start + timedelta(days=days), settings.TIME_ZONE).shift(
months=months).date() - timedelta(1)
if months == 1:
if is_camp or (date_start.month == 2 and date_start.day >= 28) or (
date_start.day == 31 and date_end.day <= 30) \
or (date_start.month == 1 and date_start.day >= 29 and date_end.day == 28):
date_end = date_start.replace(day=1, month=date_start.month + 1) - timedelta(1)
return date_end
@classmethod @classmethod
def get_date_range(cls, date_start=None, days=0, months=0, is_camp=False): def get_date_range(cls, date_start=None, days=0, months=0, is_camp=False):
date_start = date_start or now().date() date_start = date_start or now().date()
if isinstance(date_start, datetime): if isinstance(date_start, datetime):
date_start = date_start.date() date_start = date_start.date()
date_start = cls.ajust_date_bounds(date_start=date_start, is_camp=is_camp) date_start = cls.adjust_date_bounds(date_start=date_start, is_camp=is_camp)
if is_camp and date_start.month == 6 and date_start.day > 16: if is_camp and date_start.month == 6 and date_start.day > 16:
date_start = date_start.replace(month=7, day=1) date_start = date_start.replace(month=7, day=1)
date_end = arrow.get(date_start + timedelta(days=days), settings.TIME_ZONE).shift(months=months).date() - timedelta(1) date_end = cls.date_add(date_start, days, months, is_camp)
if months == 1: if is_camp:
if is_camp or (date_start.month == 2 and date_start.day >= 28) or (date_start.day == 31 and date_end.day <= 30) \ date_end = cls.adjust_date_bounds(date_end, is_camp=is_camp)
or (date_start.month == 1 and date_start.day >= 29 and date_end.day == 28):
date_end = date_start.replace(day=1, month=date_start.month + 1) - timedelta(1)
date_end = cls.ajust_date_bounds(date_end=date_end, is_camp=is_camp)
return [date_start, date_end] return [date_start, date_end]
@classmethod @classmethod
def calc_amount(cls, payment=None, user=None, course=None, date_start=None, weekdays=None, is_camp=False): def calc_amount(cls, package=None, payment=None, user=None, course=None, date_start=None, weekdays=None, is_camp=False):
price = 0 price = 0
discount = 0 discount = 0
referral_bonus = 0 referral_bonus = 0
@ -178,14 +188,14 @@ class Payment(PolymorphicModel):
user = payment.user user = payment.user
if isinstance(payment, SchoolPayment): if isinstance(payment, SchoolPayment):
user = payment.user user = payment.user
weekdays = payment.weekdays
date_start = payment.date_start date_start = payment.date_start
package=payment.package
if isinstance(payment, DrawingCampPayment): if isinstance(payment, DrawingCampPayment):
user = payment.user user = payment.user
date_start = payment.date_start date_start = payment.date_start
if issubclass(cls, DrawingCampPayment): if issubclass(cls, DrawingCampPayment):
is_camp = True is_camp = True
date_start, date_end = Payment.get_date_range(date_start, months=1, is_camp=is_camp) date_start, date_end = Payment.get_date_range(date_start, months=package.duration if package else 1, is_camp=is_camp)
if hasattr(user, 'referral') and not user.referral.payment: if hasattr(user, 'referral') and not user.referral.payment:
referral_bonus = user.referral.bonus referral_bonus = user.referral.bonus
referrer_bonus = user.referral.referrer_bonus referrer_bonus = user.referral.referrer_bonus
@ -213,40 +223,22 @@ class Payment(PolymorphicModel):
date_end__gte=date_start, date_end__gte=date_start,
status__in=Payment.PW_PAID_STATUSES, status__in=Payment.PW_PAID_STATUSES,
) )
school_schedules_purchased = school_payments.annotate(
joined_weekdays=Func(F('weekdays'), function='unnest', )
).values_list('joined_weekdays', flat=True).distinct()
weekdays = list(set(map(int, weekdays)) - set(school_schedules_purchased))
prev_school_payment = school_payments.filter(add_days=False).last() prev_school_payment = school_payments.filter(add_days=False).last()
add_days = bool(prev_school_payment) if prev_school_payment:
else: date_start, date_end = Payment.get_date_range(prev_school_payment.date_end + timedelta(1),
add_days = False months=package.duration, is_camp=False)
school_schedules = SchoolSchedule.objects.filter( school_schedules = SchoolSchedule.objects.filter(is_camp=False).exclude(title='')
weekday__in=weekdays, weekdays = list(school_schedules.values_list('weekday', flat=True))
is_camp=False, # FIXME после мая 2019 убрать?
) # Если хотят купить школу в мае, то оплатить ее можно только до 31 мая, потом школа закроется
if add_days: if date_start.month == 5:
date_end = prev_school_payment.date_end weekdays_count = weekdays_in_date_range(date_start, date_end)
weekdays_count = weekdays_in_date_range(date_start, prev_school_payment.date_end) weekdays_count = sum(weekdays_count[wd] for wd in weekdays)
all_weekdays_count = weekdays_in_date_range(prev_school_payment.date_start, prev_school_payment.date_end) all_weekdays_count = weekdays_in_date_range(date_start.replace(day=1), date_end)
for ss in school_schedules: all_weekdays_count = sum(all_weekdays_count[wd] for wd in weekdays)
price += ss.month_price // all_weekdays_count.get(ss.weekday, 0) * weekdays_count.get( price = package.price // all_weekdays_count * weekdays_count
ss.weekday, 0)
else: else:
# FIXME после мая 2019 убрать? price = package.price
# Если хотят купить школу в мае, то оплатить ее можно только до 31 мая, потом школа закроется
if date_start.month == 5:
weekdays_count = weekdays_in_date_range(date_start, date_end)
all_weekdays_count = weekdays_in_date_range(date_start.replace(day=1), date_end)
for ss in school_schedules:
price += ss.month_price // all_weekdays_count.get(ss.weekday, 0) * weekdays_count.get(
ss.weekday, 0)
else:
price = school_schedules.aggregate(
models.Sum('month_price'),
).get('month_price__sum', 0)
if not (payment and payment.id) and price >= config.SERVICE_DISCOUNT_MIN_AMOUNT:
discount = config.SERVICE_DISCOUNT
amount = price - discount amount = price - discount
referral_bonus = round(amount * referral_bonus / 100) referral_bonus = round(amount * referral_bonus / 100)
referrer_bonus = round(amount * referrer_bonus / 100) referrer_bonus = round(amount * referrer_bonus / 100)
@ -337,6 +329,7 @@ class CoursePayment(Payment):
class SchoolPayment(Payment): class SchoolPayment(Payment):
weekdays = ArrayField(models.IntegerField(), size=7, verbose_name='Дни недели') weekdays = ArrayField(models.IntegerField(), size=7, verbose_name='Дни недели')
add_days = models.BooleanField('Докупленные дни', default=False) add_days = models.BooleanField('Докупленные дни', default=False)
package = models.ForeignKey(Package, null=True, blank=True, on_delete=models.SET_NULL)
date_start = models.DateField('Дата начала подписки', null=True, blank=True) date_start = models.DateField('Дата начала подписки', null=True, blank=True)
date_end = models.DateField('Дата окончания подписки', null=True, blank=True) date_end = models.DateField('Дата окончания подписки', null=True, blank=True)

@ -0,0 +1,12 @@
{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %}
<div class="section">
<div class="section__center center center_xs">
<div class="done">
<div class="done__title title">Вы успешно приобрели подписку с {{ package.date_start }} по {{ package.date_end }}!</div>
<div class="done__foot">
<a class="done__btn btn btn_md btn_stroke" href="{% url 'index' %}">НА ГЛАВНУЮ</a>
</div>
</div>
</div>
</div>
{% endblock content %}

@ -1,9 +1,10 @@
{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %} {% extends "templates/lilcity/index.html" %} {% load static %}{% load plural %}
{% block content %}
<div class="section"> <div class="section">
<div class="section__center center center_xs"> <div class="section__center center center_xs">
<div class="done"> <div class="done">
{% if school %} {% if school %}
<div class="done__title title">Вы успешно приобрели доступ к урокам онлайн-школы!</div> <div class="done__title title">Вы успешно приобрели доступ на {{ duration|rupluralize:"месяц,месяца,месяцев" }}!</div>
<div class="done__foot"> <div class="done__foot">
<a class="done__btn btn btn_md btn_stroke" href="{% url 'school:school' %}">ПЕРЕЙТИ К ШКОЛЕ</a> <a class="done__btn btn btn_md btn_stroke" href="{% url 'school:school' %}">ПЕРЕЙТИ К ШКОЛЕ</a>
</div> </div>

@ -20,6 +20,7 @@ from django.utils.timezone import now
from paymentwall import Pingback, Product, Widget from paymentwall import Pingback, Product, Widget
from apps.content.models import Package
from apps.course.models import Course from apps.course.models import Course
from apps.payment.tasks import transaction_to_mixpanel, product_payment_to_mixpanel, transaction_to_roistat from apps.payment.tasks import transaction_to_mixpanel, product_payment_to_mixpanel, transaction_to_roistat
from apps.notification.utils import send_email from apps.notification.utils import send_email
@ -48,7 +49,12 @@ class SchoolBuySuccessView(TemplateView):
template_name = 'payment/payment_success.html' template_name = 'payment/payment_success.html'
def get(self, request, pk=None, is_camp=False, *args, **kwargs): def get(self, request, pk=None, is_camp=False, *args, **kwargs):
return self.render_to_response(context={'camp': True} if is_camp else {'school': True}) context = {
'duration': request.GET.get('duration'),
'camp': is_camp,
'school': not is_camp
}
return self.render_to_response(context=context)
@method_decorator(login_required, name='dispatch') @method_decorator(login_required, name='dispatch')
@ -116,56 +122,23 @@ class SchoolBuyView(TemplateView):
template_name = 'payment/paymentwall_widget.html' template_name = 'payment/paymentwall_widget.html'
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
raise Http404() # FIXME
host = urlsplit(self.request.META.get('HTTP_REFERER')) host = urlsplit(self.request.META.get('HTTP_REFERER'))
host = str(host[0]) + '://' + str(host[1]) host = str(host[0]) + '://' + str(host[1])
weekdays = set(request.GET.getlist('weekdays', []))
use_bonuses = request.GET.get('use_bonuses') use_bonuses = request.GET.get('use_bonuses')
roistat_visit = request.COOKIES.get('roistat_visit', None) roistat_visit = request.COOKIES.get('roistat_visit', None)
date_start = request.GET.get('date_start') date_start = request.GET.get('date_start')
duration = request.GET.get('duration')
package = get_object_or_404(Package, duration=duration)
date_start = date_start and datetime.datetime.strptime(date_start, '%Y-%m-%d').date() or now().date() date_start = date_start and datetime.datetime.strptime(date_start, '%Y-%m-%d').date() or now().date()
date_start, date_end = Payment.get_date_range(date_start, months=1) amount_data = SchoolPayment.calc_amount(package=package, user=request.user, date_start=date_start)
if not weekdays: school_payment = SchoolPayment.objects.create(
messages.error(request, 'Выберите несколько дней недели.')
return redirect('school:school')
try:
weekdays = [int(weekday) for weekday in weekdays]
except ValueError:
messages.error(request, 'Ошибка выбора дней недели.')
return redirect('school:school')
prev_school_payment = SchoolPayment.objects.filter(
user=request.user, user=request.user,
date_start__lte=date_start, weekdays=amount_data.get('weekdays'),
date_end__gte=date_start, roistat_visit=roistat_visit,
add_days=False, date_start=amount_data.get('date_start'),
status__in=[ date_end=amount_data.get('date_end'),
Pingback.PINGBACK_TYPE_REGULAR, package=package,
Pingback.PINGBACK_TYPE_GOODWILL, )
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
).last()
add_days = bool(prev_school_payment)
if add_days:
school_payment = SchoolPayment.objects.create(
user=request.user,
weekdays=weekdays,
date_start=date_start,
date_end=prev_school_payment.date_end,
add_days=True,
roistat_visit=roistat_visit,
)
# Если произойдет ошибка и оплату бонусами повторят еще раз на те же дни, то вернет ошибку
if school_payment.amount <= 0:
messages.error(request, 'Выбранные дни отсутствуют в оставшемся периоде подписки')
return redirect(reverse_lazy('school:school'))
else:
school_payment = SchoolPayment.objects.create(
user=request.user,
weekdays=weekdays,
roistat_visit=roistat_visit,
date_start=date_start,
date_end=date_end,
)
if use_bonuses and request.user.bonus: if use_bonuses and request.user.bonus:
if request.user.bonus >= school_payment.amount: if request.user.bonus >= school_payment.amount:
bonus = UserBonus.objects.create(amount= -school_payment.amount, user=request.user, payment=school_payment) bonus = UserBonus.objects.create(amount= -school_payment.amount, user=request.user, payment=school_payment)
@ -183,7 +156,7 @@ class SchoolBuyView(TemplateView):
f'school_{school_payment.id}', f'school_{school_payment.id}',
school_payment.amount, school_payment.amount,
'RUB', 'RUB',
'Школа', 'Подписка',
) )
widget = Widget( widget = Widget(
str(request.user.id), str(request.user.id),
@ -195,7 +168,7 @@ class SchoolBuyView(TemplateView):
'evaluation': 1, 'evaluation': 1,
'demo': 1, 'demo': 1,
'test_mode': 1, 'test_mode': 1,
'success_url': host + str(reverse_lazy('payment-success')), 'success_url': host + str(reverse_lazy('payment-success')) + '?duration=%s' % duration,
'failure_url': host + str(reverse_lazy('payment-error')), 'failure_url': host + str(reverse_lazy('payment-error')),
} }
) )
@ -362,6 +335,7 @@ class PaymentwallCallbackView(View):
'created_at': payment.created_at, 'created_at': payment.created_at,
'update_at': payment.update_at, 'update_at': payment.update_at,
} }
payment.save() payment.save()
product_payment_to_mixpanel.delay( product_payment_to_mixpanel.delay(

@ -70,7 +70,7 @@ class DrawingCampLessonsView(ListView):
def get_queryset(self): def get_queryset(self):
date_start = (now() - timedelta(days=7)).date() date_start = (now() - timedelta(days=7)).date()
date_start, date_end = DrawingCampPayment.ajust_date_bounds(date_start, date_start + timedelta(days=23), is_camp=True) date_start, date_end = DrawingCampPayment.adjust_date_bounds(date_start, date_start + timedelta(days=23), is_camp=True)
date_range = Q( date_range = Q(
date__range=[date_start, date_end] date__range=[date_start, date_end]
) )
@ -106,15 +106,15 @@ class LiveLessonsDetailView(DetailView):
is_purchased = DrawingCampPayment.objects.all() is_purchased = DrawingCampPayment.objects.all()
else: else:
is_purchased = SchoolPayment.objects.filter(weekdays__contains=[self.object.date.weekday() + 1],) is_purchased = SchoolPayment.objects.filter(weekdays__contains=[self.object.date.weekday() + 1],)
is_purchased = is_purchased.filter( is_purchased = is_purchased.paid().filter(
user=request.user, user=request.user,
date_start__lte=now(), date_start__lte=now(),
date_end__gte=now() - timedelta(days=7), date_end__gte=now() - timedelta(days=7),
status__in=[ ).exists() or is_purchased.paid().filter(
Pingback.PINGBACK_TYPE_REGULAR, user=request.user,
Pingback.PINGBACK_TYPE_GOODWILL, date_start__lte=now(),
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, date_end__gte=now() - timedelta(days=30),
], package__duration__lte=9,
).exists() ).exists()
if not is_purchased and request.user.role not in [User.ADMIN_ROLE, User.TEACHER_ROLE]: if not is_purchased and request.user.role not in [User.ADMIN_ROLE, User.TEACHER_ROLE]:
raise Http404 raise Http404
@ -221,7 +221,10 @@ class SchoolView(TemplateView):
# берем все подписки, которые были в периоде # берем все подписки, которые были в периоде
for sp in prev_school_payments: for sp in prev_school_payments:
# берем все уроки в оплаченном промежутке # берем все уроки в оплаченном промежутке
date_range = [max(sp.date_start, prev_range[0]), min(sp.date_end, prev_range[1])] if sp.package and sp.package.duration >= 9:
date_range = [max(sp.date_start, date_now - timedelta(30)), min(sp.date_end, prev_range[1])]
else:
date_range = [max(sp.date_start, prev_range[0]), min(sp.date_end, prev_range[1])]
prev_live_lessons += list(LiveLesson.objects.filter( prev_live_lessons += list(LiveLesson.objects.filter(
date__range=date_range, date__range=date_range,
deactivated_at__isnull=True, deactivated_at__isnull=True,

@ -12,7 +12,8 @@ User = get_user_model()
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin):
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password')}), (None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'gender', 'about', 'photo')}), (_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'gender', 'about', 'photo',)}),
('Teacher', {'fields': ('show_in_mainpage', 'trial_lesson', 'instagram_hashtag',)}),
('Facebook Auth data', {'fields': ('fb_id', 'fb_data', 'is_email_proved')}), ('Facebook Auth data', {'fields': ('fb_id', 'fb_data', 'is_email_proved')}),
(_('Permissions'), {'fields': ('role', 'is_active', 'is_staff', 'is_superuser', (_('Permissions'), {'fields': ('role', 'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions', 'show_in_mainpage')}), 'groups', 'user_permissions', 'show_in_mainpage')}),

@ -1,12 +1,23 @@
{% load static %}
<div class="anchor" id="about"></div> <div class="anchor" id="about"></div>
<div class="section section_school"> <div class="section section_main section_flex section_about">
<div class="section__center center"> <div class="section__center center">
<div class="text text_lg"> <div class="section__column section__column_img">
<p>Вы житель мегаполиса и&nbsp;у&nbsp;вас нет времени дополнительно развивать своего ребенка? </div>
Или&nbsp;вы&nbsp;живете в&nbsp;маленьком городе, <div class="section__column section__column_text">
где&nbsp;нет&nbsp;качественных школ и&nbsp;секций для&nbsp;детей?</p> <div class="title">Для кого?<img class="emoji" src="{% static 'img/emoji-students.png' %}" /></div>
<p><b>Lil School</b>&nbsp;это решение для&nbsp;тех&nbsp;родителей, кто&nbsp;стремится дать лучшее своему ребенку. <div class="text text_left">
Учитесь не&nbsp;выходя из&nbsp;дома!</p> <p>Вы житель мегаполиса и&nbsp;у&nbsp;вас нет времени дополнительно развивать своего ребенка?
Или&nbsp;вы&nbsp;живете в&nbsp;маленьком городе,
где&nbsp;нет&nbsp;качественных школ и&nbsp;секций для&nbsp;детей?</p>
<p><b>Lil-School</b>&nbsp;это решение для&nbsp;тех&nbsp;родителей, кто&nbsp;стремится дать лучшее своему ребенку.
Учитесь, не&nbsp;выходя из&nbsp;дома!</p>
</div>
<div class="section__buttons">
<a class="btn btn_width-auto" style="width: auto;"
href="{% url 'packages' %}">Узнать стоимость</a>
<a href="{% url 'gift-certificates' %}" class="btn btn_gray">Подарить другу</a>
</div>
</div> </div>
</div> </div>
</div> </div>

@ -1,5 +1,5 @@
<div class="banner" <div class="banner"
data-banner="{{banner.id}}" style="display: none; data-banner="{{banner.id}}" style="
background: {{ banner.color|default:'white' }}; background: {{ banner.color|default:'white' }};
{% if banner.color2 %} {% if banner.color2 %}
background: -moz-linear-gradient(-45deg, {{ banner.color }} 0%, {{ banner.color2 }} 100%); background: -moz-linear-gradient(-45deg, {{ banner.color }} 0%, {{ banner.color2 }} 100%);
@ -14,7 +14,12 @@
<div class="banner__text-column"> <div class="banner__text-column">
<div class="banner__text">{{ banner.text|safe }}</div> <div class="banner__text">{{ banner.text|safe }}</div>
{% if banner.url %} {% if banner.url %}
<a href="{{ banner.url }}" class="banner__link">{{ banner.button_text }}</a> {% if banner.is_video_url %}
<a href="#" class="banner__link js-video-modal" data-video-url="{{ banner.url }}"
data-trial-lesson="1">{{ banner.button_text }}</a>
{% else %}
<a href="{{ banner.url }}" class="banner__link">{{ banner.button_text }}</a>
{% endif %}
{% endif %} {% endif %}
</div> </div>
<div class="banner__image-column"> <div class="banner__image-column">

@ -16,5 +16,7 @@
<div class="swiper-pagination" slot="pagination"></div> <div class="swiper-pagination" slot="pagination"></div>
</swiper> </swiper>
{% else %} {% else %}
{% include 'templates/blocks/banner.html' with banner=banners.0 %} <div class="banners">
{% include 'templates/blocks/banner.html' with banner=banners.0 %}
</div>
{% endif %} {% endif %}

@ -1,11 +1,10 @@
{% load static %} {% load static %}
{% load ruplural from plural %} {% load ruplural from plural %}
<div class="section section_school"> <div class="section section_main section_gray section_counters">
<div class="section__center center"> <div class="section__center center">
<div class="text text_only_curve"> <div class="text text_only_curve">
<img class="text__curve text__curve_five" src="{% static 'img/curve-2.svg' %}" width="210"> <img class="text__curve text__curve_six" src="{% static 'img/curve-2.svg' %}" width="210">
<div class="title">Lil School в цифрах</div>
</div> </div>
<div class="school school_main"> <div class="school school_main">
<div class="school__col"> <div class="school__col">
@ -32,6 +31,5 @@
<div class="school__text">со&nbsp;всего мира со&nbsp;счастливыми учениками Lil&nbsp;School</div> <div class="school__text">со&nbsp;всего мира со&nbsp;счастливыми учениками Lil&nbsp;School</div>
</div> </div>
</div> </div>
<div style="margin: 50px 0; text-align: center;"><a class="btn" href="/faq">Часто задаваемые вопросы</a></div>
</div> </div>
</div> </div>

@ -1,7 +1,17 @@
{% load static %} {% load static %}
<footer class="footer"> <footer class="footer">
<div class="footer__center center"> <div class="footer__center center">
<div class="footer__row footer__row_first"> <div class="footer__row footer__row_subscr mobile-show">
<div class="footer__col footer__col_md">
<div>Подписаться на&nbsp;самую интересную рассылку на&nbsp;планете</div>
<form class="subscribe" method="post" action="{% url 'subscribe' %}">
{% csrf_token %}
<input class="subscribe__input" name="email" placeholder="Email">
<button class="subscribe__btn btn btn_light">ПОДПИСАТЬСЯ</button>
</form>
</div>
</div>
<div class="footer__row">
<div class="footer__col footer__col_md"> <div class="footer__col footer__col_md">
<a class="footer__logo logo" href="/"></a> <a class="footer__logo logo" href="/"></a>
<div class="footer__content">Lil School – первая образовательная онлайн-платформа креативного мышления <div class="footer__content">Lil School – первая образовательная онлайн-платформа креативного мышления
@ -57,15 +67,17 @@
</nav> </nav>
</div> </div>
</div> </div>
<div class="footer__row footer__row_second"> <div class="footer__row footer__row_subscr">
<div class="footer__col footer__col_md mobile-hide"></div> <div class="footer__col footer__col_md mobile-hide"></div>
<div class="footer__col footer__col_md"> <div class="footer__col footer__col_md">
<div>Подписаться на&nbsp;самую интересную рассылку на&nbsp;планете</div> <div class="mobile-hide">
<form class="subscribe" method="post" action="{% url 'subscribe' %}"> <div>Подписаться на&nbsp;самую интересную рассылку на&nbsp;планете</div>
{% csrf_token %} <form class="subscribe" method="post" action="{% url 'subscribe' %}">
<input class="subscribe__input" name="email" placeholder="Email"> {% csrf_token %}
<button class="subscribe__btn btn btn_light">ПОДПИСАТЬСЯ</button> <input class="subscribe__input" name="email" placeholder="Email">
</form> <button class="subscribe__btn btn btn_light">ПОДПИСАТЬСЯ</button>
</form>
</div>
<div class="footer__copyright"> <div class="footer__copyright">
Все права защищены ©&nbsp;Lil&nbsp;City,&nbsp;UAB. Все&nbsp;материалы принадлежат компании Lil&nbsp;City,&nbsp;UAB.<br> Все права защищены ©&nbsp;Lil&nbsp;City,&nbsp;UAB. Все&nbsp;материалы принадлежат компании Lil&nbsp;City,&nbsp;UAB.<br>
Никакая из&nbsp;частей этого сайта, текстов или&nbsp;изображений, не&nbsp;может быть&nbsp;скопирована, использована или&nbsp;передана Никакая из&nbsp;частей этого сайта, текстов или&nbsp;изображений, не&nbsp;может быть&nbsp;скопирована, использована или&nbsp;передана

@ -25,6 +25,11 @@
</form> </form>
</div> </div>
<nav class="header__nav"> <nav class="header__nav">
<div class="header__group mobile-show">
<a class="header__section {% active_link 'index' %}" href="{% url 'index' %}">
ГЛАВНАЯ
</a>
</div>
<!-- <div class="header__group"> <!-- <div class="header__group">
<a class="header__section {% active_link 'school:school' %}" href="{% url 'school:school' %}"> <a class="header__section {% active_link 'school:school' %}" href="{% url 'school:school' %}">
ОНЛАЙН-ШКОЛА {% if online or livelesson.is_online %} ОНЛАЙН-ШКОЛА {% if online or livelesson.is_online %}
@ -51,6 +56,11 @@
{% category_menu_items category %} {% category_menu_items category %}
</div> </div>
</div> </div>
<div class="header__group">
<a class="header__section {% active_link 'packages' %}" href="{% url 'packages' %}">
ЦЕНЫ
</a>
</div>
<div class="header__group"> <div class="header__group">
<a class="header__section" href="https://blog.lil.school">БЛОГ</a> <a class="header__section" href="https://blog.lil.school">БЛОГ</a>
</div> </div>

@ -1,21 +1,14 @@
<div class="section section_school"> {% load static %}
<div class="section section_main section_school">
<div class="section__center center"> <div class="section__center center">
<div class="title title_center">Процесс</div> <div class="title title_center">Процесс</div>
<div class="text"> <div class="school school_main school_three-col">
Онлайн-образование &mdash; это очень удобно! Можете учиться в&nbsp;пижаме, без&nbsp;макияжа
и&nbsp;с&nbsp;огурцами на&nbsp;лице!)
</div>
<div class="school school_main">
<div class="school__col"> <div class="school__col">
<div class="school__preview">Видеоуроки</div> <div class="school__preview">Видеоуроки</div>
<div class="school__text">Каждый день!</div> <div class="school__text">Каждый день!</div>
</div> </div>
<div class="school__col"> <div class="school__col">
<div class="school__preview">В 17:00</div> <div class="school__preview">5 дисциплин</div>
<div class="school__text">По московскому времени</div>
</div>
<div class="school__col">
<div class="school__preview">7 дисциплин</div>
<div class="school__text">В разных техниках</div> <div class="school__text">В разных техниках</div>
</div> </div>
<div class="school__col"> <div class="school__col">
@ -23,6 +16,8 @@
<div class="school__text">Хранится 7 дней</div> <div class="school__text">Хранится 7 дней</div>
</div> </div>
</div> </div>
<div class="letsgo"></div> <div style="margin: 50px 0px 15px; text-align: center;">
<a class="btn btn_white" href="/faq">Часто задаваемые вопросы</a>
</div>
</div> </div>
</div> </div>

@ -1,5 +1,5 @@
{% load static %} {% load static %}
<div class="section" id="partners"> <div class="section section_main section_partners" id="partners">
<div class="section__center center"> <div class="section__center center">
<a name="partners"> <a name="partners">
<div class="title title_center">Наши партнеры</div> <div class="title title_center">Наши партнеры</div>

@ -1,14 +1,18 @@
{% load static %} {% load static %}
<div class="section reviews-section"> <div class="section section_main reviews-section">
<div class="reviews-section__center section__center center center_md"> <div class="reviews-section__center section__center center center_md">
<div class="reviews-section__title"> <div class="reviews-section__title">
<a id="reviews" name="reviews"> <a id="reviews" name="reviews">
<div class="title title_left">Отзывы о Lil School</div> <div class="title">Отзывы о Lil School<img class="emoji" src="{% static 'img/emoji-holiday.png' %}" /></div>
</a> </a>
<div class="text">Тёплых отзывов настолько много, что потребуется несколько суток, чтобы их просмотреть <div class="text text_left">
<p>Тёплых отзывов настолько много, что потребуется несколько суток, чтобы их просмотреть
и несколько месяцев чтобы прочитать.<br> и несколько месяцев чтобы прочитать.<br>
P.S.: Но мы читаем их все!) P.S.: Но мы читаем их все!)</p>
</div> </div>
{% if not request.user.is_authenticated and not request.user_agent.is_mobile %}
<a class="btn btn_pink-blue" href="#" data-popup=".js-popup-auth">Присоединиться к Lil School</a>
{% endif %}
</div> </div>
<div class="reviews-section__reviews reviews" data-review-images="{{ review_images|join:',' }}"> <div class="reviews-section__reviews reviews" data-review-images="{{ review_images|join:',' }}">
<div class="reviews__wrap"> <div class="reviews__wrap">

@ -0,0 +1,32 @@
{% load static %}
<div class="anchor" id="students"></div>
<div class="section section_main section_flex section_students">
<div class="section__center center">
<div class="section__column section__column_text">
<div class="title">Наши ученики – многократные призеры международных конкурсов!
<img class="emoji" src="{% static 'img/emoji-winners.png' %}" /></div>
</div>
<div class="section__column section__column_text">
<div class="text text_left">
<p>Креативное мышление необходимо всем: дизайнерам, математикам, инженерам, предпринимателям и врачам.</p>
</div>
</div>
</div>
<div class="section__center center">
<div class="section__column section__column_img">
</div>
<div class="section__column section__column_text">
<div class="section__quote">
<div class="section__quote-text">
Лил Скул для меня это место, где Солнце может быть синего цвета.
</div>
<div class="section__quote-name">Злата Пыльцина, 7 лет. Город Волгоград.</div>
</div>
<div class="section__buttons">
<a class="btn js-video-modal" style="width: auto;"
data-video-url="https://www.youtube.com/watch?v=QrlR5sL_eGI"
href="#">Видео отзыв</a>
</div>
</div>
</div>
</div>

@ -1,75 +1,107 @@
{% load static %} {% load static %}
<div id="teachers" class="anchor"></div> <div id="teachers" class="anchor"></div>
<div class="section"> <div class="section section_main section_flex">
<div class="section__center center center_md"> <div class="section__center center">
<div class="title title_center">Преподаватели</div> <div class="section__column section__column_text">
<div class="text"> <div class="title">Преподаватели</div>
<p>Преподаватели Lil School имеют большой опыт, поэтому с первых минут детям будет интересно с нами.</p> <div class="text text_left text_only_curve mobile-show"><img src="/static/img/curve-3.svg" class="text__curve" style="
<img class="text__curve text__curve_three" src="{% static 'img/curve-3.svg' %}"> right: 0px;
top: -30px;
"></div>
<div class="text text_left">
<p>Преподаватели Lil School имеют большой опыт, поэтому с первых минут детям будет интересно.</p>
</div>
</div>
<div class="section__column mobile-hide" style="padding-left: 100px;">
<div class="text text_left text_only_curve">
<img class="text__curve" src="{% static 'img/curve-3.svg' %}">
</div>
</div> </div>
</div>
<div class="section__center center">
<div class="teachers"> <div class="teachers">
{% for teacher in teachers %} {% for teacher in teachers %}
<div class="teachers__item"> {% cycle '<div class="teachers__row">' '' %}
<div class="teachers__ava ava"> <div class="teachers__item">
{% if teacher.photo %} <div class="teachers__left-column">
<img class="ava__pic" src="{{ teacher.photo.url }}"> {% else %} <div class="teachers__ava ava">
<img class="ava__pic" src="{% static 'img/user_default.jpg' %}"> {% endif %} {% if teacher.photo %}
</div> <img class="ava__pic" src="{{ teacher.photo.url }}"> {% else %}
<div class="teachers__wrap"> <img class="ava__pic" src="{% static 'img/user_default.jpg' %}"> {% endif %}
<div class="teachers__title"> </div>
<div class="teachers__title-name"> <div class="teachers__social">
<a href="{{ teacher.url }}">{{ teacher.get_full_name }}</a>{% if teacher.instagram_hashtag %}, {% if teacher.facebook %}
<a href='https://www.instagram.com/explore/tags/{{ teacher.instagram_hashtag }}/' target="_blank"> <a class="social__item" href="{{ teacher.facebook }}" target="_blank">
{{ teacher.instagram_hashtag }} <svg class="icon icon-share-facebook">
</a> <use xlink:href="{% static 'img/sprite.svg' %}#icon-share-facebook"></use>
</svg>
</a>
{% endif %} {# if teacher.instagram #}
<a class="social__item" href="{{ teacher.instagram }}" target="_blank">
<svg class="icon icon-share-instagram">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-instagram"></use>
</svg>
</a>
{# endif #} {% if teacher.twitter %}
<a class="social__item" href="{{ teacher.twitter }}" target="_blank">
<svg class="icon icon-share-twitter">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-twitter"></use>
</svg>
</a>
{% endif %} {% if teacher.google %}
<a class="social__item" href="{{ teacher.google }}" target="_blank">
<svg class="icon icon-share-google">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-google"></use>
</svg>
</a>
{% endif %} {% if teacher.pinterest %}
<a class="social__item" href="{{ teacher.pinterest }}" target="_blank">
<svg class="icon icon-share-pinterest">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-pinterest"></use>
</svg>
</a>
{% endif %}
</div>
</div>
<div class="teachers__right-column">
<div class="teachers__title">
<a href="{{ teacher.url }}" style="color: black;">{{ teacher.get_full_name }}</a>{% if teacher.instagram_hashtag %}<br>
<a href='https://www.instagram.com/explore/tags/{{ teacher.instagram_hashtag }}/' target="_blank">
#{{ teacher.instagram_hashtag }}
</a>
{% endif %}
</div>
{% if teacher.about %}
<div class="teachers__content">
{{ teacher.about }}
</div>
{% endif %}
{% if teacher.trial_lesson %}
<a data-video-url="{{ teacher.trial_lesson }}" data-trial-lesson="1" href="#" class="btn btn_light js-video-modal">ПРОБНЫЙ УРОК</a>
{% endif %} {% endif %}
</div> </div>
{% if teacher.trial_lesson %}
<a data-video-url="{{ teacher.trial_lesson }}" data-trial-lesson="1" href="#" class="btn btn_light js-video-modal">ПРОБНЫЙ УРОК</a>
{% endif %}
</div> </div>
<div class="teachers__social"> {% cycle '' '</div>' %}
{% if teacher.facebook %} {% if forloop.last and not forloop.counter|divisibleby:'2' %}
<a class="social__item" href="{{ teacher.facebook }}" target="_blank"> <div class="teachers__item">
<svg class="icon icon-share-facebook"> <div class="teachers__left-column">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-facebook"></use> <div class="teachers__ava ava">
</svg> <img class="ava__pic" src="{% static 'img/user_default.jpg' %}" syle="margin-top: 10px;">
</a> </div>
{% endif %} {# if teacher.instagram #} </div>
<a class="social__item" href="{{ teacher.instagram }}" target="_blank"> <div class="teachers__right-column">
<svg class="icon icon-share-instagram"> <div class="teachers__title">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-instagram"></use> Хотите в команду<br>Lil School?
</svg> </div>
</a> <div class="teachers__content">
{# endif #} {% if teacher.twitter %} Любите развивать детей, развиваться сами и хотите зарабатывать на любимом деле?<br><br>
<a class="social__item" href="{{ teacher.twitter }}" target="_blank"> <a href="{% url 'author_request' %}">Оставьте заявку</a>
<svg class="icon icon-share-twitter"> </div>
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-twitter"></use> </div>
</svg>
</a>
{% endif %} {% if teacher.google %}
<a class="social__item" href="{{ teacher.google }}" target="_blank">
<svg class="icon icon-share-google">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-google"></use>
</svg>
</a>
{% endif %} {% if teacher.pinterest %}
<a class="social__item" href="{{ teacher.pinterest }}" target="_blank">
<svg class="icon icon-share-pinterest">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-pinterest"></use>
</svg>
</a>
{% endif %}
</div>
{% if teacher.about %}
<div class="teachers__content">
{{ teacher.about }}
</div>
{% endif %}
</div> </div>
</div> </div>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
<div class="text text_mb0">Если хотите к нам в команду, то <a href="{% url 'author_request' %}">отправьте</a> нам заявку</div>
</div> </div>
</div> </div>

@ -1,10 +1,25 @@
<div class="section section_video"> {% load static %}
<div class="section__center center center_sm" style="text-align: center;"> <div class="section section_main section_flex section_video">
<div class="title">Пробный урок</div> <div class="section__center center">
<img class="main-video-preview js-video-modal" data-video-url="{{ config.MAIN_PAGE_VIDEO_URL|safe }}" data-trial-lesson="1" <div class="section__column section__column_text">
src="{{ config.MAIN_PAGE_VIDEO_PREVIEW_IMG.url }}"/> <div class="title">Попробуйте бесплатно<img class="emoji" src="{% static 'img/emoji-cat.png' %}" /></div>
<a href="#" class="btn js-video-modal btn_stroke-black" style="margin: 20px;" <div class="text text_left">
data-video-url="{{ config.MAIN_PAGE_VIDEO_URL|safe }}" data-trial-lesson="1">Смотреть бесплатно</a> <p>Тысячи видео-уроков на разные темы для развития творческого мышления уже ждут вас. Учитесь, не выходя из дома!</p>
<div>Много развивающих видео на&nbsp;нашем <a href="{{ config.SERVICE_YOUTUBE_URL|safe }}">YouTube&nbsp;канале</a></div> </div>
<div class="section__buttons mobile-hide">
<a href="#" class="btn btn_white js-video-modal" style="width: auto;"
data-video-url="{{ config.MAIN_PAGE_VIDEO_URL|safe }}" data-trial-lesson="1">Смотреть бесплатный урок
<img class="emoji" src="{% static 'img/emoji-present.png' %}" /></a>
</div>
</div>
<div class="section__column section__column_img">
<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 }}"/>
<div class="section__buttons mobile-show">
<a href="#" class="btn btn_white js-video-modal"
data-video-url="{{ config.MAIN_PAGE_VIDEO_URL|safe }}" data-trial-lesson="1">Смотреть бесплатный урок
<img class="emoji" src="{% static 'img/emoji-present.png' %}" /></a>
</div>
</div>
</div> </div>
</div> </div>

@ -10,12 +10,14 @@
{% endif %} {% endif %}
{% endblock ogdescription %} {% endblock ogdescription %}
{% block title %}School LIL.CITY{% endblock title %} {% block title %}Lil School{% endblock title %}
{% block body_attr %}class="main-page"{% endblock body_attr %}
{% block content %} {% block content %}
{% include "templates/blocks/messages.html" %} {% include "templates/blocks/messages.html" %}
{% include "templates/blocks/about.html" %} {% include "templates/blocks/about.html" %}
{% include "templates/blocks/video.html" %} {% include "templates/blocks/video.html" %}
{% include "templates/blocks/counters.html" %} {% include "templates/blocks/counters.html" %}
{% include "templates/blocks/students.html" %}
{% include "templates/blocks/reviews.html" %} {% include "templates/blocks/reviews.html" %}
{% include "templates/blocks/online_school.html" %} {% include "templates/blocks/online_school.html" %}
{% include "templates/blocks/teachers.html" %} {% include "templates/blocks/teachers.html" %}

@ -20,7 +20,6 @@
</div> </div>
{% include "templates/blocks/footer.html" %} {% include "templates/blocks/footer.html" %}
{% include "templates/blocks/popup_auth.html" %} {% include "templates/blocks/popup_auth.html" %}
{% include "templates/blocks/popup_school_buy.html" %}
{% if is_gift_certificate_url %} {% if is_gift_certificate_url %}
{% include "templates/blocks/popup_gift_certificate.html" %} {% include "templates/blocks/popup_gift_certificate.html" %}
{% endif %} {% endif %}

@ -51,7 +51,7 @@
{% block layer_head %}{% endblock layer_head %} {% block layer_head %}{% endblock layer_head %}
</head> </head>
<body> <body {% block body_attr %}{% endblock body_attr %}>
{% block layer_body %} {% block layer_body %}
{% endblock layer_body %} {% endblock layer_body %}
<!-- Facebook Pixel Code --> <!-- Facebook Pixel Code -->

@ -0,0 +1,144 @@
{% extends "templates/lilcity/index.html" %}
{% load static %}
{% load plural %}
{% block content %}
<div class="section">
<div class="section__center center">
<div class="packages">
{% for package in packages %}
<div class="package"
{% if forloop.counter == 4 %}style="box-shadow: 0 10px 30px 0 rgba(241, 175, 50, 0.32);"{% endif %}>
<div class="package__title subtitle">
{% if package.duration == 12 %}
<b>1</b>&nbsp;год
{% else %}
<b>{{ package.duration }}</b>&nbsp;{{ package.duration|ruplural:"месяц,месяца,месяцев" }}
{% endif %}
</div>
<div class="package__desc">{{ package.description }}</div>
<div class="package__price">
{{ package.price|floatformat }}р
{% if package.high_price %}
<div class="package__high-price">&nbsp;{{ package.high_price|floatformat }}р&nbsp;</div>
{% endif %}
</div>
<div class="package__options">
<b>Включает:</b>
<div>{{ package.options_html|safe }}</div>
</div>
<div>
<a class="package__btn btn {% if forloop.counter == 2 %}btn_light{% endif %}"
{% if forloop.counter == 1 %}
style="background-image: linear-gradient(104deg, #f8f8f8, #fff2f2)"
{% endif %}
{% if forloop.counter == 4 %}
style="background-image: linear-gradient(to bottom, #ffe790, #f9d055 45%, #f9c155); padding: 12px 9px;"
{% endif %}
{% if user.is_authenticated %}
href="{% url 'school-checkout' %}?duration={{ package.duration }}"
{% else %}
href="#" data-popup=".js-popup-auth"
{% endif %}
>{% if forloop.counter == 4 %}Вступить в Lil-Клуб{% else %}Купить{% endif %}</a>
</div>
{% if next_buy_date %}
<div style="font-size: 12px; margin-top: 10px; height: 15px;">
на {{ package.duration|rupluralize:"месяц,месяца,месяцев" }} с {{ next_buy_date|date:'j b' }}
</div>
{% endif %}
{% if forloop.counter == 4 %}
<img class="package__medal" src="{% static 'img/medal.png' %}" />
{% endif %}
</div>
{% endfor %}
<div class="mobile-show">&nbsp;</div>
</div>
</div>
</div>
<div class="section section_packages">
<div class="section__center center">
<div class="subtitle3">
Учиться вы можете и бесплатно
</div>
<div class="text2">
<p>
Приглашайте друзей и знакомых на платформу и получайте 30% от суммы их первой покупки.<br>
Просто отправьте специальную ссылку, по которой ваши друзья могут присоединиться к нам.
</p>
{% if user.is_authenticated %}
<p><a href="{% url 'user-bonuses' %}">Узнать больше о том, как зарабатывать вместе с Lil School</a></p>
{% endif %}
<p style="text-align: center; margin: 35px 0;">
<a href="#" class="package__trial-btn btn btn_white btn_shadow js-video-modal"
data-video-url="{{ config.MAIN_PAGE_VIDEO_URL|safe }}" data-trial-lesson="1">
Получить бесплатный урок <img src="{% static 'img/emoji_present.png' %}" /></a>
</p>
<p></p>
</div>
<div class="subtitle3">Часто задаваемые вопросы:</div>
<div class="text2">
<p>
<b>Что значит «доступ ко всем урокам школы/лагеря»?</b><br/>
Максимальный доступ включает все уроки школы:<br>
По будням с 1 сентября по 31 мая. Пять уроков в неделю.<br>
И все уроки летнего рисовального лагеря:<br>
С 1 июня по 31 августа. Три урока в неделю.
</p>
<p>
<b>Какие дисциплины входят в программу онлайн-школы и рисовального лагеря?</b><br/>
В онлайн-школе пять дисциплин:<br>
Понедельник - иллюстрация;<br>
Вторник - пластилиновая живопись;<br>
Среда - акварель;<br>
Четверг - креативное мышление;<br>
Пятница - зенарт.<br>
В рисовальном лагере облегчённая программа. Микс из трёх дисциплин ежемесячно.
</p>
<p>
<b>Сколько хранится запись урока?</b><br/>
Запись урока онлайн-школы хранится неделю. Ровно до следующего урока. В Золотом клубе уроки хранятся месяц.
</p>
<p>
<b>Если я приобрёл доступ на месяц, а после начала занятий решил взять на год?</b><br/>
В любой момент можно стать участником клуба! Новая подписка начнется на следующий день после окончания предыдущей,
а купить ее вы можете когда захотите!
</p>
<p>
<b>Могу ли я перейти на более низкий тариф?</b><br/>
Конечно можете. Компенсацию разницы мы возместим в течение 60 банковских дней.
</p>
<p>
<b>Если я не успел использовать привилегии золотого статуса в течение года?</b><br/>
Желательно использовать все привилегии в установленный срок, т.к. доступ к ним платформа предоставляет
автоматически. В других случаях администрация школы может рассмотреть сложившуюся ситуацию в индивидуальном порядке.
</p>
<p>
<b>Что делать после оплаты? Как узнать, что меня добавили в школу?</b><br/>
Нажмите на иконку в правом верхнем углу. Перейдите в свой профиль. Все, - приобретённые вами курсы хранятся там.
</p>
<p>
<b>Если я купил доступ на месяц не первого, а восьмого сентября, какая будет цена?</b><br/>
Цена будет такая же - 1990₽. Система автоматически откроет вам доступ с восьмого сентября по восьмое октября.
</p>
<p>
<b>Если я купил доступ восьмого сентября, будет ли у меня доступ к урокам, которые вышли до этой даты?</b><br/>
Система автоматически откроет вам доступ на месяц вперёд. Прошедших уроков в нём не будет.
</p>
<p>
<b>Как получить грамоту?</b><br/>
По окончании месяца занятий именная грамота автоматически придёт вам на e-mail, который вы указывали при регистрации на платформе.
</p>
<p>
<b>Можно ли купить не все дисциплины школы, а только некоторые?</b><br/>
Такой технической возможности нет.
</p>
<p>
<b>Можно ли купить неделю занятий?</b><br/>
Минимальный срок доступа - месяц.
</p>
</div>
</div>
</div>
{% endblock content %}

@ -37,7 +37,7 @@ from apps.payment.views import (
SchoolBuyView, GiftCertificatesView, GiftCertificateBuyView, SchoolBuyView, GiftCertificatesView, GiftCertificateBuyView,
GiftCertificateBuySuccessView, GiftCertificateGetView, DrawingCampBuyView) GiftCertificateBuySuccessView, GiftCertificateGetView, DrawingCampBuyView)
from .views import AboutView, IndexView, SchoolSchedulesView, LinksView from .views import AboutView, IndexView, SchoolSchedulesView, LinksView, PackagesView
# TODO trim slash in the end # TODO trim slash in the end
urlpatterns = [ urlpatterns = [
@ -103,6 +103,7 @@ urlpatterns = [
path('gift-certificate/<str:slug>/get', GiftCertificateGetView.as_view(), name='gift-certificate-get'), path('gift-certificate/<str:slug>/get', GiftCertificateGetView.as_view(), name='gift-certificate-get'),
path('faq', FAQView.as_view(), name='faq'), path('faq', FAQView.as_view(), name='faq'),
path('links', LinksView.as_view(), name='links'), path('links', LinksView.as_view(), name='links'),
path('prices', PackagesView.as_view(), name='packages'),
] ]

@ -15,3 +15,7 @@ def date_range(start, end):
def weekdays_in_date_range(start, end): def weekdays_in_date_range(start, end):
return Counter([d.isoweekday() for d in date_range(start, end)]) return Counter([d.isoweekday() for d in date_range(start, end)])
def dates_overlap(start, end, start2, end2):
return start <= end2 and start2 <= end

@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta, date
from random import shuffle from random import shuffle
import short_url import short_url
@ -11,7 +11,7 @@ from paymentwall.pingback import Pingback
from apps.course.models import Course from apps.course.models import Course
from apps.school.models import SchoolSchedule from apps.school.models import SchoolSchedule
from apps.payment.models import SchoolPayment, UserGiftCertificate, Payment, DrawingCampPayment from apps.payment.models import SchoolPayment, UserGiftCertificate, Payment, DrawingCampPayment
from apps.content.models import Banner from apps.content.models import Banner, Package
User = get_user_model() User = get_user_model()
@ -98,6 +98,9 @@ class IndexView(TemplateView):
review_images = list(map(str, range(1, 107))) review_images = list(map(str, range(1, 107)))
shuffle(review_images) shuffle(review_images)
teachers = User.objects.filter(role=User.TEACHER_ROLE, show_in_mainpage=True)
if teachers.count() % 2 == 0:
teachers = teachers[:teachers.count() - 1]
context.update({ context.update({
'banners': Banner.get_for_page(Banner.PAGE_INDEX), 'banners': Banner.get_for_page(Banner.PAGE_INDEX),
@ -112,18 +115,15 @@ class IndexView(TemplateView):
'online_coming_soon': online_coming_soon, 'online_coming_soon': online_coming_soon,
'school_schedule': school_schedule, 'school_schedule': school_schedule,
'course_items': Course.shuffle(Course.objects.filter(status=Course.PUBLISHED)[:3]), 'course_items': Course.shuffle(Course.objects.filter(status=Course.PUBLISHED)[:3]),
'is_purchased': camp_payment_exists, # school_payment_exists, 'is_purchased': camp_payment_exists,
'camp_price': DrawingCampPayment.MONTH_PRICE, 'camp_price': DrawingCampPayment.MONTH_PRICE,
'min_school_price': SchoolSchedule.objects.aggregate(Min('month_price'))['month_price__min'], 'min_school_price': SchoolSchedule.objects.aggregate(Min('month_price'))['month_price__min'],
'school_schedules': SchoolSchedule.objects.filter(weekday__in=DrawingCampPayment.WEEKDAYS, is_camp=True), 'school_schedules': SchoolSchedule.objects.filter(weekday__in=DrawingCampPayment.WEEKDAYS, is_camp=True),
'school_schedules_purchased': DrawingCampPayment.WEEKDAYS if camp_payment_exists else [], # set(school_schedules_purchased), 'school_schedules_purchased': DrawingCampPayment.WEEKDAYS if camp_payment_exists else [],
'teachers': User.objects.filter(role=User.TEACHER_ROLE, show_in_mainpage=True), 'teachers': sorted(list(teachers), key=lambda t: 1 if t.email == 'sasha@lil.city' else 0, reverse=True),
'works_count': Payment.objects.filter(status__in=Payment.PW_PAID_STATUSES).count() * 7, 'works_count': Payment.objects.filter(status__in=Payment.PW_PAID_STATUSES).count() * 7,
# 'subscription_ends': school_payment.filter(add_days=False).first().date_end if school_payment_exists else None,
# 'subscription_ends_humanize': school_payment.filter(add_days=False).first().date_end_humanize if school_payment_exists else None,
'subscription_ends': camp_payment.latest('date_end').date_end if camp_payment_exists else None, 'subscription_ends': camp_payment.latest('date_end').date_end if camp_payment_exists else None,
'subscription_ends_humanize': camp_payment.latest('date_end').date_end_humanize if camp_payment_exists else None, 'subscription_ends_humanize': camp_payment.latest('date_end').date_end_humanize if camp_payment_exists else None,
'school_purchased_future': False, 'school_purchased_future': False,
'is_purchased_future': False, 'is_purchased_future': False,
@ -142,3 +142,33 @@ class SchoolSchedulesView(TemplateView):
class LinksView(TemplateView): class LinksView(TemplateView):
template_name = 'templates/lilcity/links.html' template_name = 'templates/lilcity/links.html'
class PackagesView(TemplateView):
template_name = 'templates/lilcity/packages.html'
def get_context_data(self):
context = super().get_context_data()
context['packages'] = Package.objects.all()[:4]
context['banners'] = Banner.get_for_page(Banner.PAGE_PACKAGES)[:1]
last_school_payment = None
school_end = date(now().year, 5, 31)
today = now().date()
if self.request.user.is_authenticated:
last_school_payment = SchoolPayment.objects.filter(
user=self.request.user,
date_end__gte=today,
status__in=Payment.PW_PAID_STATUSES,
).last()
context['last_school_payment'] = last_school_payment
if last_school_payment:
next_month = (last_school_payment.date_end + timedelta(1)).month
context['next_buy_date'] = last_school_payment.date_end + timedelta(1)
context['school_months_left'] = (school_end.month - next_month
if next_month <= school_end.month
else (school_end.month + 13) - next_month)
else:
context['school_months_left'] = (school_end.month - today.month
if today.month <= school_end.month
else (school_end.month + 13) - today.month)
return context

@ -14,14 +14,14 @@
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.6.1",
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"browser-sync": "^2.10.0", "browser-sync": "^2.26.7",
"css-loader": "^0.28.9", "css-loader": "^0.28.9",
"css-mqpacker": "^5.0.1", "css-mqpacker": "^5.0.1",
"del": "^2.2.0", "del": "^2.2.0",
"extract-text-webpack-plugin": "^3.0.2", "extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.6", "file-loader": "^1.1.6",
"lodash": "^4.3.0", "lodash": "^4.17.15",
"node-sass": "^4.9.0", "node-sass": "^4.12.0",
"require-dir": "^0.3.0", "require-dir": "^0.3.0",
"run-sequence": "^1.1.5", "run-sequence": "^1.1.5",
"sass-loader": "^7.0.1", "sass-loader": "^7.0.1",
@ -36,7 +36,7 @@
"dependencies": { "dependencies": {
"autosize": "^4.0.2", "autosize": "^4.0.2",
"autosize-input": "^1.0.2", "autosize-input": "^1.0.2",
"axios": "^0.17.1", "axios": "^0.19.0",
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"baguettebox.js": "^1.10.0", "baguettebox.js": "^1.10.0",
"bowser": "^2.1.2", "bowser": "^2.1.2",
@ -47,7 +47,7 @@
"history": "^4.7.2", "history": "^4.7.2",
"ilyabirman-likely": "^2.3.0", "ilyabirman-likely": "^2.3.0",
"inputmask": "^3.3.11", "inputmask": "^3.3.11",
"jquery": "^3.3.1", "jquery": "^3.4.1",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"modal-video": "git+https://github.com/gzbender/modal-video.git", "modal-video": "git+https://github.com/gzbender/modal-video.git",
@ -62,6 +62,7 @@
"vue": "^2.5.13", "vue": "^2.5.13",
"vue-autosize": "^1.0.2", "vue-autosize": "^1.0.2",
"vue-awesome-swiper": "^3.1.3", "vue-awesome-swiper": "^3.1.3",
"vue-tags-component": "^1.3.0",
"vuedraggable": "^2.16.0", "vuedraggable": "^2.16.0",
"vuejs-datepicker": "^0.9.25", "vuejs-datepicker": "^0.9.25",
"vuelidate": "^0.6.1" "vuelidate": "^0.6.1"

@ -119,10 +119,37 @@
placeholder="Выберите возраст"/> placeholder="Выберите возраст"/>
</div> </div>
</div> </div>
<label v-if="me && !live && me.role === ROLE_ADMIN" class="info__switch switch switch_lg"> <label v-if="me && !live && me.role === ROLE_ADMIN" class="info__switch switch switch_lg"
style="margin: 25px 0">
<input type="checkbox" class="switch__input" v-model="course.is_featured"> <input type="checkbox" class="switch__input" v-model="course.is_featured">
<span class="switch__content" name="course-is-featured">Выделить</span> <span class="switch__content" name="course-is-featured">Выделить</span>
</label> </label>
<div v-if="!live" class="info__field field">
<div class="field__label field__label_gray">Теги</div>
<div class="field__wrap">
<div class="autocomplete">
<input class="autocomplete__query" type="text" v-model="tagSearchQuery" placeholder="добавить тег"
:class="{loading: tagsLoading}" @keyup.enter="addTag" @keyup.down="selectFirstTag"
@click="tagsFocused = true"/>
<select ref="tags_options" class="autocomplete__options" multiple="multiple"
v-show="tags.length && tagsFocused" style="display: none;">
<option v-for="tag in tags" :key="tag.id" @click="selectTag(tag)"
@keyup.enter.stop.prevent="selectTag(tag)">{{ tag.tag }}</option>
</select>
</div>
<div style="margin: 0 -5px">
<a href="#" v-for="tag in course.tags" :key="tag.id" class="tag">
#{{ tag.tag }}
<span class="tag__remove" @click.prevent="removeTag(tag)">
<svg class="icon icon-close">
<use xlink:href="/static/img/sprite.svg#icon-close"></use>
</svg>
</span>
</a>
</div>
</div>
</div>
</div> </div>
<div v-if="!live" class="info__fieldset"> <div v-if="!live" class="info__fieldset">
<div class="info__field field"> <div class="info__field field">
@ -238,20 +265,21 @@
</template> </template>
<script> <script>
import { ROLE_ADMIN, ROLE_AUTHOR } from './consts' import { ROLE_ADMIN } from './consts'
import LinkInput from './inputs/LinkInput' import LinkInput from './inputs/LinkInput'
import DatePicker from 'vuejs-datepicker' import DatePicker from 'vuejs-datepicker'
import BlockContent from './blocks/BlockContent' import BlockContent from './blocks/BlockContent'
import VueRedactor from './redactor/VueRedactor'; import VueRedactor from './redactor/VueRedactor';
import LilSelect from "./inputs/LilSelect"; import LilSelect from "./inputs/LilSelect";
import LessonRedactor from "./LessonRedactor"; import LessonRedactor from "./LessonRedactor";
import {api} from "../js/modules/api"; import { api } from "../js/modules/api";
import $ from 'jquery'; import $ from 'jquery';
import {required, minValue, numeric, url } from 'vuelidate/lib/validators' import { required, minValue, numeric, url } from 'vuelidate/lib/validators'
import slugify from 'slugify'; import slugify from 'slugify';
import Draggable from 'vuedraggable'; import Draggable from 'vuedraggable';
import {showNotification} from "../js/modules/notification"; import {showNotification} from "../js/modules/notification";
import createHistory from "history/createBrowserHistory"; import createHistory from "history/createBrowserHistory";
import { VueTags } from 'vue-tags-component';
import moment from 'moment' import moment from 'moment'
import _ from 'lodash' import _ from 'lodash'
@ -262,6 +290,11 @@
props: ["authorName", "authorPicture", "accessToken", "courseId", "live", "camp"], props: ["authorName", "authorPicture", "accessToken", "courseId", "live", "camp"],
data() { data() {
return { return {
tagsFocused: false,
tagsLoading: false,
tagSearchXhr: null,
tagSearchQuery: '',
tags: [],
disabledDates: { disabledDates: {
to: new Date(new Date().setDate(new Date().getDate() - 1)), to: new Date(new Date().setDate(new Date().getDate() - 1)),
}, },
@ -298,7 +331,8 @@
content: [], content: [],
gallery: { gallery: {
images: [], images: [],
} },
tags: [],
}, },
courseLoading: false, courseLoading: false,
courseSaving: false, courseSaving: false,
@ -453,6 +487,47 @@
} }
}, },
methods: { methods: {
searchTags(){
if(this.tagSearchTimeout){
clearTimeout(this.tagSearchTimeout);
}
this.tagsLoading = true;
this.tagsFocused = false;
this.tagSearchTimeout = setTimeout(() => {
api.getTags({search: this.tagSearchQuery}).then(response => {
this.tagsLoading = false;
const tags = this.course.tags.map(t => t.id);
this.tags = response.data.filter(t => tags.indexOf(t.id) == -1);
});
}, 500);
},
selectFirstTag(){
const $opts = $(this.$refs.tags_options);
$opts.find("option:selected").prop("selected", false);
$opts.find("option:first").prop("selected", "selected");
$opts.focus();
},
selectTag(tag){
this.tagsFocused = false;
if(this.course.tags.findIndex(t => t.id == tag.id) > -1){
return;
}
this.course.tags.push(tag);
this.tags = [];
this.tagSearchQuery = '';
},
addTag(){
this.tagsLoading = true;
api.addTag({tag: this.tagSearchQuery}).then(response => {
this.tagsLoading = false;
this.tagSearchQuery = '';
this.selectTag(response.data);
});
},
removeTag(tag){
const index = this.course.tags.findIndex(i => i.id == tag.id);
this.course.tags.splice(index, 1);
},
removeCover() { removeCover() {
if(! this.course.coverImageId){ if(! this.course.coverImageId){
return; return;
@ -890,6 +965,11 @@
let promises = []; let promises = [];
api.getPopularTags().then(response => {
const tags = this.course.tags.map(t => t.id);
this.tags = response.data.filter(t => tags.indexOf(t.id) == -1);
});
let cats = api.getCategories(this.accessToken); let cats = api.getCategories(this.accessToken);
promises.push(cats); promises.push(cats);
cats.then((response) => { cats.then((response) => {
@ -977,6 +1057,13 @@
this.updateViewSection(window.location, 'load err '+this.courseId) this.updateViewSection(window.location, 'load err '+this.courseId)
}) })
}); });
$(window).click(e => {
const cssClass = $(e.target).attr('class');
if(!cssClass || cssClass.indexOf('autocomplete') == -1){
this.tagsFocused = false;
}
});
}, },
computed: { computed: {
displayPrice: { displayPrice: {
@ -1018,12 +1105,18 @@
}, },
deep: true, deep: true,
}, },
tagSearchQuery(){
if(this.tagSearchQuery){
this.searchTags();
}
}
}, },
components: { components: {
LessonRedactor, LessonRedactor,
LilSelect, LilSelect,
BlockContent, BlockContent,
VueTags,
'link-input': LinkInput, 'link-input': LinkInput,
'vue-datepicker': DatePicker, 'vue-datepicker': DatePicker,
'lesson-redactor': LessonRedactor, 'lesson-redactor': LessonRedactor,

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="460px" height="200px" viewBox="0 0 460 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
<title>Path 4</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="0%" y1="0%" x2="100%" y2="100%" id="linearGradient-1">
<stop stop-color="#FFE2EB" offset="0%"></stop>
<stop stop-color="#D8F5F5" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Main" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Main-page" transform="translate(-420.000000, -1872.000000)" fill="url(#linearGradient-1)" fill-rule="nonzero">
<path d="M547.842222,2033.31441 C567.325204,2042.1871 613.935728,2050.03802 666.138581,2053.35195 C721.237058,2056.84969 772.754776,2054.78726 803.076869,2047.09397 C832.820717,2039.54739 854.56339,2019.81542 861.600443,1995.61066 C868.792378,1970.87317 859.821858,1944.76728 834.631473,1924.42417 C808.807191,1903.56913 757.114211,1890.40587 698.058741,1888.29284 C641.152923,1886.25672 587.043875,1894.9449 559.016359,1911.04033 C542.337069,1920.61878 531.361084,1930.16539 524.069314,1940.42993 C518.801278,1947.84568 516.755616,1952.52274 511.608582,1966.97173 C505.968408,1982.80509 501.407986,1990.80823 491.078309,1999.1047 C478.943057,2008.85135 466.577598,2021.24231 454,2036.27877 C491.686444,2023.12812 522.980771,2021.99232 547.842222,2033.31441 Z M480.325244,1986.8614 C487.7323,1980.90638 490.978009,1975.20479 495.688655,1961.96771 C501.336419,1946.0973 503.799979,1940.45918 510.228317,1931.4011 C518.968002,1919.08613 531.737121,1907.96887 550.381854,1897.25108 C581.579142,1879.31754 638.507393,1870.16761 698.211489,1872.30597 C760.504126,1874.53704 815.337821,1888.51394 844.533155,1912.11477 C874.737615,1936.53136 886.031254,1969.43061 876.945509,2000.71312 C868.229112,2030.72396 841.908219,2054.6345 806.703085,2063.57558 C774.365022,2071.78851 721.330659,2073.91377 664.657805,2070.31251 C610.562224,2066.87502 562.393742,2058.75362 540.57419,2048.80696 C514.812424,2037.06322 478.547899,2041.67354 431.702156,2063.49157 C424.041202,2067.05959 416.65058,2058.09752 421.609042,2051.25237 C441.450886,2023.86075 461.003045,2002.39579 480.325244,1986.8614 Z" id="Path-4"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@ -129,6 +129,7 @@ export const api = {
}, },
content: api.convertContentJson(courseObject.content, true), content: api.convertContentJson(courseObject.content, true),
materials: courseObject.materials, materials: courseObject.materials,
tags: courseObject.tags,
}; };
if(courseObject.live) { if(courseObject.live) {
@ -209,6 +210,7 @@ export const api = {
content: api.convertContentJson(courseJSON.content), content: api.convertContentJson(courseJSON.content),
gallery: {images: (courseJSON.gallery) ? courseJSON.gallery.gallery_images:[]}, gallery: {images: (courseJSON.gallery) ? courseJSON.gallery.gallery_images:[]},
materials: courseJSON.materials, materials: courseJSON.materials,
tags: courseJSON.tags,
} }
}, },
convertGalleryImagesJson: (images) => { convertGalleryImagesJson: (images) => {
@ -513,4 +515,24 @@ export const api = {
} }
}); });
}, },
getTags: params => {
return api.get('/api/v1/tags/', {params});
},
getPopularTags: params => {
return api.get('/api/v1/tags/popular/', {params});
},
addTag: data => {
return api.post('/api/v1/tags/', data, {
headers: {
'Authorization': `Token ${window.LIL_STORE.accessToken}`,
}
});
},
removeTag: tagId => {
return api.delete(`/api/v1/tags/${tagId}/`, {
headers: {
'Authorization': `Token ${window.LIL_STORE.accessToken}`,
}
});
}
}; };

@ -1,13 +1,10 @@
import $ from 'jquery'; import $ from 'jquery';
import {showNotification} from "./notification" import {showNotification} from "./notification"
import Inputmask from "inputmask"; import Inputmask from "inputmask";
import SmoothScroll from 'smooth-scroll/dist/js/smooth-scroll';
import baguetteBox from 'baguettebox.js' import baguetteBox from 'baguettebox.js'
import createHistory from 'history/createBrowserHistory' import createHistory from 'history/createBrowserHistory'
import Cookies from 'js-cookie'
import moment from 'moment'
window.$ = window.jQuery = jQuery = $; window.$ = window.jQuery = $;
window.Inputmask = Inputmask; window.Inputmask = Inputmask;
window.baguetteBox = baguetteBox; window.baguetteBox = baguetteBox;
@ -23,26 +20,6 @@ $(document).ready(function () {
} }
}); });
//===========BANNERS===============
const $banner = $('[data-banner]');
const bannerId = $banner.data('banner') + '';
const futureDate = $banner.data('future-date') + '';
if(Cookies.get('hide_banner') !== bannerId){
$banner.show();
}
$banner.on('click', '.banner__hide', function(e){
e.preventDefault();
$banner.slideUp();
Cookies.set('hide_banner', bannerId);
});
if(futureDate){
}
$banner.find('.banner__countdown')
//===========REVIEWS=============== //===========REVIEWS===============
if(window.LIL_STORE.isIndexPage){ if(window.LIL_STORE.isIndexPage){
const $reviews = $('.reviews'); const $reviews = $('.reviews');

@ -36,6 +36,13 @@ $(document).ready(function () {
loadCourses(); loadCourses();
}); });
// Обработчик кнопки поиска
$('.course-search__search').click(function(e){
e.preventDefault();
page = 1;
loadCourses(true);
})
// Обработчик выбора категории // Обработчик выбора категории
$('div.js-select-option[data-category-option]').on('click', function (e) { $('div.js-select-option[data-category-option]').on('click', function (e) {
e.preventDefault(); e.preventDefault();
@ -58,6 +65,13 @@ $(document).ready(function () {
loadCourses(true); loadCourses(true);
}); });
// Обработчик тегов
$('.course-search__tag').on('click', function(e){
e.preventDefault();
$('.course-search__query').val($(this).text());
loadCourses(true);
});
// Обработчик лайков // Обработчик лайков
$('.container').on('click', 'a[data-course-likes]', function (e) { $('.container').on('click', 'a[data-course-likes]', function (e) {
e.preventDefault(); e.preventDefault();
@ -119,6 +133,7 @@ $(document).ready(function () {
const buttonElement = $('.courses').find('button.load__btn'); const buttonElement = $('.courses').find('button.load__btn');
let coursesUrl = window.LIL_STORE.urls.courses + '?' + $.param({ let coursesUrl = window.LIL_STORE.urls.courses + '?' + $.param({
cat: categoryName, cat: categoryName,
q: $('.course-search__query').val(),
age, age,
}); });
if (page > 1) { if (page > 1) {

@ -32,6 +32,7 @@ $green: #8ECFC0
$green-light: #5BD700 $green-light: #5BD700
$cyan: #B6DFD6 $cyan: #B6DFD6
$gray: #A7A7A7 $gray: #A7A7A7
$gray-dark: #888888
$blue: #4A90E2 $blue: #4A90E2
$viol: #B995D9 $viol: #B995D9
$viol2: #A186BD $viol2: #A186BD
@ -218,6 +219,15 @@ button
color: $cl color: $cl
&_pink-blue &_pink-blue
background-image: linear-gradient(100deg, #8febff, #ffa2cb) background-image: linear-gradient(100deg, #8febff, #ffa2cb)
&_disabled
opacity: 0.5
cursor: default
&:hover,
&:active
color: $cl
&_shadow
border-radius: 10px
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.05), 0 9px 24px 0 rgba(33, 74, 211, 0.1)
&_md &_md
padding: 18px 24px 17px padding: 18px 24px 17px
+m +m
@ -852,6 +862,11 @@ a[name]
font-size: 18px font-size: 18px
font-weight: bold font-weight: bold
.subtitle3
margin-bottom: 8px
font-size: 20px
line-height: 25px
.text .text
position: relative position: relative
max-width: 620px max-width: 620px
@ -870,8 +885,8 @@ a[name]
p p
&:not(:last-child) &:not(:last-child)
margin-bottom: 35px margin-bottom: 35px
+t +m
margin: 5px 5px 15px 5px margin-bottom: 15px
&__curve &__curve
position: absolute position: absolute
pointer-events: none pointer-events: none
@ -954,12 +969,60 @@ a[name]
+f +f
font-size: 20px font-size: 20px
.text2
p
font-size: 14px
margin-bottom: 20px
line-height: 18px
.section .section
padding: 50px 0 padding: 50px 0
+t +t
padding: 30px 0 padding: 40px 0
+m
padding: 20px 0 &__column
flex: 50%
&_img
background-position: bottom center
background-size: contain
background-repeat: no-repeat
&_text
padding-right: 20px
& .text
font-size: 20px
&__title
font-size: 30px
margin-bottom: 40px
font-family: 'ProximaNova-Bold', sans-serif
&__buttons
margin-bottom: 45px
+m
text-align: center
margin-bottom: 10px
& .btn
min-width: 200px
margin-bottom: 15px
margin-right: 10px
& .btn
border-radius: 10px
box-shadow: 0 11px 20px 0 rgba(0, 0, 0, 0.1)
&_main
padding: 80px 0
+m
padding: 50px 0
& .title
font-size: 30px
+m
font-size: 27px
&_gray &_gray
background: $bg background: $bg
&_pink-light &_pink-light
@ -978,6 +1041,7 @@ a[name]
transform: translateX(-50%) transform: translateX(-50%)
+t +t
max-width: calc(100% - 30px) max-width: calc(100% - 30px)
&_gradient &_gradient
background-image: linear-gradient(-225deg, $bg 0%, #FFF2F2 100%) background-image: linear-gradient(-225deg, $bg 0%, #FFF2F2 100%)
&_tabs &_tabs
@ -1009,51 +1073,151 @@ a[name]
background: $bg background: $bg
+t +t
padding: 80px 0 40px padding: 80px 0 40px
&_video &_video
padding: 70px 0 70px padding: 70px 0
&_flex &__center background-image: linear-gradient(345deg, #eeeefa, #dff0ff)
display: flex
&__column +m
flex: 50% padding: 30px 0
&_img & .btn.js-video-modal
background-position: bottom center padding-top: 5px
background-size: contain width: auto
box-shadow: 0 11px 24px 0 rgba(255, 147, 147, 0.3)
&_text & img
padding-right: 20px margin-bottom: -11px
&__column .text &_video &__column_img
font-size: 20px text-align: center
&__title &_video &__column_text
font-size: 30px flex: 1 0 50%
margin-bottom: 40px
font-family: 'ProximaNova-Bold', sans-serif
&__buttons &_video &__center
margin-bottom: 45px +m
flex-direction: column
& .btn &_video &__buttons
border-radius: 10px +m
box-shadow: 0 11px 20px 0 rgba(0, 0, 0, 0.1) padding-top: 15px;
&_video &__column_img &_about
text-align: center padding-bottom: 0
& img &_about &__center
height: 270px +m
border-radius: 10px flex-direction: column-reverse
box-shadow: 0 30px 60px 0 rgba(0, 0, 0, 0.2)
width: auto &_about &__column_img
background-image: url(/static/img/girl-umbrela.png)
background-position: 40% bottom
+m
flex: 0 0 300px
&_video .btn &_flex &__center
box-shadow: 0 11px 24px 0 rgba(255, 147, 147, 0.3) display: flex
&_school .title
+m
text-align: left
&_partners
.title
+m
text-align: left
.text
+m
text-align: left
&_counters
background: #eeeefa
&_counters .text_only_curve &_counters .text_only_curve
margin-bottom: 0 margin-bottom: 0
& img
+m
left: -10px
bottom: -125px
&_packages &__center
width: 680px
+m
width: 100%
&_students
padding-bottom: 0
&_students &__column_img
background-image: url(/static/img/zlata.png)
height: 400px
background-position: bottom center
+m
height: auto
&_students &__center:nth-child(1)
+m
flex-direction: column
&_students &__center:nth-child(2)
margin-top: 40px
+m
margin-top: 0
&_students &__center:nth-child(2) &__column_text
+m
padding: 0 0 0 10px
&_course .go
flex-wrap: wrap
&__quote
font-size: 18px
margin: -50px 0 -50px -100px
position: relative
padding: 50px 0 100px 100px
+m
font-size: 14px
&:before
background-image: url(/static/img/bubble-icon.svg?196dc3af196a)
background-size: contain
background-repeat: no-repeat
position: absolute
width: 100%
height: 80%
content: ' '
left: -2%
top: -3%
z-index: -1
+m
width: 90%;
left: 15%
top: 10%
& &-text
margin-bottom: 20px
& &-name
text-decoration: underline
font-size: 15px
+m
font-size: 13px
&__course-buttons
+m
margin-top: 10px
& .btn
margin-right: 10px
.reviews-section .reviews-section
background: linear-gradient(to bottom, rgba(248,248,248,1) 0%, rgba(254,244,244,1) 100%) background: linear-gradient(to bottom, rgba(248,248,248,1) 0%, rgba(254,244,244,1) 100%)
@ -1062,7 +1226,6 @@ a[name]
display: flex display: flex
height: 650px height: 650px
+m +m
padding-bottom: 20px
flex-direction: column flex-direction: column
&__title &__title
flex: 0 0 60%; flex: 0 0 60%;
@ -1071,11 +1234,6 @@ a[name]
+m +m
padding: 20px 0 0 padding: 20px 0 0
flex: 0 flex: 0
text-align: center
.text
text-align: left
+m
text-align: center
.ava .ava
display: block display: block
@ -1110,8 +1268,8 @@ a[name]
color: $cl color: $cl
align-items: center align-items: center
&__counter &__counter
color: $gray-dark
margin-right: 5px margin-right: 5px
+fb
font-size: 13px font-size: 13px
+t +t
font-size: 12px font-size: 12px
@ -1151,7 +1309,12 @@ a[name]
&__info &__info
flex: 0 0 calc(100% - 50px) flex: 0 0 calc(100% - 50px)
display: flex display: flex
&__name, &__name
font-size: 13px
text-transform: capitalize
letter-spacing: 0.5px
line-height: 1.1
color: $gray-dark
&__date &__date
+fb +fb
font-size: 10px font-size: 10px
@ -1600,47 +1763,52 @@ a.grey-link
height: 100% height: 100%
.teachers .teachers
margin-bottom: 50px &__row
+t display: flex
margin-bottom: 0 +m
flex-direction: column
&__item &__item
display: flex display: flex
margin-bottom: 40px margin-bottom: 40px
&__ava margin-right: 40px
height: 140px padding: 30px 35px
margin-right: 25px border-radius: 10px
flex: 0 0 140px box-shadow: 0 10px 25px 0 rgba(33, 74, 211, 0.2)
width: calc(50% - 20px)
+m
width: 100%
margin-bottom: 25px
padding: 20px 15px
&:nth-child(2)
margin-right: 0px
&__left-column
margin-right: 40px
flex: 0 0 80px
+t +t
height: 100px
margin-right: 20px margin-right: 20px
flex: 0 0 100px &__ava
+m height: 80px
height: 95px
margin-right: 25px
flex: 0 0 95px
&__title &__title
margin-bottom: 5px margin-bottom: 5px
display: flex
+fb
font-size: 20px font-size: 20px
letter-spacing: 2px font-family: 'ProximaNova-Regular', sans-serif
letter-spacing: normal
flex: 1
+t +t
font-size: 12px font-size: 18px
letter-spacing: 2px
+m
display: block
&-name
flex: 1
&__social &__social
margin-top: 15px
.social__item .social__item
margin-right: 3px margin-right: 5px
font-size: 25px font-size: 20px
+fb +fb
&__content &__content
font-size: 16px font-size: 14px
line-height: (22/16) line-height: (22/16)
&__wrap &__right-column
flex: 1 flex: 1
& .btn
margin-top: 10px
.toggle .toggle
font-size: 14px font-size: 14px
@ -1711,31 +1879,31 @@ a.grey-link
.courses .courses
&__list &__list
display: flex display: flex
margin: 0 -10px margin: 0 -20px
flex-wrap: wrap flex-wrap: wrap
+m +m
display: block margin: 0 -10px
margin: 0
&__item &__item
display: block display: block
margin: 0 10px 60px margin: 0 20px 75px
color: $cl color: $cl
flex: 0 0 calc(33.33% - 20px) flex: 0 0 288px
+t +t
margin-bottom: 50px !important margin-bottom: 50px !important
+m +m
margin: 0 0 30px margin: 0 5px 30px
flex: 0 0 calc(50% - 15px)
&__preview &__preview
display: block display: block
position: relative position: relative
margin-bottom: 15px margin-bottom: 15px
border-radius: 2px border-radius: 10px
color: $cl color: $cl
overflow: hidden overflow: hidden
+t +t
margin-bottom: 10px margin-bottom: 10px
&__preview:hover &__view &:hover
opacity: .9 box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 0.15)
&__label &__label
position: absolute position: absolute
top: 0 top: 0
@ -1809,19 +1977,29 @@ a.grey-link
font-size: 12px font-size: 12px
letter-spacing: 2px letter-spacing: 2px
color: $cl color: $cl
+m
font-size: 10px
&__old-price &__old-price
margin-right: -15px margin-right: -15px
&__title &__title
display: block display: block
margin-bottom: 10px margin-bottom: 10px
+fs
font-size: 18px font-size: 18px
color: $cl color: $cl
+t +t
line-height: 1.33 line-height: 1.33
+m
font-size: 13px
&__content
color: $gray-dark
font-size: 14px
+m
font-size: 11px
&__theme &__theme
text-transform: uppercase text-transform: uppercase
flex: 1 flex: 1
+m
font-size: 10px
&__user &__user
margin-top: 20px margin-top: 20px
&_two &__item &_two &__item
@ -1844,6 +2022,42 @@ a.grey-link
padding-right: 15px padding-right: 15px
flex: 0 0 235px flex: 0 0 235px
.new-courses
&__block-title
font-size: 25px
margin-bottom: 20px
margin-top: -6px
+m
margin-bottom: 10px
&__item
display: flex
margin-bottom: 10px
&__image
width: 80px
height: 57px
border-radius: 2px
&__details
margin-left: 15px
&__title
font-size: 15px
color: #333333
margin-bottom: 5px
display: block
+m
font-size: 14px
&__author
color: #888888
font-size: 10px
text-transform: uppercase
display: block
+m
font-size: 9px
.load .load
margin-top: 30px margin-top: 30px
+m +m
@ -1892,12 +2106,11 @@ a.grey-link
flex-wrap: wrap flex-wrap: wrap
&__item &__item
margin: 0 10px 20px margin: 0 10px 20px
flex: 0 0 calc(16.66% - 20px) flex: 0 0 calc(20% - 20px)
text-align: center text-align: center
height: 160px height: 170px
+m +m
margin: 0 5px 10px flex: 0 0 calc(50% - 20px)
flex: 0 0 100px
& a & a
height: 120px height: 120px
display: flex display: flex
@ -1909,6 +2122,7 @@ a.grey-link
font-weight: bold; font-weight: bold;
letter-spacing: 2px; letter-spacing: 2px;
text-transform: uppercase; text-transform: uppercase;
margin-top: 10px
&__pic &__pic
max-width: 100% max-width: 100%
object-fit: contain object-fit: contain
@ -2005,9 +2219,6 @@ a.grey-link
&_lg &__content &_lg &__content
padding: 0 0 0 30px padding: 0 0 0 30px
font-size: 16px font-size: 16px
&:before,
&:after
.footer .footer
padding: 50px 0 30px padding: 50px 0 30px
@ -2024,7 +2235,7 @@ a.grey-link
flex: 0 0 25% flex: 0 0 25%
+t +t
margin: 10px margin: 10px
&__row_second &__col:nth-child(2) &__row_subscr &__col:nth-child(2)
flex: 1 flex: 1
&__nav &__nav
+m +m
@ -2131,22 +2342,21 @@ a.grey-link
&__head &__head
position: relative position: relative
height: 36px height: 36px
border-bottom: 1px solid rgba(82, 82, 82, 0.2) border: 1px solid rgba(82, 82, 82, 0.2)
transition: border-color .2s transition: border-color .2s
font-size: 18px font-size: 16px
line-height: 36px line-height: 36px
white-space: nowrap white-space: nowrap
text-overflow: ellipsis text-overflow: ellipsis
color: $gray
cursor: pointer cursor: pointer
transition: border-color .2s transition: border-color .2s
padding-right: 15px padding: 0px 18px
overflow: hidden overflow: hidden
&:after &:after
content: '' content: ''
position: absolute position: absolute
top: 14px top: 14px
right: 0 right: 18px
+arr(8,8,$cl,b) +arr(8,8,$cl,b)
&__drop &__drop
position: absolute position: absolute
@ -2462,19 +2672,23 @@ a.grey-link
.main-video-preview .main-video-preview
z-index: 10 z-index: 10
position: relative position: relative
box-shadow: 0 10px 100px rgba(0,0,0,0.20) box-shadow: 0 30px 60px 0 rgba(0, 0, 0, 0.2)
width: 100%
height: auto
cursor: pointer cursor: pointer
max-height: 270px
border-radius: 10px
max-width: 100%
+m
width: 100%
height: auto
.head .head
display: flex display: flex
margin-bottom: 50px margin-bottom: 30px
+t +t
margin-bottom: 40px margin-bottom: 40px
flex-wrap: wrap flex-wrap: wrap
&__right &__right
flex: 0 0 220px display: flex
+t +t
margin-left: auto !important margin-left: auto !important
+m +m
@ -2484,8 +2698,7 @@ a.grey-link
&__title &__title
position: relative position: relative
margin: 0 margin: 0
padding: 0 20px 0 260px flex: 1
flex: 0 0 calc(100% - 220px)
z-index: 4 z-index: 4
+t +t
max-width: 100% max-width: 100%
@ -2499,7 +2712,7 @@ a.grey-link
transform: translateY(-50%) transform: translateY(-50%)
z-index: -2 z-index: -2
&__field &__field
margin: 0 margin: 0 0 20px
&__text &__text
padding-right: 30px padding-right: 30px
font-size: 18px font-size: 18px
@ -2605,7 +2818,7 @@ a.grey-link
//&:not(:last-child) //&:not(:last-child)
margin-right: 40px margin-right: 40px
+t +t
margin-right: 30px margin-right: 20px
&__icon &__icon
margin-right: 10px margin-right: 10px
font-size: 0 font-size: 0
@ -2914,6 +3127,8 @@ a.grey-link
height: 160px height: 160px
.questions .questions
&__items
overflow: hidden
&__anchor &__anchor
display: block display: block
position: relative position: relative
@ -3932,16 +4147,6 @@ a.grey-link
&_info &_info
background: $green background: $green
.mobile-hide
+m
display: none
.mobile-show
display: none
+m
display: block
.school .school
display: flex display: flex
position: relative position: relative
@ -3952,6 +4157,11 @@ a.grey-link
margin-bottom: -40px margin-bottom: -40px
+m +m
margin: 0 -10px -30px margin: 0 -10px -30px
&_three-col
margin-top: 50px
+m
flex-direction: column
margin-top: 40px
&__col &__col
padding: 0 15px padding: 0 15px
text-align: center text-align: center
@ -3962,6 +4172,10 @@ a.grey-link
+m +m
margin-bottom: 30px margin-bottom: 30px
padding: 0 10px padding: 0 10px
&_three-col &__col
flex: 0 0 33%
+t
flex: 0 0 33%
&__preview &__preview
margin-bottom: 25px margin-bottom: 25px
font-size: 0 font-size: 0
@ -3983,11 +4197,6 @@ a.grey-link
text-transform: uppercase text-transform: uppercase
+t +t
font-size: 10px font-size: 10px
&_main &__col
+t
flex: 0 0 25%
+m
flex: 0 0 50%
&_main &__preview &_main &__preview
margin-bottom: 10px margin-bottom: 10px
font-size: 32px font-size: 32px
@ -4438,7 +4647,8 @@ a
padding-top: 0 padding-top: 0
+m +m
padding-left: 0 padding-left: 0
height: 220px height: 260px
flex-direction: column
&__text &__text
font-size: 30px font-size: 30px
text-shadow: none text-shadow: none
@ -4448,7 +4658,6 @@ a
+m +m
font-size: 16px font-size: 16px
width: auto width: auto
font-weight: bold
&__link &__link
font-size: 15px font-size: 15px
color: black color: black
@ -4463,10 +4672,16 @@ a
text-transform: uppercase text-transform: uppercase
font-family: 'ProximaNova-Bold' font-family: 'ProximaNova-Bold'
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.05), 0 9px 24px 0 rgba(33, 74, 211, 0.1) box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.05), 0 9px 24px 0 rgba(33, 74, 211, 0.1)
z-index: 1
+m +m
margin-top: 20px margin-top: 20px
font-size: 10px font-size: 10px
padding: 10px 14px padding: 10px 14px
left: 50%
position: absolute
margin-left: -114px
width: 200px
bottom: 35px
&__link:hover &__link:hover
background: #ddd background: #ddd
&__image-column &__image-column
@ -4474,6 +4689,8 @@ a
text-align: center text-align: center
position: relative position: relative
padding-right: 20px padding-right: 20px
+m
flex: 1
& img & img
position: absolute position: absolute
bottom: 0 bottom: 0
@ -4489,6 +4706,7 @@ a
padding-top: 40px padding-top: 40px
+m +m
padding-top: 20px padding-top: 20px
flex: 0
&__countdown-title &__countdown-title
color: black color: black
font-size: 15px font-size: 15px
@ -4499,6 +4717,11 @@ a
position: absolute position: absolute
transform: translateX(-50%) transform: translateX(-50%)
left: 50% left: 50%
padding: 5px
background: rgba(255, 255, 255, 0.5)
border-radius: 5px
+m
padding: 5px 20px
&__countdown &__countdown
display: flex display: flex
color: black color: black
@ -4649,6 +4872,9 @@ a
height: 200px height: 200px
+t +t
margin-bottom: 10px margin-bottom: 10px
+m
margin-left: auto
margin-right: auto
&__cover &__cover
object-fit: cover; object-fit: cover;
width: 100%; width: 100%;
@ -4837,3 +5063,185 @@ a
padding-top: 80px padding-top: 80px
min-height: 200px min-height: 200px
padding-bottom: 60px padding-bottom: 60px
.course-search
text-align: center
&__form
position: relative
&__query
width: 600px
height: 50px
border-radius: 30px
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.1)
border: solid 1px rgba(0, 0, 0, 0.03)
background-color: #ffffff
padding: 10px 22px
font-size: 16px
+m
width: 100%
&__search
width: 58px
height: 40px
border-radius: 20px
top: 5px
position: absolute
margin-left: -65px
+m
margin-left: 0
right: 10px
& .icon
width: 17px
height: 17px
opacity: 0.5
margin-left: -4px
top: -2px
+m
margin-left: 0
&__text
font-size: 10px
color: #9b9b9b
margin: 30px 0 15px
&__tags
width: 460px
max-width: 100%
margin: 0 auto
.tag
color: black
height: 30px
border-radius: 20px
border: solid 1px #e7e7e7
padding: 5px 15px
display: inline-block
margin: 0 3px 8px
&__remove .icon
fill: #888
width: 9px
height: 9px
margin-left: 7px
.courses-filter
+m
display: none
&__select
width: 200px
margin-left: 20px
.autocomplete
position: relative
font-size: 15px
margin: 8px 0 22px
&__query
width: 100%
background: none
padding: 5px 0
border-bottom: 1px solid #e6e6e6
&__options
position: absolute
width: 100%
overflow: auto
top: 22px
height: auto
max-height: 70px
z-index: 1
.emoji
height: 32px
margin-bottom: -7px
.packages
display: flex
overflow-x: auto
background: white
padding: 30px
margin: -50px
+m
margin: 0
.package
position: relative
border-radius: 10px
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.1)
margin-right: 20px
padding: 20px
width: calc(25% - 20px)
display: flex
flex-direction: column
text-align: center
+m
min-width: 190px
padding: 20px 28px
&__btn
width: 100%
&__title
font-size: 14px
text-align: center
margin-top: 20px
font-family: 'ProximaNova-Regular', sans-serif
+m
margin-top: 0
&__desc
font-size: 13px
height: 35px
&__price
font-size: 40px
margin: 20px 0 40px
+m
margin: 17px 0 13px
&__high-price
text-decoration: line-through
font-size: 15px
color: #9b9b9b
height: 15px
margin-bottom: -15px
+m
margin-top: -15px
margin-bottom: 0
&__options
padding: 30px 0
border-image-source: linear-gradient(to right, #ffe2eb, #d8f5f5)
border-image-slice: 1
border-top: 1px solid
text-align: left
font-size: 12px
line-height: 18px
flex: 1
+m
padding: 19px 0 30px
& p
margin-top: 6px
&__medal
position: absolute
height: 53px
right: 18px
top: -17px
&__trial-btn img
width: 24px
margin-bottom: -6px
.mobile-hide
+m
display: none
.mobile-show
display: none
+m
display: block

Loading…
Cancel
Save