Merge remote-tracking branch 'origin/dev' into dev

remotes/origin/hasaccess
Vitaly Baev 8 years ago
commit c0b71f4dd4
  1. 29
      apps/course/migrations/0017_auto_20180130_0810.py
  2. 17
      apps/course/migrations/0018_auto_20180130_1248.py
  3. 12
      apps/course/models.py
  4. 27
      apps/course/templates/course/blocks/comment.html
  5. 29
      apps/course/templates/course/blocks/comments.html
  6. 23
      apps/course/templates/course/course.html
  7. 131
      apps/course/templates/course/lesson.html
  8. 63
      apps/course/views.py
  9. 4
      project/urls.py
  10. 24
      web/build/css/app.css
  11. 2
      web/build/css/app.css.map
  12. 17897
      web/build/js/app.js
  13. 4
      web/src/js/app.js
  14. 60
      web/src/js/modules/comments.js
  15. 19
      web/src/sass/_common.sass

@ -0,0 +1,29 @@
# Generated by Django 2.0.1 on 2018-01-30 08:10
from django.db import migrations
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
dependencies = [
('course', '0016_auto_20180129_1756'),
]
operations = [
migrations.RemoveField(
model_name='comment',
name='parent',
),
migrations.AddField(
model_name='coursecomment',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='children', to='course.CourseComment'),
),
migrations.AddField(
model_name='lessoncomment',
name='parent',
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='children', to='course.LessonComment'),
),
]

@ -0,0 +1,17 @@
# Generated by Django 2.0.1 on 2018-01-30 12:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('course', '0017_auto_20180130_0810'),
]
operations = [
migrations.AlterModelOptions(
name='comment',
options={'ordering': ('-created_at',)},
),
]

@ -121,7 +121,6 @@ class Material(models.Model):
class Comment(MPTTModel):
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.PROTECT)
author = models.ForeignKey(User, on_delete=models.PROTECT)
content = models.TextField('Текст комментария', default='')
@ -135,22 +134,27 @@ class Comment(MPTTModel):
def __str__(self):
return self.content
class Meta:
ordering = ('-created_at',)
class MPTTMeta:
order_insertion_by = ['created_at']
order_insertion_by = ['-created_at']
abstract = True
class CourseComment(Comment):
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.PROTECT)
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='comments')
class Meta:
class Meta(Comment.Meta):
verbose_name = 'Комментарий курса'
verbose_name_plural = 'Комментарии курсов'
class LessonComment(Comment):
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.PROTECT)
lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE, related_name='comments')
class Meta:
class Meta(Comment.Meta):
verbose_name = 'Комментарий урока'
verbose_name_plural = 'Комментарии уроков'

@ -0,0 +1,27 @@
{% load static %}
<div id="question__{{ node.id }}" class="questions__item {% if node.is_child_node %}questions__item_reply{% endif %}">
{% if node.author.photo %}
<div class="questions__ava ava">
<img class="ava__pic" src="{{ node.author.photo.url }}">
</div>
{% else %}
<div class="questions__ava ava">
<img class="ava__pic" src="{% static 'img/user.jpg' %}">
</div>
{% endif %}
<div class="questions__wrap">
<div class="questions__details">
<div class="questions__head">
<span class="questions__author">{{ node.author.get_full_name }}</span>
<span class="questions__date">{{ node.created_at_humanize }}</span>
</div>
<div class="questions__content">{{ node.content }}</div>
</div>
<div class="questions__foot">
{% if user.is_authenticated %}
<button class="questions__action question__reply-button" data-reply-id="{{ node.id }}">ОТВЕТИТЬ</button>
{% endif %}
</div>
</div>
</div>

@ -1,30 +1,5 @@
{% load static %}
{% load mptt_tags %}
{% recursetree course.comments.all %}
<!-- Node template -->
<div class="questions__item {% if node.is_child_node %}questions__item_reply{% endif %}">
{% if node.author.photo %}
<div class="questions__ava ava">
<img class="ava__pic" src="{{ node.author.photo.url }}">
</div>
{% else %}
<div class="questions__ava ava">
<img class="ava__pic" src="{% static 'img/user.jpg' %}">
</div>
{% endif %}
<div class="questions__wrap">
<div class="questions__details">
<div class="questions__head">
<span class="questions__author">{{ node.author.get_full_name }}</span>
<span class="questions__date">{{ node.created_at_humanize }}</span>
</div>
<div class="questions__content">{{ node.content }}</div>
</div>
<div class="questions__foot">
<button class="questions__action">ОТВЕТИТЬ</button>
</div>
</div>
</div>
<!-- End Node template -->
{% recursetree object.comments.all %}
{% include './comment.html' %}
{{ children }} {% endrecursetree %}

