Merge branch 'master' of https://gitlab.com/lilcity/backend into feature/lil-583

remotes/origin/hotfix/LIL-691
gzbender 7 years ago
commit 2b52d9852c
  1. 71
      api/v1/serializers/course.py
  2. 98
      api/v1/serializers/mixins.py
  3. 54
      api/v1/serializers/payment.py
  4. 3
      api/v1/urls.py
  5. 109
      api/v1/views.py
  6. 26
      apps/content/templates/content/blocks/video.html
  7. 3
      apps/course/filters.py
  8. 18
      apps/course/migrations/0044_course_age.py
  9. 30
      apps/course/migrations/0044_livelessoncomment.py
  10. 32
      apps/course/models.py
  11. 2
      apps/course/templates/course/_items.html
  12. 44
      apps/course/templates/course/course.html
  13. 51
      apps/course/templates/course/course_only_lessons.html
  14. 25
      apps/course/templates/course/courses.html
  15. 6
      apps/course/templates/course/inclusion/category_items.html
  16. 50
      apps/course/templates/course/lesson.html
  17. 13
      apps/course/views.py
  18. 16
      apps/payment/models.py
  19. 35
      apps/payment/views.py
  20. 5
      apps/school/templates/blocks/online.html
  21. 12
      apps/school/templates/school/livelesson_detail.html
  22. 2
      apps/school/templates/school/livelessons_list.html
  23. 7
      apps/school/templates/summer/online.html
  24. 6
      apps/school/templates/summer/prolong_btn.html
  25. 4
      apps/school/templates/summer/promo.html
  26. 6
      apps/school/templates/summer/schedule_purchased.html
  27. 5
      apps/school/views.py
  28. 7
      apps/user/templates/user/profile.html
  29. 17
      project/context_processors.py
  30. 14
      project/pusher.py
  31. 9
      project/settings.py
  32. 2
      project/templates/blocks/about.html
  33. 118
      project/templates/blocks/gallery.html
  34. 5
      project/templates/blocks/last_courses.html
  35. 15
      project/templates/blocks/lil_store_js.html
  36. 7
      project/templates/blocks/live.html
  37. 37
      project/templates/blocks/popup_buy.html
  38. 2
      project/templates/blocks/popup_course_lock.html
  39. 2
      project/templates/blocks/promo.html
  40. 2
      project/templates/lilcity/edit_index.html
  41. 1
      project/templates/lilcity/index.html
  42. 2
      project/templates/lilcity/main.html
  43. 6
      project/urls.py
  44. 2
      project/views.py
  45. 4
      requirements.txt
  46. 71
      web/src/components/Comment.vue
  47. 53
      web/src/components/CommentForm.vue
  48. 134
      web/src/components/Comments.vue
  49. 136
      web/src/components/ContestRedactor.vue
  50. 186
      web/src/components/CourseRedactor.vue
  51. 73
      web/src/components/LessonRedactor.vue
  52. 93
      web/src/components/blocks/BlockContent.vue
  53. 20
      web/src/components/blocks/BlockImages.vue
  54. 3
      web/src/components/blocks/Image.vue
  55. 28
      web/src/components/inputs/LilSelect.vue
  56. 4
      web/src/components/redactor/VueRedactor.vue
  57. 19
      web/src/js/app.js
  58. 17
      web/src/js/contest-redactor.js
  59. 20
      web/src/js/course-redactor.js
  60. 222
      web/src/js/modules/api.js
  61. 1
      web/src/js/modules/comments.js
  62. 112
      web/src/js/modules/courses.js
  63. 68
      web/src/js/modules/popup.js
  64. 143
      web/src/sass/_common.sass

@ -10,7 +10,7 @@ from apps.course.models import (
Comment, CourseComment, LessonComment,
Material, Lesson,
Like,
)
LiveLessonComment)
from .content import (
ImageObjectSerializer, ContentSerializer, ContentCreateSerializer,
GallerySerializer, GalleryImageSerializer,
@ -137,6 +137,7 @@ class CourseCreateSerializer(DispatchContentMixin,
'from_author',
'cover',
'price',
'age',
'is_infinite',
'deferred_start_at',
'category',
@ -288,6 +289,7 @@ class CourseSerializer(serializers.ModelSerializer):
'from_author',
'cover',
'price',
'age',
'is_infinite',
'deferred_start_at',
'category',
@ -327,6 +329,7 @@ class CommentSerializer(serializers.ModelSerializer):
'parent',
'deactivated_at',
'created_at',
'created_at_humanize',
'update_at',
)
@ -342,6 +345,8 @@ class CommentSerializer(serializers.ModelSerializer):
return CourseCommentSerializer(instance, context=self.context).to_representation(instance)
elif isinstance(instance, LessonComment):
return LessonCommentSerializer(instance, context=self.context).to_representation(instance)
elif isinstance(instance, LiveLessonComment):
return LiveLessonCommentSerializer(instance, context=self.context).to_representation(instance)
class CourseCommentSerializer(serializers.ModelSerializer):
@ -374,3 +379,67 @@ class LessonCommentSerializer(serializers.ModelSerializer):
read_only_fields = CommentSerializer.Meta.read_only_fields + (
'children',
)
class LiveLessonCommentSerializer(serializers.ModelSerializer):
author = UserSerializer()
children = CommentSerializer(many=True)
class Meta:
model = LiveLessonComment
fields = CommentSerializer.Meta.fields + (
'live_lesson',
'children',
)
read_only_fields = CommentSerializer.Meta.read_only_fields + (
'children',
)
class CommentCreateSerializer(serializers.ModelSerializer):
obj_type = serializers.CharField(required=True)
obj_id = serializers.IntegerField(required=True)
class Meta:
model = Comment
fields = (
'id',
'content',
'author',
'parent',
'deactivated_at',
'created_at',
'update_at',
'obj_type',
'obj_id',
)
read_only_fields = (
'id',
'deactivated_at',
'created_at',
'update_at',
)
def create(self, validated_data):
obj_type = validated_data.pop('obj_type', None)
obj_id = validated_data.pop('obj_id', None)
if obj_type == Comment.OBJ_TYPE_COURSE:
validated_data['course_id'] = obj_id
return CourseCommentSerializer().create(validated_data)
elif obj_type == Comment.OBJ_TYPE_LESSON:
validated_data['lesson_id'] = obj_id
return LessonCommentSerializer().create(validated_data)
elif obj_type == Comment.OBJ_TYPE_LIVE_LESSON:
validated_data['live_lesson_id'] = obj_id
return LiveLessonCommentSerializer().create(validated_data)
def to_representation(self, instance):
if isinstance(instance, CourseComment):
return CourseCommentSerializer(instance, context=self.context).to_representation(instance)
elif isinstance(instance, LessonComment):
return LessonCommentSerializer(instance, context=self.context).to_representation(instance)
elif isinstance(instance, LiveLessonComment):
return LiveLessonCommentSerializer(instance, context=self.context).to_representation(instance)

@ -32,35 +32,23 @@ class DispatchContentMixin(object):
if 'id' in cdata and cdata['id']:
t = Text.objects.get(id=cdata.pop('id'))
serializer = TextCreateSerializer(t, data=cdata)
if serializer.is_valid():
serializer.save()
else:
serializer = TextCreateSerializer(data=cdata)
if serializer.is_valid():
serializer.save()
if serializer.is_valid():
serializer.save()
elif ctype == 'image':
if 'id' in cdata and cdata['id']:
image = Image.objects.get(id=cdata.pop('id'))
serializer = ImageCreateSerializer(image, data=cdata)
if serializer.is_valid():
image = serializer.save()
else:
continue
try:
image_object = ImageObject.objects.get(id=cdata['img'])
except ImageObject.DoesNotExist:
pass
else:
image.img = image_object
image.save()
else:
serializer = ImageCreateSerializer(data=cdata)
if serializer.is_valid():
image = serializer.save()
else:
continue
if serializer.is_valid():
image = serializer.save()
else:
continue
if 'img' in cdata:
try:
image_object = ImageObject.objects.get(id=cdata['img'])
image_object = ImageObject.objects.get(id=cdata.get('img'))
except ImageObject.DoesNotExist:
pass
else:
@ -71,25 +59,15 @@ class DispatchContentMixin(object):
if 'id' in cdata and cdata['id']:
it = ImageText.objects.get(id=cdata.pop('id'))
serializer = ImageTextCreateSerializer(it, data=cdata)
if serializer.is_valid():
it = serializer.save()
else:
continue
try:
image_object = ImageObject.objects.get(id=cdata['img'])
except ImageObject.DoesNotExist:
pass
else:
it.img = image_object
it.save()
else:
serializer = ImageTextCreateSerializer(data=cdata)
if serializer.is_valid():
it = serializer.save()
else:
continue
if serializer.is_valid():
it = serializer.save()
else:
continue
if 'img' in cdata:
try:
image_object = ImageObject.objects.get(id=cdata['img'])
image_object = ImageObject.objects.get(id=cdata.get('img'))
except ImageObject.DoesNotExist:
pass
else:
@ -100,52 +78,36 @@ class DispatchContentMixin(object):
if 'id' in cdata and cdata['id']:
v = Video.objects.get(id=cdata.pop('id'))
serializer = VideoCreateSerializer(v, data=cdata)
if serializer.is_valid():
serializer.save()
else:
serializer = VideoCreateSerializer(data=cdata)
if serializer.is_valid():
serializer.save()
if serializer.is_valid():
serializer.save()
elif ctype == 'images':
if 'id' in cdata and cdata['id']:
g = Gallery.objects.get(id=cdata['id'])
g.position = cdata['position']
g.title = cdata['title']
g.uuid = cdata['uuid']
setattr(g, obj_type, obj)
g.save()
if 'images' in cdata:
for image in cdata['images']:
if 'img' in image and image['img']:
if 'id' in image and image['id']:
gi = GalleryImage.objects.get(id=image['id'])
gi.img = ImageObject.objects.get(id=image['img'])
gi.save()
else:
gi = GalleryImage.objects.create(
gallery=g,
img=ImageObject.objects.get(id=image['img'])
)
else:
g = Gallery(
position=cdata['position'],
title=cdata['title'],
uuid=cdata['uuid'],
)
setattr(g, obj_type, obj)
g.save()
if 'images' in cdata:
for image in cdata['images']:
if 'img' in image and image['img']:
if 'id' in image and image['id']:
gi = GalleryImage.objects.get(id=image['id'])
gi.img = ImageObject.objects.get(id=image['img'])
gi.save()
else:
gi = GalleryImage.objects.create(
gallery=g,
img=ImageObject.objects.get(id=image['img'])
)
setattr(g, obj_type, obj)
g.save()
if 'images' in cdata:
for image in cdata['images']:
if 'img' in image and image['img']:
if 'id' in image and image['id']:
gi = GalleryImage.objects.get(id=image['id'])
gi.img = ImageObject.objects.get(id=image['img'])
gi.save()
else:
gi = GalleryImage.objects.create(
gallery=g,
img=ImageObject.objects.get(id=image['img'])
)
class DispatchMaterialMixin(object):

@ -68,7 +68,6 @@ class PaymentSerializer(serializers.ModelSerializer):
class AuthorBalanceSerializer(serializers.ModelSerializer):
author = UserSerializer()
# payment = PaymentSerializer()
payment = serializers.SerializerMethodField()
class Meta:
@ -111,6 +110,41 @@ class AuthorBalanceSerializer(serializers.ModelSerializer):
return data
class PaymentSerializer(serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = Payment
fields = BASE_PAYMENT_FIELDS
read_only_fields = (
'id',
'user',
'data',
'created_at',
'update_at',
)
def to_representation(self, instance):
if isinstance(instance, CoursePayment):
return CoursePaymentSerializer(instance, context=self.context).to_representation(instance)
elif isinstance(instance, SchoolPayment):
return SchoolPaymentSerializer(instance, context=self.context).to_representation(instance)
class CoursePaymentCreateSerializer(serializers.ModelSerializer):
class Meta:
model = CoursePayment
fields = BASE_PAYMENT_FIELDS + ('course',)
read_only_fields = (
'id',
'user',
'course',
'created_at',
'update_at',
)
class CoursePaymentSerializer(serializers.ModelSerializer):
user = UserSerializer()
course = CourseSerializer()
@ -127,6 +161,24 @@ class CoursePaymentSerializer(serializers.ModelSerializer):
)
class SchoolPaymentCreateSerializer(serializers.ModelSerializer):
class Meta:
model = SchoolPayment
fields = BASE_PAYMENT_FIELDS + (
'weekdays',
'date_start',
'date_end',
)
read_only_fields = (
'id',
'user',
'course',
'created_at',
'update_at',
)
class SchoolPaymentSerializer(serializers.ModelSerializer):
user = UserSerializer()

@ -17,7 +17,7 @@ from .views import (
GalleryViewSet, GalleryImageViewSet,
UserViewSet, LessonViewSet, ImageObjectViewSet,
SchoolScheduleViewSet, LiveLessonViewSet,
PaymentViewSet,
PaymentViewSet, ObjectCommentsViewSet,
ContestViewSet, ContestWorkViewSet)
router = DefaultRouter()
@ -27,6 +27,7 @@ router.register(r'baners', BanerViewSet, base_name='baners')
router.register(r'categories', CategoryViewSet, base_name='categories')
router.register(r'courses', CourseViewSet, base_name='courses')
router.register(r'comments', CommentViewSet, base_name='comments')
router.register(r'obj-comments', ObjectCommentsViewSet, base_name='obj-comments')
router.register(r'materials', MaterialViewSet, base_name='materials')
router.register(r'lessons', LessonViewSet, base_name='lessons')
router.register(r'likes', LikeViewSet, base_name='likes')

@ -1,7 +1,9 @@
from datetime import datetime
from django.contrib.auth import get_user_model
from rest_framework import status, views, viewsets, generics
from rest_framework.decorators import detail_route, list_route, action
from rest_framework.decorators import detail_route, list_route, action, permission_classes, authentication_classes
from rest_framework.response import Response
from . import ExtendedModelViewSet
@ -11,10 +13,11 @@ from .serializers.course import (
CategorySerializer, LikeSerializer,
CourseSerializer, CourseCreateSerializer,
CourseBulkChangeCategorySerializer,
CommentSerializer,
CommentSerializer, CommentCreateSerializer,
MaterialSerializer, MaterialCreateSerializer,
LessonSerializer, LessonCreateSerializer,
LikeCreateSerializer)
LikeCreateSerializer, CourseCommentSerializer, LessonCommentSerializer,
LiveLessonCommentSerializer)
from .serializers.content import (
BanerSerializer,
ImageSerializer, ImageCreateSerializer,
@ -34,7 +37,7 @@ from .serializers.payment import (
AuthorBalanceSerializer, AuthorBalanceCreateSerializer,
PaymentSerializer, CoursePaymentSerializer,
SchoolPaymentSerializer,
)
CoursePaymentCreateSerializer, SchoolPaymentCreateSerializer)
from .serializers.user import (
AuthorRequestSerializer,
UserSerializer, UserPhotoSerializer,
@ -54,7 +57,7 @@ from apps.course.models import (
Comment, CourseComment, LessonComment,
Material, Lesson,
Like,
)
LiveLessonComment)
from apps.config.models import Config
from apps.content.models import (
Baner, Image, Text, ImageText, Video,
@ -66,6 +69,7 @@ from apps.payment.models import (
)
from apps.school.models import SchoolSchedule, LiveLesson
from apps.user.models import AuthorRequest
from project.pusher import pusher
User = get_user_model()
@ -429,15 +433,15 @@ class ConfigViewSet(generics.RetrieveUpdateAPIView):
class CommentViewSet(ExtendedModelViewSet):
queryset = Comment.objects.filter(level=0)
queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = (IsAdmin,)
permission_classes = (IsAuthorObjectOrAdmin,)
def get_queryset(self):
queryset = self.queryset
is_deactivated = self.request.query_params.get('is_deactivated', '0')
if is_deactivated == '0':
queryset = queryset
queryset = queryset.filter(level=0)
elif is_deactivated == '1':
queryset = queryset.filter(deactivated_at__isnull=True)
elif is_deactivated == '2':
@ -446,6 +450,79 @@ class CommentViewSet(ExtendedModelViewSet):
return queryset
class ObjectCommentsViewSet(ExtendedModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentCreateSerializer
ordering_fields = ('update_at', )
def get_queryset(self):
queryset = self.queryset
obj_type = self.request.query_params.get('obj_type')
obj_id = self.request.query_params.get('obj_id')
is_deactivated = self.request.query_params.get('is_deactivated')
if obj_type == Comment.OBJ_TYPE_COURSE:
queryset = CourseComment.objects.filter(course=obj_id)
elif obj_type == Comment.OBJ_TYPE_LESSON:
queryset = LessonComment.objects.filter(lesson=obj_id)
elif obj_type == Comment.OBJ_TYPE_LIVE_LESSON:
queryset = LiveLessonComment.objects.filter(live_lesson=obj_id)
if is_deactivated == '0':
queryset = queryset.filter(level=0)
elif is_deactivated == '1':
queryset = queryset.filter(deactivated_at__isnull=True)
elif is_deactivated == '2':
queryset = queryset.filter(deactivated_at__isnull=False)
return queryset
def get_serializer_class(self):
if self.request.method == 'POST':
return CommentCreateSerializer
obj_type = self.request.query_params.get('obj_type')
serializer_class = CommentSerializer
if obj_type == Comment.OBJ_TYPE_COURSE:
serializer_class = CourseCommentSerializer
elif obj_type == Comment.OBJ_TYPE_LESSON:
serializer_class = LessonCommentSerializer
elif obj_type == Comment.OBJ_TYPE_LIVE_LESSON:
serializer_class = LiveLessonCommentSerializer
return serializer_class
def perform_create(self, serializer):
obj_type = self.request.data.get('obj_type')
obj_id = self.request.data.get('obj_id')
serializer.save()
try:
pusher().trigger(f'comments_{obj_type}_{obj_id}', 'add', serializer.data)
except Exception as e:
print(e)
@permission_classes((IsAuthorObjectOrAdmin,))
def perform_destroy(self, instance):
obj_type = None
obj_id = None
if isinstance(instance, LessonComment):
obj_type = Comment.OBJ_TYPE_LESSON
obj_id = instance.lesson_id
elif isinstance(instance, CourseComment):
obj_type = Comment.OBJ_TYPE_COURSE
obj_id = instance.course_id
elif isinstance(instance, LiveLessonComment):
obj_type = Comment.OBJ_TYPE_LIVE_LESSON
obj_id = instance.live_lesson_id
serializer = self.get_serializer(instance)
try:
pusher().trigger(f'comments_{obj_type}_{obj_id}', 'delete', serializer.data)
except Exception as e:
print(e)
instance.delete()
@authentication_classes([])
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset()).filter(parent__isnull=True)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class AuthorRequestViewSet(ExtendedModelViewSet):
queryset = AuthorRequest.objects.all()
serializer_class = AuthorRequestSerializer
@ -453,7 +530,7 @@ class AuthorRequestViewSet(ExtendedModelViewSet):
filter_fields = ('status',)
class PaymentViewSet(ExtendedModelViewSet):
class PaymentViewSet(viewsets.ModelViewSet):
queryset = Payment.objects.all()
serializer_class = PaymentSerializer
permission_classes = (IsAdmin,)
@ -465,6 +542,16 @@ class PaymentViewSet(ExtendedModelViewSet):
)
search_fields = ('user__email', 'user__first_name', 'user__last_name',)
def get_serializer(self, instance, *args, **kwargs):
serializer_class = self.get_serializer_class()
if 'update' in self.action:
if isinstance(instance, CoursePayment):
serializer_class = CoursePaymentCreateSerializer
elif isinstance(instance, SchoolPayment):
serializer_class = SchoolPaymentCreateSerializer
kwargs['context'] = self.get_serializer_context()
return serializer_class(instance, *args, **kwargs)
def get_queryset(self):
queryset = self.queryset
course = self.request.query_params.get('course')
@ -481,10 +568,12 @@ class PaymentViewSet(ExtendedModelViewSet):
user = request.query_params.get('user')
course = request.query_params.get('course')
weekdays = request.query_params.getlist('weekdays[]')
date_start = request.query_params.get('date_start')
user = user and User.objects.get(pk=user)
course = course and Course.objects.get(pk=course)
date_start = date_start and datetime.strptime(date_start, '%Y-%m-%d')
return Response(Payment.calc_amount(user=user, course=course, weekdays=weekdays))
return Response(Payment.calc_amount(user=user, course=course, date_start=date_start, weekdays=weekdays))
class ContestViewSet(ExtendedModelViewSet):

