Merge branch 'master' of https://gitlab.com/lilcity/backend into hotfix/LIL-601

remotes/origin/hotfix/LIL-691
gzbender 7 years ago
commit 8cdfc144be
  1. 69
      api/v1/serializers/course.py
  2. 98
      api/v1/serializers/mixins.py
  3. 72
      api/v1/serializers/payment.py
  4. 3
      api/v1/urls.py
  5. 108
      api/v1/views.py
  6. 30
      apps/course/migrations/0044_livelessoncomment.py
  7. 14
      apps/course/models.py
  8. 36
      apps/course/templates/course/lesson.html
  9. 33
      apps/payment/models.py
  10. 50
      apps/payment/views.py
  11. 1
      apps/school/models.py
  12. 4
      apps/school/templates/blocks/schedule_item.html
  13. 10
      apps/school/templates/school/livelesson_detail.html
  14. 2
      apps/school/templates/school/summer_school.html
  15. 6
      apps/school/templates/summer/prolong_btn.html
  16. 6
      apps/school/templates/summer/schedule_purchased.html
  17. 53
      apps/school/views.py
  18. 12
      apps/user/models.py
  19. 60
      apps/user/templates/user/profile.html
  20. 30
      apps/user/views.py
  21. 16
      project/context_processors.py
  22. 14
      project/pusher.py
  23. 9
      project/settings.py
  24. 7
      project/templates/blocks/lil_store_js.html
  25. 37
      project/templates/blocks/popup_buy.html
  26. 2
      project/templates/lilcity/edit_index.html
  27. 1
      project/templates/lilcity/index.html
  28. 6
      project/urls.py
  29. 2
      requirements.txt
  30. 64
      web/src/components/Comment.vue
  31. 53
      web/src/components/CommentForm.vue
  32. 134
      web/src/components/Comments.vue
  33. 136
      web/src/components/ContestRedactor.vue
  34. 92
      web/src/components/CourseRedactor.vue
  35. 68
      web/src/components/LessonRedactor.vue
  36. 87
      web/src/components/blocks/BlockContent.vue
  37. 20
      web/src/components/blocks/BlockImages.vue
  38. 3
      web/src/components/blocks/Image.vue
  39. 19
      web/src/js/app.js
  40. 17
      web/src/js/contest-redactor.js
  41. 20
      web/src/js/course-redactor.js
  42. 225
      web/src/js/modules/api.js
  43. 1
      web/src/js/modules/comments.js
  44. 68
      web/src/js/modules/popup.js
  45. 82
      web/src/sass/_common.sass

@ -10,7 +10,7 @@ from apps.course.models import (
Comment, CourseComment, LessonComment, Comment, CourseComment, LessonComment,
Material, Lesson, Material, Lesson,
Like, Like,
) LiveLessonComment)
from .content import ( from .content import (
ImageObjectSerializer, ContentSerializer, ContentCreateSerializer, ImageObjectSerializer, ContentSerializer, ContentCreateSerializer,
GallerySerializer, GalleryImageSerializer, GallerySerializer, GalleryImageSerializer,
@ -329,6 +329,7 @@ class CommentSerializer(serializers.ModelSerializer):
'parent', 'parent',
'deactivated_at', 'deactivated_at',
'created_at', 'created_at',
'created_at_humanize',
'update_at', 'update_at',
) )
@ -344,6 +345,8 @@ class CommentSerializer(serializers.ModelSerializer):
return CourseCommentSerializer(instance, context=self.context).to_representation(instance) return CourseCommentSerializer(instance, context=self.context).to_representation(instance)
elif isinstance(instance, LessonComment): elif isinstance(instance, LessonComment):
return LessonCommentSerializer(instance, context=self.context).to_representation(instance) 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): class CourseCommentSerializer(serializers.ModelSerializer):
@ -376,3 +379,67 @@ class LessonCommentSerializer(serializers.ModelSerializer):
read_only_fields = CommentSerializer.Meta.read_only_fields + ( read_only_fields = CommentSerializer.Meta.read_only_fields + (
'children', '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']: if 'id' in cdata and cdata['id']:
t = Text.objects.get(id=cdata.pop('id')) t = Text.objects.get(id=cdata.pop('id'))
serializer = TextCreateSerializer(t, data=cdata) serializer = TextCreateSerializer(t, data=cdata)
if serializer.is_valid():
serializer.save()
else: else:
serializer = TextCreateSerializer(data=cdata) serializer = TextCreateSerializer(data=cdata)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
elif ctype == 'image': elif ctype == 'image':
if 'id' in cdata and cdata['id']: if 'id' in cdata and cdata['id']:
image = Image.objects.get(id=cdata.pop('id')) image = Image.objects.get(id=cdata.pop('id'))
serializer = ImageCreateSerializer(image, data=cdata) 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: else:
serializer = ImageCreateSerializer(data=cdata) serializer = ImageCreateSerializer(data=cdata)
if serializer.is_valid(): if serializer.is_valid():
image = serializer.save() image = serializer.save()
else: else:
continue continue
if 'img' in cdata:
try: try:
image_object = ImageObject.objects.get(id=cdata['img']) image_object = ImageObject.objects.get(id=cdata.get('img'))
except ImageObject.DoesNotExist: except ImageObject.DoesNotExist:
pass pass
else: else:
@ -71,25 +59,15 @@ class DispatchContentMixin(object):
if 'id' in cdata and cdata['id']: if 'id' in cdata and cdata['id']:
it = ImageText.objects.get(id=cdata.pop('id')) it = ImageText.objects.get(id=cdata.pop('id'))
serializer = ImageTextCreateSerializer(it, data=cdata) 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: else:
serializer = ImageTextCreateSerializer(data=cdata) serializer = ImageTextCreateSerializer(data=cdata)
if serializer.is_valid(): if serializer.is_valid():
it = serializer.save() it = serializer.save()
else: else:
continue continue
if 'img' in cdata:
try: try:
image_object = ImageObject.objects.get(id=cdata['img']) image_object = ImageObject.objects.get(id=cdata.get('img'))
except ImageObject.DoesNotExist: except ImageObject.DoesNotExist:
pass pass
else: else:
@ -100,52 +78,36 @@ class DispatchContentMixin(object):
if 'id' in cdata and cdata['id']: if 'id' in cdata and cdata['id']:
v = Video.objects.get(id=cdata.pop('id')) v = Video.objects.get(id=cdata.pop('id'))
serializer = VideoCreateSerializer(v, data=cdata) serializer = VideoCreateSerializer(v, data=cdata)
if serializer.is_valid():
serializer.save()
else: else:
serializer = VideoCreateSerializer(data=cdata) serializer = VideoCreateSerializer(data=cdata)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
elif ctype == 'images': elif ctype == 'images':
if 'id' in cdata and cdata['id']: if 'id' in cdata and cdata['id']:
g = Gallery.objects.get(id=cdata['id']) g = Gallery.objects.get(id=cdata['id'])
g.position = cdata['position'] g.position = cdata['position']
g.title = cdata['title'] g.title = cdata['title']
g.uuid = cdata['uuid'] 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: else:
g = Gallery( g = Gallery(
position=cdata['position'], position=cdata['position'],
title=cdata['title'], title=cdata['title'],
uuid=cdata['uuid'], uuid=cdata['uuid'],
) )
setattr(g, obj_type, obj) setattr(g, obj_type, obj)
g.save() g.save()
if 'images' in cdata: if 'images' in cdata:
for image in cdata['images']: for image in cdata['images']:
if 'img' in image and image['img']: if 'img' in image and image['img']:
if 'id' in image and image['id']: if 'id' in image and image['id']:
gi = GalleryImage.objects.get(id=image['id']) gi = GalleryImage.objects.get(id=image['id'])
gi.img = ImageObject.objects.get(id=image['img']) gi.img = ImageObject.objects.get(id=image['img'])
gi.save() gi.save()
else: else:
gi = GalleryImage.objects.create( gi = GalleryImage.objects.create(
gallery=g, gallery=g,
img=ImageObject.objects.get(id=image['img']) img=ImageObject.objects.get(id=image['img'])
) )
class DispatchMaterialMixin(object): class DispatchMaterialMixin(object):

@ -46,8 +46,29 @@ class AuthorBalanceCreateSerializer(serializers.ModelSerializer):
return AuthorBalanceSerializer(instance, context=self.context).to_representation(instance) return AuthorBalanceSerializer(instance, context=self.context).to_representation(instance)
class PaymentSerializer(serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = Payment
fields = BASE_PAYMENT_FIELDS
read_only_fields = (
'id',
'user',
'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 AuthorBalanceSerializer(serializers.ModelSerializer): class AuthorBalanceSerializer(serializers.ModelSerializer):
author = UserSerializer() author = UserSerializer()
payment = serializers.SerializerMethodField()
class Meta: class Meta:
model = AuthorBalance model = AuthorBalance
@ -70,6 +91,24 @@ class AuthorBalanceSerializer(serializers.ModelSerializer):
'payment', 'payment',
) )
def get_payment(self, instance):
try:
p = instance.payment
except Exception:
return None
data = {
'id': p.id,
'created_at': p.created_at,
'amount': p.amount,
'data': p.data,
}
if isinstance(instance.payment, CoursePayment):
data['course'] = {
'id': p.course.id,
'title': p.course.title,
}
return data
class PaymentSerializer(serializers.ModelSerializer): class PaymentSerializer(serializers.ModelSerializer):
user = UserSerializer() user = UserSerializer()
@ -80,6 +119,7 @@ class PaymentSerializer(serializers.ModelSerializer):
read_only_fields = ( read_only_fields = (
'id', 'id',
'user', 'user',
'data',
'created_at', 'created_at',
'update_at', 'update_at',
) )
@ -91,6 +131,20 @@ class PaymentSerializer(serializers.ModelSerializer):
return SchoolPaymentSerializer(instance, context=self.context).to_representation(instance) 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): class CoursePaymentSerializer(serializers.ModelSerializer):
user = UserSerializer() user = UserSerializer()
course = CourseSerializer() course = CourseSerializer()
@ -107,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): class SchoolPaymentSerializer(serializers.ModelSerializer):
user = UserSerializer() user = UserSerializer()