@ -195,14 +195,25 @@
<div class="title">Задавайте вопросы:</div>
<div class="questions">
<div class="questions__form">
<div class="questions__ava ava"><img class="ava__pic" src="{% static 'img/user.jpg' %}"></div>
<div class="questions__wrap">
<div class="questions__field"><textarea class="questions__textarea" placeholder="Спросите автора курса интересующие вас вопросы"></textarea></div><button class="questions__btn btn btn_light">ОТПРАВИТЬ</button></div>
</div>
<div class="questions__list">
{% include "./blocks/comments.html" with course=course %}
{% include "./blocks/comments.html" with object=course %}
</div>
<form class="questions__form" method="post" action="{% url 'coursecomment' course_id=course.id %}">
<input type="hidden" name="reply_id">
<div class="questions__ava ava"><img class="ava__pic" src="{% static 'img/user.jpg' %}"></div>
<div class="questions__wrap">
{% if user.is_authenticated %}
<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>
{% else %}
<div>Только зарегистрированные пользователи могут оставлять комментарии.</div>
{% endif %}
</div>
</form>
</div>
</div>
</div>

@ -0,0 +1,131 @@
{% extends "templates/lilcity/index.html" %}
{% load static %}
{% block title %}{{ lesson.title }} - {{ block.super }}{% endblock title %}
{% block content %}
<div class="section">
<div class="section__center center center_sm">
<div class="go">
<a class="go__item" href="{% url 'course' lesson.course.id %}">
<div class="go__arrow">
<svg class="icon icon-arrow-left">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-left"></use>
</svg>
</div>
<div class="go__title">Вернуться к&nbsp;списку уроков</div>
</a>
<a class="go__item" href="#">
<div class="go__title">Перейти к&nbsp;следующему уроку</div>
<div class="go__arrow">
<svg class="icon icon-arrow-right">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-right"></use>
</svg>
</div>
</a>
</div>
<div class="lesson">
<div class="lesson__subtitle subtitle">{{ lesson.title }}</div>
<div class="lesson__content">{{ lesson.short_description }}</div>
<a class="lesson__video video" href="#">
<img class="video__pic" src="{% static 'img/video-1.jpg' %}">
<svg class="icon icon-play">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-play"></use>
</svg>
</a>
</div>
</div>
</div>
<div class="section section_gradient">
<div class="section__center center center_sm">
<div class="title">Примеры техники</div>
<div class="examples">
<div class="examples__item">
<img class="examples__pic" src="{% static 'img/box.jpg' %}">
</div>
<div class="examples__item">
<img class="examples__pic" src="{% static 'img/box.jpg' %}">
</div>
<div class="examples__item">
<img class="examples__pic" src="{% static 'img/box.jpg' %}">
</div>
<div class="examples__item">
<img class="examples__pic" src="{% static 'img/box.jpg' %}">
</div>
<div class="examples__item">
<img class="examples__pic" src="{% static 'img/box.jpg' %}">
</div>
<div class="examples__item">
<img class="examples__pic" src="{% static 'img/box.jpg' %}">
</div>
<div class="examples__item">
<img class="examples__pic" src="{% static 'img/box.jpg' %}">
</div>
<div class="examples__item">
<img class="examples__pic" src="{% static 'img/box.jpg' %}">
</div>
<div class="examples__item">
<img class="examples__pic" src="{% static 'img/box.jpg' %}">
</div>
<div class="examples__item">
<img class="examples__pic" src="{% static 'img/box.jpg' %}">
</div>
<div class="examples__item">
<img class="examples__pic" src="{% static 'img/box.jpg' %}">
</div>
<div class="examples__item">
<img class="examples__pic" src="{% static 'img/box.jpg' %}">
</div>
</div>
</div>
</div>
<div class="section section_gray">
<div class="section__center center center_sm">
<div class="title">Задавайте вопросы:</div>
<div class="questions">
<div class="questions__form">
<div class="questions__ava ava">
<img class="ava__pic" src="{% static 'img/user.jpg' %}">
</div>
<div class="questions__wrap">
<div class="questions__field">
<textarea class="questions__textarea" placeholder="Спросите автора курса интересующие вас вопросы"></textarea>
</div>
<button class="questions__btn btn btn_light">ОТПРАВИТЬ</button>
</div>
</div>
<div class="questions__list">
{% include "./blocks/comments.html" with object=lesson %}
</div>
</div>
</div>
</div>
<div class="section">
<div class="section__center center center_sm">
<div class="share">
<div class="share__title">Поделиться уроком</div>
<div class="share__list">
<a class="share__item" href="#">
<svg class="icon icon-share-facebook">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-facebook"></use>
</svg>
</a>
<a class="share__item" href="#">
<svg class="icon icon-share-twitter">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-twitter"></use>
</svg>
</a>
<a class="share__item" href="#">
<svg class="icon icon-share-google">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-google"></use>
</svg>
</a>
<a class="share__item" href="#">
<svg class="icon icon-share-pinterest">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-pinterest"></use>
</svg>
</a>
</div>
</div>
</div>
</div>
{% endblock content %}