@ -15,29 +15,3 @@
</div>
</div>
</div>
<style>
@media only screen and (max-width: 639px) {
iframe {
width: 100%;
height: 240px;
}
.iframe__container {
text-align: center;
width: 100%;
padding-bottom: 56.25%;
margin-bottom: -56.25%;
}
}
@media only screen and (min-width: 640px) {
.iframe__container {
text-align: center;
}
iframe {
width: 640px;
height: 360px;
}
}
</style>

@ -5,7 +5,8 @@ from .models import Course
class CourseFilter(django_filters.FilterSet):
category = django_filters.CharFilter(field_name='category')
age = django_filters.ChoiceFilter(field_name='age', choices=Course.AGE_CHOICES)
class Meta:
model = Course
fields = ['category']
fields = ['category', 'age']

@ -0,0 +1,18 @@
# Generated by Django 2.0.6 on 2018-09-07 00:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0043_auto_20180824_2132'),
]
operations = [
migrations.AddField(
model_name='course',
name='age',
field=models.SmallIntegerField(choices=[(0, 'Любой возраст'), (1, 'до 5'), (2, '5-7'), (3, '7-9'), (4, '9-12'), (5, '12-15'), (6, '15-18'), (7, 'от 18')], default=0),
),
]

@ -0,0 +1,30 @@
# Generated by Django 2.0.6 on 2018-09-19 15:41
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('school', '0021_schoolschedule_trial_lesson'),
('course', '0043_auto_20180824_2132'),
]
operations = [
migrations.CreateModel(
name='LiveLessonComment',
fields=[
('comment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='course.Comment')),
('live_lesson', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='school.LiveLesson')),
],
options={
'verbose_name': 'Комментарий урока школы',
'verbose_name_plural': 'Комментарии уроков школы',
'ordering': ('tree_id', 'lft'),
'abstract': False,
'base_manager_name': 'objects',
},
bases=('course.comment',),
),
]

