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

remotes/origin/hotfix/LIL-691
gzbender 7 years ago
commit d182e7ccc3
  1. 69
      api/v1/serializers/course.py
  2. 72
      api/v1/serializers/payment.py
  3. 3
      api/v1/urls.py
  4. 100
      api/v1/views.py
  5. 30
      apps/course/migrations/0044_livelessoncomment.py
  6. 14
      apps/course/models.py
  7. 36
      apps/course/templates/course/lesson.html
  8. 1
      apps/school/models.py
  9. 4
      apps/school/templates/blocks/schedule_item.html
  10. 10
      apps/school/templates/school/livelesson_detail.html
  11. 48
      apps/school/views.py
  12. 12
      apps/user/models.py
  13. 60
      apps/user/templates/user/profile.html
  14. 32
      apps/user/views.py
  15. 14
      project/pusher.py
  16. 7
      project/settings.py
  17. 4
      project/templates/blocks/lil_store_js.html
  18. 1
      project/templates/lilcity/index.html
  19. 2
      requirements.txt
  20. 64
      web/src/components/Comment.vue
  21. 53
      web/src/components/CommentForm.vue
  22. 134
      web/src/components/Comments.vue
  23. 3
      web/src/js/app.js
  24. 29
      web/src/js/modules/api.js
  25. 1
      web/src/js/modules/comments.js
  26. 59
      web/src/js/modules/comments_vue.js
  27. 82
      web/src/sass/_common.sass

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

@ -46,8 +46,29 @@ class AuthorBalanceCreateSerializer(serializers.ModelSerializer):
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):
author = UserSerializer()
payment = serializers.SerializerMethodField()
class Meta:
model = AuthorBalance
@ -70,6 +91,24 @@ class AuthorBalanceSerializer(serializers.ModelSerializer):
'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):
user = UserSerializer()
@ -80,6 +119,7 @@ class PaymentSerializer(serializers.ModelSerializer):
read_only_fields = (
'id',
'user',
'data',
'created_at',
'update_at',
)
@ -91,6 +131,20 @@ class PaymentSerializer(serializers.ModelSerializer):
return SchoolPaymentSerializer(instance, context=self.context).to_representation(instance)
class CoursePaymentCreateSerializer(serializers.ModelSerializer):
class Meta:
model = CoursePayment
fields = BASE_PAYMENT_FIELDS + ('course',)
read_only_fields = (
'id',
'user',
'course',
'created_at',
'update_at',
)
class CoursePaymentSerializer(serializers.ModelSerializer):
user = UserSerializer()
course = CourseSerializer()
@ -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):
user = UserSerializer()

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

