diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index 4382aae4..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, @@ -327,6 +327,7 @@ class CommentSerializer(serializers.ModelSerializer): 'parent', 'deactivated_at', 'created_at', + 'created_at_humanize', 'update_at', ) @@ -342,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): @@ -374,3 +377,67 @@ class LessonCommentSerializer(serializers.ModelSerializer): read_only_fields = CommentSerializer.Meta.read_only_fields + ( '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) diff --git a/api/v1/serializers/payment.py b/api/v1/serializers/payment.py index 6268a63d..b388be21 100644 --- a/api/v1/serializers/payment.py +++ b/api/v1/serializers/payment.py @@ -46,8 +46,29 @@ class AuthorBalanceCreateSerializer(serializers.ModelSerializer): return AuthorBalanceSerializer(instance, context=self.context).to_representation(instance) +class PaymentSerializer(serializers.ModelSerializer): + user = UserSerializer() + + class Meta: + model = Payment + fields = BASE_PAYMENT_FIELDS + read_only_fields = ( + 'id', + 'user', + 'created_at', + 'update_at', + ) + + def to_representation(self, instance): + if isinstance(instance, CoursePayment): + return CoursePaymentSerializer(instance, context=self.context).to_representation(instance) + elif isinstance(instance, SchoolPayment): + return SchoolPaymentSerializer(instance, context=self.context).to_representation(instance) + + class AuthorBalanceSerializer(serializers.ModelSerializer): author = UserSerializer() + payment = serializers.SerializerMethodField() class Meta: model = AuthorBalance @@ -70,6 +91,24 @@ class AuthorBalanceSerializer(serializers.ModelSerializer): 'payment', ) + def get_payment(self, instance): + try: + p = instance.payment + except Exception: + return None + data = { + 'id': p.id, + 'created_at': p.created_at, + 'amount': p.amount, + 'data': p.data, + } + if isinstance(instance.payment, CoursePayment): + data['course'] = { + 'id': p.course.id, + 'title': p.course.title, + } + return data + class PaymentSerializer(serializers.ModelSerializer): user = UserSerializer() @@ -80,6 +119,7 @@ class PaymentSerializer(serializers.ModelSerializer): read_only_fields = ( 'id', 'user', + 'data', 'created_at', 'update_at', ) @@ -91,6 +131,20 @@ class PaymentSerializer(serializers.ModelSerializer): return SchoolPaymentSerializer(instance, context=self.context).to_representation(instance) +class CoursePaymentCreateSerializer(serializers.ModelSerializer): + + class Meta: + model = CoursePayment + fields = BASE_PAYMENT_FIELDS + ('course',) + read_only_fields = ( + 'id', + 'user', + 'course', + 'created_at', + 'update_at', + ) + + class CoursePaymentSerializer(serializers.ModelSerializer): user = UserSerializer() course = CourseSerializer() @@ -107,6 +161,24 @@ class CoursePaymentSerializer(serializers.ModelSerializer): ) +class SchoolPaymentCreateSerializer(serializers.ModelSerializer): + + class Meta: + model = SchoolPayment + fields = BASE_PAYMENT_FIELDS + ( + 'weekdays', + 'date_start', + 'date_end', + ) + read_only_fields = ( + 'id', + 'user', + 'course', + 'created_at', + 'update_at', + ) + + class SchoolPaymentSerializer(serializers.ModelSerializer): user = UserSerializer() diff --git a/api/v1/urls.py b/api/v1/urls.py index 51610cb6..0eafbe10 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, ContestViewSet, ContestWorkViewSet) 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 d5dae90d..c2bc1f07 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -11,10 +11,11 @@ from .serializers.course import ( CategorySerializer, LikeSerializer, CourseSerializer, CourseCreateSerializer, CourseBulkChangeCategorySerializer, - CommentSerializer, + CommentSerializer, CommentCreateSerializer, MaterialSerializer, MaterialCreateSerializer, LessonSerializer, LessonCreateSerializer, - LikeCreateSerializer) + LikeCreateSerializer, CourseCommentSerializer, LessonCommentSerializer, + LiveLessonCommentSerializer) from .serializers.content import ( BanerSerializer, ImageSerializer, ImageCreateSerializer, @@ -34,7 +35,7 @@ from .serializers.payment import ( AuthorBalanceSerializer, AuthorBalanceCreateSerializer, PaymentSerializer, CoursePaymentSerializer, SchoolPaymentSerializer, -) + CoursePaymentCreateSerializer, SchoolPaymentCreateSerializer) from .serializers.user import ( AuthorRequestSerializer, UserSerializer, UserPhotoSerializer, @@ -54,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, @@ -66,6 +67,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() @@ -429,15 +431,15 @@ class ConfigViewSet(generics.RetrieveUpdateAPIView): class CommentViewSet(ExtendedModelViewSet): - queryset = Comment.objects.filter(level=0) + queryset = Comment.objects.all() serializer_class = CommentSerializer - permission_classes = (IsAdmin,) + 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': @@ -446,6 +448,78 @@ class CommentViewSet(ExtendedModelViewSet): 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): queryset = AuthorRequest.objects.all() serializer_class = AuthorRequestSerializer @@ -453,7 +527,7 @@ class AuthorRequestViewSet(ExtendedModelViewSet): filter_fields = ('status',) -class PaymentViewSet(ExtendedModelViewSet): +class PaymentViewSet(viewsets.ModelViewSet): queryset = Payment.objects.all() serializer_class = PaymentSerializer permission_classes = (IsAdmin,) @@ -465,6 +539,16 @@ class PaymentViewSet(ExtendedModelViewSet): ) search_fields = ('user__email', 'user__first_name', 'user__last_name',) + def get_serializer(self, instance, *args, **kwargs): + serializer_class = self.get_serializer_class() + if 'update' in self.action: + if isinstance(instance, CoursePayment): + serializer_class = CoursePaymentCreateSerializer + elif isinstance(instance, SchoolPayment): + serializer_class = SchoolPaymentCreateSerializer + kwargs['context'] = self.get_serializer_context() + return serializer_class(instance, *args, **kwargs) + def get_queryset(self): queryset = self.queryset course = self.request.query_params.get('course') 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/apps/course/templates/course/lesson.html b/apps/course/templates/course/lesson.html index 9c6b1583..d36219f7 100644 --- a/apps/course/templates/course/lesson.html +++ b/apps/course/templates/course/lesson.html @@ -2,6 +2,9 @@ {% load static %} {% block title %}{{ lesson.title }} - {{ block.super }}{% endblock title %} +{% block twurl %}{{ request.build_absolute_uri }}{% endblock twurl %} +{% block ogtitle %}{{ lesson.title }} - {{ block.super }}{% endblock ogtitle %} +{% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %} {% block ogimage %}http://{{request.META.HTTP_HOST}}{% if lesson.course.cover %}{{ lesson.course.cover.image.url }}{% else %}{% static 'img/og_courses.jpg' %}{% endif %}{% endblock ogimage %} {% block content %}
@@ -114,37 +117,8 @@
Задавайте вопросы:
-
- {% if request.user.is_authenticated %} -
- -
- -
-
- -
- -
- -
-
- {% else %} -
Только зарегистрированные пользователи могут оставлять комментарии.
- {% endif %} -
- {% include "templates/blocks/comments.html" with object=lesson %} -
+
+
diff --git a/apps/school/models.py b/apps/school/models.py index a059172f..4eb71350 100644 --- a/apps/school/models.py +++ b/apps/school/models.py @@ -14,6 +14,7 @@ from apps.payment import models as payment_models class SchoolSchedule(models.Model): + WEEKDAY_SHORT_NAMES = ('пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'вс') WEEKDAY_CHOICES = ( (1, 'понедельник'), (2, 'вторник'), diff --git a/apps/school/templates/blocks/schedule_item.html b/apps/school/templates/blocks/schedule_item.html index 7e860a4e..272ae2db 100644 --- a/apps/school/templates/blocks/schedule_item.html +++ b/apps/school/templates/blocks/schedule_item.html @@ -4,7 +4,7 @@
{{ school_schedule }} - {% if request.user_agent.is_mobile and school_schedule.trial_lesson %} + {% if not is_purchased and request.user_agent.is_mobile and school_schedule.trial_lesson %} Пробный урок {% endif %}
@@ -20,7 +20,7 @@ {% else %} {% include './day_pay_btn.html' %} {% endif %} - {% if not request.user_agent.is_mobile and school_schedule.trial_lesson %} + {% if not is_purchased and not request.user_agent.is_mobile and school_schedule.trial_lesson %} Пробный урок {% endif %}
diff --git a/apps/school/templates/school/livelesson_detail.html b/apps/school/templates/school/livelesson_detail.html index cfa77ba3..96db3a06 100644 --- a/apps/school/templates/school/livelesson_detail.html +++ b/apps/school/templates/school/livelesson_detail.html @@ -2,6 +2,13 @@ {% load static %} {% block title %}{{ livelesson.title }} - {{ block.super }}{% endblock title %} +{% block twurl %}{{ request.build_absolute_uri }}{% endblock twurl %} +{% block ogtitle %}{{ livelesson.title }} - {{ block.super }}{% endblock ogtitle %} +{% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %} +{% if livelesson.cover and livelesson.cover.image %} +{% block ogimage %}http://{{request.META.HTTP_HOST}}{{ livelesson.cover.image.url }}{% endblock ogimage %} +{% endif %} + {% block content %}
@@ -14,7 +21,8 @@ mozallowfullscreen allowfullscreen> Если видео не загрузилось, - уменьшите качество видео или обновите страницу - + + {% else %} {% if livelesson.cover %} diff --git a/apps/school/views.py b/apps/school/views.py index 3816c0eb..1728d443 100644 --- a/apps/school/views.py +++ b/apps/school/views.py @@ -140,21 +140,9 @@ class SchoolView(TemplateView): pass school_schedules_dict = {ss.weekday: ss for ss in school_schedules} school_schedules_dict[0] = school_schedules_dict.get(7) - all_schedules_purchased = [] + live_lessons = None + live_lessons_exists = False if self.request.user.is_authenticated: - all_schedules_purchased = SchoolPayment.objects.filter( - user=self.request.user, - status__in=[ - Pingback.PINGBACK_TYPE_REGULAR, - Pingback.PINGBACK_TYPE_GOODWILL, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, - ], - date_start__range=[month_start, date_now], - ).annotate( - joined_weekdays=Func(F('weekdays'), function='unnest',) - ).distinct().values_list('joined_weekdays', flat=True) - all_schedules_purchased = list(map(lambda x: 1 if x == 7 else x+1, all_schedules_purchased)) - school_payment = SchoolPayment.objects.filter( user=self.request.user, status__in=[ @@ -173,18 +161,32 @@ class SchoolView(TemplateView): else: school_payment_exists = False school_schedules_purchased = [] - if all_schedules_purchased and is_previous: - live_lessons = LiveLesson.objects.filter( - date__range=[yesterday - timedelta(days=7), yesterday], - deactivated_at__isnull=True, - date__week_day__in=all_schedules_purchased, - ).order_by('-date') + if is_previous: + prev_range = [yesterday - timedelta(days=7), yesterday] + live_lessons = [] + # берем все подписки, которые были в периоде + for sp in SchoolPayment.objects.filter( + date_start__lte=prev_range[1], + date_end__gte=prev_range[0], + user=self.request.user, + status__in=[ + Pingback.PINGBACK_TYPE_REGULAR, + Pingback.PINGBACK_TYPE_GOODWILL, + Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, + ], + ): + # берем все уроки в оплаченном промежутке + date_range = [max(sp.date_start, prev_range[0]), min(sp.date_end, prev_range[1])] + live_lessons += LiveLesson.objects.filter( + date__range=date_range, + deactivated_at__isnull=True, + date__week_day__in=list(map(lambda x: 1 if x == 7 else x+1, sp.weekdays)), + ).values_list('id', flat=True) + live_lessons = LiveLesson.objects.filter(id__in=set(live_lessons)).order_by('-date') for ll in live_lessons: ll.school_schedule = school_schedules_dict.get(ll.date.isoweekday()) live_lessons_exists = live_lessons.exists() - else: - live_lessons = None - live_lessons_exists = False + live_lessons = live_lessons or None context.update({ 'online': online, 'live_lessons': live_lessons, diff --git a/apps/user/models.py b/apps/user/models.py index 0df7cd01..47008960 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -92,7 +92,7 @@ class User(AbstractUser): @property def balance(self): - aggregate = self.balances.filter( + income = self.balances.filter( type=0, payment__isnull=False, payment__status__isnull=False @@ -100,9 +100,13 @@ class User(AbstractUser): models.Sum('amount'), models.Sum('commission'), ) - amount = aggregate.get('amount__sum') or 0 - commission = aggregate.get('commission__sum') or 0 - return amount - commission + income_amount = income.get('amount__sum') or 0 + income_commission = income.get('commission__sum') or 0 + + payout = self.balances.filter(type=1, status=1).aggregate(models.Sum('amount')) + payout_amount = payout.get('amount__sum') or 0 + + return income_amount - income_commission - payout_amount @receiver(post_save, sender=User) diff --git a/apps/user/templates/user/profile.html b/apps/user/templates/user/profile.html index e8a81d33..9ad4d552 100644 --- a/apps/user/templates/user/profile.html +++ b/apps/user/templates/user/profile.html @@ -64,10 +64,7 @@
- - + {% if is_author %}
- {% if is_purchased_future %} -
-
-
Ваша подписка начинается {{school_purchased_future.date_start}}
-
-
- {% else %} - {% if is_purchased %} - {% include "blocks/schedule_purchased.html" %} - {% else %} -
-
-
Вы не подписаны на онлайн-школу!
-
- Купить подписку + -
-
-
+ {% endif %} {% if paid.exists %} {% include "course/course_items.html" with course_items=paid %} - {% else %} + {% endif %} + {% if not is_school_purchased and not paid.exists %}
-
Нет приобретённых курсов!
+
Вы пока ничего не приобрели...
@@ -125,7 +111,7 @@
{% if is_author %}
-
+
{% if published.exists %} {% include "course/course_items.html" with course_items=published %} diff --git a/apps/user/views.py b/apps/user/views.py index 41a18cb0..103ff126 100644 --- a/apps/user/views.py +++ b/apps/user/views.py @@ -16,7 +16,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.hashers import check_password, make_password from django.http import Http404 -from django.db.models import F, Func +from django.db.models import F, Func, Sum, Min, Max from django.urls import reverse_lazy from django.utils.decorators import method_decorator from django.utils.timezone import now @@ -55,11 +55,11 @@ class ProfileView(TemplateView): def get_context_data(self, object): context = super().get_context_data() context['user'] = self.request.user - context['is_author'] = self.request.user.role == User.AUTHOR_ROLE context['published'] = Course.objects.filter( author=self.object, ) + context['is_author'] = context['published'] or self.request.user.role == User.AUTHOR_ROLE context['paid'] = Course.objects.filter( payments__in=CoursePayment.objects.filter( user=self.object, @@ -80,22 +80,18 @@ class ProfileView(TemplateView): Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, ], ) - school_schedules_purchased = school_payment.annotate( - joined_weekdays=Func(F('weekdays'), function='unnest',) - ).values_list('joined_weekdays', flat=True).distinct() - context['school_schedules_purchased'] = school_schedules_purchased - context['school_payment'] = school_payment - context['is_purchased'] = school_payment.exists() - context['profile'] = True - if school_payment.exists() and school_payment.last().date_end: - context['subscription_ends'] = school_payment.last().date_end - context['school_schedules'] = SchoolSchedule.objects.filter( - weekday__in=school_schedules_purchased if school_payment.exists() else [], - ).all() - context['all_school_schedules'] = SchoolSchedule.objects.all() + context['is_school_purchased'] = school_payment.exists() + if context['is_school_purchased']: + school_schedules_purchased = school_payment.annotate( + joined_weekdays=Func(F('weekdays'), function='unnest',) + ).values_list('joined_weekdays', flat=True).distinct() + aggregated = school_payment.aggregate(Sum('amount'), Min('date_start'), Max('date_end'),) + context['school_purchased_weekdays'] = '-'.join(map(lambda wd: SchoolSchedule.WEEKDAY_SHORT_NAMES[wd-1], + set(sorted(school_schedules_purchased)))) + context['school_purchased_price'] = aggregated.get('amount__sum') or 0 + context['school_purchased_dates'] = [aggregated.get('date_start__min'), aggregated.get('date_end__max')] - context['is_purchased_future'] = False - context['school_purchased_future'] = False + context['profile'] = True return context @@ -107,7 +103,7 @@ class UserView(DetailView): def get_context_data(self, object): context = super().get_context_data() context['published'] = Course.objects.filter( - author=self.object, + author=self.object, status=Course.PUBLISHED, ) return context 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 b9232bcd..6d6a1770 100644 --- a/project/settings.py +++ b/project/settings.py @@ -197,6 +197,13 @@ TWILIO_FROM_PHONE = os.getenv('TWILIO_FROM_PHONE', '+37128914409') ACTIVE_LINK_STRICT = True +# PUSHER settings + +PUSHER_APP_ID = os.getenv('PUSHER_APP_ID', '') +PUSHER_KEY = os.getenv('PUSHER_KEY', '') +PUSHER_SECRET = os.getenv('PUSHER_SECRET', '') +PUSHER_CLUSTER = 'eu' + # DRF settings REST_FRAMEWORK = { diff --git a/project/templates/blocks/lil_store_js.html b/project/templates/blocks/lil_store_js.html index 25600834..21d19bea 100644 --- a/project/templates/blocks/lil_store_js.html +++ b/project/templates/blocks/lil_store_js.html @@ -1,11 +1,15 @@ {% load static %} +{% load setting from settings %} + diff --git a/web/src/components/CommentForm.vue b/web/src/components/CommentForm.vue new file mode 100644 index 00000000..a7c9fd29 --- /dev/null +++ b/web/src/components/CommentForm.vue @@ -0,0 +1,53 @@ + + + + diff --git a/web/src/components/Comments.vue b/web/src/components/Comments.vue new file mode 100644 index 00000000..ad7e88d1 --- /dev/null +++ b/web/src/components/Comments.vue @@ -0,0 +1,134 @@ + + + + diff --git a/web/src/js/app.js b/web/src/js/app.js index 1106e020..f72d6767 100644 --- a/web/src/js/app.js +++ b/web/src/js/app.js @@ -15,6 +15,7 @@ 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"; @@ -25,6 +26,7 @@ import "../sass/app.sass"; import Vue from 'vue'; import Vuelidate from 'vuelidate'; import VueAutosize from '../components/directives/autosize' +import Comments from '../components/Comments'; import UploadContestWork from '../components/UploadContestWork.vue'; import ContestWorks from '../components/ContestWorks.vue'; import Likes from '../components/blocks/Likes.vue'; @@ -41,6 +43,7 @@ const components = { UploadContestWork, ContestWorks, Likes, + Comments, }; Object.assign(components, window.LIL_STORE.components); diff --git a/web/src/js/modules/api.js b/web/src/js/modules/api.js index 9d28c6e2..e486e604 100644 --- a/web/src/js/modules/api.js +++ b/web/src/js/modules/api.js @@ -384,5 +384,34 @@ export const api = { '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}`, + } + }); } }; 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/src/sass/_common.sass b/web/src/sass/_common.sass index c959b913..a1e6a5bb 100755 --- a/web/src/sass/_common.sass +++ b/web/src/sass/_common.sass @@ -1710,6 +1710,8 @@ a.grey-link color: $cl +t line-height: 1.33 + &__theme + text-transform: uppercase &__user margin-top: 20px &_two &__item @@ -2809,16 +2811,23 @@ a.grey-link &__item display: flex &__form - margin-bottom: 40px + position: relative; + margin-top: 20px padding-bottom: 20px border-bottom: 1px solid $border + &__form-loader + display: none; + &__form-foot + text-align: center; &__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 @@ -2857,9 +2866,10 @@ a.grey-link +m height: 64px &__btn - display: block - margin: 0 auto + margin: 0 15px; border-radius: 20px + +m + margin-left: 0 &__details margin-bottom: 5px &__head, @@ -2878,11 +2888,70 @@ a.grey-link &__author margin-right: 15px &__date + font-size: 10px display: inline-block &__foot + height: 20px text-align: right &__action 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 + 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 &__title @@ -3664,6 +3733,11 @@ a.grey-link +m display: none +.mobile-show + display: none + +m + display: block + .school display: flex position: relative