@ -10,6 +10,7 @@ from django.urls import reverse_lazy
from django.conf import settings
from polymorphic_tree.models import PolymorphicMPTTModel, PolymorphicTreeForeignKey
from apps.school.models import LiveLesson
from project.mixins import BaseModel, DeactivatedMixin
from apps.content.models import ImageObject, Gallery, Video, ContestWork
@ -49,6 +50,23 @@ class Course(BaseModel, DeactivatedMixin):
(ARCHIVED, 'Archived'),
(DENIED, 'Denied')
)
AGE_LT5 = 1
AGE_57 = 2
AGE_79 = 3
AGE_912 = 4
AGE_1215 = 5
AGE_1518 = 6
AGE_GT18 = 7
AGE_CHOICES = (
(0, 'Любой возраст'),
(AGE_LT5, 'до 5'),
(AGE_57, '5-7'),
(AGE_79, '7-9'),
(AGE_912, '9-12'),
(AGE_1215, '12-15'),
(AGE_1518, '15-18'),
(AGE_GT18, 'от 18'),
)
slug = models.SlugField(
allow_unicode=True, null=True, blank=True,
max_length=100, unique=True, db_index=True,
@ -72,6 +90,7 @@ class Course(BaseModel, DeactivatedMixin):
'Цена курса', help_text='Если цена не выставлена, то курс бесплатный',
max_digits=10, decimal_places=2, null=True, blank=True
)
age = models.SmallIntegerField(choices=AGE_CHOICES, default=0)
is_infinite = models.BooleanField(default=False)
deferred_start_at = models.DateTimeField(
'Отложенный запуск курса', help_text='Заполнить если курс отложенный',
@ -240,6 +259,9 @@ class Material(models.Model):
class Comment(PolymorphicMPTTModel, DeactivatedMixin):
OBJ_TYPE_COURSE = 'course'
OBJ_TYPE_LESSON = 'lesson'
OBJ_TYPE_LIVE_LESSON = 'live-lesson'
content = models.TextField('Текст комментария', default='')
author = models.ForeignKey(User, on_delete=models.CASCADE)
parent = PolymorphicTreeForeignKey(
@ -284,5 +306,15 @@ class LessonComment(Comment):
verbose_name_plural = 'Комментарии уроков'
class LiveLessonComment(Comment):
live_lesson = models.ForeignKey(
LiveLesson, on_delete=models.CASCADE, related_name='comments'
)
class Meta(Comment.Meta):
verbose_name = 'Комментарий урока школы'
verbose_name_plural = 'Комментарии уроков школы'
class ContestWorkComment(Comment):
contest_work = models.ForeignKey(ContestWork, on_delete=models.CASCADE, related_name='comments')

@ -54,7 +54,7 @@
{% endif %}
</div>
<a class="courses__title" href="{% url 'course' course.id %}?next={{ request.get_full_path }}">{{ course.title }}</a>
<div class="courses__content">{{ course.short_description }}
<div class="courses__content">{{ course.short_description | safe | linebreaks | truncatechars_html:300 }}
</div>
<div class="courses__user user">
<a href="{% if course.author %}{% url 'user' course.author.id %}{% endif %}">

@ -4,14 +4,14 @@
{% load rupluralize from plural %}
{% block title %}{{ course.title }} - {{ block.super }}{% endblock title %}
{% comment %} seo tags {% endcomment %}
{% block description %}{{ course.short_description }}{% endblock description%}
{% block description %}{{ course.short_description | striptags }}{% endblock description%}
{% block twurl %}{{ request.build_absolute_uri }}{% endblock twurl %}
{% block ogtitle %}{{ course.title }} - {{ block.super }}{% endblock ogtitle %}
{% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %}
{% if course.cover and course.cover.image %}
{% block ogimage %}http://{{request.META.HTTP_HOST}}{% if course.cover %}{{ course.cover.image.url }}{% else %}{% static 'img/og_courses.jpg' %}{% endif %}{% endblock ogimage %}
{% endif %}
{% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %}
{% block ogdescription %}{{ course.short_description | striptags }}{% endblock ogdescription %}
{% block content %}
<div class="section section_border course">
@ -25,6 +25,9 @@
</div>
<div class="go__title">Вернуться</div>
</a>
{% if has_full_access %}
<a class="go__btn btn" href="{% url 'course_edit' course.id %}">Редактировать</a>
{% endif %}
{% if course.author != request.user and not paid and course.price %}
<a
class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md"
@ -70,7 +73,7 @@
</a>
</div>
<div class="course__title title">{{ course.title }}</div>
<div class="course__content">{{ course.short_description | linebreaks }}</div>
<div class="course__content">{{ course.short_description | safe | linebreaks }}</div>
<a href="{% url 'user' course.author.id %}">
<div class="course__user user">
{% if course.author.photo %}
@ -270,7 +273,7 @@
</a>
</div>
<div class="course__title title">{{ course.title }}</div>
<div class="course__content">{{ course.short_description | linebreaks }}</div>
<div class="course__content">{{ course.short_description | safe | linebreaks }}</div>
<a href="{% url 'user' course.author.id %}">
<div class="course__user user user_white">
{% if course.author.photo %}
@ -340,38 +343,7 @@
<div class="section__center center center_sm">
<div class="title">Задавайте вопросы:</div>
<div class="questions">
{% if request.user.is_authenticated %}
<form class="questions__form" method="post" action="{% url 'coursecomment' course_id=course.id %}">
<input type="hidden" name="reply_id">
<div class="questions__ava ava">
<img
class="ava__pic"
{% if request.user.photo %}
src="{{ request.user.photo.url }}"
{% else %}
src="{% static 'img/user_default.jpg' %}"
{% endif %}
>
</div>
<div class="questions__wrap">
<div class="questions__reply-info">В ответ на
<a href="" class="questions__reply-anchor">этот комментарий</a>.
<a href="#" class="questions__reply-cancel grey-link">Отменить</a>
</div>
<div class="questions__field">
<textarea class="questions__textarea" placeholder="Спросите автора курса интересующие вас вопросы"></textarea>
</div>
<button class="questions__btn btn btn_light">ОТПРАВИТЬ</button>
</div>
</form>
{% else %}
<div>Только зарегистрированные пользователи могут оставлять комментарии.</div>
{% endif %}
<div class="questions__list">
{% include "templates/blocks/comments.html" with object=course %}
</div>
</div>
<comments obj-type="course" obj-id="{{ course.id }}"></comments>
</div>
</div>
<div class="section">

@ -4,14 +4,14 @@
{% load rupluralize from plural %}
{% block title %}{{ course.title }} - {{ block.super }}{% endblock title %}
{% comment %} seo tags {% endcomment %}
{% block description %}{{ course.short_description }}{% endblock description%}
{% block description %}{{ course.short_description | striptags }}{% endblock description%}
{% block twurl %}{{ request.build_absolute_uri }}{% endblock twurl %}
{% block ogtitle %}{{ course.title }} - {{ block.super }}{% endblock ogtitle %}
{% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %}
{% if course.cover %}
{% block ogimage %}http://{{request.META.HTTP_HOST}}{% if course.cover %}{{ course.cover.image.url }}{% else %}{% static 'img/og_courses.jpg' %}{% endif %}{% endblock ogimage %}
{% endif %}
{% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %}
{% block ogdescription %}{{ course.short_description | striptags }}{% endblock ogdescription %}
{% block content %}
<div class="section section_border">
@ -25,6 +25,9 @@
</div>
<div class="go__title">Вернуться</div>
</a>
{% if has_full_access %}
<a class="go__btn btn" href="{% url 'course_edit' course.id %}">Редактировать</a>
{% endif %}
{% if not paid and course.price and not has_full_access %}
<a
class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md"
@ -68,7 +71,7 @@
</a>
</div>
<div class="course__title title">{{ course.title }}</div>
<div class="course__content">{{ course.short_description }}</div>
<div class="course__content">{{ course.short_description | safe | linebreaks }}</div>
<a href="{% url 'user' course.author.id %}">
<div class="course__user user">
{% if course.author.photo %}
@ -174,27 +177,21 @@
<div class="lessons__title title">Содержание курса</div>
<div class="lessons__list">
{% for lesson in lessons %}
{% if course.author == request.user or request.user.role >= request.user.TEACHER_ROLE or paid %}
<a href="{% url 'lesson' pk=lesson.id %}">
{% else %}
<a href="#">
{% endif %}
<div class="lessons__item">
<div class="lessons__row">
<div class="lessons__content-wrapper">
<div class="lessons__subtitle subtitle">{{ forloop.counter }}. {{ lesson.title }}</div>
<div class="lessons__content">{{ lesson.short_description | truncatechars_html:800 | safe }}</div>
<a href="{% url 'lesson' pk=lesson.id %}" class="btn btn_stroke">Перейти к уроку</a>
</div>
<div class="lessons__preview">
<div class="lessons__pic-wrapper">
<img class="lessons__pic"
src="{% if lesson.cover %}{{ lesson.cover.image_thumbnail.url }}{% else %}{% static 'img/no_cover.png' %}{% endif %}">
</div>
</div>
<div>
<div class="lessons__subtitle subtitle">{{ forloop.counter }}. {{ lesson.title }}</div>
<div class="lessons__content">{{ lesson.short_description | truncatechars_html:800 | safe }}</div>
<a href="{% url 'lesson' pk=lesson.id %}" class="btn btn_stroke">Перейти к уроку</a>
</div>
</div>
</div>
</a>
{% endfor %}
</div>
</div>
@ -204,31 +201,7 @@
<div class="section section_gray">
<div class="section__center center center_sm">
<div class="title">Задавайте вопросы:</div>
<div class="questions">
{% if user.is_authenticated %}
<form class="questions__form" method="post" action="{% url 'coursecomment' course_id=course.id %}">
<input type="hidden" name="reply_id">
<div class="questions__ava ava">
<img class="ava__pic" src="{% static 'img/user_default.jpg' %}">
</div>
<div class="questions__wrap">
<div class="questions__reply-info">В ответ на
<a href="" class="questions__reply-anchor">этот комментарий</a>.
<a href="#" class="questions__reply-cancel grey-link">Отменить</a>
</div>
<div class="questions__field">
<textarea class="questions__textarea" placeholder="Задайте автору курса интересующие вас вопросы"></textarea>
</div>
<button class="questions__btn btn btn_light">ОТПРАВИТЬ</button>
</div>
</form>
{% else %}
<div>Только зарегистрированные пользователи могут оставлять комментарии.</div>
{% endif %}
<div class="questions__list">
{% include "templates/blocks/comments.html" with object=course %}
</div>
</div>
<comments obj-type="course" obj-id="{{ course.id }}"></comments>
</div>
</div>
<div class="section">

@ -23,15 +23,32 @@
<div class="field__select select js-select{% if category %} selected{% endif %}" data-category-select>
<div class="select__head js-select-head">Категории</div>
<div class="select__drop js-select-drop">
<div class="select__option js-select-option{% if not category %} active{% endif %}"
data-category-option data-category-url="{% url 'courses' %}">
<div class="select__title">Все курсы</div>
<div class="select__option js-select-option{% if not category.0 %} active{% endif %}" data-category-option>
<div class="select__title">Все категории</div>
</div>
{% category_items category %}
</div>
<input class="select__input" type="hidden"></div>
</div>
</div>
<div class="head__field field">
<div class="field__wrap">
<div class="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__drop js-select-drop">
<div class="select__option js-select-option{% if not age.0 %} active{% endif %}" data-age-option>
<div class="select__title">Любой возраст</div>
</div>
{% for a in ages %}
<div class="select__option js-select-option{% if age and age.0 == a.0 %} active{% endif %}" data-age-option
data-age="{{ a.0 }}" data-age-name="{{ a.1 }}">
<div class="select__title">{{ a.1 }}</div>
</div>
{% endfor %}
</div>
<input class="select__input" type="hidden"></div>
</div>
</div>
</div>
</div>
<div class="courses">
@ -40,7 +57,7 @@
</div>
<div class="courses__load load">
{% if page_obj.has_next %}
<button class="load__btn btn" data-next-page-url="{% url 'courses' %}?page={{ page_obj.next_page_number }}">Подгрузить еще</button>
<button class="load__btn btn" data-next-page="{{ page_obj.next_page_number }}">Подгрузить еще</button>
{% endif %}
</div>
</div>

@ -1,6 +1,6 @@
{% for cat in category_items %}
<div class="select__option js-select-option{% if category == cat.id %} active{% endif %}"
data-category-option data-category-name="{{ cat.title }}" data-category-url="{% url 'courses' %}?category={{ cat.id }}">
<div class="select__option js-select-option{% if category == cat.id %} active{% endif %}" data-category-option
data-category-name="{{ cat.title }}" data-category="{{ cat.id }}">
<div class="select__title">{{ cat.title }}</div>
</div>
{% endfor %}
{% endfor %}

@ -2,6 +2,9 @@
{% load static %}
{% block title %}{{ lesson.title }} - {{ block.super }}{% endblock title %}
{% block twurl %}{{ request.build_absolute_uri }}{% endblock twurl %}
{% block ogtitle %}{{ lesson.title }} - {{ block.super }}{% endblock ogtitle %}
{% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %}
{% block ogimage %}http://{{request.META.HTTP_HOST}}{% if lesson.course.cover %}{{ lesson.course.cover.image.url }}{% else %}{% static 'img/og_courses.jpg' %}{% endif %}{% endblock ogimage %}
{% block content %}
<div class="section" style="margin-bottom:0;padding-bottom:0">
@ -28,15 +31,9 @@
</div>
<div class="lesson">
<div class="lesson__row">
<div class="lesson__preview">
<div class="lesson__pic-wrapper">
<img class="lesson__pic"
src="{% if lesson.cover %}{{ lesson.cover.image_thumbnail.url }}{% else %}{% static 'img/no_cover.png' %}{% endif %}">
</div>
</div>
<div>
<div class="lesson__subtitle subtitle">{{ lesson.title }}</div>
<div class="lesson__content">{{ lesson.short_description | linebreaks }}</div>
<div class="lesson__content">{{ lesson.short_description | safe | linebreaks }}</div>
<a href="{% url 'user' lesson.author.id %}">
<div class="lesson__user user">
{% if lesson.author.photo %}
@ -57,6 +54,12 @@
</div>
</a>
</div>
<div class="lesson__preview">
<div class="lesson__pic-wrapper">
<img class="lesson__pic"
src="{% if lesson.cover %}{{ lesson.cover.image_thumbnail.url }}{% else %}{% static 'img/no_cover.png' %}{% endif %}">
</div>
</div>
</div>
</div>
</div>
@ -114,38 +117,7 @@
<div class="section section_gray">
<div class="section__center center center_sm">
<div class="title">Задавайте вопросы:</div>
<div class="questions">
{% if request.user.is_authenticated %}
<form class="questions__form" method="post" action="{% url 'lessoncomment' lesson_id=lesson.id %}">
<input type="hidden" name="reply_id">
<div class="questions__ava ava">
<img
class="ava__pic"
{% if request.user.photo %}
src="{{ request.user.photo.url }}"
{% else %}
src="{% static 'img/user_default.jpg' %}"
{% endif %}
>
</div>
<div class="questions__wrap">
<div class="questions__reply-info">В ответ на
<a href="" class="questions__reply-anchor">этот комментарий</a>.
<a href="#" class="questions__reply-cancel grey-link">Отменить</a>
</div>
<div class="questions__field">
<textarea class="questions__textarea" placeholder="Спросите автора курса интересующие вас вопросы"></textarea>
</div>
<button class="questions__btn btn btn_light">ОТПРАВИТЬ</button>
</div>
</form>
{% else %}
<div>Только зарегистрированные пользователи могут оставлять комментарии.</div>
{% endif %}
<div class="questions__list">
{% include "templates/blocks/comments.html" with object=lesson %}
</div>
</div>
<comments obj-type="lesson" obj-id="{{ lesson.id }}"></comments>
</div>
</div>
<div class="section">

@ -167,9 +167,8 @@ class CourseEditView(TemplateView):
self.object = Course.objects.create(
author=request.user,
)
#TODO
#if (request.user != self.object.author and request.user.role < User.AUTHOR_ROLE) or request.user.role != User.ADMIN_ROLE:
# raise Http404
if request.user != self.object.author and request.user.role != User.ADMIN_ROLE:
raise Http404
return super().get(request)
def get_context_data(self):
@ -283,6 +282,14 @@ class CoursesView(ListView):
filtered = CourseFilter(self.request.GET)
context.update(filtered.data)
context['course_items'] = Course.shuffle(context.get('course_items'))
context['ages'] = Course.AGE_CHOICES[1:]
age = context.get('age')
if age and age[0]:
age = int(age[0])
context['age'] = [age]
context['age_name'] = dict(Course.AGE_CHOICES).get(age, '')
else:
context['age_name'] = ''
return context
def get_template_names(self):

@ -116,13 +116,15 @@ class Payment(PolymorphicModel):
def add_months(cls, sourcedate, months=1):
result = arrow.get(sourcedate, settings.TIME_ZONE).shift(months=months)
if months == 1:
if (sourcedate.month == 2 and sourcedate.day >= 28) or (sourcedate.day == 31 and result.day <= 30)\
if (sourcedate.month == 2 and sourcedate.day >= 28) or (sourcedate.day == 31 and result.day <= 30) \
or (sourcedate.month == 1 and sourcedate.day >= 29 and result.day == 28):
result = result.replace(day=1, month=result.month + 1)
return result.datetime
@classmethod
def calc_amount(cls, payment=None, user=None, course=None, weekdays=None):
def calc_amount(cls, payment=None, user=None, course=None, date_start=None, weekdays=None):
date_start = date_start or now().date()
date_end = Payment.add_months(date_start, 1)
if isinstance(payment, CoursePayment):
course = payment.course
user = payment.user
@ -135,6 +137,7 @@ class Payment(PolymorphicModel):
if hasattr(user, 'referral') and not user.referral.payment:
referral_bonus = user.referral.bonus
referrer_bonus = user.referral.referrer_bonus
date_start = school_payment.date_start
discount = 0
if course:
price = course.price
@ -142,8 +145,8 @@ class Payment(PolymorphicModel):
if user:
school_payments = SchoolPayment.objects.filter(
user=user,
date_start__lte=now().date(),
date_end__gte=now().date(),
date_start__lte=date_start,
date_end__gte=date_start,
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
@ -162,7 +165,8 @@ class Payment(PolymorphicModel):
weekday__in=weekdays,
)
if add_days:
weekdays_count = weekdays_in_date_range(now().date(), prev_school_payment.date_end)
date_end = prev_school_payment.date_end
weekdays_count = weekdays_in_date_range(date_start, prev_school_payment.date_end)
all_weekdays_count = weekdays_in_date_range(prev_school_payment.date_start, prev_school_payment.date_end)
for ss in school_schedules:
price += ss.month_price // all_weekdays_count.get(ss.weekday, 0) * weekdays_count.get(
@ -182,6 +186,8 @@ class Payment(PolymorphicModel):
'referral_bonus': referral_bonus,
'referrer_bonus': referrer_bonus,
'discount': discount,
'date_start': date_start,
'date_end': date_end,
'weekdays': weekdays,
}

@ -102,6 +102,8 @@ class SchoolBuyView(TemplateView):
weekdays = set(request.GET.getlist('weekdays', []))
use_bonuses = request.GET.get('use_bonuses')
roistat_visit = request.COOKIES.get('roistat_visit', None)
date_start = request.GET.get('date_start')
date_start = date_start and datetime.datetime.strptime(date_start, '%Y-%m-%d') or now().date()
if not weekdays:
messages.error(request, 'Выберите несколько дней недели.')
return redirect('school:school')
@ -112,8 +114,8 @@ class SchoolBuyView(TemplateView):
return redirect('school:school')
prev_school_payment = SchoolPayment.objects.filter(
user=request.user,
date_start__lte=now().date(),
date_end__gte=now().date(),
date_start__lte=date_start,
date_end__gte=date_start,
add_days=False,
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
@ -126,7 +128,7 @@ class SchoolBuyView(TemplateView):
school_payment = SchoolPayment.objects.create(
user=request.user,
weekdays=weekdays,
date_start=now().date(),
date_start=date_start,
date_end=prev_school_payment.date_end,
add_days=True,
roistat_visit=roistat_visit,
@ -140,6 +142,8 @@ class SchoolBuyView(TemplateView):
user=request.user,
weekdays=weekdays,
roistat_visit=roistat_visit,
date_start=date_start,
date_end=Payment.add_months(date_start),
)
if use_bonuses:
if request.user.bonus >= school_payment.amount:
@ -224,30 +228,7 @@ class PaymentwallCallbackView(View):
product_type_name,
)
if product_type_name == 'school':
school_payment = SchoolPayment.objects.filter(
user=payment.user,
add_days=False,
date_start__lte=now().date(),
date_end__gte=now().date(),
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
).last()
if school_payment:
if payment.add_days:
date_start = now().date()
date_end = school_payment.date_end
else:
date_start = arrow.get(school_payment.date_end, settings.TIME_ZONE).shift(days=1).datetime
date_end = Payment.add_months(date_start)
else:
date_start = now().date()
date_end = Payment.add_months(date_start)
payment.date_start = date_start
payment.date_end = date_end
if product_type_name == 'course':
properties = {
'payment_id': payment.id,

@ -4,9 +4,6 @@
<div class="online__type">ВИДЕОУРОКИ</div>
<div class="online__title">Каждый день в 17.00 (по Мск) </div>
<div class="online__text text">Кроме выходных. Запись эфира доступна по завершению трансляции</div>
<div class="online__action">
<svg class="icon icon-play">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-play"></use>
</svg>СМОТРЕТЬ ПРИМЕР ЭФИРА</div>
<div class="online__action"></div>
</div>
</a>

@ -2,19 +2,27 @@
{% load static %}
{% block title %}{{ livelesson.title }} - {{ block.super }}{% endblock title %}
{% block twurl %}{{ request.build_absolute_uri }}{% endblock twurl %}
{% block ogtitle %}{{ livelesson.title }} - {{ block.super }}{% endblock ogtitle %}
{% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %}
{% if livelesson.cover and livelesson.cover.image %}
{% block ogimage %}http://{{request.META.HTTP_HOST}}{{ livelesson.cover.image.url }}{% endblock ogimage %}
{% endif %}
{% block content %}
<div class="section" style="margin-bottom:0;padding-bottom:0">
<div class="section__center center center_sm">
<div class="lesson">
<div class="lesson__subtitle subtitle">{{ livelesson.title }}</div>
<div class="lesson__content">{{ livelesson.short_description | linebreaks }}</div>
<div class="lesson__content">{{ livelesson.short_description | safe | linebreaks }}</div>
<div class="lesson__video video">
{% if livelesson.stream_index %}
<iframe class="lesson__video_frame" src="https://player.vimeo.com/video/{{ livelesson.stream_index }}?autoplay=1" frameborder="0" webkitallowfullscreen
mozallowfullscreen allowfullscreen>
</iframe>
<span>Если видео не загрузилось, - уменьшите качество видео или <a href="#" onclick="location.reload();">обновите страницу</a></span>
<iframe class="lesson__chat_frame" src="https://vimeo.com/live-chat/{{ livelesson.stream_index }}" frameborder="0"></iframe>
<!--<iframe class="lesson__chat_frame" src="https://vimeo.com/live-chat/{{ livelesson.stream_index }}" frameborder="0"></iframe>-->
<comments obj-type="live-lesson" obj-id="{{ livelesson.id }}" :is-chat="true"></comments>
{% else %}
{% if livelesson.cover %}
<img class="video__pic" src="{{ livelesson.cover.image.url }}"/>

@ -24,7 +24,7 @@
</div>
<div class="lessons__subtitle subtitle">{{ livelesson.date }} // {{ livelesson.title }}</div>
<div class="lessons__row">
<div class="lessons__content">{{ livelesson.short_description | linebreaks }}</div>
<div class="lessons__content">{{ livelesson.short_description | safe | linebreaks | truncatechars_html:800 }}</div>
</div>
</div>
{% endfor %}

@ -3,10 +3,7 @@
<div class="online__center center">
<div class="online__type">ВИДЕОУРОКИ</div>
<div class="online__title">В 17.00 (по Мск) </div>
<div class="online__text text">Каждый день с 1 сентября</div>
<div class="online__action">
<svg class="icon icon-play">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-play"></use>
</svg>СМОТРЕТЬ ПРИМЕР ЭФИРА</div>
<div class="online__text text">Каждый день</div>
<div class="online__action"></div>
</div>
</a>

@ -1,9 +1,5 @@
<a
{% if not user.is_authenticated %}
data-popup=".js-popup-auth"
{% else %}
data-popup=".js-popup-buy"
{% endif %}
data-popup=".js-popup-buy" data-prolong="1" data-date-start="{{ prolong_date_start|date:'Y-m-d' }}"
class="casing__btn btn{% if pink %} btn_pink{% endif %}"
href="#"
>продлить</a>

@ -5,7 +5,7 @@
<span class="main__bold">Lil School</span> — первая образовательная онлайн-платформа креативного мышления для детей
</div>
<div class="main__subtitle">
Старт школы - 1 сентября
<!--Старт школы - 1 сентября-->
</div>
<div class="main__actions">
<a
@ -29,7 +29,7 @@
</svg>
</div>
<div class="school__title">Видеоуроки</div>
<div class="school__text">Каждый день с 1 сентября</div>
<div class="school__text">Каждый день</div>
</div>
<div class="school__col">
<div class="school__preview">

@ -5,14 +5,10 @@
<div class="casing">
<div class="casing__col">
<div class="casing__subscribe">
{% if is_purchased %}
<div class="casing__msg">Подписка истекает
<span class="bold">{{ subscription_ends }}</span>
</div>
{% else %}
<div class="casing__msg">Подписка
<span class="bold">истекла</span>
</div>
{% if allow_prolong %}
{% include './prolong_btn.html' with pink=True %}
{% endif %}
</div>

@ -187,6 +187,7 @@ class SchoolView(TemplateView):
ll.school_schedule = school_schedules_dict.get(ll.date.isoweekday())
live_lessons_exists = live_lessons.exists()
live_lessons = live_lessons or None
subscription_ends = school_payment.filter(add_days=False).last().date_end if school_payment_exists else None
context.update({
'online': online,
'live_lessons': live_lessons,
@ -199,7 +200,9 @@ class SchoolView(TemplateView):
'school_schedules': school_schedules,
'school_schedules_purchased': school_schedules_purchased,
'school_purchased_future': False,
'subscription_ends': school_payment.filter(add_days=False).first().date_end if school_payment_exists else None,
'subscription_ends': subscription_ends,
'prolong_date_start': subscription_ends + timedelta(days=1) if subscription_ends else None,
'allow_prolong': subscription_ends - date_now <= timedelta(days=14) if subscription_ends else False,
})
return context

@ -73,13 +73,14 @@
</div>
<div class="tabs__container">
<div class="tabs__item js-tabs-item" style="display: block;">
<div class="courses courses_scroll">
<div class="courses">
<div class="courses__list">
{% if is_school_purchased %}
<div class="courses__item">
<a class="courses__preview" href="{% url 'school:school' %}">
<img class="courses__pic" src="{% static 'img/og_main.jpg' %}"
style="height: 201px; width: 300px; object-fit: cover;" />
style="height: 200px; object-fit: cover;" />
<div class="courses__view">Подробнее</div>
</a>
<div class="courses__details">
<a class="courses__theme theme">{{ school_purchased_weekdays }}</a>
@ -110,7 +111,7 @@
</div>
{% if is_author %}
<div class="tabs__item js-tabs-item">
<div class="courses courses_scroll">
<div class="courses">
<div class="courses__list">
{% if published.exists %}
{% include "course/course_items.html" with course_items=published %}

@ -1,3 +1,4 @@
from django.db.models import Func, F
from django.utils.timezone import now
from paymentwall.pingback import Pingback
from django.contrib.auth import get_user_model
@ -17,7 +18,7 @@ def baner(request):
return {'baner': Baner.objects.filter(use=True).first()}
def is_summer_school_purchased(request):
def school_purchased(request):
if request.user.is_authenticated:
n = now().date()
school_payment = SchoolPayment.objects.filter(
@ -30,8 +31,18 @@ def is_summer_school_purchased(request):
date_start__lte=n,
date_end__gte=n
)
return {'is_summer_school_purchased': school_payment.exists()}
return {'is_summer_school_purchased': False}
school_schedules_purchased = school_payment.annotate(
joined_weekdays=Func(F('weekdays'), function='unnest', )
).values_list('joined_weekdays', flat=True).distinct()
return {
'is_school_purchased': school_payment.exists(),
'school_schedules_purchased': school_schedules_purchased,
}
return {
'is_school_purchased': False,
'school_schedules_purchased': [],
}
def referrer(request):
referrer = request.session.get('referrer')

@ -0,0 +1,14 @@
from django.conf import settings
from pusher import Pusher
def pusher():
try:
pusher_cluster = settings.PUSHER_CLUSTER
except AttributeError:
pusher_cluster = 'mt1'
return Pusher(app_id=settings.PUSHER_APP_ID,
key=settings.PUSHER_KEY,
secret=settings.PUSHER_SECRET,
cluster=pusher_cluster)

@ -95,7 +95,7 @@ TEMPLATES = [
'context_processors': [
'project.context_processors.config',
'project.context_processors.baner',
'project.context_processors.is_summer_school_purchased',
'project.context_processors.school_purchased',
'project.context_processors.referrer',
'project.context_processors.settings',
'django.template.context_processors.debug',
@ -200,6 +200,13 @@ TWILIO_FROM_PHONE = os.getenv('TWILIO_FROM_PHONE', '+37128914409')
ACTIVE_LINK_STRICT = True
# PUSHER settings
PUSHER_APP_ID = os.getenv('PUSHER_APP_ID', '')
PUSHER_KEY = os.getenv('PUSHER_KEY', '')
PUSHER_SECRET = os.getenv('PUSHER_SECRET', '')
PUSHER_CLUSTER = 'eu'
# DRF settings
REST_FRAMEWORK = {

@ -13,7 +13,7 @@
</svg>
</div>
<div class="school__title">Видеоуроки</div>
<div class="school__text">Каждый день с 1 сентября</div>
<div class="school__text">Каждый день</div>
</div>
<div class="school__col">
<div class="school__preview">

@ -11,80 +11,56 @@
</div>
<div class="gallery">
<div class="gallery__grid">
<div
class="gallery__item gallery__item_lg"
style="background-image: url({% get_media_prefix %}instagram/results/0.jpg);"
>
<a href="{% get_media_prefix %}instagram/results/0.jpg"
class="gallery__item gallery__item_lg">
<img class="gallery__pic" src="{% get_media_prefix %}instagram/results/0.jpg" onerror="this.style.display='none'">
</div>
<div
class="gallery__item"
style="background-image: url({% get_media_prefix %}instagram/results/1.jpg);"
>
{% comment %} <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/1.jpg" onerror="this.style.display='none'"> {% endcomment %}
</div>
<div
class="gallery__item"
style="background-image: url({% get_media_prefix %}instagram/results/2.jpg);"
>
{% comment %} <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/2.jpg" onerror="this.style.display='none'"> {% endcomment %}
</div>
<div
class="gallery__item"
style="background-image: url({% get_media_prefix %}instagram/results/3.jpg);"
>
{% comment %} <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/3.jpg" onerror="this.style.display='none'"> {% endcomment %}
</div>
<div
class="gallery__item"
style="background-image: url({% get_media_prefix %}instagram/results/4.jpg);"
>
{% comment %} <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/4.jpg" onerror="this.style.display='none'"> {% endcomment %}
</div>
<div
class="gallery__item"
style="background-image: url({% get_media_prefix %}instagram/results/5.jpg);"
>
{% comment %} <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/5.jpg" onerror="this.style.display='none'"> {% endcomment %}
</div>
</a>
<a href="{% get_media_prefix %}instagram/results/1.jpg"
class="gallery__item">
<img class="gallery__pic" src="{% get_media_prefix %}instagram/results/1.jpg" onerror="this.style.display='none'">
</a>
<a href="{% get_media_prefix %}instagram/results/2.jpg"
class="gallery__item">
<img class="gallery__pic" src="{% get_media_prefix %}instagram/results/2.jpg" onerror="this.style.display='none'">
</a>
<a href="{% get_media_prefix %}instagram/results/3.jpg"
class="gallery__item">
<img class="gallery__pic" src="{% get_media_prefix %}instagram/results/3.jpg" onerror="this.style.display='none'">
</a>
<a href="{% get_media_prefix %}instagram/results/4.jpg"
class="gallery__item">
<img class="gallery__pic" src="{% get_media_prefix %}instagram/results/4.jpg" onerror="this.style.display='none'">
</a>
<a href="{% get_media_prefix %}instagram/results/5.jpg"
class="gallery__item">
<img class="gallery__pic" src="{% get_media_prefix %}instagram/results/5.jpg" onerror="this.style.display='none'">
</a>
</div>
<div class="gallery__grid">
<div
class="gallery__item"
style="background-image: url({% get_media_prefix %}instagram/results/6.jpg);"
>
{% comment %} <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/6.jpg" onerror="this.style.display='none'"> {% endcomment %}
</div>
<div
class="gallery__item"
style="background-image: url({% get_media_prefix %}instagram/results/7.jpg);"
>
{% comment %} <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/7.jpg" onerror="this.style.display='none'"> {% endcomment %}
</div>
<div
class="gallery__item"
style="background-image: url({% get_media_prefix %}instagram/results/8.jpg);"
>
{% comment %} <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/8.jpg" onerror="this.style.display='none'"> {% endcomment %}
</div>
<div class="gallery__item gallery__item_lg"
style="background-image: url({% get_media_prefix %}instagram/results/9.jpg);"
>
{% comment %} <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/9.jpg" onerror="this.style.display='none'"> {% endcomment %}
</div>
<div
class="gallery__item"
style="background-image: url({% get_media_prefix %}instagram/results/10.jpg);"
>
{% comment %} <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/10.jpg" onerror="this.style.display='none'"> {% endcomment %}
</div>
<div
class="gallery__item"
style="background-image: url({% get_media_prefix %}instagram/results/11.jpg);"
>
{% comment %} <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/11.jpg" onerror="this.style.display='none'"> {% endcomment %}
</div>
<a href="{% get_media_prefix %}instagram/results/6.jpg"
class="gallery__item">
<img class="gallery__pic" src="{% get_media_prefix %}instagram/results/6.jpg" onerror="this.style.display='none'">
</a>
<a href="{% get_media_prefix %}instagram/results/7.jpg"
class="gallery__item">
<img class="gallery__pic" src="{% get_media_prefix %}instagram/results/7.jpg" onerror="this.style.display='none'">
</a>
<a href="{% get_media_prefix %}instagram/results/8.jpg"
class="gallery__item">
<img class="gallery__pic" src="{% get_media_prefix %}instagram/results/8.jpg" onerror="this.style.display='none'">
</a>
<a href="{% get_media_prefix %}instagram/results/9.jpg"
class="gallery__item gallery__item_lg">
<img class="gallery__pic" src="{% get_media_prefix %}instagram/results/9.jpg" onerror="this.style.display='none'">
</a>
<a href="{% get_media_prefix %}instagram/results/10.jpg"
class="gallery__item">
<img class="gallery__pic" src="{% get_media_prefix %}instagram/results/10.jpg" onerror="this.style.display='none'">
</a>
<a href="{% get_media_prefix %}instagram/results/11.jpg"
class="gallery__item">
<img class="gallery__pic" src="{% get_media_prefix %}instagram/results/11.jpg" onerror="this.style.display='none'">
</a>
</div>
</div>
</div>

@ -3,9 +3,8 @@
<div class="section__center center">
<div class="title title_center">Видео-курсы без расписания</div>
<div class="text text_courses">Если вам не совсем удобно заниматься с нами каждый день в нашей онлайн-школе, специально для вас мы
делаем отдельные уроки в записи, которые вы можете проходить когда вам будем удобно.</div>
<div class="head">
<div class="head__text">Учите и развивайте креативное мышление когда и где угодно</div>
делаем отдельные уроки в записи, которые вы можете проходить когда вам будем удобно.<br><br>
Учите и развивайте креативное мышление когда и где угодно
</div>
<div class="courses">
<div class="courses__list">

@ -1,11 +1,26 @@
{% load static %}
{% load setting from settings %}
<script>
window.LIL_STORE = {
roles: {
USER_ROLE: 0,
AUTHOR_ROLE: 1,
TEACHER_ROLE: 2,
ADMIN_ROLE: 3,
},
pusherKey: '{% setting "PUSHER_KEY" %}',
staticUrl: '{% static "" %}',
accessToken: '{{ request.user.auth_token }}',
isMobile: {{ request.user_agent.is_mobile|yesno:"true,false" }},
defaultUserPhoto: "{% static 'img/user_default.jpg' %}",
user: {
id: '{{ request.user.id|default:'' }}',
role: +'{{ request.user.role }}',
photo: '{% if request.user.photo %}{{ request.user.photo.url }}{% else %}{% static 'img/user_default.jpg' %}{% endif %}',
},
components: {},
urls: {
courses: "{% url 'courses' %}"
},
flags: {
referrer: '{{ referrer.id|default:'' }}',

@ -3,10 +3,7 @@
<div class="online__center center">
<div class="online__type">ВИДЕОУРОКИ</div>
<div class="online__title">В 17.00 (по Мск) </div>
<div class="online__text text">Каждый день с 1 сентября</div>
<div class="online__action">
<svg class="icon icon-play">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-play"></use>
</svg>СМОТРЕТЬ ПРИМЕР ЭФИРА</div>
<div class="online__text text">Каждый день</div>
<div class="online__action"></div>
</div>
</a>

@ -20,40 +20,6 @@
</div>
<div class="buy__col">
<div class="buy__list">
{% if all_school_schedules %}
{% for school_schedule in all_school_schedules %}
<label class="switch switch_lesson">
<input
class="switch__input"
type="checkbox"
data-day="{{school_schedule.weekday}}"
data-price="{{school_schedule.month_price}}"
autocomplete="off"
{% if school_schedule.weekday in school_schedules_purchased %}
disabled
{% endif %}
{% if not is_purchased %}
checked
{% endif %}>
<span class="switch__content">
<span class="switch__cell">{{ school_schedule }}</span>
{% comment %} dont delete {% endcomment %}
<span class="switch__cell"></span>
<span class="switch__cell">{{ school_schedule.title }}</span>
<span class="buy__trial-lesson switch__cell">
{% if school_schedule.weekday in school_schedules_purchased %}
Куплено
{% else %}
{% if school_schedule.trial_lesson %}
<a class="js-video-modal" data-video-url="{{ school_schedule.trial_lesson }}" href="">Пробный урок</a>
{% endif %}
{% endif %}
</span>
<span class="switch__cell">{{school_schedule.month_price}}р в мес.</span>
</span>
</label>
{% endfor %}
{% else %}
{% for school_schedule in school_schedules %}
<label class="switch switch_lesson">
<input
@ -63,6 +29,7 @@
data-price="{{school_schedule.month_price}}"
autocomplete="off"
{% if school_schedule.weekday in school_schedules_purchased %}
data-purchased="1"
disabled
{% endif %}
{% if not is_purchased %}
@ -86,7 +53,6 @@
</span>
</label>
{% endfor %}
{% endif %}
</div>
</div>
<div class="buy__col">
@ -102,6 +68,7 @@
</div>
<div class="order__info">
<div class="order__label">ШКОЛА</div>
<div class="order__dates"></div>
<div class="order__days"></div>
{% if request.user.bonuses %}
<label class="buy__bonuses switch" style="display: none;">

@ -6,7 +6,7 @@
<div class="lock">
<div class="lock__label">ЧТОБЫ ПРОДОЛЖИТЬ ПРОСМОТР</div>
<div class="lock__title title">{{ course.title }}</div>
<div class="lock__content">{{ course.short_description }}</div>
<div class="lock__content">{{ course.short_description | safe | linebreaks }}</div>
<a href="{% url 'course-checkout' course.id %}" class="lock__btn btn">КУПИТЬ КУРС</a>
</div>
</div>

@ -48,7 +48,7 @@
</div>
{% else %}
<div class="main__subtitle">
Приглашаем вас присоединиться к онлайн-школе с 1 сентября!
Приглашаем вас присоединиться к онлайн-школе
</div>
<div class="main__actions">
{% if not is_purchased and not is_purchased_future %}

@ -316,12 +316,12 @@
</div>
</div>
{% include 'templates/blocks/lil_store_js.html' %}
<script type="text/javascript" src={% static "app.js" %}></script>
<script>
var schoolDiscount = parseFloat({{ config.SERVICE_DISCOUNT }});
var schoolAmountForDiscount = parseFloat({{ config.SERVICE_DISCOUNT_MIN_AMOUNT }});
</script>
{% block foot %}{% endblock foot %}
<script type="text/javascript" src={% static "app.js" %}></script>
</body>
</html>

@ -56,6 +56,7 @@
powered by <a href="https://www.livechatinc.com/?welcome" rel="noopener" target="_blank">LiveChat</a>
</noscript>
<!-- End of LiveChat code -->
<script src="https://js.pusher.com/4.1/pusher.min.js"></script>
<script>
var viewportmeta = document.querySelector('meta[name="viewport"]');
if (viewportmeta) {

@ -9,7 +9,7 @@
{% include "templates/blocks/about.html" %}
{% include "templates/blocks/last_courses.html" %}
{% include "templates/blocks/gallery.html" %}
{% include "templates/blocks/game.html" %}
{% include "templates/blocks/teachers.html" %}
{% include "templates/blocks/game.html" %}
{% include "templates/blocks/partners.html" %}
{% endblock content %}

@ -47,14 +47,14 @@ urlpatterns = [
path('author-request/success/', TemplateView.as_view(template_name='user/become-author-success.html'), name='author-request-success'),
path('courses/', CoursesView.as_view(), name='courses'),
path('course/create', CourseEditView.as_view(), name='course_create'),
path('course/create/lessons', CourseEditView.as_view(), name='course_create_lessons'),
path('course/create/lessons/new', CourseEditView.as_view(), name='course_create_lessons_new'),
path('course/create/lessons/edit/<int:lesson>', CourseEditView.as_view(), name='course_create_lessons_edit'),
path('course/<int:pk>/lessons/new', CourseEditView.as_view(), name='course_lessons_new'),
path('course/<int:pk>/lessons/<int:lesson>/edit', CourseEditView.as_view(), name='course_lessons_edit'),
path('course/on-moderation', CourseOnModerationView.as_view(), name='course-on-moderation'),
path('course/<int:pk>/', CourseView.as_view(), name='course'),
path('course/<str:slug>/', CourseView.as_view(), name='course'),
path('course/<int:pk>/checkout', CourseBuyView.as_view(), name='course-checkout'),
path('course/<int:pk>/edit', CourseEditView.as_view(), name='course_edit'),
path('course/<int:pk>/edit/lessons', CourseEditView.as_view(), name='course_edit_lessons'),
path('course/<int:pk>/lessons', CourseView.as_view(template_name='course/course_only_lessons.html', only_lessons=True), name='course-only-lessons'),
path('course/<int:course_id>/like', likes, name='likes'),
path('course/<int:course_id>/comment', coursecomment, name='coursecomment'),

@ -83,7 +83,7 @@ class IndexView(TemplateView):
'online': online,
'online_coming_soon': online_coming_soon,
'school_schedule': school_schedule,
'course_items': Course.shuffle(Course.objects.filter(status=Course.PUBLISHED)[:6]),
'course_items': Course.shuffle(Course.objects.filter(status=Course.PUBLISHED)[:3]),
'is_purchased': school_payment_exists,
'min_school_price': SchoolSchedule.objects.aggregate(Min('month_price'))['month_price__min'],
'school_schedules': SchoolSchedule.objects.all(),

@ -30,5 +30,5 @@ user-agents==1.1.0
ua-parser==0.8.0
django-ipware
django-imagekit
short_url
pusher==2.0.1
short_url

@ -0,0 +1,71 @@
<template>
<div>
<div v-if="! comment.deactivated_at">
<a class="questions__anchor" :id="'question__' + comment.id"></a>
<div :id="'question__replyto__' + comment.id" :class="{'questions__item_reply': comment.parent && ! controller.isChat}" class="questions__item">
<div v-if="comment.author.photo" class="questions__ava ava">
<img class="ava__pic" :src="comment.author.photo">
</div>
<div v-if="! comment.author.photo" class="questions__ava ava">
<img class="ava__pic" :src="$root.store.defaultUserPhoto">
</div>
<div class="questions__wrap">
<div class="questions__details">
<div class="questions__head">
<span class="questions__author">{{ comment.author.first_name }} {{ comment.author.last_name }}</span>
<span class="questions__date">{{ comment.created_at_humanize }}</span>
</div>
<div class="questions__content">
<svg v-if="isHeart" class="icon questions__heart"><use xlink:href="/static/img/sprite.svg#icon-like"></use></svg>
<span v-if="! isHeart">{{ comment.content }}</span>
</div>
</div>
<div class="questions__foot">
<button @click="controller.reply(comment)" v-if="$root.store.user.id && ! controller.isChat" class="questions__action question__reply-button">ОТВЕТИТЬ</button>
<button @click="controller.remove(comment)"
v-if="$root.store.user.id == comment.author.id || $root.store.user.role == $root.store.roles.ADMIN_ROLE"
class="questions__action question__reply-button">
<span v-if="! controller.isChat">УДАЛИТЬ</span>
<span v-if="controller.isChat">
<svg class="icon questions__delete-icon"><use xlink:href="/static/img/sprite.svg#icon-delete"></use></svg>
</span>
</button>
</div>
</div>
</div>
</div>
<comment-form v-if="$root.store.user.id && !controller.$data.isChat && controller.$data.replyTo && controller.$data.replyTo.id == comment.id"
:controller="controller"></comment-form>
<ul v-if="comment.children" v-for="(node, index) in comment.children" :key="index">
<li>
<comment v-if="! node.deactivated_at" :controller="controller" :comment="node"></comment>
</li>
</ul>
</div>
</template>
<script>
import CommentForm from './CommentForm';
export default {
name: 'comment',
props: ['controller', 'comment',],
computed: {
isHeart(){
return this.comment.content === '❤';
},
},
mounted(){
this.controller.flatComments[this.comment.id] = this.comment;
},
components: {
CommentForm
}
}
</script>

@ -0,0 +1,53 @@
<template>
<div class="questions__form" :class="{'questions__item_reply': controller.$data.replyTo}">
<div class="questions__form-loader loading-loader"></div>
<div class="questions__ava ava">
<img class="ava__pic" :src="$root.store.user.photo || $root.store.defaultUserPhoto">
</div>
<div class="questions__wrap">
<div class="questions__field">
<textarea v-model="content" class="questions__textarea" @keyup.enter.exact="addOnEnter"
:placeholder="controller.$data.replyTo ? 'Ответ на комментарий' : 'Ваш комментарий или вопрос'"></textarea>
</div>
<div class="questions__form-foot">
<button v-if="controller.isChat" class="questions__btn"
@click="controller.addHeart"><svg class="icon questions__heart"><use xlink:href="/static/img/sprite.svg#icon-like"></use></svg></button>
<button class="questions__btn" :class="{'btn btn_light': ! controller.isChat}" @click="add">
<span :class="{'mobile-hide': controller.isChat }">ОТПРАВИТЬ</span>
<span class="mobile-show" v-if="controller.isChat">
<svg class="icon questions__send-icon"><use xlink:href="/static/img/sprite.svg#icon-plus"></use></svg>
</span>
</button>
<button v-show="! controller.isChat && controller.$data.replyTo" class="questions__btn" @click="controller.cancelReply">
<span>ОТМЕНИТЬ</span>
</button>
</div>
</div>
</div>
</template>
<script>
import {api} from "../js/modules/api";
export default {
name: 'comment-form',
props: ['controller',],
data() {
return {
content: '',
}
},
methods: {
addOnEnter() {
if(this.controller.isChat) {
this.add();
}
},
add() {
this.controller.add(this.content);
this.content = '';
},
}
}
</script>

@ -0,0 +1,134 @@
<template>
<div class="questions" :class="{'questions--chat': isChat, 'questions--loading': loading}">
<div v-show="nodes.length" class="questions__items">
<ul v-for="(node, index) in nodes" :key="index">
<li>
<comment v-if="! node.deactivated_at" :comment="node" :controller="controller" v-on:remove="remove"></comment>
</li>
</ul>
</div>
<comment-form v-if="$root.store.user.id && ! replyTo" :controller="controller"></comment-form>
</div>
</template>
<script type="text/javascript">
import Comment from './Comment';
import CommentForm from './CommentForm';
import {api} from "../js/modules/api";
export default {
name: 'comments',
props: ['objType', 'objId', 'isChat'],
data() {
return {
loading: false,
replyTo: null,
nodes: [],
controller: this,
flatComments: {},
}
},
methods: {
reply(comment) {
this.replyTo = comment;
},
cancelReply(){
this.replyTo = null;
},
addHeart(){
this.add('❤');
},
add(content){
let vm = this;
this.loading = true;
let request = api.addObjComment(this.objId, this.objType, {
content: content,
author: this.$root.store.user.id,
parent: this.replyTo && this.replyTo.id,
});
request.then((response) => {
vm.loading = false;
vm.onAdd(response.data);
if(vm.replyTo){
vm.cancelReply();
}
}).catch(() => {
vm.loading = false;
});
},
remove(comment){
if(! confirm('Удалить комментарий?')){
return;
}
let vm = this;
this.loading = true;
let request = api.removeObjComment(comment.id);
request.then((response) => {
vm.loading = false;
vm.onRemove(comment);
});
},
onAdd(comment){
if(this.flatComments[comment.id]){
return;
}
const method = this.isChat ? 'push' : 'unshift';
if(comment.parent){
this.flatComments[comment.parent].children[method](comment);
}
else{
this.nodes[method](comment);
}
this.flatComments[comment.id] = comment;
},
onRemove(comment){
let comments = [];
if(comment.parent){
comments = this.flatComments[comment.parent].children;
}
else{
comments = this.nodes;
}
let index = comments.findIndex((c) => +c.id === +comment.id);
if(index === -1){
return;
}
comments.splice(index, 1);
delete this.flatComments[comment.id];
},
connectToPusher(){
let vm = this;
// Enable pusher logging - don't include this in production
Pusher.logToConsole = true;
let pusher = new Pusher(this.$root.store.pusherKey, {
cluster: 'eu',
encrypted: true
});
let channel = pusher.subscribe('comments_' + this.objType + '_' + this.objId);
channel.bind('add', this.onAdd);
channel.bind('delete', this.onRemove);
}
},
mounted() {
let vm = this;
this.loading = true;
let request = api.getObjComments(this.objId, this.objType, this.isChat ? 'update_at' : '-update_at');
request
.then((response) => {
vm.loading = false;
vm.nodes = response.data;
vm.connectToPusher();
})
.catch(() => {
vm.loading = false;
});
},
components: {
Comment,
CommentForm
}
}
</script>

@ -66,45 +66,7 @@
<div class="section__center center">
<div class="kit">
<div class="kit__body">
<vue-draggable v-model="contest.content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }">
<div v-for="(block, index) in contest.content" :key="block.data.id ? block.data.id : block.data.guid">
<block-text v-if="block.type === 'text'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-image-text v-if="block.type === 'image-text'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
:image-id.sync="block.data.image_id"
:image-url.sync="block.data.image_url"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-image v-if="block.type === 'image'"
:index="index"
:title.sync="block.data.title"
:image-id.sync="block.data.image_id"
:image-url.sync="block.data.image_url"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-images v-if="block.type === 'images'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
:images.sync="block.data.images"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-video v-if="block.type === 'video'"
:index="index"
:title.sync="block.data.title"
v-on:remove="onBlockRemoved"
:video-url.sync="block.data.video_url"/>
</div>
</vue-draggable>
<block-add v-on:added="onBlockAdded"/>
<block-content :content.sync="contest.content"></block-content>
</div>
</div>
</div>
@ -113,15 +75,9 @@
</template>
<script>
import BlockText from './blocks/BlockText'
import BlockImage from './blocks/BlockImage'
import BlockImages from './blocks/BlockImages'
import BlockImageText from './blocks/BlockImageText'
import BlockVideo from './blocks/BlockVideo'
import BlockAdd from "./blocks/BlockAdd";
import {api} from "../js/modules/api";
import DatePicker from 'vuejs-datepicker';
import Draggable from 'vuedraggable';
import BlockContent from './blocks/BlockContent'
import slugify from 'slugify';
import {required, minValue, numeric, url } from 'vuelidate/lib/validators'
import _ from 'lodash';
@ -175,18 +131,6 @@
}
},
methods: {
onBlockRemoved(blockIndex) {
const blockToRemove = this.contest.content[blockIndex];
// Удаляем блок из Vue
this.contest.content.splice(blockIndex, 1);
// Если блок уже был записан в БД, отправляем запрос на сервер на удаление блока из БД
if (blockToRemove.data.id) {
api.removeContentBlock(blockToRemove, this.accessToken);
}
},
onBlockAdded(blockData) {
this.contest.content.push(blockData);
},
onTitleInput() {
this.$v.contest.title.$touch();
if (!this.slugChanged) {
@ -219,7 +163,7 @@
id: data.id,
title: data.title,
description: data.description,
content: api.convertContentResponse(data.content),
content: api.convertContentJson(data.content),
date_start: data.date_start,
date_end: data.date_end,
slug: data.slug,
@ -252,70 +196,7 @@
data.date_start = data.date_start ? moment(data.date_start).format('MM-DD-YYYY') : null;
data.date_end = data.date_end ? moment(data.date_end).format('MM-DD-YYYY') : null;
data.cover = this.contest.coverImageId || '';
data.content = this.contest.content.map((block, index) => {
if (block.type === 'text') {
return {
'type': 'text',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'txt': block.data.text,
}
}
} else if (block.type === 'image') {
return {
'type': 'image',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'img': block.data.image_id,
}
}
} else if (block.type === 'image-text') {
return {
'type': 'image-text',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'img': block.data.image_id,
'txt': block.data.text,
}
}
} else if (block.type === 'images') {
return {
'type': 'images',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'images': block.data.images.map((galleryImage) => {
return {
'id': galleryImage.id ? galleryImage.id : null,
'img': galleryImage.img,
}
}),
}
}
} else if (block.type === 'video') {
return {
'type': 'video',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'url': block.data.video_url,
}
}
}
});
data.content = api.convertContentJson(this.contest.content, true);
const request = this.contest.id
? api.put(`/api/v1/contests/${this.contest.id}/`, data, {
@ -343,15 +224,8 @@
}
},
components: {
BlockAdd,
BlockContent,
'vue-datepicker': DatePicker,
'block-text': BlockText,
'block-image': BlockImage,
'block-image-text': BlockImageText,
'block-images': BlockImages,
'block-video': BlockVideo,
'vue-draggable': Draggable,
}
};
</script>

@ -37,10 +37,8 @@
v-bind:class="{ error: ($v.course.short_description.$dirty || showErrors) && $v.course.short_description.$invalid }">
<div class="field__label">{{titles.shortDescription}}</div>
<div class="field__wrap">
<textarea class="field__textarea"
v-autosize="course.short_description"
@input="$v.course.short_description.$touch()"
v-model="course.short_description"></textarea>
<vue-redactor :value="course.short_description"
v-on:update:value="(value) => { this.course.short_description = value; }" />
</div>
</div>
</div>
@ -49,7 +47,7 @@
v-bind:class="{ error: ($v.course.category.$dirty || showErrors) && $v.course.category.$invalid }">
<div class="field__label field__label_gray">КАТЕГОРИЯ</div>
<div class="field__wrap">
<lil-select :value.sync="categorySelect" :options="categoryOptions"
<lil-select :value.sync="course.category" :options="categoryOptions"
placeholder="Выберите категорию"/>
</div>
</div>
@ -113,6 +111,13 @@
<button disabled class="field__append">руб.</button>
</div>
</div>
<div v-if="!live" class="info__field field">
<div class="field__label field__label_gray">ВОЗРАСТ</div>
<div class="field__wrap">
<lil-select :value.sync="course.age" :options="ages" value-key="value"
placeholder="Выберите возраст"/>
</div>
</div>
<label v-if="me && !live && me.role === ROLE_ADMIN" class="info__switch switch switch_lg">
<input type="checkbox" class="switch__input" v-model="course.is_featured">
<span class="switch__content">Выделить</span>
@ -166,45 +171,7 @@
</button>
</div>
<div v-if="viewSection === 'course'" class="kit__body">
<vue-draggable v-model="course.content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }">
<div v-for="(block, index) in course.content" :key="block.data.id ? block.data.id : block.data.guid">
<block-text v-if="block.type === 'text'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-image-text v-if="block.type === 'image-text'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
:image-id.sync="block.data.image_id"
:image-url.sync="block.data.image_url"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-image v-if="block.type === 'image'"
:index="index"
:title.sync="block.data.title"
:image-id.sync="block.data.image_id"
:image-url.sync="block.data.image_url"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-images v-if="block.type === 'images'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
:images.sync="block.data.images"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-video v-if="block.type === 'video'"
:index="index"
:title.sync="block.data.title"
v-on:remove="onBlockRemoved"
:video-url.sync="block.data.video_url"/>
</div>
</vue-draggable>
<block-add v-on:added="onBlockAdded"/>
<block-content :content.sync="course.content"></block-content>
<!--<div class="kit__foot">
<button type="submit" class="kit__submit btn btn_md" v-bind:class="{ loading: courseSaving }">
@ -236,7 +203,7 @@
</div>
<div class="lessons__subtitle subtitle">{{ lesson.title }}</div>
<div class="lessons__row">
<div class="lessons__content">{{ lesson.short_description | linebreaks }}</div>
<div class="lessons__content" v-html="lesson.short_description"></div>
</div>
</div>
</vue-draggable>
@ -269,15 +236,11 @@
import { ROLE_ADMIN, ROLE_AUTHOR } from './consts'
import LinkInput from './inputs/LinkInput'
import DatePicker from 'vuejs-datepicker'
import BlockText from './blocks/BlockText'
import BlockImage from './blocks/BlockImage'
import BlockImages from './blocks/BlockImages'
import BlockImageText from './blocks/BlockImageText'
import BlockVideo from './blocks/BlockVideo'
import BlockContent from './blocks/BlockContent'
import VueRedactor from './redactor/VueRedactor';
import LilSelect from "./inputs/LilSelect";
import LessonRedactor from "./LessonRedactor";
import {api} from "../js/modules/api";
import BlockAdd from "./blocks/BlockAdd";
import $ from 'jquery';
import {required, minValue, numeric, url } from 'vuelidate/lib/validators'
import slugify from 'slugify';
@ -312,6 +275,7 @@
duration: null,
author: null,
price: null,
age: 0,
url: '',
coverImage: '',
kit__body: null,
@ -376,6 +340,40 @@
'value': '18:00',
}
],
ages: [
{
'title': 'Любой возраст',
'value': 0,
},
{
'title': 'до 5',
'value': 1,
},
{
'title': '5-7',
'value': 2,
},
{
'title': '7-9',
'value': 3,
},
{
'title': '9-12',
'value': 4,
},
{
'title': '12-15',
'value': 5,
},
{
'title': '15-18',
'value': 6,
},
{
'title': 'от 18',
'value': 7,
},
],
weekdays: [
'',
@ -484,27 +482,6 @@
this.course.url = slugify(this.course.title);
}
},
updateCategory() {
if (this.categoryOptions && Array.isArray(this.categoryOptions) && this.course.category) {
this.categoryOptions.forEach((category) => {
if (category.id === this.course.category) {
this.course.categorySelect = category;
}
});
}
},
onBlockRemoved(blockIndex) {
const blockToRemove = this.course.content[blockIndex];
// Удаляем блок из Vue
this.course.content.splice(blockIndex, 1);
// Если блок уже был записан в БД, отправляем запрос на сервер на удаление блока из БД
if (blockToRemove.data.id) {
api.removeContentBlock(blockToRemove, this.accessToken);
}
},
onBlockAdded(blockData) {
this.course.content.push(blockData);
},
removeLesson(lessonIndex) {
if (!confirm('Вы действительно хотите удалить этот урок?')) {
return;
@ -518,18 +495,18 @@
},
editLesson(lessonIndex) {
this.currentLesson = this.lessons[lessonIndex];
history.push("/course/create/lessons/edit/"+this.currentLesson.id);
history.push(`/course/${this.course.id}/lessons/${this.currentLesson.id}/edit`);
this.viewSection = 'lessons-edit';
},
showCourse() {
if (this.viewSection !== 'course') {
history.push("/course/create");
history.push(this.course.id ? `/course/${this.course.id}/edit` : "/course/create");
}
this.viewSection = 'course'
},
showLessons() {
if (this.viewSection !== 'lessons') {
history.push("/course/create/lessons");
history.push(`/course/${this.course.id}/edit/lessons`);
}
this.viewSection = 'lessons';
},
@ -540,7 +517,7 @@
content: [],
};
if (this.viewSection !== 'lessons-edit') {
history.push("/course/create/lessons/new");
history.push(`/course/${this.course.id}/lessons/new`);
}
this.viewSection = 'lessons-edit';
window.scrollTo(0, 0);
@ -613,7 +590,7 @@
return req;
},
goToLessons() {
history.push("/course/create/lessons");
history.push(`/course/${this.course.id}/edit/lessons`);
this.viewSection = 'lessons';
this.$nextTick(() => {
const elementTop = $('#course-redactor__nav').position().top - 130;
@ -868,18 +845,17 @@
},
updateViewSection(location, action) {
//console.log('updateViewSection[action]', action);
if (location.pathname === '/course/create/lessons') {
if (location.pathname.match(/course\/\d+\/edit\/lessons/)) {
this.viewSection = 'lessons';
} else if (location.pathname === '/course/create') {
this.viewSection = 'course';
} else if (location.pathname === '/course/create/lessons/new') {
} else if (location.pathname.match(/course\/\d+\/lessons\/new/)){
this.viewSection = 'lessons-edit';
} else if (location.pathname.indexOf('/course/create/lessons/edit') !== -1) {
let lessonId = parseInt(location.pathname.split('/').pop());
//console.log('lessonId', lessonId, this.lessons.toString());
//console.log('lessod edit', this.lessons.find((i)=>{return i.id === lessonId}));
} else if (location.pathname.match(/course\/\d+\/lessons\/\d+\/edit/)) {
// let lessonId = parseInt(location.pathname.split('/').pop());
const lessonId = +location.pathname.match(/lessons\/(\d+)\/edit/)[1];
this.currentLesson = this.lessons.find((i)=>{return i.id === lessonId});
this.viewSection = 'lessons-edit';
} else {
this.viewSection = 'course';
}
},
onLessonsChanged() {
@ -941,14 +917,8 @@
promises.push(cats);
cats.then((response) => {
if (response.data) {
this.categoryOptions = response.data.results.map((category) => {
return {
title: category.title,
value: category.id
}
});
this.categoryOptions = response.data.results;
}
this.updateCategory();
});
if(this.live) {
@ -988,7 +958,6 @@
this.scheduleOptions = _.orderBy(options, (item)=>{return moment(item.value)});
}
this.updateCategory();
});
}
@ -1059,23 +1028,6 @@
this.course.price = value || 0;
}
},
categorySelect: {
get() {
if (!this.categoryOptions || this.categoryOptions.length === 0 || !this.course || !this.course.category) {
return null;
}
let value;
this.categoryOptions.forEach((category) => {
if (category.value === this.course.category) {
value = category;
}
});
return value;
},
set(value) {
this.course.category = value.value;
}
},
// userSelect: {
// get() {
// if (!this.users || this.users.length === 0 || !this.course || !this.course.author) {
@ -1127,19 +1079,14 @@
},
components: {
BlockAdd,
LessonRedactor,
LilSelect,
BlockText,
BlockContent,
'link-input': LinkInput,
'vue-datepicker': DatePicker,
'block-text': BlockText,
'block-image': BlockImage,
'block-image-text': BlockImageText,
'block-images': BlockImages,
'block-video': BlockVideo,
'lesson-redactor': LessonRedactor,
'vue-draggable': Draggable,
'vue-redactor': VueRedactor,
}
}
</script>
@ -1240,9 +1187,10 @@
overflow: scroll;
}
.field_short_description {
.field_short_description .redactor-box {
overflow-x: visible;
overflow-y: auto;
max-height: 200px;
overflow: scroll;
}
</style>

@ -27,50 +27,14 @@
<div class="kit__field field"
v-bind:class="{ error: $v.currentLesson.short_description.$invalid }">
<div class="field__wrap">
<textarea class="field__input" v-autosize="lesson.short_description" placeholder="Описание урока" v-model="lesson.short_description"></textarea>
<vue-redactor :value="lesson.short_description"
v-on:update:value="(value) => { this.lesson.short_description = value; }" placeholder="Описание урока"/>
</div>
</div>
</div>
</div>
</div>
<vue-draggable v-model="lesson.content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }">
<div v-for="(block, index) in lesson.content">
<block-text v-if="block.type === 'text'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-image-text v-if="block.type === 'image-text'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
:image-id.sync="block.data.image_id"
:image-url.sync="block.data.image_url"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-image v-if="block.type === 'image'"
:index="index"
:title.sync="block.data.title"
:image-id.sync="block.data.image_id"
:image-url.sync="block.data.image_url"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-images v-if="block.type === 'images'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
:images.sync="block.data.images"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-video v-if="block.type === 'video'"
:index="index"
:title.sync="block.data.title"
v-on:remove="onBlockRemoved"
:video-url.sync="block.data.video_url"/>
</div>
</vue-draggable>
<block-add v-on:added="onBlockAdded" />
<block-content :content.sync="lesson.content"></block-content>
<div class="kit__foot">
<button class="kit__submit btn btn_md" v-bind:class="{ loading: saving }">Сохранить</button>
@ -81,13 +45,9 @@
</template>
<script>
import BlockAdd from "./blocks/BlockAdd";
import BlockText from './blocks/BlockText'
import BlockImage from './blocks/BlockImage'
import BlockImages from './blocks/BlockImages'
import BlockImageText from './blocks/BlockImageText'
import BlockVideo from './blocks/BlockVideo'
import BlockContent from './blocks/BlockContent'
import LilImage from "./blocks/Image"
import VueRedactor from './redactor/VueRedactor';
import {api} from "../js/modules/api";
import Draggable from 'vuedraggable';
import _ from 'lodash'
@ -99,20 +59,6 @@
goBack() {
this.$emit('back');
},
onBlockAdded(blockData) {
this.lesson.content.push(blockData);
this.$emit('update:lesson', this.lesson);
},
onBlockRemoved(blockIndex) {
const blockToRemove = this.lesson.content[blockIndex];
// Удаляем блок из Vue
this.lesson.content.splice(blockIndex, 1);
this.$emit('update:lesson', this.lesson);
// Если блок уже был записан в БД, отправляем запрос на сервер на удаление блока из БД
if (blockToRemove.data.id) {
api.removeContentBlock(blockToRemove, this.accessToken);
}
},
onUpdateCoverUrl(newValue) {
this.lesson.coverImage = newValue;
},
@ -126,14 +72,9 @@
}
},
components: {
BlockAdd,
'block-text': BlockText,
'block-image': BlockImage,
'block-image-text': BlockImageText,
'block-images': BlockImages,
'block-video': BlockVideo,
'vue-draggable': Draggable,
BlockContent,
'lil-image': LilImage,
'vue-redactor': VueRedactor,
}
}
</script>