@ -17,7 +17,7 @@ from .views import (
GalleryViewSet, GalleryImageViewSet, GalleryViewSet, GalleryImageViewSet,
UserViewSet, LessonViewSet, ImageObjectViewSet, UserViewSet, LessonViewSet, ImageObjectViewSet,
SchoolScheduleViewSet, LiveLessonViewSet, SchoolScheduleViewSet, LiveLessonViewSet,
PaymentViewSet, PaymentViewSet, ObjectCommentsViewSet,
ContestViewSet, ContestWorkViewSet) ContestViewSet, ContestWorkViewSet)
router = DefaultRouter() 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'categories', CategoryViewSet, base_name='categories')
router.register(r'courses', CourseViewSet, base_name='courses') router.register(r'courses', CourseViewSet, base_name='courses')
router.register(r'comments', CommentViewSet, base_name='comments') 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'materials', MaterialViewSet, base_name='materials')
router.register(r'lessons', LessonViewSet, base_name='lessons') router.register(r'lessons', LessonViewSet, base_name='lessons')
router.register(r'likes', LikeViewSet, base_name='likes') router.register(r'likes', LikeViewSet, base_name='likes')

@ -1,3 +1,5 @@
from datetime import datetime
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework import status, views, viewsets, generics from rest_framework import status, views, viewsets, generics
@ -11,10 +13,11 @@ from .serializers.course import (
CategorySerializer, LikeSerializer, CategorySerializer, LikeSerializer,
CourseSerializer, CourseCreateSerializer, CourseSerializer, CourseCreateSerializer,
CourseBulkChangeCategorySerializer, CourseBulkChangeCategorySerializer,
CommentSerializer, CommentSerializer, CommentCreateSerializer,
MaterialSerializer, MaterialCreateSerializer, MaterialSerializer, MaterialCreateSerializer,
LessonSerializer, LessonCreateSerializer, LessonSerializer, LessonCreateSerializer,
LikeCreateSerializer) LikeCreateSerializer, CourseCommentSerializer, LessonCommentSerializer,
LiveLessonCommentSerializer)
from .serializers.content import ( from .serializers.content import (
BanerSerializer, BanerSerializer,
ImageSerializer, ImageCreateSerializer, ImageSerializer, ImageCreateSerializer,
@ -34,7 +37,7 @@ from .serializers.payment import (
AuthorBalanceSerializer, AuthorBalanceCreateSerializer, AuthorBalanceSerializer, AuthorBalanceCreateSerializer,
PaymentSerializer, CoursePaymentSerializer, PaymentSerializer, CoursePaymentSerializer,
SchoolPaymentSerializer, SchoolPaymentSerializer,
) CoursePaymentCreateSerializer, SchoolPaymentCreateSerializer)
from .serializers.user import ( from .serializers.user import (
AuthorRequestSerializer, AuthorRequestSerializer,
UserSerializer, UserPhotoSerializer, UserSerializer, UserPhotoSerializer,
@ -54,7 +57,7 @@ from apps.course.models import (
Comment, CourseComment, LessonComment, Comment, CourseComment, LessonComment,
Material, Lesson, Material, Lesson,
Like, Like,
) LiveLessonComment)
from apps.config.models import Config from apps.config.models import Config
from apps.content.models import ( from apps.content.models import (
Baner, Image, Text, ImageText, Video, Baner, Image, Text, ImageText, Video,
@ -66,13 +69,14 @@ from apps.payment.models import (
) )
from apps.school.models import SchoolSchedule, LiveLesson from apps.school.models import SchoolSchedule, LiveLesson
from apps.user.models import AuthorRequest from apps.user.models import AuthorRequest
from project.pusher import pusher
User = get_user_model() User = get_user_model()
class AuthorBalanceViewSet(ExtendedModelViewSet): class AuthorBalanceViewSet(ExtendedModelViewSet):
queryset = AuthorBalance.objects.filter( queryset = AuthorBalance.objects.filter(
author__role__in=[User.AUTHOR_ROLE, User.ADMIN_ROLE], author__role__in=[User.AUTHOR_ROLE, User.ADMIN_ROLE, User.TEACHER_ROLE],
) )
serializer_class = AuthorBalanceCreateSerializer serializer_class = AuthorBalanceCreateSerializer
serializer_class_map = { serializer_class_map = {
@ -429,15 +433,15 @@ class ConfigViewSet(generics.RetrieveUpdateAPIView):
class CommentViewSet(ExtendedModelViewSet): class CommentViewSet(ExtendedModelViewSet):
queryset = Comment.objects.filter(level=0) queryset = Comment.objects.all()
serializer_class = CommentSerializer serializer_class = CommentSerializer
permission_classes = (IsAdmin,) permission_classes = (IsAuthorObjectOrAdmin,)
def get_queryset(self): def get_queryset(self):
queryset = self.queryset queryset = self.queryset
is_deactivated = self.request.query_params.get('is_deactivated', '0') is_deactivated = self.request.query_params.get('is_deactivated', '0')
if is_deactivated == '0': if is_deactivated == '0':
queryset = queryset queryset = queryset.filter(level=0)
elif is_deactivated == '1': elif is_deactivated == '1':
queryset = queryset.filter(deactivated_at__isnull=True) queryset = queryset.filter(deactivated_at__isnull=True)
elif is_deactivated == '2': elif is_deactivated == '2':
@ -446,6 +450,78 @@ class CommentViewSet(ExtendedModelViewSet):
return queryset return queryset
class ObjectCommentsViewSet(ExtendedModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentCreateSerializer
permission_classes = (IsAuthorObjectOrAdmin,)
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)
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()
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): class AuthorRequestViewSet(ExtendedModelViewSet):
queryset = AuthorRequest.objects.all() queryset = AuthorRequest.objects.all()
serializer_class = AuthorRequestSerializer serializer_class = AuthorRequestSerializer
@ -453,7 +529,7 @@ class AuthorRequestViewSet(ExtendedModelViewSet):
filter_fields = ('status',) filter_fields = ('status',)
class PaymentViewSet(ExtendedModelViewSet): class PaymentViewSet(viewsets.ModelViewSet):
queryset = Payment.objects.all() queryset = Payment.objects.all()
serializer_class = PaymentSerializer serializer_class = PaymentSerializer
permission_classes = (IsAdmin,) permission_classes = (IsAdmin,)
@ -465,6 +541,16 @@ class PaymentViewSet(ExtendedModelViewSet):
) )
search_fields = ('user__email', 'user__first_name', 'user__last_name',) 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): def get_queryset(self):
queryset = self.queryset queryset = self.queryset
course = self.request.query_params.get('course') course = self.request.query_params.get('course')
@ -481,10 +567,12 @@ class PaymentViewSet(ExtendedModelViewSet):
user = request.query_params.get('user') user = request.query_params.get('user')
course = request.query_params.get('course') course = request.query_params.get('course')
weekdays = request.query_params.getlist('weekdays[]') weekdays = request.query_params.getlist('weekdays[]')
date_start = request.query_params.get('date_start')
user = user and User.objects.get(pk=user) user = user and User.objects.get(pk=user)
course = course and Course.objects.get(pk=course) course = course and Course.objects.get(pk=course)
date_start = date_start and datetime.strptime(date_start, '%Y-%m-%d')
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): class ContestViewSet(ExtendedModelViewSet):

