Feature/lil 559 See merge request lilcity/backend!67remotes/origin/hotfix/LIL-661
commit
5b6102d968
20 changed files with 644 additions and 47 deletions
@ -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',), |
||||
), |
||||
] |
||||
@ -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) |
||||
@ -1,11 +1,14 @@ |
||||
{% load static %} |
||||
<script> |
||||
window.LIL_STORE = { |
||||
pusherKey: '{{ settings.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 %}', |
||||
} |
||||
}; |
||||
</script> |
||||
|
||||
@ -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> |
||||
|
||||
@ -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, |
||||
} |
||||
}); |
||||
Loading…
Reference in new issue