@ -0,0 +1,93 @@
<template>
<div>
<vue-draggable :list="content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }">
<div v-for="(block, index) in content" :key="block.id ? block.id : block.uuid">
<block-text v-if="block.type === 'text'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
v-on:remove="onBlockRemoved"/>
<block-image-text v-if="block.type === 'image-text'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
:image-id.sync="block.data.image_id"
:image-url.sync="block.data.image_thumbnail_url"
v-on:remove="onBlockRemoved"
:access-token="$root.store.accessToken"/>
<block-image v-if="block.type === 'image'"
:index="index"
:title.sync="block.data.title"
:image-id.sync="block.data.image_id"
:image-url.sync="block.data.image_thumbnail_url"
v-on:remove="onBlockRemoved"
:access-token="$root.store.accessToken"/>
<block-images v-if="block.type === 'images'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
:images.sync="block.data.images"
v-on:remove="onBlockRemoved"
:access-token="$root.store.accessToken"/>
<block-video v-if="block.type === 'video'"
:index="index"
:title.sync="block.data.title"
v-on:remove="onBlockRemoved"
:video-url.sync="block.data.video_url"/>
</div>
</vue-draggable>
<block-add v-on:added="onBlockAdded"/>
</div>
</template>
<script>
import {api} from "../../js/modules/api";
import Draggable from 'vuedraggable';
import BlockText from './BlockText'
import BlockImage from './BlockImage'
import BlockImages from './BlockImages'
import BlockImageText from './BlockImageText'
import BlockVideo from './BlockVideo'
import BlockAdd from "./BlockAdd"
export default {
name: 'block-content',
props: ['content'],
methods: {
onBlockRemoved(blockIndex) {
const remove = () => {
// Удаляем блок из Vue
content.splice(blockIndex, 1);
this.$emit('update:content', content);
}
const content = this.content;
const blockToRemove = this.content[blockIndex];
// Если блок уже был записан в БД, отправляем запрос на сервер на удаление блока из БД
if (blockToRemove.data.id) {
api.removeContentBlock(blockToRemove, this.$root.store.accessToken).then(response => {
remove();
});
}
else {
remove();
}
},
onBlockAdded(blockData) {
const content = this.content;
content.push(blockData);
this.$emit('update:content', content);
},
},
components: {
BlockAdd,
'block-text': BlockText,
'block-image': BlockImage,
'block-image-text': BlockImageText,
'block-images': BlockImages,
'block-video': BlockVideo,
'vue-draggable': Draggable,
}
}
</script>

