Merge branch 'master' of https://gitlab.com/lilcity/backend into feature/lil-583
commit
2b52d9852c
64 changed files with 1373 additions and 1015 deletions
@ -0,0 +1,18 @@ |
|||||||
|
# Generated by Django 2.0.6 on 2018-09-07 00:20 |
||||||
|
|
||||||
|
from django.db import migrations, models |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('course', '0043_auto_20180824_2132'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.AddField( |
||||||
|
model_name='course', |
||||||
|
name='age', |
||||||
|
field=models.SmallIntegerField(choices=[(0, 'Любой возраст'), (1, 'до 5'), (2, '5-7'), (3, '7-9'), (4, '9-12'), (5, '12-15'), (6, '15-18'), (7, 'от 18')], default=0), |
||||||
|
), |
||||||
|
] |
||||||
@ -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',), |
||||||
|
), |
||||||
|
] |
||||||
@ -1,6 +1,6 @@ |
|||||||
{% for cat in category_items %} |
{% for cat in category_items %} |
||||||
<div class="select__option js-select-option{% if category == cat.id %} active{% endif %}" |
<div class="select__option js-select-option{% if category == cat.id %} active{% endif %}" data-category-option |
||||||
data-category-option data-category-name="{{ cat.title }}" data-category-url="{% url 'courses' %}?category={{ cat.id }}"> |
data-category-name="{{ cat.title }}" data-category="{{ cat.id }}"> |
||||||
<div class="select__title">{{ cat.title }}</div> |
<div class="select__title">{{ cat.title }}</div> |
||||||
</div> |
</div> |
||||||
{% endfor %} |
{% endfor %} |
||||||
@ -1,9 +1,5 @@ |
|||||||
<a |
<a |
||||||
{% if not user.is_authenticated %} |
data-popup=".js-popup-buy" data-prolong="1" data-date-start="{{ prolong_date_start|date:'Y-m-d' }}" |
||||||
data-popup=".js-popup-auth" |
|
||||||
{% else %} |
|
||||||
data-popup=".js-popup-buy" |
|
||||||
{% endif %} |
|
||||||
class="casing__btn btn{% if pink %} btn_pink{% endif %}" |
class="casing__btn btn{% if pink %} btn_pink{% endif %}" |
||||||
href="#" |
href="#" |
||||||
>продлить</a> |
>продлить</a> |
||||||
|
|||||||
@ -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) |
||||||
@ -0,0 +1,71 @@ |
|||||||
|
<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"> |
||||||
|
<button @click="controller.reply(comment)" v-if="$root.store.user.id && ! controller.isChat" class="questions__action question__reply-button">ОТВЕТИТЬ</button> |
||||||
|
<button @click="controller.remove(comment)" |
||||||
|
v-if="$root.store.user.id == comment.author.id || $root.store.user.role == $root.store.roles.ADMIN_ROLE" |
||||||
|
class="questions__action question__reply-button"> |
||||||
|
<span v-if="! controller.isChat">УДАЛИТЬ</span> |
||||||
|
<span v-if="controller.isChat"> |
||||||
|
<svg class="icon questions__delete-icon"><use xlink:href="/static/img/sprite.svg#icon-delete"></use></svg> |
||||||
|
</span> |
||||||
|
</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" :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,93 @@ |
|||||||
|
<template> |
||||||
|
<div> |
||||||
|
<vue-draggable :list="content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }"> |
||||||
|
<div v-for="(block, index) in content" :key="block.id ? block.id : block.uuid"> |
||||||
|
<block-text v-if="block.type === 'text'" |
||||||
|
:index="index" |
||||||
|
:title.sync="block.data.title" |
||||||
|
:text.sync="block.data.text" |
||||||
|
v-on:remove="onBlockRemoved"/> |
||||||
|
<block-image-text v-if="block.type === 'image-text'" |
||||||
|
:index="index" |
||||||
|
:title.sync="block.data.title" |
||||||
|
:text.sync="block.data.text" |
||||||
|
:image-id.sync="block.data.image_id" |
||||||
|
:image-url.sync="block.data.image_thumbnail_url" |
||||||
|
v-on:remove="onBlockRemoved" |
||||||
|
:access-token="$root.store.accessToken"/> |
||||||
|
<block-image v-if="block.type === 'image'" |
||||||
|
:index="index" |
||||||
|
:title.sync="block.data.title" |
||||||
|
:image-id.sync="block.data.image_id" |
||||||
|
:image-url.sync="block.data.image_thumbnail_url" |
||||||
|
v-on:remove="onBlockRemoved" |
||||||
|
:access-token="$root.store.accessToken"/> |
||||||
|
<block-images v-if="block.type === 'images'" |
||||||
|
:index="index" |
||||||
|
:title.sync="block.data.title" |
||||||
|
:text.sync="block.data.text" |
||||||
|
:images.sync="block.data.images" |
||||||
|
v-on:remove="onBlockRemoved" |
||||||
|
:access-token="$root.store.accessToken"/> |
||||||
|
<block-video v-if="block.type === 'video'" |
||||||
|
:index="index" |
||||||
|
:title.sync="block.data.title" |
||||||
|
v-on:remove="onBlockRemoved" |
||||||
|
:video-url.sync="block.data.video_url"/> |
||||||
|
</div> |
||||||
|
</vue-draggable> |
||||||
|
|
||||||
|
<block-add v-on:added="onBlockAdded"/> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import {api} from "../../js/modules/api"; |
||||||
|
import Draggable from 'vuedraggable'; |
||||||
|
import BlockText from './BlockText' |
||||||
|
import BlockImage from './BlockImage' |
||||||
|
import BlockImages from './BlockImages' |
||||||
|
import BlockImageText from './BlockImageText' |
||||||
|
import BlockVideo from './BlockVideo' |
||||||
|
import BlockAdd from "./BlockAdd" |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'block-content', |
||||||
|
props: ['content'], |
||||||
|
methods: { |
||||||
|
onBlockRemoved(blockIndex) { |
||||||
|
const remove = () => { |
||||||
|
// Удаляем блок из Vue |
||||||
|
content.splice(blockIndex, 1); |
||||||
|
this.$emit('update:content', content); |
||||||
|
} |
||||||
|
const content = this.content; |
||||||
|
const blockToRemove = this.content[blockIndex]; |
||||||
|
// Если блок уже был записан в БД, отправляем запрос на сервер на удаление блока из БД |
||||||
|
if (blockToRemove.data.id) { |
||||||
|
api.removeContentBlock(blockToRemove, this.$root.store.accessToken).then(response => { |
||||||
|
remove(); |
||||||
|
}); |
||||||
|
} |
||||||
|
else { |
||||||
|
remove(); |
||||||
|
} |
||||||
|
}, |
||||||
|
onBlockAdded(blockData) { |
||||||
|
const content = this.content; |
||||||
|
content.push(blockData); |
||||||
|
this.$emit('update:content', content); |
||||||
|
}, |
||||||
|
}, |
||||||
|
|
||||||
|
components: { |
||||||
|
BlockAdd, |
||||||
|
'block-text': BlockText, |
||||||
|
'block-image': BlockImage, |
||||||
|
'block-image-text': BlockImageText, |
||||||
|
'block-images': BlockImages, |
||||||
|
'block-video': BlockVideo, |
||||||
|
'vue-draggable': Draggable, |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
Loading…
Reference in new issue