@ -11,10 +11,11 @@ from .serializers.course import (
CategorySerializer, LikeSerializer,
CourseSerializer, CourseCreateSerializer,
CourseBulkChangeCategorySerializer,
CommentSerializer,
CommentSerializer, CommentCreateSerializer,
MaterialSerializer, MaterialCreateSerializer,
LessonSerializer, LessonCreateSerializer,
LikeCreateSerializer)
LikeCreateSerializer, CourseCommentSerializer, LessonCommentSerializer,
LiveLessonCommentSerializer)
from .serializers.content import (
BanerSerializer,
ImageSerializer, ImageCreateSerializer,
@ -34,7 +35,7 @@ from .serializers.payment import (
AuthorBalanceSerializer, AuthorBalanceCreateSerializer,
PaymentSerializer, CoursePaymentSerializer,
SchoolPaymentSerializer,
)
CoursePaymentCreateSerializer, SchoolPaymentCreateSerializer)
from .serializers.user import (
AuthorRequestSerializer,
UserSerializer, UserPhotoSerializer,
@ -54,7 +55,7 @@ from apps.course.models import (
Comment, CourseComment, LessonComment,
Material, Lesson,
Like,
)
LiveLessonComment)
from apps.config.models import Config
from apps.content.models import (
Baner, Image, Text, ImageText, Video,
@ -66,6 +67,7 @@ from apps.payment.models import (
)
from apps.school.models import SchoolSchedule, LiveLesson
from apps.user.models import AuthorRequest
from project.pusher import pusher
User = get_user_model()
@ -429,15 +431,15 @@ class ConfigViewSet(generics.RetrieveUpdateAPIView):
class CommentViewSet(ExtendedModelViewSet):
queryset = Comment.objects.filter(level=0)
queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = (IsAdmin,)
permission_classes = (IsAuthorObjectOrAdmin,)
def get_queryset(self):
queryset = self.queryset
is_deactivated = self.request.query_params.get('is_deactivated', '0')
if is_deactivated == '0':
queryset = queryset
queryset = queryset.filter(level=0)
elif is_deactivated == '1':
queryset = queryset.filter(deactivated_at__isnull=True)
elif is_deactivated == '2':
@ -446,6 +448,78 @@ class CommentViewSet(ExtendedModelViewSet):
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):
queryset = AuthorRequest.objects.all()
serializer_class = AuthorRequestSerializer
@ -453,7 +527,7 @@ class AuthorRequestViewSet(ExtendedModelViewSet):
filter_fields = ('status',)
class PaymentViewSet(ExtendedModelViewSet):
class PaymentViewSet(viewsets.ModelViewSet):
queryset = Payment.objects.all()
serializer_class = PaymentSerializer
permission_classes = (IsAdmin,)
@ -465,6 +539,16 @@ class PaymentViewSet(ExtendedModelViewSet):
)
search_fields = ('user__email', 'user__first_name', 'user__last_name',)
def get_serializer(self, instance, *args, **kwargs):
serializer_class = self.get_serializer_class()
if 'update' in self.action:
if isinstance(instance, CoursePayment):
serializer_class = CoursePaymentCreateSerializer
elif isinstance(instance, SchoolPayment):
serializer_class = SchoolPaymentCreateSerializer
kwargs['context'] = self.get_serializer_context()
return serializer_class(instance, *args, **kwargs)
def get_queryset(self):
queryset = self.queryset
course = self.request.query_params.get('course')

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