@ -23,7 +23,7 @@
</div>
<div class="kit__gallery">
<div class="kit__preview" v-for="(image, index) in images" v-bind:class="{ 'kit__preview--loading': image.loading }">
<img :src="image.src" class="kit__pic">
<img :src="image.image_thumbnail_url" class="kit__pic">
<button type="button" @click="onRemoveImage(index)">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
@ -66,12 +66,12 @@
api.uploadImage(reader.result, this.accessToken)
.then((response) => {
let images = this.images;
console.log('images before', JSON.stringify(images));
images.forEach((image, index) => {
if (image.src === reader.result) {
images[index].img = response.data.id;
images[index].image_id = response.data.id;
images[index].loading = false;
images[index].src = response.data.image;
images[index].image_url = response.data.image;
images[index].image_thumbnail_url = response.data.image_thumbnail;
}
});
console.log('images after', JSON.stringify(images));
@ -91,12 +91,14 @@
},
onRemoveImage(index) {
let images = this.images;
let id = images[index].img;
images.splice(index, 1);
this.$emit('update:images', images);
let id = images[index].image_id;
api.removeImage(id, this.accessToken);
api.removeImage(id, this.accessToken)
.then(response => {
images.splice(index, 1);
this.$emit('update:images', images);
});
}
}
}
</script>
</script>