@ -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 django.conf import settings
from polymorphic_tree.models import PolymorphicMPTTModel, PolymorphicTreeForeignKey from polymorphic_tree.models import PolymorphicMPTTModel, PolymorphicTreeForeignKey
from apps.school.models import LiveLesson
from project.mixins import BaseModel, DeactivatedMixin from project.mixins import BaseModel, DeactivatedMixin
from apps.content.models import ImageObject, Gallery, Video, ContestWork from apps.content.models import ImageObject, Gallery, Video, ContestWork
@ -258,6 +259,9 @@ class Material(models.Model):
class Comment(PolymorphicMPTTModel, DeactivatedMixin): class Comment(PolymorphicMPTTModel, DeactivatedMixin):
OBJ_TYPE_COURSE = 'course'
OBJ_TYPE_LESSON = 'lesson'
OBJ_TYPE_LIVE_LESSON = 'live-lesson'
content = models.TextField('Текст комментария', default='') content = models.TextField('Текст комментария', default='')
author = models.ForeignKey(User, on_delete=models.CASCADE) author = models.ForeignKey(User, on_delete=models.CASCADE)
parent = PolymorphicTreeForeignKey( parent = PolymorphicTreeForeignKey(
@ -302,5 +306,15 @@ class LessonComment(Comment):
verbose_name_plural = 'Комментарии уроков' 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): class ContestWorkComment(Comment):
contest_work = models.ForeignKey(ContestWork, on_delete=models.CASCADE, related_name='comments') contest_work = models.ForeignKey(ContestWork, on_delete=models.CASCADE, related_name='comments')

@ -2,6 +2,9 @@
{% load static %} {% load static %}
{% block title %}{{ lesson.title }} - {{ block.super }}{% endblock title %} {% 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 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 %} {% block content %}
<div class="section" style="margin-bottom:0;padding-bottom:0"> <div class="section" style="margin-bottom:0;padding-bottom:0">
@ -114,37 +117,8 @@
<div class="section section_gray"> <div class="section section_gray">
<div class="section__center center center_sm"> <div class="section__center center center_sm">
<div class="title">Задавайте вопросы:</div> <div class="title">Задавайте вопросы:</div>
<div class="questions"> <div class="questions" id="comments_block">
{% if request.user.is_authenticated %} <comments obj-type="lesson" obj-id="{{ lesson.id }}"></comments>
<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> </div>
</div> </div>
</div> </div>

@ -111,13 +111,25 @@ class Payment(PolymorphicModel):
ordering = ('created_at',) ordering = ('created_at',)
@classmethod @classmethod
def calc_amount(cls, course_payment=None, school_payment=None, user=None, course=None, weekdays=None): 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) \
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, course_payment=None, school_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 course_payment: if course_payment:
course = course_payment.course course = course_payment.course
user = course_payment.user user = course_payment.user
if school_payment: if school_payment:
user = school_payment.user user = school_payment.user
weekdays = school_payment.weekdays weekdays = school_payment.weekdays
date_start = school_payment.date_start
discount = 0 discount = 0
price = 0 price = 0
if course: if course:
@ -126,8 +138,8 @@ class Payment(PolymorphicModel):
if user: if user:
school_payments = SchoolPayment.objects.filter( school_payments = SchoolPayment.objects.filter(
user=user, user=user,
date_start__lte=now().date(), date_start__lte=date_start,
date_end__gte=now().date(), date_end__gte=date_start,
add_days=False, add_days=False,
status__in=[ status__in=[
Pingback.PINGBACK_TYPE_REGULAR, Pingback.PINGBACK_TYPE_REGULAR,
@ -147,7 +159,8 @@ class Payment(PolymorphicModel):
weekday__in=weekdays, weekday__in=weekdays,
) )
if add_days: 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) all_weekdays_count = weekdays_in_date_range(prev_school_payment.date_start, prev_school_payment.date_end)
for ss in school_schedules: for ss in school_schedules:
price += ss.month_price // all_weekdays_count.get(ss.weekday, 0) * weekdays_count.get( price += ss.month_price // all_weekdays_count.get(ss.weekday, 0) * weekdays_count.get(
@ -163,6 +176,8 @@ class Payment(PolymorphicModel):
'price': price, 'price': price,
'amount': amount, 'amount': amount,
'discount': discount, 'discount': discount,
'date_start': date_start,
'date_end': date_end,
} }
def calc_commission(self): def calc_commission(self):
@ -193,8 +208,9 @@ class CoursePayment(Payment):
verbose_name_plural = 'Платежи за курсы' verbose_name_plural = 'Платежи за курсы'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
amount_data = Payment.calc_amount(course_payment=self) if self.status is None:
self.amount = amount_data.get('amount') amount_data = Payment.calc_amount(course_payment=self)
self.amount = amount_data.get('amount')
super().save(*args, **kwargs) super().save(*args, **kwargs)
author_balance = getattr(self, 'authorbalance', None) author_balance = getattr(self, 'authorbalance', None)
if not author_balance: if not author_balance:
@ -226,8 +242,9 @@ class SchoolPayment(Payment):
return days return days
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
amount_data = Payment.calc_amount(school_payment=self) if self.status is None:
self.amount = amount_data.get('amount') amount_data = Payment.calc_amount(school_payment=self)
self.amount = amount_data.get('amount')
super().save(*args, **kwargs) super().save(*args, **kwargs)
@property @property