@ -1,10 +1,10 @@
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.template import loader
from django.views.generic import View, DetailView, ListView
from django.template import loader, Context, Template
from django.views.generic import View, CreateView, DetailView, ListView
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from .models import Course, Like
from .models import Course, Like, Lesson, CourseComment, LessonComment
from .filters import CourseFilter
@ -18,7 +18,7 @@ def likes(request, course_id):
return JsonResponse({
'success': False,
'errors': ['Course with id f{cource_id} not found']
})
}, status=400)
else:
course_user_likes = course.likes.filter(user=request.user)
if course_user_likes.exists():
@ -37,6 +37,55 @@ def likes(request, course_id):
})
@login_required
@csrf_exempt
@require_http_methods(['POST'])
def coursecomment(request, course_id):
try:
course = Course.objects.get(id=course_id)
except Course.DoesNotExist:
return JsonResponse({
'success': False,
'errors': ['Course with id f{cource_id} not found']
}, status=400)
else:
reply_to = request.POST.get('reply_id', 0)
comment = request.POST.get('comment', '')
if not comment:
return JsonResponse({
'success': False,
'errors': ['Comment can not be empty']
}, status=400)
if not int(reply_to):
coursecomment = CourseComment.objects.create(
author=request.user,
content=comment,
course=course,
)
else:
try:
_coursecomment = CourseComment.objects.get(id=reply_to)
except CourseComment.DoesNotExist:
return JsonResponse({
'success': False,
'errors': ['CourseComment with id f{reply_to} not found']
}, status=400)
else:
coursecomment = CourseComment.objects.create(
author=request.user,
content=comment,
course=course,
parent=_coursecomment,
)
ctx = {'node': coursecomment}
html = loader.render_to_string('course/blocks/comment.html', ctx)
return JsonResponse({
'success': True,
'comment': html,
})
class CourseView(DetailView):
model = Course
context_object_name = 'course'
@ -81,3 +130,9 @@ class CoursesView(ListView):
if self.request.is_ajax():
return 'course/course_items.html'
return 'course/courses.html'
class LessonView(DetailView):
model = Lesson
context_object_name = 'lesson'
template_name = 'course/lesson.html'