@ -18,6 +18,7 @@
data() {
return {
loading: false,
}
},
methods: {
@ -45,7 +46,7 @@
.then((response) => {
this.loading = false;
this.$emit('update:imageId', response.data.id);
this.$emit('update:imageUrl', response.data.image);
this.$emit('update:imageUrl', response.data.image_thumbnail);
})
.catch((error) => {
this.loading = false;

@ -4,8 +4,9 @@
{{ selectedTitle }}
</div>
<div class="select__drop">
<div v-for="option in options" :class="{ select__option: true, active: value && option.value == value.value }" @click.stop.prevent="selectOption(option)">
<div class="select__title">{{ option.title }}</div>
<div v-for="option in options" :class="{ select__option: true, active: option[vk] == selectedValue }"
@click.stop.prevent="selectOption(option)">
<div class="select__title">{{ option[tk] }}</div>
</div>
</div>
</div>
@ -14,11 +15,14 @@
<script>
export default {
name: "lil-select",
props: ["options", "value", "valueKey", "placeholder"],
props: ["options", "value", "titleKey", "valueKey", "isObj", "placeholder"],
data() {
return {
key: this.valueKey ? this.valueKey : 'title',
tk: this.titleKey || 'title',
vk: this.valueKey || 'id',
isOpened: false,
selected: null,
optionsDict: {},
}
},
methods: {
@ -30,22 +34,30 @@
},
selectOption(option) {
this.isOpened = !this.isOpened;
this.$emit('update:value', option);
this.$emit('update:value', this.isObj ? option : option[this.vk]);
this.selected = option;
}
},
mounted() {
document.addEventListener("click", this.clickListener);
this.options.forEach((option) => {
this.optionsDict[option[this.vk]] = option;
});
this.selected = this.optionsDict[this.selectedValue];
},
destroyed() {
document.removeEventListener("click", this.clickListener);
},
computed: {
isSelected() {
return this.value && this.value[this.key] ? true : false;
return !!this.selected;
},
selectedValue() {
return this.isObj ? this.value && this.value[this.vk] : this.value;
},
selectedTitle() {
if (this.isSelected) {
return this.value[this.key];
return this.selected[this.tk];
}
return this.placeholder ? this.placeholder : '';
}
@ -55,4 +67,4 @@
<style scoped>
</style>
</style>

@ -7,7 +7,7 @@
import './redactor-full.js';
export default {
props: ['value', 'placeholder'],
props: ['value', 'placeholder', 'air', 'buttonsHide'],
name: "vue-redactor",
data() {
return {
@ -18,7 +18,7 @@
const me = this;
$(me.$refs.input).redactor({
air: true,
buttonsHide: ['image', 'format'],
buttonsHide: this.buttonsHide || ['image', 'format'],
lang: 'ru',
placeholder: this.placeholder,
callbacks: {

@ -15,6 +15,7 @@ import "./modules/tabs";
import "./modules/popup";
import "./modules/courses";
import "./modules/comments";
import "./modules/comments";
import "./modules/password-show";
import "./modules/profile";
import "./modules/notification";
@ -24,17 +25,29 @@ import "../sass/app.sass";
import Vue from 'vue';
import Vuelidate from 'vuelidate';
import VueAutosize from '../components/directives/autosize'
import Comments from '../components/Comments';
import UploadContestWork from '../components/UploadContestWork.vue';
import ContestWorks from '../components/ContestWorks.vue';
import Likes from '../components/blocks/Likes.vue';
Vue.use(Vuelidate);
Vue.use(VueAutosize);
if (process.env.NODE_ENV === 'development') {
// Enable vue-devtools
Vue.config.devtools = true;
}
const components = {
UploadContestWork,
ContestWorks,
Likes,
Comments,
};
Object.assign(components, window.LIL_STORE.components);
const app = new Vue({
el: '#lilcity-vue-app',
data() {
@ -42,9 +55,5 @@ const app = new Vue({
store: window.LIL_STORE,
}
},
components: {
UploadContestWork,
ContestWorks,
Likes,
}
components: components
});

@ -1,19 +1,4 @@
import Vue from 'vue'
import Vuelidate from 'vuelidate'
import VueAutosize from '../components/directives/autosize'
import ContestRedactor from '../components/ContestRedactor.vue'
if (process.env.NODE_ENV === 'development') {
// Enable vue-devtools
Vue.config.devtools = true;
}
window.LIL_STORE.components['contest-redactor'] = ContestRedactor;
Vue.use(VueAutosize);
Vue.use(Vuelidate);
let app = new Vue({
el: '#lilcity-vue-app',
components: {
'contest-redactor': ContestRedactor,
}
});

@ -1,24 +1,8 @@
import Vue from 'vue'
import VueAutosize from '../components/directives/autosize'
import Vuelidate from 'vuelidate'
import 'babel-polyfill'
import CourseRedactor from '../components/CourseRedactor.vue'
import $ from 'jquery';
if (process.env.NODE_ENV === 'development') {
// Enable vue-devtools
Vue.config.devtools = true;
}
Vue.use(VueAutosize);
Vue.use(Vuelidate);
let app = new Vue({
el: '#lilcity-vue-app',
components: {
'course-redactor': CourseRedactor,
}
});
window.LIL_STORE.components['course-redactor'] = CourseRedactor;
$(document).ready(function () {
$('#course-redactor__publish-button').on('click', function () {
@ -29,4 +13,4 @@ $(document).ready(function () {
let event = new Event('course_preview');
document.getElementById('lilcity__course-redactor').dispatchEvent(event);
});
});
});

@ -108,6 +108,7 @@ export const api = {
short_description: courseObject.short_description,
category: courseObject.category,
price: courseObject.is_paid && courseObject.price || 0,
age: courseObject.age,
deferred_start_at: deferredStart,
duration: courseObject.duration || 0,
is_featured: courseObject.is_featured,
@ -116,72 +117,9 @@ export const api = {
stream: courseObject.stream,
cover: courseObject.coverImageId ? courseObject.coverImageId : null,
gallery: {
gallery_images: courseObject.gallery && courseObject.gallery.images ? courseObject.gallery.images : []
gallery_images: courseObject.gallery && courseObject.gallery.images || []
},
content: courseObject.content.map((block, index) => {
if (block.type === 'text') {
return {
'type': 'text',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'txt': block.data.text,
}
}
} else if (block.type === 'image') {
return {
'type': 'image',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'img': block.data.image_id,
}
}
} else if (block.type === 'image-text') {
return {
'type': 'image-text',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'img': block.data.image_id,
'txt': block.data.text,
}
}
} else if (block.type === 'images') {
return {
'type': 'images',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'images': block.data.images.map((galleryImage) => {
return {
'id': galleryImage.id ? galleryImage.id : null,
'img': galleryImage.img,
}
}),
}
}
} else if (block.type === 'video') {
return {
'type': 'video',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'url': block.data.video_url,
}
}
}
}),
content: api.convertContentJson(courseObject.content, true),
};
if(courseObject.live) {
@ -209,16 +147,70 @@ export const api = {
short_description: lessonObject.short_description,
course: lessonObject.course_id,
position: lessonObject.position,
content: lessonObject.content.map((block, index) => {
content: api.convertContentJson(lessonObject.content, true),
};
if (isAdding) {
return api.addLesson(lessonJson, accessToken);
} else {
return api.updateLesson(lessonObject.id, lessonJson, accessToken);
}
},
convertLessonJson: (lessonJSON) => {
return {
id: lessonJSON.id,
title: lessonJSON.title,
short_description: lessonJSON.short_description,
coverImageId: lessonJSON.cover && lessonJSON.cover.id ? lessonJSON.cover.id : null,
coverImage: lessonJSON.cover && lessonJSON.cover.image ? lessonJSON.cover.image : null,
content: api.convertContentJson(lessonJSON.content),
position: lessonJSON.position,
}
},
convertCourseJson: (courseJSON) => {
let isDeferred = false;
let deferredDate = false;
let deferredTime = '';
if (courseJSON.deferred_start_at) {
let deferredDateTime = moment(courseJSON.deferred_start_at);
isDeferred = true;
deferredDate = deferredDateTime.format('MM-DD-YYYY');
deferredTime = deferredDateTime.format('HH:mm');
}
return {
id: courseJSON.id,
title: courseJSON.title,
status: courseJSON.status,
short_description: courseJSON.short_description,
category: courseJSON.category && courseJSON.category.id ? courseJSON.category.id : courseJSON.category,
author: courseJSON.author && courseJSON.author.id ? courseJSON.author.id : courseJSON.author,
price: parseFloat(courseJSON.price),
is_paid: parseFloat(courseJSON.price) > 0,
is_deferred: isDeferred,
date: deferredDate || courseJSON.date,
time: deferredTime ? {title: deferredTime, value: deferredTime} : null,
duration: courseJSON.duration,
is_featured: courseJSON.is_featured,
url: courseJSON.slug,
stream: courseJSON.stream,
coverImageId: courseJSON.cover && courseJSON.cover.id ? courseJSON.cover.id : null,
coverImage: courseJSON.cover && courseJSON.cover.image ? courseJSON.cover.image : null,
content: api.convertContentJson(courseJSON.content),
gallery: {images: (courseJSON.gallery) ? courseJSON.gallery.gallery_images:[]},
}
},
convertContentJson: (contentJson, forSaving) => {
if(forSaving){
return contentJson.map((block, index) => {
if (block.type === 'text') {
return {
'type': 'text',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'txt': block.data.text,
'uuid': block.uuid,
}
}
} else if (block.type === 'image') {
@ -226,10 +218,10 @@ export const api = {
'type': 'image',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'img': block.data.image_id,
'uuid': block.uuid,
}
}
} else if (block.type === 'image-text') {
@ -237,11 +229,11 @@ export const api = {
'type': 'image-text',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'img': block.data.image_id,
'txt': block.data.text,
'uuid': block.uuid,
}
}
} else if (block.type === 'images') {
@ -249,15 +241,15 @@ export const api = {
'type': 'images',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'images': block.data.images.map((galleryImage) => {
return {
'id': galleryImage.id ? galleryImage.id : null,
'img': galleryImage.img,
'img': galleryImage.image_id,
}
}),
'uuid': block.uuid,
}
}
} else if (block.type === 'video') {
@ -265,66 +257,15 @@ export const api = {
'type': 'video',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'url': block.data.video_url,
'uuid': block.uuid,
}
}
}
}),
};
if (isAdding) {
return api.addLesson(lessonJson, accessToken);
} else {
return api.updateLesson(lessonObject.id, lessonJson, accessToken);
}
},
convertLessonJson: (lessonJSON) => {
return {
id: lessonJSON.id,
title: lessonJSON.title,
short_description: lessonJSON.short_description,
coverImageId: lessonJSON.cover && lessonJSON.cover.id ? lessonJSON.cover.id : null,
coverImage: lessonJSON.cover && lessonJSON.cover.image ? lessonJSON.cover.image : null,
content: api.convertContentResponse(lessonJSON.content),
position: lessonJSON.position,
});
}
},
convertCourseJson: (courseJSON) => {
let isDeferred = false;
let deferredDate = false;
let deferredTime = '';
if (courseJSON.deferred_start_at) {
let deferredDateTime = moment(courseJSON.deferred_start_at);
isDeferred = true;
deferredDate = deferredDateTime.format('MM-DD-YYYY');
deferredTime = deferredDateTime.format('HH:mm');
}
return {
id: courseJSON.id,
title: courseJSON.title,
status: courseJSON.status,
short_description: courseJSON.short_description,
category: courseJSON.category && courseJSON.category.id ? courseJSON.category.id : courseJSON.category,
author: courseJSON.author && courseJSON.author.id ? courseJSON.author.id : courseJSON.author,
price: parseFloat(courseJSON.price),
is_paid: parseFloat(courseJSON.price) > 0,
is_deferred: isDeferred,
date: deferredDate || courseJSON.date,
time: deferredTime ? {title: deferredTime, value: deferredTime} : null,
duration: courseJSON.duration,
is_featured: courseJSON.is_featured,
url: courseJSON.slug,
stream: courseJSON.stream,
coverImageId: courseJSON.cover && courseJSON.cover.id ? courseJSON.cover.id : null,
coverImage: courseJSON.cover && courseJSON.cover.image ? courseJSON.cover.image : null,
content: api.convertContentResponse(courseJSON.content),
gallery: {images: (courseJSON.gallery) ? courseJSON.gallery.gallery_images:[]},
}
},
convertContentResponse: (contentJson) => {
return contentJson.sort((a, b) => {
if (a.position < b.position) {
return -1;
@ -353,6 +294,7 @@ export const api = {
'title': contentItem.title,
'image_id': (contentItem.img) ? contentItem.img.id:null,
'image_url': (contentItem.img) ? contentItem.img.image:null,
'image_thumbnail_url': (contentItem.img) ? contentItem.img.image_thumbnail:null,
}
}
} else if (contentItem.type === 'image-text') {
@ -364,6 +306,7 @@ export const api = {
'title': contentItem.title,
'image_id': (contentItem.img) ? contentItem.img.id:null,
'image_url': (contentItem.img) ? contentItem.img.image:null,
'image_thumbnail_url': (contentItem.img) ? contentItem.img.image_thumbnail:null,
'text': contentItem.txt,
}
}
@ -377,8 +320,9 @@ export const api = {
'images': contentItem.gallery_images.map((galleryImage) => {
return {
'id': galleryImage.id,
'img': galleryImage.img.id,
'src': galleryImage.img.image,
'image_id': galleryImage.img.id,
'image_url': galleryImage.img.image,
'image_thumbnail_url': galleryImage.img.image_thumbnail,
}
}),
}
@ -509,5 +453,31 @@ export const api = {
'Authorization': `Token ${window.LIL_STORE.accessToken}`,
}
});
},
getObjComments: (objId, objType, ordering) => {
return api.get('/api/v1/obj-comments/', {
params: {
obj_id: objId,
obj_type: objType,
ordering: ordering || '',
}
});
},
addObjComment: (objId, objType, commentJson) => {
let data = commentJson;
data.obj_id = objId;
data.obj_type = objType;
return api.post('/api/v1/obj-comments/', data, {
headers: {
'Authorization': `Token ${window.LIL_STORE.accessToken}`,
}
});
},
removeObjComment: (commentId) => {
return api.delete(`/api/v1/obj-comments/${commentId}/`, {
headers: {
'Authorization': `Token ${window.LIL_STORE.accessToken}`,
}
});
}
};

