From 771dc336811abf50ed4880dabfce5aefd883a764 Mon Sep 17 00:00:00 2001 From: gzbender Date: Fri, 6 Jul 2018 15:44:19 +0500 Subject: [PATCH 01/37] =?UTF-8?q?LIL-559=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B1=D0=BB=D0=BE=D0=BA=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B5=D0=B2=20?= =?UTF-8?q?=D0=B2=20=D1=83=D1=80=D0=BE=D0=BA=D0=B8=20=D0=BE=D0=BD=D0=BB?= =?UTF-8?q?=D0=B0=D0=B9=D0=BD-=D1=88=D0=BA=D0=BE=D0=BB=D1=8B=20=D0=92?= =?UTF-8?q?=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BC=D0=B8=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/course/templates/course/lesson.html | 10 ++++ web/src/components/Comment.vue | 44 ++++++++++++++++++ web/src/components/Comments.vue | 38 +++++++++++++++ web/src/js/modules/comments.js | 1 + web/src/js/modules/comments_vue.js | 59 ++++++++++++++++++++++++ web/webpack.config.js | 4 ++ 6 files changed, 156 insertions(+) create mode 100644 web/src/components/Comment.vue create mode 100644 web/src/components/Comments.vue create mode 100644 web/src/js/modules/comments_vue.js diff --git a/apps/course/templates/course/lesson.html b/apps/course/templates/course/lesson.html index 662a4dd5..9fbe5a88 100644 --- a/apps/course/templates/course/lesson.html +++ b/apps/course/templates/course/lesson.html @@ -92,6 +92,14 @@ {% endcomment %} +
+
+
Задавайте вопросы:
+
+ +
+
+
Задавайте вопросы:
@@ -129,4 +137,6 @@ {% include 'templates/blocks/share.html' %}
+ {% endblock content %} diff --git a/web/src/components/Comment.vue b/web/src/components/Comment.vue new file mode 100644 index 00000000..b688c6b7 --- /dev/null +++ b/web/src/components/Comment.vue @@ -0,0 +1,44 @@ + + + + diff --git a/web/src/components/Comments.vue b/web/src/components/Comments.vue new file mode 100644 index 00000000..c89a3924 --- /dev/null +++ b/web/src/components/Comments.vue @@ -0,0 +1,38 @@ + + + diff --git a/web/src/js/modules/comments.js b/web/src/js/modules/comments.js index 30b0a815..084b172f 100644 --- a/web/src/js/modules/comments.js +++ b/web/src/js/modules/comments.js @@ -62,3 +62,4 @@ $(document).ready(function () { form.find('.questions__reply-info').hide(); } }); + diff --git a/web/src/js/modules/comments_vue.js b/web/src/js/modules/comments_vue.js new file mode 100644 index 00000000..f511dac5 --- /dev/null +++ b/web/src/js/modules/comments_vue.js @@ -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, + } +}); diff --git a/web/webpack.config.js b/web/webpack.config.js index 3c6785ab..3d47d057 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -9,6 +9,7 @@ module.exports = { entry: { app: "./src/js/app.js", courseRedactor: "./src/js/course-redactor.js", + comments_vue: "./src/js/modules/comments_vue.js", mixpanel: "./src/js/third_party/mixpanel-2-latest.js", sprite: glob('./src/icons/*.svg'), images: glob('./src/img/*'), @@ -116,6 +117,9 @@ module.exports = { }, watch: NODE_ENV === 'development', + watchOptions: { + poll: true, + }, devtool: NODE_ENV === 'development' ? 'source-map' : false }; From f9b72b81e7a46d03b75503f1c64e345365f09646 Mon Sep 17 00:00:00 2001 From: gzbender Date: Tue, 10 Jul 2018 14:29:05 +0500 Subject: [PATCH 02/37] =?UTF-8?q?LIL-559,=20API=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F-=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=BE=D0=B2,=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D0=B8=D0=B5=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/serializers/course.py | 46 ++++++++++++++ api/v1/urls.py | 3 +- api/v1/views.py | 37 ++++++++++- apps/course/templates/course/lesson.html | 9 ++- web/src/components/Comment.vue | 32 +++++----- web/src/components/Comments.vue | 79 +++++++++++++++++++++--- web/src/js/app.js | 16 +++++ web/src/js/modules/api.js | 21 +++++++ 8 files changed, 211 insertions(+), 32 deletions(-) diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index bdb1a289..298528bc 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -300,6 +300,7 @@ class CommentSerializer(serializers.ModelSerializer): 'parent', 'deactivated_at', 'created_at', + 'created_at_humanize', 'update_at', ) @@ -347,3 +348,48 @@ class LessonCommentSerializer(serializers.ModelSerializer): read_only_fields = CommentSerializer.Meta.read_only_fields + ( 'children', ) + + +class CommentCreateSerializer(serializers.ModelSerializer): + OBJ_TYPE_COURSE = 'course' + OBJ_TYPE_LESSON = 'lesson' + + 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 == self.OBJ_TYPE_COURSE: + validated_data['course_id'] = obj_id + return CourseCommentSerializer().create(validated_data) + elif obj_type == self.OBJ_TYPE_LESSON: + validated_data['lesson_id'] = obj_id + return LessonCommentSerializer().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) diff --git a/api/v1/urls.py b/api/v1/urls.py index 409ed79f..5905dc8b 100644 --- a/api/v1/urls.py +++ b/api/v1/urls.py @@ -17,7 +17,7 @@ from .views import ( GalleryViewSet, GalleryImageViewSet, UserViewSet, LessonViewSet, ImageObjectViewSet, SchoolScheduleViewSet, LiveLessonViewSet, - PaymentViewSet, + PaymentViewSet, ObjectCommentsViewSet, ) 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') diff --git a/api/v1/views.py b/api/v1/views.py index 41cd881a..fb79a335 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -11,9 +11,10 @@ from .serializers.course import ( CategorySerializer, LikeSerializer, CourseSerializer, CourseCreateSerializer, CourseBulkChangeCategorySerializer, - CommentSerializer, + CommentSerializer, CommentCreateSerializer, MaterialSerializer, MaterialCreateSerializer, LessonSerializer, LessonCreateSerializer, + CourseCommentSerializer, LessonCommentSerializer, ) from .serializers.content import ( BanerSerializer, @@ -395,7 +396,7 @@ class ConfigViewSet(generics.RetrieveUpdateAPIView): class CommentViewSet(ExtendedModelViewSet): queryset = Comment.objects.filter(level=0) - serializer_class = CommentSerializer + serializer_class = CommentCreateSerializer permission_classes = (IsAdmin,) def get_queryset(self): @@ -411,6 +412,38 @@ class CommentViewSet(ExtendedModelViewSet): return queryset +class ObjectCommentsViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Comment.objects.filter(level=0) + OBJ_TYPE_COURSE = 'course' + OBJ_TYPE_LESSON = 'lesson' + permission_classes = (IsAdmin,) + + 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', '0') + if obj_type == self.OBJ_TYPE_COURSE: + queryset = CourseComment.objects.filter(course=obj_id) + elif obj_type == self.OBJ_TYPE_LESSON: + queryset = LessonComment.objects.filter(lesson=obj_id) + if is_deactivated == '0': + queryset = queryset + 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): + obj_type = self.request.query_params.get('obj_type') + if obj_type == self.OBJ_TYPE_COURSE: + serializer_class = CourseCommentSerializer + elif obj_type == self.OBJ_TYPE_LESSON: + serializer_class = LessonCommentSerializer + return serializer_class + + class AuthorRequestViewSet(ExtendedModelViewSet): queryset = AuthorRequest.objects.all() serializer_class = AuthorRequestSerializer diff --git a/apps/course/templates/course/lesson.html b/apps/course/templates/course/lesson.html index 9fbe5a88..18e51e78 100644 --- a/apps/course/templates/course/lesson.html +++ b/apps/course/templates/course/lesson.html @@ -96,7 +96,12 @@
Задавайте вопросы:
- +
@@ -137,6 +142,4 @@ {% include 'templates/blocks/share.html' %} - {% endblock content %} diff --git a/web/src/components/Comment.vue b/web/src/components/Comment.vue index b688c6b7..1abdd00b 100644 --- a/web/src/components/Comment.vue +++ b/web/src/components/Comment.vue @@ -1,26 +1,26 @@ @@ -15,19 +31,62 @@ export default { name: 'comments', - props: ['objType', 'objId', 'nodes', 'userId', 'accessToken'], - methods: { - reply() { - // + props: ['objType', 'objId', 'comments', 'userId', 'userPhoto', 'accessToken', 'defaultAuthorPicture'], + data() { + return { + loading: false, + replyTo: null, + content: '', + nodes: [], + } + }, + computed: { + replyAnchor(){ + return this.replyTo ? `#question__${this.replyTo.id}` : ''; } }, + methods: { + clear() { + this.content = '', + this.replyTo = null + }, + cancelReply() { + this.replyTo = null; + }, + post() { + this.loading = true; + let request = api.addComment(this.objId, this.objType, { + content: this.content, + author: this.userId, + parent: this.replyTo && this.replyTo.id, + }, this.accessToken); + request.then((response) => { + this.loading = false; + if(this.replyTo){ + this.replyTo.children.push(response.data); + } + else { + this.nodes.push(response.data); + } + this.clear(); + }).catch(() => { + this.loading = false; + }); + }, + onReply(replyTo) { + this.replyTo = replyTo; + }, + }, mounted() { - if(! this.nodes && this.objType && this.objId){ - request = api.loadComments(this.objId, this.objType, this.accessToken); + if(this.comments && this.comments.length){ + this.nodes = this.comments; + } + else if(this.objType && this.objId){ + let request = api.getObjComments(this.objId, this.objType, this.accessToken); request .then((response) => { - this.nodes = response.data; - }) + this.nodes = response.data.results; + }); } console.log('nodes', this.nodes); }, diff --git a/web/src/js/app.js b/web/src/js/app.js index 16651100..7be8af4e 100644 --- a/web/src/js/app.js +++ b/web/src/js/app.js @@ -11,9 +11,25 @@ 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"; import "./modules/mixpanel"; import "../sass/app.sass"; + +import Vue from 'vue'; +import Comments from '../components/Comments'; + +if (process.env.NODE_ENV === 'development') { + // Enable vue-devtools + Vue.config.devtools = true; +} + +let app = new Vue({ + el: '.js-outer', + components: { + 'comments': Comments, + } +}) diff --git a/web/src/js/modules/api.js b/web/src/js/modules/api.js index 990ac7b2..003153a3 100644 --- a/web/src/js/modules/api.js +++ b/web/src/js/modules/api.js @@ -502,4 +502,25 @@ export const api = { } }); }, + getObjComments: (objId, objType, accessToken) => { + return api.get('/api/v1/obj-comments/', { + params: { + obj_id: objId, + obj_type: objType, + }, + headers: { + 'Authorization': `Token ${accessToken}`, + } + }); + }, + addComment: (objId, objType, commentJson, accessToken) => { + let data = commentJson; + data.obj_id = objId; + data.obj_type = objType; + return api.post('/api/v1/comments/', data, { + headers: { + 'Authorization': `Token ${accessToken}`, + } + }); + } }; From 3f4426618f1814fd642705646413c45297efcac6 Mon Sep 17 00:00:00 2001 From: gzbender Date: Thu, 12 Jul 2018 02:25:33 +0500 Subject: [PATCH 03/37] =?UTF-8?q?LIL-559=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B1=D0=BB=D0=BE=D0=BA=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B5=D0=B2=20?= =?UTF-8?q?=D0=B2=20=D1=83=D1=80=D0=BE=D0=BA=D0=B8=20=D0=BE=D0=BD=D0=BB?= =?UTF-8?q?=D0=B0=D0=B9=D0=BD-=D1=88=D0=BA=D0=BE=D0=BB=D1=8B:=20comments?= =?UTF-8?q?=20with=20using=20Pusher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/views.py | 40 ++++-- apps/course/templates/course/lesson.html | 34 +---- project/pusher.py | 14 ++ project/settings.py | 7 + project/templates/lilcity/index.html | 11 ++ requirements.txt | 2 + web/src/components/Comment.vue | 76 ++++++---- web/src/components/CommentForm.vue | 42 ++++++ web/src/components/Comments.vue | 169 ++++++++++++++--------- web/src/js/modules/api.js | 11 +- web/webpack.config.js | 4 +- 11 files changed, 277 insertions(+), 133 deletions(-) create mode 100644 project/pusher.py create mode 100644 web/src/components/CommentForm.vue diff --git a/api/v1/views.py b/api/v1/views.py index fb79a335..5505310a 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -64,6 +64,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() @@ -395,15 +396,15 @@ class ConfigViewSet(generics.RetrieveUpdateAPIView): class CommentViewSet(ExtendedModelViewSet): - queryset = Comment.objects.filter(level=0) - serializer_class = CommentCreateSerializer - permission_classes = (IsAdmin,) + queryset = Comment.objects.all() + serializer_class = CommentSerializer + 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': @@ -412,11 +413,12 @@ class CommentViewSet(ExtendedModelViewSet): return queryset -class ObjectCommentsViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Comment.objects.filter(level=0) +class ObjectCommentsViewSet(ExtendedModelViewSet): OBJ_TYPE_COURSE = 'course' OBJ_TYPE_LESSON = 'lesson' - permission_classes = (IsAdmin,) + queryset = Comment.objects.all() + serializer_class = CommentCreateSerializer + permission_classes = (IsAuthorObjectOrAdmin,) def get_queryset(self): queryset = self.queryset @@ -424,11 +426,11 @@ class ObjectCommentsViewSet(viewsets.ReadOnlyModelViewSet): obj_id = self.request.query_params.get('obj_id') is_deactivated = self.request.query_params.get('is_deactivated', '0') if obj_type == self.OBJ_TYPE_COURSE: - queryset = CourseComment.objects.filter(course=obj_id) + queryset = CourseComment.objects.filter(course=obj_id, parent__isnull=True) elif obj_type == self.OBJ_TYPE_LESSON: - queryset = LessonComment.objects.filter(lesson=obj_id) + queryset = LessonComment.objects.filter(lesson=obj_id, parent__isnull=True) 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': @@ -436,6 +438,8 @@ class ObjectCommentsViewSet(viewsets.ReadOnlyModelViewSet): return queryset def get_serializer_class(self): + if self.request.method == 'POST': + return CommentCreateSerializer obj_type = self.request.query_params.get('obj_type') if obj_type == self.OBJ_TYPE_COURSE: serializer_class = CourseCommentSerializer @@ -443,6 +447,22 @@ class ObjectCommentsViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = LessonCommentSerializer 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() + pusher().trigger(f'comments_{obj_type}_{obj_id}', 'add', serializer.data) + + def perform_destroy(self, instance): + obj_type = None + if isinstance(instance, LessonComment): + obj_type = self.OBJ_TYPE_LESSON + elif isinstance(instance, CourseComment): + obj_type = self.OBJ_TYPE_COURSE + serializer = self.get_serializer(instance) + pusher().trigger(f'comments_{obj_type}_{obj_id}', 'delete', serializer.data) + instance.delete() + class AuthorRequestViewSet(ExtendedModelViewSet): queryset = AuthorRequest.objects.all() diff --git a/apps/course/templates/course/lesson.html b/apps/course/templates/course/lesson.html index 18e51e78..9e79279d 100644 --- a/apps/course/templates/course/lesson.html +++ b/apps/course/templates/course/lesson.html @@ -96,7 +96,7 @@
Задавайте вопросы:
-
-
-
-
Задавайте вопросы:
-
- {% if request.user.is_authenticated %} -
-
- -
-
-
- -
- -
-
- {% else %} -
Только зарегистрированные пользователи могут оставлять комментарии.
- {% endif %} -
- {% include "templates/blocks/comments.html" with object=lesson %} -
-
-
-
{% include 'templates/blocks/share.html' %} diff --git a/project/pusher.py b/project/pusher.py new file mode 100644 index 00000000..af6c74a8 --- /dev/null +++ b/project/pusher.py @@ -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) diff --git a/project/settings.py b/project/settings.py index 9323745c..80c9e4ce 100644 --- a/project/settings.py +++ b/project/settings.py @@ -194,6 +194,13 @@ TWILIO_FROM_PHONE = os.getenv('TWILIO_FROM_PHONE', '+37128914409') ACTIVE_LINK_STRICT = True +# PUSHER settings + +PUSHER_APP_ID = u"" +PUSHER_KEY = u"" +PUSHER_SECRET = u"" +PUSHER_CLUSTER = u"" + # DRF settings REST_FRAMEWORK = { diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index 5d7b9d2d..187cd913 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -38,6 +38,7 @@ {% endcompress %} + + + diff --git a/web/src/components/CommentForm.vue b/web/src/components/CommentForm.vue new file mode 100644 index 00000000..80041646 --- /dev/null +++ b/web/src/components/CommentForm.vue @@ -0,0 +1,42 @@ + + + + diff --git a/web/src/components/Comments.vue b/web/src/components/Comments.vue index dcbf2a0d..40c7ea60 100644 --- a/web/src/components/Comments.vue +++ b/web/src/components/Comments.vue @@ -1,97 +1,142 @@ + + diff --git a/web/src/js/modules/api.js b/web/src/js/modules/api.js index 003153a3..7aa95c8a 100644 --- a/web/src/js/modules/api.js +++ b/web/src/js/modules/api.js @@ -513,11 +513,18 @@ export const api = { } }); }, - addComment: (objId, objType, commentJson, accessToken) => { + addObjComment: (objId, objType, commentJson, accessToken) => { let data = commentJson; data.obj_id = objId; data.obj_type = objType; - return api.post('/api/v1/comments/', data, { + return api.post('/api/v1/obj-comments/', data, { + headers: { + 'Authorization': `Token ${accessToken}`, + } + }); + }, + removeComment: (commentId, accessToken) => { + return api.delete(`/api/v1/comments/${commentId}/`, { headers: { 'Authorization': `Token ${accessToken}`, } diff --git a/web/webpack.config.js b/web/webpack.config.js index 3d47d057..8cd8b8f4 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -9,7 +9,6 @@ module.exports = { entry: { app: "./src/js/app.js", courseRedactor: "./src/js/course-redactor.js", - comments_vue: "./src/js/modules/comments_vue.js", mixpanel: "./src/js/third_party/mixpanel-2-latest.js", sprite: glob('./src/icons/*.svg'), images: glob('./src/img/*'), @@ -118,7 +117,8 @@ module.exports = { watch: NODE_ENV === 'development', watchOptions: { - poll: true, + ignored: "/node_modules/", + poll: 2000, }, devtool: NODE_ENV === 'development' ? 'source-map' : false From 470d7252eba9231664003e9a50fce7ae91a73479 Mon Sep 17 00:00:00 2001 From: gzbender Date: Thu, 12 Jul 2018 19:10:06 +0500 Subject: [PATCH 04/37] =?UTF-8?q?LIL-559=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B1=D0=BB=D0=BE=D0=BA=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B5=D0=B2=20?= =?UTF-8?q?=D0=B2=20=D1=83=D1=80=D0=BE=D0=BA=D0=B8=20=D0=BE=D0=BD=D0=BB?= =?UTF-8?q?=D0=B0=D0=B9=D0=BD-=D1=88=D0=BA=D0=BE=D0=BB=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/views.py | 27 ++++++++++++++----- web/src/components/Comment.vue | 13 ++++----- web/src/components/CommentForm.vue | 10 +++---- web/src/components/Comments.vue | 20 +++++--------- web/src/js/modules/api.js | 4 +-- web/src/sass/_common.sass | 42 +++++++++++++++++++++++++++--- 6 files changed, 78 insertions(+), 38 deletions(-) diff --git a/api/v1/views.py b/api/v1/views.py index 5505310a..bc184a6d 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -424,23 +424,24 @@ class ObjectCommentsViewSet(ExtendedModelViewSet): 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', '0') + is_deactivated = self.request.query_params.get('is_deactivated') if obj_type == self.OBJ_TYPE_COURSE: - queryset = CourseComment.objects.filter(course=obj_id, parent__isnull=True) + queryset = CourseComment.objects.filter(course=obj_id) elif obj_type == self.OBJ_TYPE_LESSON: - queryset = LessonComment.objects.filter(lesson=obj_id, parent__isnull=True) + queryset = LessonComment.objects.filter(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 + return queryset.order_by('update_at') 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 == self.OBJ_TYPE_COURSE: serializer_class = CourseCommentSerializer elif obj_type == self.OBJ_TYPE_LESSON: @@ -451,18 +452,32 @@ class ObjectCommentsViewSet(ExtendedModelViewSet): obj_type = self.request.data.get('obj_type') obj_id = self.request.data.get('obj_id') serializer.save() - pusher().trigger(f'comments_{obj_type}_{obj_id}', 'add', serializer.data) + 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 = self.OBJ_TYPE_LESSON + obj_id = instance.lesson_id elif isinstance(instance, CourseComment): obj_type = self.OBJ_TYPE_COURSE + obj_id = instance.course_id serializer = self.get_serializer(instance) - pusher().trigger(f'comments_{obj_type}_{obj_id}', 'delete', serializer.data) + 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() diff --git a/web/src/components/Comment.vue b/web/src/components/Comment.vue index e5f262d7..279992f9 100644 --- a/web/src/components/Comment.vue +++ b/web/src/components/Comment.vue @@ -1,6 +1,6 @@ diff --git a/web/src/components/CourseRedactor.vue b/web/src/components/CourseRedactor.vue index b3703785..0f5ae924 100644 --- a/web/src/components/CourseRedactor.vue +++ b/web/src/components/CourseRedactor.vue @@ -166,45 +166,7 @@
- -
- - - - - -
-
- - + + {% else %} {% if livelesson.cover %} diff --git a/web/src/components/Comment.vue b/web/src/components/Comment.vue index 279992f9..376d1788 100644 --- a/web/src/components/Comment.vue +++ b/web/src/components/Comment.vue @@ -9,7 +9,7 @@
- +
@@ -24,13 +24,13 @@
- - + +
-
    @@ -48,11 +48,6 @@ export default { name: 'comment', props: ['controller', 'comment',], - data() { - return { - store: window.VUE_STORE, - } - }, computed: { isHeart(){ return this.comment.content === '❤'; diff --git a/web/src/components/CommentForm.vue b/web/src/components/CommentForm.vue index c6eae9f6..d83c8dd1 100644 --- a/web/src/components/CommentForm.vue +++ b/web/src/components/CommentForm.vue @@ -1,7 +1,7 @@ @@ -25,7 +25,6 @@ replyTo: null, nodes: [], controller: this, - store: window.VUE_STORE, flatComments: {}, } }, @@ -44,9 +43,9 @@ this.loading = true; let request = api.addObjComment(this.objId, this.objType, { content: content, - author: this.store.user.id, + author: this.$root.store.user.id, parent: this.replyTo && this.replyTo.id, - }, this.store.accessToken); + }); request.then((response) => { vm.loading = false; vm.onAdd(response.data); @@ -63,7 +62,7 @@ } let vm = this; this.loading = true; - let request = api.removeObjComment(comment.id, this.store.accessToken); + let request = api.removeObjComment(comment.id); request.then((response) => { vm.loading = false; vm.onRemove(comment); @@ -73,11 +72,12 @@ if(this.flatComments[comment.id]){ return; } + const method = this.isChat ? 'push' : 'unshift'; if(comment.parent){ - this.flatComments[comment.parent].children.unshift(comment); + this.flatComments[comment.parent].children[method](comment); } else{ - this.nodes.unshift(comment); + this.nodes[method](comment); } this.flatComments[comment.id] = comment; }, @@ -114,7 +114,7 @@ mounted() { let vm = this; this.loading = true; - let request = api.getObjComments(this.objId, this.objType, this.store.accessToken); + let request = api.getObjComments(this.objId, this.objType, this.isChat ? 'update_at' : '-update_at'); request .then((response) => { vm.loading = false; diff --git a/web/src/js/modules/api.js b/web/src/js/modules/api.js index ee0f6337..9d7704b3 100644 --- a/web/src/js/modules/api.js +++ b/web/src/js/modules/api.js @@ -510,31 +510,32 @@ export const api = { } }); }, - getObjComments: (objId, objType, accessToken) => { + getObjComments: (objId, objType, ordering) => { return api.get('/api/v1/obj-comments/', { params: { obj_id: objId, obj_type: objType, + ordering: ordering || '', }, headers: { - 'Authorization': `Token ${accessToken}`, + 'Authorization': `Token ${window.LIL_STORE.accessToken}`, } }); }, - addObjComment: (objId, objType, commentJson, 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 ${accessToken}`, + 'Authorization': `Token ${window.LIL_STORE.accessToken}`, } }); }, - removeObjComment: (commentId, accessToken) => { + removeObjComment: (commentId) => { return api.delete(`/api/v1/obj-comments/${commentId}/`, { headers: { - 'Authorization': `Token ${accessToken}`, + 'Authorization': `Token ${window.LIL_STORE.accessToken}`, } }); } diff --git a/web/src/sass/_common.sass b/web/src/sass/_common.sass index c1e958b1..862e31b1 100755 --- a/web/src/sass/_common.sass +++ b/web/src/sass/_common.sass @@ -2819,10 +2819,12 @@ a.grey-link &__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 @@ -2863,6 +2865,8 @@ a.grey-link &__btn margin: 0 15px; border-radius: 20px + +m + margin-left: 0 &__details margin-bottom: 5px &__head, @@ -2881,6 +2885,7 @@ a.grey-link &__author margin-right: 15px &__date + font-size: 10px display: inline-block &__foot height: 20px @@ -2891,36 +2896,51 @@ a.grey-link fill: #d40700 width: 28px height: 28px + &__send-icon + fill: $gray + width: 20px + height: 20px &--heart &__content - font-size: 24px; - color: #d40700; - + font-size: 24px; + color: #d40700; &--chat &__items - background: white; - border: 1px solid #ccc; - padding: 10px; - border-radius: 5px; - max-height: 400px; - overflow: auto; + 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 + height: 40px + margin-right: 10px + flex: 0 0 40px + +m + display: none &--chat &__content - margin-bottom: 10px + margin-bottom: 10px &--chat &__wrap - display: flex - flex: 0 0 calc(100% - 60px); + display: flex + flex: 0 0 calc(100% - 60px); + +m + flex: 1 &--chat &__field - margin-bottom: 0; - flex: 0 0 calc(100% - 160px); + margin-bottom: 0; + flex: 0 0 calc(100% - 160px); + +m + flex: 1 &--chat &__btn - font-weight: 600; - text-shadow: 1px 1px #fff; + font-weight: 600; + text-shadow: 1px 1px #fff; + +m + margin: 0 0 0 15px; &--chat &__form-foot - display: flex; - + display: flex; .share &__title @@ -3702,6 +3722,11 @@ a.grey-link +m display: none +.mobile-show + display: none + +m + display: block + .school display: flex position: relative From 9c8090916d489485c99c78a1297b4f1143a76fed Mon Sep 17 00:00:00 2001 From: gzbender Date: Wed, 19 Sep 2018 16:57:28 +0500 Subject: [PATCH 28/37] LIL-559, LiveLessonComment and etc --- api/v1/serializers/course.py | 33 +++++++++++++++---- api/v1/views.py | 25 ++++++++------ .../migrations/0044_livelessoncomment.py | 30 +++++++++++++++++ apps/course/models.py | 14 ++++++++ project/templates/blocks/lil_store_js.html | 1 + web/src/components/CommentForm.vue | 8 ++++- web/src/components/Comments.vue | 4 +-- web/src/sass/_common.sass | 12 +++++++ 8 files changed, 108 insertions(+), 19 deletions(-) create mode 100644 apps/course/migrations/0044_livelessoncomment.py diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index c97fd13c..fecd2b3a 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -10,7 +10,7 @@ from apps.course.models import ( Comment, CourseComment, LessonComment, Material, Lesson, Like, -) + LiveLessonComment) from .content import ( ImageObjectSerializer, ContentSerializer, ContentCreateSerializer, GallerySerializer, GalleryImageSerializer, @@ -343,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): @@ -377,10 +379,24 @@ class LessonCommentSerializer(serializers.ModelSerializer): ) -class CommentCreateSerializer(serializers.ModelSerializer): - OBJ_TYPE_COURSE = 'course' - OBJ_TYPE_LESSON = 'lesson' +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) @@ -408,15 +424,20 @@ class CommentCreateSerializer(serializers.ModelSerializer): def create(self, validated_data): obj_type = validated_data.pop('obj_type', None) obj_id = validated_data.pop('obj_id', None) - if obj_type == self.OBJ_TYPE_COURSE: + if obj_type == Comment.OBJ_TYPE_COURSE: validated_data['course_id'] = obj_id return CourseCommentSerializer().create(validated_data) - elif obj_type == self.OBJ_TYPE_LESSON: + 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) diff --git a/api/v1/views.py b/api/v1/views.py index 96a0255a..c2bc1f07 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -15,7 +15,7 @@ from .serializers.course import ( MaterialSerializer, MaterialCreateSerializer, LessonSerializer, LessonCreateSerializer, LikeCreateSerializer, CourseCommentSerializer, LessonCommentSerializer, -) + LiveLessonCommentSerializer) from .serializers.content import ( BanerSerializer, ImageSerializer, ImageCreateSerializer, @@ -55,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, @@ -449,8 +449,6 @@ class CommentViewSet(ExtendedModelViewSet): class ObjectCommentsViewSet(ExtendedModelViewSet): - OBJ_TYPE_COURSE = 'course' - OBJ_TYPE_LESSON = 'lesson' queryset = Comment.objects.all() serializer_class = CommentCreateSerializer permission_classes = (IsAuthorObjectOrAdmin,) @@ -461,10 +459,12 @@ class ObjectCommentsViewSet(ExtendedModelViewSet): 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 == self.OBJ_TYPE_COURSE: + if obj_type == Comment.OBJ_TYPE_COURSE: queryset = CourseComment.objects.filter(course=obj_id) - elif obj_type == self.OBJ_TYPE_LESSON: + 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': @@ -478,10 +478,12 @@ class ObjectCommentsViewSet(ExtendedModelViewSet): return CommentCreateSerializer obj_type = self.request.query_params.get('obj_type') serializer_class = CommentSerializer - if obj_type == self.OBJ_TYPE_COURSE: + if obj_type == Comment.OBJ_TYPE_COURSE: serializer_class = CourseCommentSerializer - elif obj_type == self.OBJ_TYPE_LESSON: + 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): @@ -497,11 +499,14 @@ class ObjectCommentsViewSet(ExtendedModelViewSet): obj_type = None obj_id = None if isinstance(instance, LessonComment): - obj_type = self.OBJ_TYPE_LESSON + obj_type = Comment.OBJ_TYPE_LESSON obj_id = instance.lesson_id elif isinstance(instance, CourseComment): - obj_type = self.OBJ_TYPE_COURSE + 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) diff --git a/apps/course/migrations/0044_livelessoncomment.py b/apps/course/migrations/0044_livelessoncomment.py new file mode 100644 index 00000000..be5102af --- /dev/null +++ b/apps/course/migrations/0044_livelessoncomment.py @@ -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',), + ), + ] diff --git a/apps/course/models.py b/apps/course/models.py index 843e768e..41dcfe29 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -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') diff --git a/project/templates/blocks/lil_store_js.html b/project/templates/blocks/lil_store_js.html index 43926192..b892425a 100644 --- a/project/templates/blocks/lil_store_js.html +++ b/project/templates/blocks/lil_store_js.html @@ -1,6 +1,7 @@ {% load static %}