@ -18,7 +18,7 @@ from django.urls import path, include
from django.views.generic import TemplateView
from django.conf import settings
from apps.course.views import CoursesView, likes, CourseView
from apps.course.views import CoursesView, likes, coursecomment, CourseView, LessonView
urlpatterns = [
path('admin/', admin.site.urls),
@ -26,6 +26,8 @@ urlpatterns = [
path('courses/', CoursesView.as_view(), name='courses'),
path('course/<int:pk>/', CourseView.as_view(), name='course'),
path('course/<int:course_id>/like', likes, name='likes'),
path('course/<int:course_id>/comment', coursecomment, name='coursecomment'),
path('lesson/<int:pk>/', LessonView.as_view(), name='lesson'),
path('', TemplateView.as_view(template_name="templates/lilcity/main.html"), name='index'),
]

@ -1578,6 +1578,17 @@ a.btn {
max-height: 277px;
}
a.grey-link {
color: #A7A7A7;
border-bottom: 1px #ccc solid;
font-size: 14px;
}
a.grey-link:hover {
color: #000;
border-bottom: 1px #545454 solid;
}
.game {
position: relative;
padding: 70px 0 80px;
@ -3074,9 +3085,9 @@ a.btn {
}
.questions__form {
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 1px solid #E6E6E6;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #E6E6E6;
}
.questions__item:not(:last-child) {
@ -3087,6 +3098,11 @@ a.btn {
padding-left: 80px;
}
.questions__reply-info {
display: none;
margin-bottom: 10px;
}
.questions__item_reply .questions__details {
margin-bottom: 10px;
padding: 20px;
@ -3118,7 +3134,7 @@ a.btn {
.questions__textarea {
display: block;
width: 100%;
height: 44px;
height: 70px;
padding: 11px 15px;
border-radius: 2px;
font-size: 16px;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

@ -7,8 +7,6 @@ import "./modules/search";
import "./modules/toggle";
import "./modules/auth";
import "./modules/select";
// import "./modules/datepicker";
import "./modules/tabs";
import "./modules/popup";
import "./modules/courses";
import "./modules/comments";

@ -0,0 +1,60 @@
import $ from 'jquery';
$(document).ready(function () {
// Обработчик отправки комментария. Нам не важно, комментарий к курсу или к уроку - URL берется из action формы
$('form.questions__form').on('submit', function (e) {
e.preventDefault();
const submitButton = $(this).find('button.questions__action');
const replyToInput = $(this).find('input[name=reply_id]');
const replyToValue = replyToInput.val() ? parseInt(replyToInput.val()) : 0;
const commentTextarea = $(this).find('textarea.questions__textarea');
commentTextarea.attr('disabled', 'disabled');
submitButton.attr('disabled', 'disabled');
$.ajax($(this).attr('action'), {
method: 'post',
data: {
reply_id: replyToValue,
comment: commentTextarea.val(),
}
})
.done(function (data) {
console.log(data);
if (data.success === true) {
if (replyToValue > 0) {
$(`#question__${replyToValue}`).after(data.comment);
} else {
$('.questions__list').append(data.comment);
}
commentTextarea.val('');
}
})
.fail(function (xhr) {
})
.always(function () {
commentTextarea.removeAttr('disabled');
submitButton.removeAttr('disabled');
});
});
// Обработчик кнопки "Ответить"
$('.questions__list').on('click', 'button.question__reply-button', function () {
const replyId = $(this).attr('data-reply-id');
const form = $('form.questions__form');
form.find('input[name=reply_id]').val(replyId);
form.find('.questions__reply-anchor').attr('href', `#question__${replyId}`);
form.find('.questions__reply-info').show();
});
// Обработчик отмены комментирования в ответ на комментарий
$('.questions__reply-cancel').on('click', function (e) {
e.preventDefault();
const form = $('form.questions__form');
form.find('input[name=reply_id]').val(0);
form.find('.questions__reply-info').hide();
});
});

@ -1184,6 +1184,14 @@ a.btn
+m
max-height: 185px
a.grey-link
color: #A7A7A7
border-bottom: 1px #ccc solid
font-size: 14px
&:hover
color: #000
border-bottom: 1px #545454 solid
.game
position: relative
padding: 70px 0 80px
@ -2395,9 +2403,9 @@ a.btn
&__item
display: flex
&__form
margin-bottom: 40px
padding-bottom: 20px
border-bottom: 1px solid $border
margin-top: 40px
padding-top: 20px
border-top: 1px solid $border
&__item
&:not(:last-child)
margin-bottom: 25px
@ -2405,6 +2413,9 @@ a.btn
padding-left: 80px
+m
padding: 0
&__reply-info
display: none
margin-bottom: 10px
&__item_reply &__details
margin-bottom: 10px
padding: 20px
@ -2432,7 +2443,7 @@ a.btn
&__textarea
display: block
width: 100%
height: 44px
height: 70px
padding: 11px 15px
border-radius: 2px
font-size: 16px

Loading…
Cancel
Save