@ -1,3 +1,5 @@
from decimal import Decimal
import arrow import arrow
import json import json
import logging import logging
@ -25,7 +27,7 @@ from apps.course.models import Course
from apps.school.models import SchoolSchedule from apps.school.models import SchoolSchedule
from apps.payment.tasks import transaction_to_mixpanel, product_payment_to_mixpanel, transaction_to_roistat from apps.payment.tasks import transaction_to_mixpanel, product_payment_to_mixpanel, transaction_to_roistat
from .models import AuthorBalance, CoursePayment, SchoolPayment from .models import AuthorBalance, CoursePayment, SchoolPayment, Payment
logger = logging.getLogger('django') logger = logging.getLogger('django')
@ -99,6 +101,8 @@ class SchoolBuyView(TemplateView):
host = str(host[0]) + '://' + str(host[1]) host = str(host[0]) + '://' + str(host[1])
weekdays = set(request.GET.getlist('weekdays', [])) weekdays = set(request.GET.getlist('weekdays', []))
roistat_visit = request.COOKIES.get('roistat_visit', None) roistat_visit = request.COOKIES.get('roistat_visit', None)
date_start = request.GET.get('date_start')
date_start = date_start and datetime.datetime.strptime(date_start, '%Y-%m-%d') or now().date()
if not weekdays: if not weekdays:
messages.error(request, 'Выберите несколько дней недели.') messages.error(request, 'Выберите несколько дней недели.')
return redirect('school:summer-school') return redirect('school:summer-school')
@ -109,21 +113,21 @@ class SchoolBuyView(TemplateView):
return redirect('school:summer-school') return redirect('school:summer-school')
prev_school_payment = SchoolPayment.objects.filter( prev_school_payment = SchoolPayment.objects.filter(
user=request.user, user=request.user,
date_start__lte=now().date(), date_start__lte=date_start,
date_end__gte=now().date(), date_end__gte=date_start,
add_days=False, add_days=False,
status__in=[ status__in=[
Pingback.PINGBACK_TYPE_REGULAR, Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL, Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
], ],
).first() # ??? first? ).last()
add_days = bool(prev_school_payment) add_days = bool(prev_school_payment)
if add_days: if add_days:
school_payment = SchoolPayment.objects.create( school_payment = SchoolPayment.objects.create(
user=request.user, user=request.user,
weekdays=weekdays, weekdays=weekdays,
date_start=now().date(), date_start=date_start,
date_end=prev_school_payment.date_end, date_end=prev_school_payment.date_end,
add_days=True, add_days=True,
roistat_visit=roistat_visit, roistat_visit=roistat_visit,
@ -136,6 +140,8 @@ class SchoolBuyView(TemplateView):
user=request.user, user=request.user,
weekdays=weekdays, weekdays=weekdays,
roistat_visit=roistat_visit, roistat_visit=roistat_visit,
date_start=date_start,
date_end=Payment.add_months(date_start),
) )
product = Product( product = Product(
f'school_{school_payment.id}', f'school_{school_payment.id}',
@ -162,14 +168,6 @@ class SchoolBuyView(TemplateView):
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
class PaymentwallCallbackView(View): class PaymentwallCallbackView(View):
def add_months(self, 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)\
or (sourcedate.month == 1 and sourcedate.day >= 29 and result.day == 28):
result = result.replace(day=1, month=result.month + 1)
return result.datetime
def get_request_ip(self): def get_request_ip(self):
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR') x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for: if x_forwarded_for:
@ -206,7 +204,7 @@ class PaymentwallCallbackView(View):
effective_amount = payment_raw_data.get('effective_price_amount') effective_amount = payment_raw_data.get('effective_price_amount')
if effective_amount: if effective_amount:
payment.amount = effective_amount payment.amount = Decimal(effective_amount)
transaction_to_mixpanel.delay( transaction_to_mixpanel.delay(
payment.user.id, payment.user.id,
@ -215,30 +213,6 @@ class PaymentwallCallbackView(View):
product_type_name, 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 = self.add_months(date_start)
else:
date_start = now().date()
date_end = self.add_months(date_start)
payment.date_start = date_start
payment.date_end = date_end
if product_type_name == 'course': if product_type_name == 'course':
properties = { properties = {
'payment_id': payment.id, 'payment_id': payment.id,

@ -14,6 +14,7 @@ from apps.payment import models as payment_models
class SchoolSchedule(models.Model): class SchoolSchedule(models.Model):
WEEKDAY_SHORT_NAMES = ('пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'вс')
WEEKDAY_CHOICES = ( WEEKDAY_CHOICES = (
(1, 'понедельник'), (1, 'понедельник'),
(2, 'вторник'), (2, 'вторник'),

@ -4,7 +4,7 @@
<div class="timing__info"> <div class="timing__info">
<div class="timing__day{% if school_schedule.is_online %} active{% endif %}"> <div class="timing__day{% if school_schedule.is_online %} active{% endif %}">
{{ school_schedule }} {{ school_schedule }}
{% if request.user_agent.is_mobile and school_schedule.trial_lesson %} {% if not is_purchased and request.user_agent.is_mobile and school_schedule.trial_lesson %}
<a class="timing__trial-lesson js-video-modal" href="#" data-video-url="{{ school_schedule.trial_lesson }}">Пробный урок</a> <a class="timing__trial-lesson js-video-modal" href="#" data-video-url="{{ school_schedule.trial_lesson }}">Пробный урок</a>
{% endif %} {% endif %}
</div> </div>
@ -20,7 +20,7 @@
{% else %} {% else %}
{% include './day_pay_btn.html' %} {% include './day_pay_btn.html' %}
{% endif %} {% endif %}
{% if not request.user_agent.is_mobile and school_schedule.trial_lesson %} {% if not is_purchased and not request.user_agent.is_mobile and school_schedule.trial_lesson %}
<a class="timing__trial-lesson js-video-modal" href="#" data-video-url="{{ school_schedule.trial_lesson }}">Пробный урок</a> <a class="timing__trial-lesson js-video-modal" href="#" data-video-url="{{ school_schedule.trial_lesson }}">Пробный урок</a>
{% endif %} {% endif %}
</div> </div>

@ -2,6 +2,13 @@
{% load static %} {% load static %}
{% block title %}{{ livelesson.title }} - {{ block.super }}{% endblock title %} {% 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 %} {% block content %}
<div class="section" style="margin-bottom:0;padding-bottom:0"> <div class="section" style="margin-bottom:0;padding-bottom:0">
<div class="section__center center center_sm"> <div class="section__center center center_sm">
@ -14,7 +21,8 @@
mozallowfullscreen allowfullscreen> mozallowfullscreen allowfullscreen>
</iframe> </iframe>
<span>Если видео не загрузилось, - уменьшите качество видео или <a href="#" onclick="location.reload();">обновите страницу</a></span> <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 %} {% else %}
{% if livelesson.cover %} {% if livelesson.cover %}
<img class="video__pic" src="{{ livelesson.cover.image.url }}"/> <img class="video__pic" src="{{ livelesson.cover.image.url }}"/>

@ -1,6 +1,6 @@
{% extends "templates/lilcity/index.html" %} {% load static %} {% extends "templates/lilcity/index.html" %} {% load static %}
{% block title %}Онлайн-школа LilCity{% endblock title%} {% block title %}Онлайн-школа LilCity{% endblock title%}
{% block ogimage %}http://{{request.META.HTTP_HOST}}{% static 'img/og_summer_school.jpg' %}{% endblock %} {% block ogimage %}http://{{request.META.HTTP_HOST}}{% static 'img/og_main.jpg' %}{% endblock %}
{% block content %} {% block content %}
{% if not is_purchased %} {% if not is_purchased %}
{% include "../summer/promo.html" %} {% include "../summer/promo.html" %}

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

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

@ -140,21 +140,9 @@ class SchoolView(TemplateView):
pass pass
school_schedules_dict = {ss.weekday: ss for ss in school_schedules} school_schedules_dict = {ss.weekday: ss for ss in school_schedules}
school_schedules_dict[0] = school_schedules_dict.get(7) school_schedules_dict[0] = school_schedules_dict.get(7)
all_schedules_purchased = [] live_lessons = None
live_lessons_exists = False
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
all_schedules_purchased = SchoolPayment.objects.filter(
user=self.request.user,
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
date_start__range=[month_start, date_now],
).annotate(
joined_weekdays=Func(F('weekdays'), function='unnest',)
).distinct().values_list('joined_weekdays', flat=True)
all_schedules_purchased = list(map(lambda x: 1 if x == 7 else x+1, all_schedules_purchased))
school_payment = SchoolPayment.objects.filter( school_payment = SchoolPayment.objects.filter(
user=self.request.user, user=self.request.user,
status__in=[ status__in=[
@ -173,18 +161,33 @@ class SchoolView(TemplateView):
else: else:
school_payment_exists = False school_payment_exists = False
school_schedules_purchased = [] school_schedules_purchased = []
if all_schedules_purchased and is_previous: if is_previous:
live_lessons = LiveLesson.objects.filter( prev_range = [yesterday - timedelta(days=7), yesterday]
date__range=[yesterday - timedelta(days=7), yesterday], live_lessons = []
deactivated_at__isnull=True, # берем все подписки, которые были в периоде
date__week_day__in=all_schedules_purchased, for sp in SchoolPayment.objects.filter(
).order_by('-date') date_start__lte=prev_range[1],
date_end__gte=prev_range[0],
user=self.request.user,
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
):
# берем все уроки в оплаченном промежутке
date_range = [max(sp.date_start, prev_range[0]), min(sp.date_end, prev_range[1])]
live_lessons += LiveLesson.objects.filter(
date__range=date_range,
deactivated_at__isnull=True,
date__week_day__in=list(map(lambda x: 1 if x == 7 else x+1, sp.weekdays)),
).values_list('id', flat=True)
live_lessons = LiveLesson.objects.filter(id__in=set(live_lessons)).order_by('-date')
for ll in live_lessons: for ll in live_lessons:
ll.school_schedule = school_schedules_dict.get(ll.date.isoweekday()) ll.school_schedule = school_schedules_dict.get(ll.date.isoweekday())
live_lessons_exists = live_lessons.exists() live_lessons_exists = live_lessons.exists()
else: live_lessons = live_lessons or None
live_lessons = None subscription_ends = school_payment.filter(add_days=False).last().date_end if school_payment_exists else None
live_lessons_exists = False
context.update({ context.update({
'online': online, 'online': online,
'live_lessons': live_lessons, 'live_lessons': live_lessons,
@ -197,7 +200,9 @@ class SchoolView(TemplateView):
'school_schedules': school_schedules, 'school_schedules': school_schedules,
'school_schedules_purchased': school_schedules_purchased, 'school_schedules_purchased': school_schedules_purchased,
'school_purchased_future': False, '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),
'allow_prolong': subscription_ends - date_now <= timedelta(days=14)
}) })
return context return context

@ -92,7 +92,7 @@ class User(AbstractUser):
@property @property
def balance(self): def balance(self):
aggregate = self.balances.filter( income = self.balances.filter(
type=0, type=0,
payment__isnull=False, payment__isnull=False,
payment__status__isnull=False payment__status__isnull=False
@ -100,9 +100,13 @@ class User(AbstractUser):
models.Sum('amount'), models.Sum('amount'),
models.Sum('commission'), models.Sum('commission'),
) )
amount = aggregate.get('amount__sum') or 0 income_amount = income.get('amount__sum') or 0
commission = aggregate.get('commission__sum') or 0 income_commission = income.get('commission__sum') or 0
return amount - commission
payout = self.balances.filter(type=1, status=1).aggregate(models.Sum('amount'))
payout_amount = payout.get('amount__sum') or 0
return income_amount - income_commission - payout_amount
@receiver(post_save, sender=User) @receiver(post_save, sender=User)

@ -64,10 +64,7 @@
<div class="section__center center"> <div class="section__center center">
<div class="tabs js-tabs"> <div class="tabs js-tabs">
<div class="tabs__nav"> <div class="tabs__nav">
<button class="tabs__btn js-tabs-btn active">ОНЛАЙН-ШКОЛА</button> <button class="tabs__btn js-tabs-btn active">МОИ ПОКУПКИ</button>
<button class="tabs__btn js-tabs-btn">ПРИОБРЕТЕННЫЕ
<span class="mobile-hide">КУРСЫ</span>
</button>
{% if is_author %} {% if is_author %}
<button class="tabs__btn js-tabs-btn">ОПУБЛИКОВАННЫЕ <button class="tabs__btn js-tabs-btn">ОПУБЛИКОВАННЫЕ
<span class="mobile-hide">КУРСЫ</span> <span class="mobile-hide">КУРСЫ</span>
@ -76,45 +73,34 @@
</div> </div>
<div class="tabs__container"> <div class="tabs__container">
<div class="tabs__item js-tabs-item" style="display: block;"> <div class="tabs__item js-tabs-item" style="display: block;">
{% if is_purchased_future %} <div class="courses">
<div class="center center_xs"> <div class="courses__list">
<div class="done"> {% if is_school_purchased %}
<div class="done__title title">Ваша подписка начинается {{school_purchased_future.date_start}}</div> <div class="courses__item">
</div> <a class="courses__preview" href="{% url 'school:school' %}">
</div> <img class="courses__pic" src="{% static 'img/og_main.jpg' %}"
{% else %} style="height: 200px; object-fit: cover;" />
{% if is_purchased %} <div class="courses__view">Подробнее</div>
{% include "blocks/schedule_purchased.html" %} </a>
{% else %} <div class="courses__details">
<div class="center center_xs"> <a class="courses__theme theme">{{ school_purchased_weekdays }}</a>
<div class="done"> <div class="courses__price">{{ school_purchased_price|floatformat:"-2" }}₽</div>
<div class="done__title title">Вы не подписаны на онлайн-школу!</div>
<div class="done__foot">
<a
{% if not user.is_authenticated %}
data-popup=".js-popup-auth"
{% else %}
data-popup=".js-popup-buy"
{% endif %}
href="#"
class="done__btn btn btn_md btn_stroke"
>Купить подписку</a>
</div> </div>
<a class="courses__title">Онлайн-школа&nbsp;{{ school_purchased_dates.0|date:"j b" }}&nbsp;-&nbsp;{{ school_purchased_dates.1|date:"j b" }}</a>
<a class="btn" href="{% url 'school:school' %}">Перейти в онлайн-школу</a>
</div> </div>
</div> {% endif %}
{% endif %}
{% endif %}
</div>
<div class="tabs__item js-tabs-item">
<div class="courses courses_scroll">
<div class="courses__list">
{% if paid.exists %} {% if paid.exists %}
{% include "course/course_items.html" with course_items=paid %} {% include "course/course_items.html" with course_items=paid %}
{% else %} {% endif %}
{% if not is_school_purchased and not paid.exists %}
<div class="center center_xs"> <div class="center center_xs">
<div class="done"> <div class="done">
<div class="done__title title">Нет приобретённых курсов!</div> <div class="done__title">Вы пока ничего не приобрели...</div>
<div class="done__foot"> <div class="done__foot">
<a class="done__btn btn btn_md btn_stroke" href="{% url 'school:school' %}"
style="margin-bottom: 20px;">Записаться в школу</a>
<a class="done__btn btn btn_md btn_stroke" href="{% url 'courses' %}">Купить курсы</a> <a class="done__btn btn btn_md btn_stroke" href="{% url 'courses' %}">Купить курсы</a>
</div> </div>
</div> </div>
@ -125,7 +111,7 @@
</div> </div>
{% if is_author %} {% if is_author %}
<div class="tabs__item js-tabs-item"> <div class="tabs__item js-tabs-item">
<div class="courses courses_scroll"> <div class="courses">
<div class="courses__list"> <div class="courses__list">
{% if published.exists %} {% if published.exists %}
{% include "course/course_items.html" with course_items=published %} {% include "course/course_items.html" with course_items=published %}

@ -16,7 +16,7 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.hashers import check_password, make_password from django.contrib.auth.hashers import check_password, make_password
from django.http import Http404 from django.http import Http404
from django.db.models import F, Func from django.db.models import F, Func, Sum, Min, Max
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.timezone import now from django.utils.timezone import now
@ -55,11 +55,11 @@ class ProfileView(TemplateView):
def get_context_data(self, object): def get_context_data(self, object):
context = super().get_context_data() context = super().get_context_data()
context['user'] = self.request.user context['user'] = self.request.user
context['is_author'] = self.request.user.role == User.AUTHOR_ROLE
context['published'] = Course.objects.filter( context['published'] = Course.objects.filter(
author=self.object, author=self.object,
) )
context['is_author'] = context['published'] or self.request.user.role == User.AUTHOR_ROLE
context['paid'] = Course.objects.filter( context['paid'] = Course.objects.filter(
payments__in=CoursePayment.objects.filter( payments__in=CoursePayment.objects.filter(
user=self.object, user=self.object,
@ -80,22 +80,18 @@ class ProfileView(TemplateView):
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
], ],
) )
school_schedules_purchased = school_payment.annotate( context['is_school_purchased'] = school_payment.exists()
joined_weekdays=Func(F('weekdays'), function='unnest',) if context['is_school_purchased']:
).values_list('joined_weekdays', flat=True).distinct() school_schedules_purchased = school_payment.annotate(
context['school_schedules_purchased'] = school_schedules_purchased joined_weekdays=Func(F('weekdays'), function='unnest',)
context['school_payment'] = school_payment ).values_list('joined_weekdays', flat=True).distinct()
context['is_purchased'] = school_payment.exists() aggregated = school_payment.aggregate(Sum('amount'), Min('date_start'), Max('date_end'),)
context['school_purchased_weekdays'] = '-'.join(map(lambda wd: SchoolSchedule.WEEKDAY_SHORT_NAMES[wd-1],
set(sorted(school_schedules_purchased))))
context['school_purchased_price'] = aggregated.get('amount__sum') or 0
context['school_purchased_dates'] = [aggregated.get('date_start__min'), aggregated.get('date_end__max')]
context['profile'] = True context['profile'] = True
if school_payment.exists() and school_payment.last().date_end:
context['subscription_ends'] = school_payment.last().date_end
context['school_schedules'] = SchoolSchedule.objects.filter(
weekday__in=school_schedules_purchased if school_payment.exists() else [],
).all()
context['all_school_schedules'] = SchoolSchedule.objects.all()
context['is_purchased_future'] = False
context['school_purchased_future'] = False
return context return context

@ -1,3 +1,4 @@
from django.db.models import Func, F
from django.utils.timezone import now from django.utils.timezone import now
from paymentwall.pingback import Pingback from paymentwall.pingback import Pingback
@ -14,7 +15,7 @@ def baner(request):
return {'baner': Baner.objects.filter(use=True).first()} return {'baner': Baner.objects.filter(use=True).first()}
def is_summer_school_purchased(request): def school_purchased(request):
if request.user.is_authenticated: if request.user.is_authenticated:
n = now().date() n = now().date()
school_payment = SchoolPayment.objects.filter( school_payment = SchoolPayment.objects.filter(
@ -27,5 +28,14 @@ def is_summer_school_purchased(request):
date_start__lte=n, date_start__lte=n,
date_end__gte=n date_end__gte=n
) )
return {'is_summer_school_purchased': school_payment.exists()} school_schedules_purchased = school_payment.annotate(
return {'is_summer_school_purchased': False} 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': [],
}

@ -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)

@ -94,7 +94,7 @@ TEMPLATES = [
'context_processors': [ 'context_processors': [
'project.context_processors.config', 'project.context_processors.config',
'project.context_processors.baner', 'project.context_processors.baner',
'project.context_processors.is_summer_school_purchased', 'project.context_processors.school_purchased',
'django.template.context_processors.debug', 'django.template.context_processors.debug',
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
@ -197,6 +197,13 @@ TWILIO_FROM_PHONE = os.getenv('TWILIO_FROM_PHONE', '+37128914409')
ACTIVE_LINK_STRICT = True 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 # DRF settings
REST_FRAMEWORK = { REST_FRAMEWORK = {

@ -1,14 +1,19 @@
{% load static %} {% load static %}
{% load setting from settings %}
<script> <script>
window.LIL_STORE = { window.LIL_STORE = {
pusherKey: '{% setting "PUSHER_KEY" %}',
staticUrl: '{% static "" %}', staticUrl: '{% static "" %}',
accessToken: '{{ request.user.auth_token }}', accessToken: '{{ request.user.auth_token }}',
isMobile: {{ request.user_agent.is_mobile|yesno:"true,false" }}, isMobile: {{ request.user_agent.is_mobile|yesno:"true,false" }},
defaultUserPhoto: "{% static 'img/user_default.jpg' %}",
user: { user: {
id: '{{ request.user.id|default:'' }}', id: '{{ request.user.id|default:'' }}',
photo: '{% if request.user.photo %}{{ request.user.photo.url }}{% else %}{% static 'img/user_default.jpg' %}{% endif %}',
}, },
components: {},
urls: { urls: {
courses: "{% url 'courses' %}" courses: "{% url 'courses' %}"
} },
}; };
</script> </script>

@ -20,40 +20,6 @@
</div> </div>
<div class="buy__col"> <div class="buy__col">
<div class="buy__list"> <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 %} {% for school_schedule in school_schedules %}
<label class="switch switch_lesson"> <label class="switch switch_lesson">
<input <input
@ -63,6 +29,7 @@
data-price="{{school_schedule.month_price}}" data-price="{{school_schedule.month_price}}"
autocomplete="off" autocomplete="off"
{% if school_schedule.weekday in school_schedules_purchased %} {% if school_schedule.weekday in school_schedules_purchased %}
data-purchased="1"
disabled disabled
{% endif %} {% endif %}
{% if not is_purchased %} {% if not is_purchased %}
@ -86,7 +53,6 @@
</span> </span>
</label> </label>
{% endfor %} {% endfor %}
{% endif %}
</div> </div>
</div> </div>
<div class="buy__col"> <div class="buy__col">
@ -102,6 +68,7 @@
</div> </div>
<div class="order__info"> <div class="order__info">
<div class="order__label">ШКОЛА</div> <div class="order__label">ШКОЛА</div>
<div class="order__dates"></div>
<div class="order__days"></div> <div class="order__days"></div>
</div> </div>
<div class="order__foot"> <div class="order__foot">

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

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

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

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

@ -0,0 +1,64 @@
<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" v-if="! controller.isChat">
<button @click="controller.reply(comment)" v-if="$root.store.user.id" class="questions__action question__reply-button">ОТВЕТИТЬ</button>
<button @click="controller.remove(comment)" v-if="$root.store.user.id == comment.author.id" class="questions__action question__reply-button">УДАЛИТЬ</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--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="section__center center">
<div class="kit"> <div class="kit">
<div class="kit__body"> <div class="kit__body">
<vue-draggable v-model="contest.content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }"> <block-content :content.sync="contest.content"></block-content>
<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"/>
</div> </div>
</div> </div>
</div> </div>
@ -113,15 +75,9 @@
</template> </template>
<script> <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 {api} from "../js/modules/api";
import DatePicker from 'vuejs-datepicker'; import DatePicker from 'vuejs-datepicker';
import Draggable from 'vuedraggable'; import BlockContent from './blocks/BlockContent'
import slugify from 'slugify'; import slugify from 'slugify';
import {required, minValue, numeric, url } from 'vuelidate/lib/validators' import {required, minValue, numeric, url } from 'vuelidate/lib/validators'
import _ from 'lodash'; import _ from 'lodash';
@ -175,18 +131,6 @@
} }
}, },
methods: { 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() { onTitleInput() {
this.$v.contest.title.$touch(); this.$v.contest.title.$touch();
if (!this.slugChanged) { if (!this.slugChanged) {
@ -219,7 +163,7 @@
id: data.id, id: data.id,
title: data.title, title: data.title,
description: data.description, description: data.description,
content: api.convertContentResponse(data.content), content: api.convertContentJson(data.content),
date_start: data.date_start, date_start: data.date_start,
date_end: data.date_end, date_end: data.date_end,
slug: data.slug, slug: data.slug,
@ -252,70 +196,7 @@
data.date_start = data.date_start ? moment(data.date_start).format('MM-DD-YYYY') : null; 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.date_end = data.date_end ? moment(data.date_end).format('MM-DD-YYYY') : null;
data.cover = this.contest.coverImageId || ''; data.cover = this.contest.coverImageId || '';
data.content = this.contest.content.map((block, index) => { data.content = api.convertContentJson(this.contest.content, true);
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,
}
}
}
});
const request = this.contest.id const request = this.contest.id
? api.put(`/api/v1/contests/${this.contest.id}/`, data, { ? api.put(`/api/v1/contests/${this.contest.id}/`, data, {
@ -343,15 +224,8 @@
} }
}, },
components: { components: {
BlockAdd, BlockContent,
'vue-datepicker': DatePicker, 'vue-datepicker': DatePicker,
'block-text': BlockText,
'block-image': BlockImage,
'block-image-text': BlockImageText,
'block-images': BlockImages,
'block-video': BlockVideo,
'vue-draggable': Draggable,
} }
}; };
</script> </script>

@ -171,45 +171,7 @@
</button> </button>
</div> </div>
<div v-if="viewSection === 'course'" class="kit__body"> <div v-if="viewSection === 'course'" class="kit__body">
<vue-draggable v-model="course.content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }"> <block-content :content.sync="course.content"></block-content>
<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"/>
<!--<div class="kit__foot"> <!--<div class="kit__foot">
<button type="submit" class="kit__submit btn btn_md" v-bind:class="{ loading: courseSaving }"> <button type="submit" class="kit__submit btn btn_md" v-bind:class="{ loading: courseSaving }">
@ -274,16 +236,11 @@
import { ROLE_ADMIN, ROLE_AUTHOR } from './consts' import { ROLE_ADMIN, ROLE_AUTHOR } from './consts'
import LinkInput from './inputs/LinkInput' import LinkInput from './inputs/LinkInput'
import DatePicker from 'vuejs-datepicker' import DatePicker from 'vuejs-datepicker'
import BlockText from './blocks/BlockText' import BlockContent from './blocks/BlockContent'
import BlockImage from './blocks/BlockImage'
import BlockImages from './blocks/BlockImages'
import BlockImageText from './blocks/BlockImageText'
import BlockVideo from './blocks/BlockVideo'
import VueRedactor from './redactor/VueRedactor'; import VueRedactor from './redactor/VueRedactor';
import LilSelect from "./inputs/LilSelect"; import LilSelect from "./inputs/LilSelect";
import LessonRedactor from "./LessonRedactor"; import LessonRedactor from "./LessonRedactor";
import {api} from "../js/modules/api"; import {api} from "../js/modules/api";
import BlockAdd from "./blocks/BlockAdd";
import $ from 'jquery'; import $ from 'jquery';
import {required, minValue, numeric, url } from 'vuelidate/lib/validators' import {required, minValue, numeric, url } from 'vuelidate/lib/validators'
import slugify from 'slugify'; import slugify from 'slugify';
@ -525,18 +482,6 @@
this.course.url = slugify(this.course.title); this.course.url = slugify(this.course.title);
} }
}, },
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) { removeLesson(lessonIndex) {
if (!confirm('Вы действительно хотите удалить этот урок?')) { if (!confirm('Вы действительно хотите удалить этот урок?')) {
return; return;
@ -550,18 +495,18 @@
}, },
editLesson(lessonIndex) { editLesson(lessonIndex) {
this.currentLesson = this.lessons[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'; this.viewSection = 'lessons-edit';
}, },
showCourse() { showCourse() {
if (this.viewSection !== 'course') { if (this.viewSection !== 'course') {
history.push("/course/create"); history.push(this.course.id ? `/course/${this.course.id}/edit` : "/course/create");
} }
this.viewSection = 'course' this.viewSection = 'course'
}, },
showLessons() { showLessons() {
if (this.viewSection !== 'lessons') { if (this.viewSection !== 'lessons') {
history.push("/course/create/lessons"); history.push(`/course/${this.course.id}/edit/lessons`);
} }
this.viewSection = 'lessons'; this.viewSection = 'lessons';
}, },
@ -572,7 +517,7 @@
content: [], content: [],
}; };
if (this.viewSection !== 'lessons-edit') { if (this.viewSection !== 'lessons-edit') {
history.push("/course/create/lessons/new"); history.push(`/course/${this.course.id}/lessons/new`);
} }
this.viewSection = 'lessons-edit'; this.viewSection = 'lessons-edit';
window.scrollTo(0, 0); window.scrollTo(0, 0);
@ -645,7 +590,7 @@
return req; return req;
}, },
goToLessons() { goToLessons() {
history.push("/course/create/lessons"); history.push(`/course/${this.course.id}/edit/lessons`);
this.viewSection = 'lessons'; this.viewSection = 'lessons';
this.$nextTick(() => { this.$nextTick(() => {
const elementTop = $('#course-redactor__nav').position().top - 130; const elementTop = $('#course-redactor__nav').position().top - 130;
@ -900,18 +845,17 @@
}, },
updateViewSection(location, action) { updateViewSection(location, action) {
//console.log('updateViewSection[action]', action); //console.log('updateViewSection[action]', action);
if (location.pathname === '/course/create/lessons') { if (location.pathname.match(/course\/\d+\/edit\/lessons/)) {
this.viewSection = 'lessons'; this.viewSection = 'lessons';
} else if (location.pathname === '/course/create') { } else if (location.pathname.match(/course\/\d+\/lessons\/new/)){
this.viewSection = 'course';
} else if (location.pathname === '/course/create/lessons/new') {
this.viewSection = 'lessons-edit'; this.viewSection = 'lessons-edit';
} else if (location.pathname.indexOf('/course/create/lessons/edit') !== -1) { } else if (location.pathname.match(/course\/\d+\/lessons\/\d+\/edit/)) {
let lessonId = parseInt(location.pathname.split('/').pop()); // let lessonId = parseInt(location.pathname.split('/').pop());
//console.log('lessonId', lessonId, this.lessons.toString()); const lessonId = +location.pathname.match(/lessons\/(\d+)\/edit/)[1];
//console.log('lessod edit', this.lessons.find((i)=>{return i.id === lessonId}));
this.currentLesson = this.lessons.find((i)=>{return i.id === lessonId}); this.currentLesson = this.lessons.find((i)=>{return i.id === lessonId});
this.viewSection = 'lessons-edit'; this.viewSection = 'lessons-edit';
} else {
this.viewSection = 'course';
} }
}, },
onLessonsChanged() { onLessonsChanged() {
@ -1135,17 +1079,11 @@
}, },
components: { components: {
BlockAdd,
LessonRedactor, LessonRedactor,
LilSelect, LilSelect,
BlockText, BlockContent,
'link-input': LinkInput, 'link-input': LinkInput,
'vue-datepicker': DatePicker, 'vue-datepicker': DatePicker,
'block-text': BlockText,
'block-image': BlockImage,
'block-image-text': BlockImageText,
'block-images': BlockImages,
'block-video': BlockVideo,
'lesson-redactor': LessonRedactor, 'lesson-redactor': LessonRedactor,
'vue-draggable': Draggable, 'vue-draggable': Draggable,
'vue-redactor': VueRedactor, 'vue-redactor': VueRedactor,

@ -34,44 +34,7 @@
</div> </div>
</div> </div>
</div> </div>
<vue-draggable v-model="lesson.content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }"> <block-content :content.sync="lesson.content"></block-content>
<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" />
<div class="kit__foot"> <div class="kit__foot">
<button class="kit__submit btn btn_md" v-bind:class="{ loading: saving }">Сохранить</button> <button class="kit__submit btn btn_md" v-bind:class="{ loading: saving }">Сохранить</button>
@ -82,12 +45,7 @@
</template> </template>
<script> <script>
import BlockAdd from "./blocks/BlockAdd"; import BlockContent from './blocks/BlockContent'
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 LilImage from "./blocks/Image" import LilImage from "./blocks/Image"
import VueRedactor from './redactor/VueRedactor'; import VueRedactor from './redactor/VueRedactor';
import {api} from "../js/modules/api"; import {api} from "../js/modules/api";
@ -101,20 +59,6 @@
goBack() { goBack() {
this.$emit('back'); 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) { onUpdateCoverUrl(newValue) {
this.lesson.coverImage = newValue; this.lesson.coverImage = newValue;
}, },
@ -128,13 +72,7 @@
} }
}, },
components: { components: {
BlockAdd, BlockContent,
'block-text': BlockText,
'block-image': BlockImage,
'block-image-text': BlockImageText,
'block-images': BlockImages,
'block-video': BlockVideo,
'vue-draggable': Draggable,
'lil-image': LilImage, 'lil-image': LilImage,
'vue-redactor': VueRedactor, 'vue-redactor': VueRedactor,
} }

@ -0,0 +1,87 @@
<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 content = this.content;
const blockToRemove = this.content[blockIndex];
// Если блок уже был записан в БД, отправляем запрос на сервер на удаление блока из БД
if (blockToRemove.data.id) {
api.removeContentBlock(blockToRemove, this.$root.store.accessToken).then(response => {
// Удаляем блок из Vue
content.splice(blockIndex, 1);
this.$emit('update:content', content);
});
}
},
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>
<div class="kit__gallery"> <div class="kit__gallery">
<div class="kit__preview" v-for="(image, index) in images" v-bind:class="{ 'kit__preview--loading': image.loading }"> <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)"> <button type="button" @click="onRemoveImage(index)">
<svg class="icon icon-delete"> <svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use> <use xlink:href="/static/img/sprite.svg#icon-delete"></use>
@ -66,12 +66,12 @@
api.uploadImage(reader.result, this.accessToken) api.uploadImage(reader.result, this.accessToken)
.then((response) => { .then((response) => {
let images = this.images; let images = this.images;
console.log('images before', JSON.stringify(images));
images.forEach((image, index) => { images.forEach((image, index) => {
if (image.src === reader.result) { if (image.src === reader.result) {
images[index].img = response.data.id; images[index].image_id = response.data.id;
images[index].loading = false; 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)); console.log('images after', JSON.stringify(images));
@ -91,12 +91,14 @@
}, },
onRemoveImage(index) { onRemoveImage(index) {
let images = this.images; let images = this.images;
let id = images[index].img; let id = images[index].image_id;
images.splice(index, 1);
this.$emit('update:images', images);
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() { data() {
return { return {
loading: false, loading: false,
} }
}, },
methods: { methods: {
@ -45,7 +46,7 @@
.then((response) => { .then((response) => {
this.loading = false; this.loading = false;
this.$emit('update:imageId', response.data.id); this.$emit('update:imageId', response.data.id);
this.$emit('update:imageUrl', response.data.image); this.$emit('update:imageUrl', response.data.image_thumbnail);
}) })
.catch((error) => { .catch((error) => {
this.loading = false; this.loading = false;

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

@ -1,19 +1,4 @@
import Vue from 'vue'
import Vuelidate from 'vuelidate'
import VueAutosize from '../components/directives/autosize'
import ContestRedactor from '../components/ContestRedactor.vue' import ContestRedactor from '../components/ContestRedactor.vue'
if (process.env.NODE_ENV === 'development') { window.LIL_STORE.components['contest-redactor'] = ContestRedactor;
// Enable vue-devtools
Vue.config.devtools = true;
}
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 'babel-polyfill'
import CourseRedactor from '../components/CourseRedactor.vue' import CourseRedactor from '../components/CourseRedactor.vue'
import $ from 'jquery'; import $ from 'jquery';
if (process.env.NODE_ENV === 'development') { window.LIL_STORE.components['course-redactor'] = CourseRedactor;
// 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,
}
});
$(document).ready(function () { $(document).ready(function () {
$('#course-redactor__publish-button').on('click', function () { $('#course-redactor__publish-button').on('click', function () {
@ -29,4 +13,4 @@ $(document).ready(function () {
let event = new Event('course_preview'); let event = new Event('course_preview');
document.getElementById('lilcity__course-redactor').dispatchEvent(event); document.getElementById('lilcity__course-redactor').dispatchEvent(event);
}); });
}); });

@ -117,72 +117,9 @@ export const api = {
stream: courseObject.stream, stream: courseObject.stream,
cover: courseObject.coverImageId ? courseObject.coverImageId : null, cover: courseObject.coverImageId ? courseObject.coverImageId : null,
gallery: { gallery: {
gallery_images: courseObject.gallery && courseObject.gallery.images ? courseObject.gallery.images : [] gallery_images: courseObject.gallery && courseObject.gallery.images || []
}, },
content: courseObject.content.map((block, index) => { content: api.convertContentJson(courseObject.content, true),
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,
}
}
}
}),
}; };
if(courseObject.live) { if(courseObject.live) {
@ -210,16 +147,70 @@ export const api = {
short_description: lessonObject.short_description, short_description: lessonObject.short_description,
course: lessonObject.course_id, course: lessonObject.course_id,
position: lessonObject.position, 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') { if (block.type === 'text') {
return { return {
'type': 'text', 'type': 'text',
'data': { 'data': {
'id': block.data.id ? block.data.id : null, 'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index, 'position': ++index,
'title': block.data.title, 'title': block.data.title,
'txt': block.data.text, 'txt': block.data.text,
'uuid': block.uuid,
} }
} }
} else if (block.type === 'image') { } else if (block.type === 'image') {
@ -227,10 +218,10 @@ export const api = {
'type': 'image', 'type': 'image',
'data': { 'data': {
'id': block.data.id ? block.data.id : null, 'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index, 'position': ++index,
'title': block.data.title, 'title': block.data.title,
'img': block.data.image_id, 'img': block.data.image_id,
'uuid': block.uuid,
} }
} }
} else if (block.type === 'image-text') { } else if (block.type === 'image-text') {
@ -238,11 +229,11 @@ export const api = {
'type': 'image-text', 'type': 'image-text',
'data': { 'data': {
'id': block.data.id ? block.data.id : null, 'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index, 'position': ++index,
'title': block.data.title, 'title': block.data.title,
'img': block.data.image_id, 'img': block.data.image_id,
'txt': block.data.text, 'txt': block.data.text,
'uuid': block.uuid,
} }
} }
} else if (block.type === 'images') { } else if (block.type === 'images') {
@ -250,15 +241,15 @@ export const api = {
'type': 'images', 'type': 'images',
'data': { 'data': {
'id': block.data.id ? block.data.id : null, 'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index, 'position': ++index,
'title': block.data.title, 'title': block.data.title,
'images': block.data.images.map((galleryImage) => { 'images': block.data.images.map((galleryImage) => {
return { return {
'id': galleryImage.id ? galleryImage.id : null, 'id': galleryImage.id ? galleryImage.id : null,
'img': galleryImage.img, 'img': galleryImage.image_id,
} }
}), }),
'uuid': block.uuid,
} }
} }
} else if (block.type === 'video') { } else if (block.type === 'video') {
@ -266,67 +257,15 @@ export const api = {
'type': 'video', 'type': 'video',
'data': { 'data': {
'id': block.data.id ? block.data.id : null, 'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index, 'position': ++index,
'title': block.data.title, 'title': block.data.title,
'url': block.data.video_url, '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),
age: courseJSON.age,
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) => { return contentJson.sort((a, b) => {
if (a.position < b.position) { if (a.position < b.position) {
return -1; return -1;
@ -355,6 +294,7 @@ export const api = {
'title': contentItem.title, 'title': contentItem.title,
'image_id': (contentItem.img) ? contentItem.img.id:null, 'image_id': (contentItem.img) ? contentItem.img.id:null,
'image_url': (contentItem.img) ? contentItem.img.image: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') { } else if (contentItem.type === 'image-text') {
@ -366,6 +306,7 @@ export const api = {
'title': contentItem.title, 'title': contentItem.title,
'image_id': (contentItem.img) ? contentItem.img.id:null, 'image_id': (contentItem.img) ? contentItem.img.id:null,
'image_url': (contentItem.img) ? contentItem.img.image:null, 'image_url': (contentItem.img) ? contentItem.img.image:null,
'image_thumbnail_url': (contentItem.img) ? contentItem.img.image_thumbnail:null,
'text': contentItem.txt, 'text': contentItem.txt,
} }
} }
@ -379,8 +320,9 @@ export const api = {
'images': contentItem.gallery_images.map((galleryImage) => { 'images': contentItem.gallery_images.map((galleryImage) => {
return { return {
'id': galleryImage.id, 'id': galleryImage.id,
'img': galleryImage.img.id, 'image_id': galleryImage.img.id,
'src': galleryImage.img.image, 'image_url': galleryImage.img.image,
'image_thumbnail_url': galleryImage.img.image_thumbnail,
} }
}), }),
} }
@ -511,5 +453,34 @@ export const api = {
'Authorization': `Token ${window.LIL_STORE.accessToken}`, '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 || '',
},
headers: {
'Authorization': `Token ${window.LIL_STORE.accessToken}`,
}
});
},
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(); form.find('.questions__reply-info').hide();
} }
}); });

@ -1,6 +1,9 @@
import $ from 'jquery'; import $ from 'jquery';
import moment from 'moment';
import {api} from './api'; import {api} from './api';
moment.locale('ru');
var selectedWeekdays = {}; var selectedWeekdays = {};
$(document).ready(function () { $(document).ready(function () {
$(".js-video-modal").each(function(){ $(".js-video-modal").each(function(){
@ -46,21 +49,12 @@ $(document).ready(function () {
popup = $(data); popup = $(data);
showPopup(); showPopup();
let is_extend = false;
if(data === '.js-popup-buy') { if(data === '.js-popup-buy') {
console.log('reset selected'); console.log('reset selected');
popup.data('date-start', $this.data('date-start') || '');
popup.data('day', $this.data('day') || '');
$('[data-day]').prop('checked', false); $('[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) { if(! window.LIL_STORE.user.id) {
const $btn = popup.find('.buy__btn'); const $btn = popup.find('.buy__btn');
$btn.click(function(event) { $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') { if( data === '.js-popup-auth') {
let nextUrl = $this.data('auth-next-url'); let nextUrl = $this.data('auth-next-url');
if(nextUrl === 'href') { if(nextUrl === 'href') {
@ -80,20 +97,6 @@ $(document).ready(function () {
} }
popup.data('next-url', nextUrl); 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){ $('.js-popup-close').on('click', function(e){
@ -147,7 +150,10 @@ $(document).ready(function () {
}); });
function updateCart(){ function updateCart(){
var link = $('.but_btn_popup').data('link');
var $orderPrice = $('.order_price_text'); var $orderPrice = $('.order_price_text');
var $orderDates = $('.order__dates');
var dateStart = popup.data('date-start');
var days = ['', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье']; var days = ['', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'];
var weekdays = []; var weekdays = [];
var daysText = []; var daysText = [];
@ -160,7 +166,7 @@ $(document).ready(function () {
}); });
if(weekdays.length){ 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) => { .then((response) => {
var text = ''; var text = '';
if(response.data.price != response.data.amount) { if(response.data.price != response.data.amount) {
@ -169,16 +175,16 @@ $(document).ready(function () {
text = response.data.amount+'p.'; text = response.data.amount+'p.';
} }
$orderPrice.html(text); $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')
}, true)));
}); });
} }
else { else {
$orderPrice.html('0p.'); $orderPrice.html('0p.');
} }
$('.order__days').html(daysText.length ? daysText.join(', ') : 'Ничего не выбрано'); $('.order__days').html(daysText.length ? daysText.join(', ') : 'Ничего не выбрано');
var link = $('.but_btn_popup').data('link');
link = link+'?'+decodeURIComponent($.param({weekdays: weekdays}, true));
$('.but_btn_popup').attr('href', link);
} }
}); });

@ -1712,6 +1712,8 @@ a.grey-link
color: $cl color: $cl
+t +t
line-height: 1.33 line-height: 1.33
&__theme
text-transform: uppercase
&__user &__user
margin-top: 20px margin-top: 20px
&_two &__item &_two &__item
@ -2817,16 +2819,23 @@ a.grey-link
&__item &__item
display: flex display: flex
&__form &__form
margin-bottom: 40px position: relative;
margin-top: 20px
padding-bottom: 20px padding-bottom: 20px
border-bottom: 1px solid $border border-bottom: 1px solid $border
&__form-loader
display: none;
&__form-foot
text-align: center;
&__item &__item
&:not(:last-child) &:not(:last-child)
margin-bottom: 25px margin-bottom: 25px
+m
padding: 10px 0
&_reply &_reply
padding-left: 80px padding-left: 80px
+m +m
padding: 0 padding: 10px 0
&__reply-info &__reply-info
display: none display: none
margin-bottom: 10px margin-bottom: 10px
@ -2865,9 +2874,10 @@ a.grey-link
+m +m
height: 64px height: 64px
&__btn &__btn
display: block margin: 0 15px;
margin: 0 auto
border-radius: 20px border-radius: 20px
+m
margin-left: 0
&__details &__details
margin-bottom: 5px margin-bottom: 5px
&__head, &__head,
@ -2886,11 +2896,70 @@ a.grey-link
&__author &__author
margin-right: 15px margin-right: 15px
&__date &__date
font-size: 10px
display: inline-block display: inline-block
&__foot &__foot
height: 20px
text-align: right text-align: right
&__action &__action
margin-left: auto margin-left: auto
&__heart
fill: #d40700
width: 28px
height: 28px
&__send-icon
fill: $gray
width: 20px
height: 20px
&--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 .share
&__title &__title
@ -3672,6 +3741,11 @@ a.grey-link
+m +m
display: none display: none
.mobile-show
display: none
+m
display: block
.school .school
display: flex display: flex
position: relative position: relative

Loading…
Cancel
Save