@ -62,3 +62,4 @@ $(document).ready(function () {
form.find('.questions__reply-info').hide();
}
});

@ -12,9 +12,12 @@ moment.locale('ru');
const history = createHistory();
$(document).ready(function () {
const currentCategory = $('div.js-select-option.active[data-category-option]').attr('data-category-name');
if(currentCategory) {
$('.js-select[data-category-select] .js-select-head').text(currentCategory);
const categoryName = $('div.js-select-option.active[data-category-option]').attr('data-category-name');
let category = $('div.js-select-option.active[data-category-option]').attr('data-category');
let age = $('div.js-select-option.active[data-age-option]').data('age');
let page = 1;
if(category) {
$('.js-select[data-category-select] .js-select-head').text(categoryName);
}
// Обработчик отложенных курсов
setInterval(() => {
@ -29,17 +32,30 @@ $(document).ready(function () {
// Обработчик кнопки "Подгрузить еще"
$('.courses').on('click', 'button.load__btn', function () {
load_courses($(this).attr('data-next-page-url'), false);
page = $(this).attr('data-next-page');
loadCourses();
});
// Обработчик выбора категории
$('div.js-select-option[data-category-option]').on('click', function (e) {
e.preventDefault();
const currentCategory = $(this).attr('data-category-name');
const categoryName = $(this).attr('data-category-name');
$('[data-category-name]').removeClass('active');
$(`[data-category-name='${currentCategory}']`).addClass('active');
history.replace($(this).attr('data-category-url'));
load_courses($(this).attr('data-category-url'), true);
$(`[data-category-name='${categoryName}']`).addClass('active');
category = $(this).attr('data-category');
page = 1;
loadCourses(true);
});
// Обработчик выбора возраста
$('div.js-select-option[data-age-option]').on('click', function (e) {
e.preventDefault();
const currentAge = $(this).attr('data-age-name');
$('[data-age-name]').removeClass('active');
$(`[data-age-name='${currentAge}']`).addClass('active');
age = $(this).attr('data-age');
page = 1;
loadCourses(true);
});
// Обработчик лайков
@ -95,42 +111,50 @@ $(document).ready(function () {
}
});
})
});
function load_courses(coursesUrl, fromStart) {
$('.courses__list').css('opacity', '0.9');
const buttonElement = $('.courses').find('button.load__btn');
if (!fromStart) {
buttonElement.addClass('loading');
}
$.ajax(coursesUrl, {
method: 'GET'
})
.done(function (data) {
if (data.success === true) {
if (!fromStart) {
$('.courses__list').append(data.content);
} else {
$('.courses__list').html(data.content);
function loadCourses(replaceHistory) {
$('.courses__list').css('opacity', '0.9');
const buttonElement = $('.courses').find('button.load__btn');
let coursesUrl = window.LIL_STORE.urls.courses + '?' + $.param({
category: category,
age: age,
});
if (page > 1) {
buttonElement.addClass('loading');
}
else{
history.replace(coursesUrl);
}
coursesUrl += `&page=${page}`;
$.ajax(coursesUrl, {
method: 'GET'
})
.done(function (data) {
if (data.success === true) {
if (page > 1) {
$('.courses__list').append(data.content);
} else {
$('.courses__list').html(data.content);
}
if (data.next_url) {
buttonElement.attr('data-next-page-url', data.next_url);
buttonElement.show();
} else {
buttonElement.hide()
}
}
if (data.next_url) {
buttonElement.attr('data-next-page-url', data.next_url);
buttonElement.show();
} else {
buttonElement.hide()
})
.fail(function (xhr) {
if (xhr.status === 404) {
// Нет результатов, скрываем кнопку
buttonElement.hide();
}
}
})
.fail(function (xhr) {
if (xhr.status === 404) {
// Нет результатов, скрываем кнопку
buttonElement.hide();
}
})
.always(function () {
$('.courses__list').css('opacity', '1');
if (buttonElement) {
buttonElement.removeClass('loading');
}
});
}
})
.always(function () {
$('.courses__list').css('opacity', '1');
if (buttonElement) {
buttonElement.removeClass('loading');
}
});
}
});

@ -1,6 +1,9 @@
import $ from 'jquery';
import moment from 'moment';
import {api} from './api';
moment.locale('ru');
var selectedWeekdays = {};
$(document).ready(function () {
$(".js-video-modal").each(function(){
@ -46,21 +49,12 @@ $(document).ready(function () {
popup = $(data);
showPopup();
let is_extend = false;
if(data === '.js-popup-buy') {
console.log('reset selected');
popup.data('date-start', $this.data('date-start') || '');
popup.data('day', $this.data('day') || '');
$('[data-day]').prop('checked', false);
if ($this.text() === 'продлить') {
//data-purchased
//restore purchased selection
console.log('restore purchased');
$('[data-purchased]').each(function(){
$('[data-day='+$(this).data('purchased')+']').prop('checked', true);
});
is_extend = true;
}
if(! window.LIL_STORE.user.id) {
const $btn = popup.find('.buy__btn');
$btn.click(function(event) {
@ -72,7 +66,30 @@ $(document).ready(function () {
});
});
}
if ($this.data('prolong')) {
$('[data-day][data-purchased]').each(function(){
$(this).prop('checked', true).prop('disabled', false);
});
}
else{
if($this.data('day')) {
let day = $this.data('day');
$('[data-day][data-purchased]').each(function(){
$(this).prop('checked', false).prop('disabled', true);
});
$('[data-day='+day+']').prop('checked', true);
}
else{
$('[data-day]').each(function(){
$(this).prop('checked', true).prop('disabled', false);
});
}
}
updateCart();
}
if( data === '.js-popup-auth') {
let nextUrl = $this.data('auth-next-url');
if(nextUrl === 'href') {
@ -80,20 +97,6 @@ $(document).ready(function () {
}
popup.data('next-url', nextUrl);
}
if($this.data('day')) {
let day = $this.data('day');
$('[data-day='+day+']').prop('checked', true);
}
if(!is_extend && !$this.data('day')) {
console.log('check all');
$('[data-day]').each(function(){
$(this).prop('checked', true);
});
}
updateCart();
});
$('.js-popup-close').on('click', function(e){
@ -151,7 +154,10 @@ $(document).ready(function () {
});
function updateCart(){
var link = $('.but_btn_popup').data('link');
var $orderPrice = $('.order_price_text');
var $orderDates = $('.order__dates');
var dateStart = popup.data('date-start');
var days = ['', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'];
var weekdays = [], daysText = [];
var $bonuses = $('.buy__bonuses');
@ -167,7 +173,7 @@ $(document).ready(function () {
});
if(weekdays.length){
api.getPaymentAmount({ user: window.LIL_STORE.user.id, weekdays: weekdays })
api.getPaymentAmount({ user: window.LIL_STORE.user.id, weekdays: weekdays, date_start: dateStart})
.then((response) => {
var text = '';
var amount = +response.data.amount;
@ -183,15 +189,17 @@ $(document).ready(function () {
text = amount + 'p.';
}
$orderPrice.html(text);
$orderDates.text(moment(response.data.date_start).format('D MMM') + ' - ' + moment(response.data.date_end).format('D MMM'));
$('.but_btn_popup').attr('href', link+'?'+decodeURIComponent($.param({
weekdays: weekdays,
date_start: moment(response.data.date_start).format('YYYY-MM-DD'),
use_bonuses: useBonuses || '',
}, true)));
});
}
else {
$orderPrice.html('0p.');
}
$('.order__days').html((daysText.length) ? daysText.join(', '):'Ничего не выбрано');
var link = $('.but_btn_popup').data('link');
link = link+'?'+decodeURIComponent($.param({weekdays: weekdays, use_bonuses: useBonuses || ''}, true));
$('.but_btn_popup').attr('href', link);
}
});

@ -891,11 +891,11 @@ a[name]
font-size: 20px
.section
padding: 60px 0
padding: 50px 0
+t
padding: 40px 0
+m
padding: 30px 0
+m
padding: 20px 0
&_review
background: url(../img/bg-elephants.jpg) 0 0 / 100px 102px
&_gray
@ -1352,6 +1352,8 @@ a[name]
&__pic
display: block
width: 100%
height: 100%
object-fit: cover
.app
position: relative
@ -2456,11 +2458,11 @@ a.grey-link
.go
display: flex
margin-bottom: 50px
margin-bottom: 40px
justify-content: space-between
align-items: center
+t
margin-bottom: 30px
margin-bottom: 20px
&__item
display: flex
max-width: calc(50% - 20px)
@ -2517,11 +2519,11 @@ a.grey-link
fill: white
.course
margin-bottom: 60px
margin-bottom: 50px
&__head
display: flex
min-height: 40px
margin-bottom: 30px
margin-bottom: 20px
align-items: center
justify-content: space-between
+t
@ -2531,9 +2533,9 @@ a.grey-link
+t
margin-bottom: 10px
&__content
margin-bottom: 40px
margin-bottom: 30px
+t
margin-bottom: 25px
margin-bottom: 15px
&__about
position: relative
margin-bottom: 40px
@ -2563,9 +2565,9 @@ a.grey-link
&:active
color: rgba(white,.4)
&__user
margin-bottom: 40px
margin-bottom: 30px
+t
margin-bottom: 30px
margin-bottom: 20px
&__info
display: flex
align-items: center
@ -2580,17 +2582,17 @@ a.grey-link
margin: 20px 0 0
&__metas
display: flex
margin-bottom: 45px
margin-bottom: 35px
justify-content: space-between
+m
display: block
margin-bottom: 25px
margin-bottom: 15px
&__metas &__meta
+m
margin-bottom: 20px
&__actions
display: flex
margin: 0 -10px 50px
margin: 0 -10px 40px
&__action
position: relative
margin: 0 10px
@ -2658,6 +2660,8 @@ a.grey-link
visibility: visible
&__subtitle
margin-bottom: 20px
text-transform: none
letter-spacing: normal
&__row
display: flex
+m
@ -2676,6 +2680,8 @@ a.grey-link
object-fit: cover
width: 100%
height: 100%
&__content-wrapper
flex: 1
&__content
flex: 0 0 calc(100% - 165px)
&__actions
@ -2715,6 +2721,8 @@ a.grey-link
.lesson
&__subtitle
letter-spacing: normal
text-transform: none
margin-bottom: 10px
color: #191919
&__content
@ -2811,16 +2819,23 @@ a.grey-link
&__item
display: flex
&__form
margin-bottom: 40px
position: relative;
margin-top: 20px
padding-bottom: 20px
border-bottom: 1px solid $border
&__form-loader
display: none;
&__form-foot
text-align: center;
&__item
&:not(:last-child)
margin-bottom: 25px
+m
padding: 10px 0
&_reply
padding-left: 80px
+m
padding: 0
padding: 10px 0
&__reply-info
display: none
margin-bottom: 10px
@ -2859,11 +2874,13 @@ a.grey-link
+m
height: 64px
&__btn
display: block
margin: 0 auto
margin: 0 15px;
border-radius: 20px
+m
margin-left: 0
&__details
margin-bottom: 5px
flex: 1
&__head,
&__action
+fb
@ -2880,11 +2897,76 @@ a.grey-link
&__author
margin-right: 15px
&__date
font-size: 10px
display: inline-block
&__foot
height: 20px
text-align: right
&__action
margin-left: auto
&__heart
fill: #d40700
width: 28px
height: 28px
&__send-icon
fill: $gray
width: 20px
height: 20px
&__delete-icon
fill: #C8C8C8
width: 17px
height: 17px
:hover
fill: #000000
&--heart
&__content
font-size: 24px
color: #d40700
&--chat
margin-top: 15px
&--chat &__items
background: white
padding: 10px
border-radius: 5px
max-height: 400px
overflow: auto
+m
max-height: inherit
&--chat &__item, &--chat &__item_reply
+m
padding: 0
&--chat &__ava
height: 40px
margin-right: 10px
flex: 0 0 40px
+m
display: none
&--chat &__content
margin-bottom: 10px
&--chat &__wrap
display: flex
flex: 0 0 calc(100% - 60px);
+m
flex: 1
&--chat &__field
margin-bottom: 0;
flex: 0 0 calc(100% - 160px);
+m
flex: 1
&--chat &__btn
font-weight: 600;
text-shadow: 1px 1px #fff;
+m
margin: 0 0 0 15px;
&--chat &__form-foot
display: flex;
&--loading &__form-loader
display: block
&--loading &__form &__ava, &--loading &__form &__wrap
opacity: 0.4
.share
&__title
@ -3676,6 +3758,11 @@ a.grey-link
+m
display: none
.mobile-show
display: none
+m
display: block
.school
display: flex
position: relative
@ -4118,6 +4205,8 @@ a
font-size: 16px
padding-bottom: 24px
text-align: left
margin: 0
max-width: 100%
&.pic
position: relative
width: 100%
@ -4127,6 +4216,24 @@ a
max-width: 100%;
max-height: 60%;
@media only screen and (max-width: 639px)
.content-block__video iframe
width: 100%;
height: 240px;
.content-block__video.iframe__container
text-align: center;
width: 100%;
padding-bottom: 56.25%;
margin-bottom: -56.25%;
@media only screen and (min-width: 640px)
.content-block__video.iframe__container
text-align: center;
iframe
width: 640px;
height: 360px;
.banner
height: 140px

Loading…
Cancel
Save