@ -10,6 +10,7 @@ from django.urls import reverse_lazy
from django.conf import settings
from polymorphic_tree.models import PolymorphicMPTTModel, PolymorphicTreeForeignKey
from apps.school.models import LiveLesson
from project.mixins import BaseModel, DeactivatedMixin
from apps.content.models import ImageObject, Gallery, Video, ContestWork
@ -240,6 +241,9 @@ class Material(models.Model):
class Comment(PolymorphicMPTTModel, DeactivatedMixin):
OBJ_TYPE_COURSE = 'course'
OBJ_TYPE_LESSON = 'lesson'
OBJ_TYPE_LIVE_LESSON = 'live-lesson'
content = models.TextField('Текст комментария', default='')
author = models.ForeignKey(User, on_delete=models.CASCADE)
parent = PolymorphicTreeForeignKey(
@ -284,5 +288,15 @@ class LessonComment(Comment):
verbose_name_plural = 'Комментарии уроков'
class LiveLessonComment(Comment):
live_lesson = models.ForeignKey(
LiveLesson, on_delete=models.CASCADE, related_name='comments'
)
class Meta(Comment.Meta):
verbose_name = 'Комментарий урока школы'
verbose_name_plural = 'Комментарии уроков школы'
class ContestWorkComment(Comment):
contest_work = models.ForeignKey(ContestWork, on_delete=models.CASCADE, related_name='comments')

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

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

@ -4,7 +4,7 @@
<div class="timing__info">
<div class="timing__day{% if school_schedule.is_online %} active{% endif %}">
{{ 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>
{% endif %}
</div>
@ -20,7 +20,7 @@
{% else %}
{% include './day_pay_btn.html' %}
{% 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>
{% endif %}
</div>

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

@ -140,21 +140,9 @@ class SchoolView(TemplateView):
pass
school_schedules_dict = {ss.weekday: ss for ss in school_schedules}
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:
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(
user=self.request.user,
status__in=[
@ -173,18 +161,32 @@ class SchoolView(TemplateView):
else:
school_payment_exists = False
school_schedules_purchased = []
if all_schedules_purchased and is_previous:
live_lessons = LiveLesson.objects.filter(
date__range=[yesterday - timedelta(days=7), yesterday],
deactivated_at__isnull=True,
date__week_day__in=all_schedules_purchased,
).order_by('-date')
if is_previous:
prev_range = [yesterday - timedelta(days=7), yesterday]
live_lessons = []
# берем все подписки, которые были в периоде
for sp in SchoolPayment.objects.filter(
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:
ll.school_schedule = school_schedules_dict.get(ll.date.isoweekday())
live_lessons_exists = live_lessons.exists()
else:
live_lessons = None
live_lessons_exists = False
live_lessons = live_lessons or None
context.update({
'online': online,
'live_lessons': live_lessons,

@ -92,7 +92,7 @@ class User(AbstractUser):
@property
def balance(self):
aggregate = self.balances.filter(
income = self.balances.filter(
type=0,
payment__isnull=False,
payment__status__isnull=False
@ -100,9 +100,13 @@ class User(AbstractUser):
models.Sum('amount'),
models.Sum('commission'),
)
amount = aggregate.get('amount__sum') or 0
commission = aggregate.get('commission__sum') or 0
return amount - commission
income_amount = income.get('amount__sum') or 0
income_commission = income.get('commission__sum') or 0
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)

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

@ -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.hashers import check_password, make_password
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.utils.decorators import method_decorator
from django.utils.timezone import now
@ -55,11 +55,11 @@ class ProfileView(TemplateView):
def get_context_data(self, object):
context = super().get_context_data()
context['user'] = self.request.user
context['is_author'] = self.request.user.role == User.AUTHOR_ROLE
context['published'] = Course.objects.filter(
author=self.object,
)
context['is_author'] = context['published'] or self.request.user.role == User.AUTHOR_ROLE
context['paid'] = Course.objects.filter(
payments__in=CoursePayment.objects.filter(
user=self.object,
@ -80,22 +80,18 @@ class ProfileView(TemplateView):
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
)
school_schedules_purchased = school_payment.annotate(
joined_weekdays=Func(F('weekdays'), function='unnest',)
).values_list('joined_weekdays', flat=True).distinct()
context['school_schedules_purchased'] = school_schedules_purchased
context['school_payment'] = school_payment
context['is_purchased'] = school_payment.exists()
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_school_purchased'] = school_payment.exists()
if context['is_school_purchased']:
school_schedules_purchased = school_payment.annotate(
joined_weekdays=Func(F('weekdays'), function='unnest',)
).values_list('joined_weekdays', flat=True).distinct()
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['is_purchased_future'] = False
context['school_purchased_future'] = False
context['profile'] = True
return context
@ -107,7 +103,7 @@ class UserView(DetailView):
def get_context_data(self, object):
context = super().get_context_data()
context['published'] = Course.objects.filter(
author=self.object,
author=self.object, status=Course.PUBLISHED,
)
return context

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

@ -197,6 +197,13 @@ TWILIO_FROM_PHONE = os.getenv('TWILIO_FROM_PHONE', '+37128914409')
ACTIVE_LINK_STRICT = True
# PUSHER settings
PUSHER_APP_ID = os.getenv('PUSHER_APP_ID', '')
PUSHER_KEY = os.getenv('PUSHER_KEY', '')
PUSHER_SECRET = os.getenv('PUSHER_SECRET', '')
PUSHER_CLUSTER = 'eu'
# DRF settings
REST_FRAMEWORK = {

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

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

@ -30,4 +30,4 @@ user-agents==1.1.0
ua-parser==0.8.0
django-ipware
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>

@ -15,6 +15,7 @@ import "./modules/tabs";
import "./modules/popup";
import "./modules/courses";
import "./modules/comments";
import "./modules/comments";
import "./modules/password-show";
import "./modules/profile";
import "./modules/notification";
@ -25,6 +26,7 @@ import "../sass/app.sass";
import Vue from 'vue';
import Vuelidate from 'vuelidate';
import VueAutosize from '../components/directives/autosize'
import Comments from '../components/Comments';
import UploadContestWork from '../components/UploadContestWork.vue';
import ContestWorks from '../components/ContestWorks.vue';
import Likes from '../components/blocks/Likes.vue';
@ -41,6 +43,7 @@ const components = {
UploadContestWork,
ContestWorks,
Likes,
Comments,
};
Object.assign(components, window.LIL_STORE.components);

@ -384,5 +384,34 @@ export const api = {
'Authorization': `Token ${window.LIL_STORE.accessToken}`,
}
});
},
getObjComments: (objId, objType, ordering) => {
return api.get('/api/v1/obj-comments/', {
params: {
obj_id: objId,
obj_type: objType,
ordering: ordering || '',
},
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();
}
});

@ -0,0 +1,59 @@
import Vue from 'vue';
import Comments from '../../components/Comments';
//import $ from 'jquery';
if (process.env.NODE_ENV === 'development') {
// Enable vue-devtools
Vue.config.devtools = true;
}
let app = new Vue({
el: '#comments_block',
data(){
return {
userId: 123,
accessToken: 123,
comments: [{
author: {
get_full_name: 'John Doe',
photo: {url: ''},
},
created_at_humanize: '12 07 18',
content: 'content content content content',
id: 1,
is_child_node: false
}, {
author: {
get_full_name: 'Sarah Conor',
photo: {url: ''},
},
created_at_humanize: '5 05 18',
content: 'hasta la vista',
id: 2,
is_child_node: false,
children: [{
author: {
get_full_name: 'John Doe',
photo: {url: ''},
},
created_at_humanize: '12 07 18',
content: 'content content content content',
id: 10,
is_child_node: true
}, {
author: {
get_full_name: 'Sarah Conor',
photo: {url: ''},
},
created_at_humanize: '5 05 18',
content: 'hasta la vista',
id: 20,
is_child_node: true,
}]
}]
}
},
components: {
'comments': Comments,
}
});

@ -1710,6 +1710,8 @@ a.grey-link
color: $cl
+t
line-height: 1.33
&__theme
text-transform: uppercase
&__user
margin-top: 20px
&_two &__item
@ -2809,16 +2811,23 @@ a.grey-link
&__item
display: flex
&__form
margin-bottom: 40px
position: relative;
margin-top: 20px
padding-bottom: 20px
border-bottom: 1px solid $border
&__form-loader
display: none;
&__form-foot
text-align: center;
&__item
&:not(:last-child)
margin-bottom: 25px
+m
padding: 10px 0
&_reply
padding-left: 80px
+m
padding: 0
padding: 10px 0
&__reply-info
display: none
margin-bottom: 10px
@ -2857,9 +2866,10 @@ a.grey-link
+m
height: 64px
&__btn
display: block
margin: 0 auto
margin: 0 15px;
border-radius: 20px
+m
margin-left: 0
&__details
margin-bottom: 5px
&__head,
@ -2878,11 +2888,70 @@ a.grey-link
&__author
margin-right: 15px
&__date
font-size: 10px
display: inline-block
&__foot
height: 20px
text-align: right
&__action
margin-left: auto
&__heart
fill: #d40700
width: 28px
height: 28px
&__send-icon
fill: $gray
width: 20px
height: 20px
&--heart
&__content
font-size: 24px
color: #d40700
&--chat
margin-top: 15px
&--chat &__items
background: white
padding: 10px
border-radius: 5px
max-height: 400px
overflow: auto
+m
max-height: inherit
&--chat &__item, &--chat &__item_reply
+m
padding: 0
&--chat &__ava
height: 40px
margin-right: 10px
flex: 0 0 40px
+m
display: none
&--chat &__content
margin-bottom: 10px
&--chat &__wrap
display: flex
flex: 0 0 calc(100% - 60px);
+m
flex: 1
&--chat &__field
margin-bottom: 0;
flex: 0 0 calc(100% - 160px);
+m
flex: 1
&--chat &__btn
font-weight: 600;
text-shadow: 1px 1px #fff;
+m
margin: 0 0 0 15px;
&--chat &__form-foot
display: flex;
&--loading &__form-loader
display: block
&--loading &__form &__ava, &--loading &__form &__wrap
opacity: 0.4
.share
&__title
@ -3664,6 +3733,11 @@ a.grey-link
+m
display: none
.mobile-show
display: none
+m
display: block
.school
display: flex
position: relative

Loading…
Cancel
Save