Merge branch 'feature/LIL-559' into 'master'

Feature/lil 559

See merge request lilcity/backend!67
remotes/origin/hotfix/LIL-661
cfwme 8 years ago
commit 5b6102d968
  1. 69
      api/v1/serializers/course.py
  2. 3
      api/v1/urls.py
  3. 88
      api/v1/views.py
  4. 30
      apps/course/migrations/0044_livelessoncomment.py
  5. 14
      apps/course/models.py
  6. 33
      apps/course/templates/course/lesson.html
  7. 3
      apps/school/templates/school/livelesson_detail.html
  8. 14
      project/pusher.py
  9. 7
      project/settings.py
  10. 3
      project/templates/blocks/lil_store_js.html
  11. 1
      project/templates/lilcity/index.html
  12. 2
      requirements.txt
  13. 64
      web/src/components/Comment.vue
  14. 53
      web/src/components/CommentForm.vue
  15. 134
      web/src/components/Comments.vue
  16. 3
      web/src/js/app.js
  17. 29
      web/src/js/modules/api.js
  18. 1
      web/src/js/modules/comments.js
  19. 59
      web/src/js/modules/comments_vue.js
  20. 81
      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,
@ -327,6 +327,7 @@ class CommentSerializer(serializers.ModelSerializer):
'parent', 'parent',
'deactivated_at', 'deactivated_at',
'created_at', 'created_at',
'created_at_humanize',
'update_at', 'update_at',
) )
@ -342,6 +343,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):
@ -374,3 +377,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)

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

@ -11,10 +11,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,
@ -54,7 +55,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,6 +67,7 @@ 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()
@ -429,15 +431,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 +448,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
@ -466,14 +540,12 @@ class PaymentViewSet(viewsets.ModelViewSet):
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): def get_serializer(self, instance, *args, **kwargs):
print('instance', type(instance), self.action)
serializer_class = self.get_serializer_class() serializer_class = self.get_serializer_class()
if 'update' in self.action: if 'update' in self.action:
if isinstance(instance, CoursePayment): if isinstance(instance, CoursePayment):
serializer_class = CoursePaymentCreateSerializer serializer_class = CoursePaymentCreateSerializer
elif isinstance(instance, SchoolPayment): elif isinstance(instance, SchoolPayment):
serializer_class = SchoolPaymentCreateSerializer serializer_class = SchoolPaymentCreateSerializer
print('serializer_class', serializer_class)
kwargs['context'] = self.get_serializer_context() kwargs['context'] = self.get_serializer_context()
return serializer_class(instance, *args, **kwargs) return serializer_class(instance, *args, **kwargs)

@ -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
@ -240,6 +241,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(
@ -284,5 +288,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')

@ -117,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>

@ -21,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 }}"/>

@ -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 ACTIVE_LINK_STRICT = True
# PUSHER settings
PUSHER_APP_ID = u""
PUSHER_KEY = u""
PUSHER_SECRET = u""
PUSHER_CLUSTER = u""
# DRF settings # DRF settings
REST_FRAMEWORK = { REST_FRAMEWORK = {

@ -1,11 +1,14 @@
{% load static %} {% load static %}
<script> <script>
window.LIL_STORE = { window.LIL_STORE = {
pusherKey: '{{ settings.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 %}',
} }
}; };
</script> </script>

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

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

@ -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,6 +25,7 @@ import "../sass/app.sass";
import Vue from 'vue'; import Vue from 'vue';
import Vuelidate from 'vuelidate'; import Vuelidate from 'vuelidate';
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';
@ -46,5 +48,6 @@ const app = new Vue({
UploadContestWork, UploadContestWork,
ContestWorks, ContestWorks,
Likes, Likes,
Comments,
} }
}); });

@ -509,5 +509,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();
} }
}); });

@ -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,
}
});

@ -2811,16 +2811,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
@ -2859,9 +2866,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,
@ -2880,11 +2888,71 @@ 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;
border: 1px solid #ccc;
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
@ -3666,6 +3734,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