diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index 4382aae4..551830ad 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, @@ -137,6 +137,7 @@ class CourseCreateSerializer(DispatchContentMixin, 'from_author', 'cover', 'price', + 'age', 'is_infinite', 'deferred_start_at', 'category', @@ -288,6 +289,7 @@ class CourseSerializer(serializers.ModelSerializer): 'from_author', 'cover', 'price', + 'age', 'is_infinite', 'deferred_start_at', 'category', @@ -327,6 +329,7 @@ class CommentSerializer(serializers.ModelSerializer): 'parent', 'deactivated_at', 'created_at', + 'created_at_humanize', 'update_at', ) @@ -342,6 +345,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 +379,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/mixins.py b/api/v1/serializers/mixins.py index 40584503..50e7d19d 100644 --- a/api/v1/serializers/mixins.py +++ b/api/v1/serializers/mixins.py @@ -32,35 +32,23 @@ class DispatchContentMixin(object): if 'id' in cdata and cdata['id']: t = Text.objects.get(id=cdata.pop('id')) serializer = TextCreateSerializer(t, data=cdata) - if serializer.is_valid(): - serializer.save() else: serializer = TextCreateSerializer(data=cdata) - if serializer.is_valid(): - serializer.save() + if serializer.is_valid(): + serializer.save() elif ctype == 'image': if 'id' in cdata and cdata['id']: image = Image.objects.get(id=cdata.pop('id')) serializer = ImageCreateSerializer(image, data=cdata) - if serializer.is_valid(): - image = serializer.save() - else: - continue - try: - image_object = ImageObject.objects.get(id=cdata['img']) - except ImageObject.DoesNotExist: - pass - else: - image.img = image_object - image.save() else: serializer = ImageCreateSerializer(data=cdata) - if serializer.is_valid(): - image = serializer.save() - else: - continue + if serializer.is_valid(): + image = serializer.save() + else: + continue + if 'img' in cdata: try: - image_object = ImageObject.objects.get(id=cdata['img']) + image_object = ImageObject.objects.get(id=cdata.get('img')) except ImageObject.DoesNotExist: pass else: @@ -71,25 +59,15 @@ class DispatchContentMixin(object): if 'id' in cdata and cdata['id']: it = ImageText.objects.get(id=cdata.pop('id')) serializer = ImageTextCreateSerializer(it, data=cdata) - if serializer.is_valid(): - it = serializer.save() - else: - continue - try: - image_object = ImageObject.objects.get(id=cdata['img']) - except ImageObject.DoesNotExist: - pass - else: - it.img = image_object - it.save() else: serializer = ImageTextCreateSerializer(data=cdata) - if serializer.is_valid(): - it = serializer.save() - else: - continue + if serializer.is_valid(): + it = serializer.save() + else: + continue + if 'img' in cdata: try: - image_object = ImageObject.objects.get(id=cdata['img']) + image_object = ImageObject.objects.get(id=cdata.get('img')) except ImageObject.DoesNotExist: pass else: @@ -100,52 +78,36 @@ class DispatchContentMixin(object): if 'id' in cdata and cdata['id']: v = Video.objects.get(id=cdata.pop('id')) serializer = VideoCreateSerializer(v, data=cdata) - if serializer.is_valid(): - serializer.save() else: serializer = VideoCreateSerializer(data=cdata) - if serializer.is_valid(): - serializer.save() + if serializer.is_valid(): + serializer.save() elif ctype == 'images': if 'id' in cdata and cdata['id']: g = Gallery.objects.get(id=cdata['id']) g.position = cdata['position'] g.title = cdata['title'] g.uuid = cdata['uuid'] - setattr(g, obj_type, obj) - g.save() - if 'images' in cdata: - for image in cdata['images']: - if 'img' in image and image['img']: - if 'id' in image and image['id']: - gi = GalleryImage.objects.get(id=image['id']) - gi.img = ImageObject.objects.get(id=image['img']) - gi.save() - else: - gi = GalleryImage.objects.create( - gallery=g, - img=ImageObject.objects.get(id=image['img']) - ) else: g = Gallery( position=cdata['position'], title=cdata['title'], uuid=cdata['uuid'], ) - setattr(g, obj_type, obj) - g.save() - if 'images' in cdata: - for image in cdata['images']: - if 'img' in image and image['img']: - if 'id' in image and image['id']: - gi = GalleryImage.objects.get(id=image['id']) - gi.img = ImageObject.objects.get(id=image['img']) - gi.save() - else: - gi = GalleryImage.objects.create( - gallery=g, - img=ImageObject.objects.get(id=image['img']) - ) + setattr(g, obj_type, obj) + g.save() + if 'images' in cdata: + for image in cdata['images']: + if 'img' in image and image['img']: + if 'id' in image and image['id']: + gi = GalleryImage.objects.get(id=image['id']) + gi.img = ImageObject.objects.get(id=image['img']) + gi.save() + else: + gi = GalleryImage.objects.create( + gallery=g, + img=ImageObject.objects.get(id=image['img']) + ) class DispatchMaterialMixin(object): diff --git a/api/v1/serializers/payment.py b/api/v1/serializers/payment.py index 0d4aec8b..b388be21 100644 --- a/api/v1/serializers/payment.py +++ b/api/v1/serializers/payment.py @@ -68,7 +68,6 @@ class PaymentSerializer(serializers.ModelSerializer): class AuthorBalanceSerializer(serializers.ModelSerializer): author = UserSerializer() - # payment = PaymentSerializer() payment = serializers.SerializerMethodField() class Meta: @@ -111,6 +110,41 @@ class AuthorBalanceSerializer(serializers.ModelSerializer): return data +class PaymentSerializer(serializers.ModelSerializer): + user = UserSerializer() + + class Meta: + model = Payment + fields = BASE_PAYMENT_FIELDS + read_only_fields = ( + 'id', + 'user', + 'data', + '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 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() @@ -127,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..a01f9d6d 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -1,7 +1,9 @@ +from datetime import datetime + from django.contrib.auth import get_user_model from rest_framework import status, views, viewsets, generics -from rest_framework.decorators import detail_route, list_route, action +from rest_framework.decorators import detail_route, list_route, action, permission_classes, authentication_classes from rest_framework.response import Response from . import ExtendedModelViewSet @@ -11,10 +13,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 +37,7 @@ from .serializers.payment import ( AuthorBalanceSerializer, AuthorBalanceCreateSerializer, PaymentSerializer, CoursePaymentSerializer, SchoolPaymentSerializer, -) + CoursePaymentCreateSerializer, SchoolPaymentCreateSerializer) from .serializers.user import ( AuthorRequestSerializer, UserSerializer, UserPhotoSerializer, @@ -54,7 +57,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 +69,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 +433,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 +450,79 @@ class CommentViewSet(ExtendedModelViewSet): return queryset +class ObjectCommentsViewSet(ExtendedModelViewSet): + queryset = Comment.objects.all() + serializer_class = CommentCreateSerializer + 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) + + @permission_classes((IsAuthorObjectOrAdmin,)) + 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() + + @authentication_classes([]) + 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 +530,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 +542,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') @@ -481,10 +568,12 @@ class PaymentViewSet(ExtendedModelViewSet): user = request.query_params.get('user') course = request.query_params.get('course') weekdays = request.query_params.getlist('weekdays[]') + date_start = request.query_params.get('date_start') user = user and User.objects.get(pk=user) course = course and Course.objects.get(pk=course) + date_start = date_start and datetime.strptime(date_start, '%Y-%m-%d') - return Response(Payment.calc_amount(user=user, course=course, weekdays=weekdays)) + return Response(Payment.calc_amount(user=user, course=course, date_start=date_start, weekdays=weekdays)) class ContestViewSet(ExtendedModelViewSet): diff --git a/apps/content/templates/content/blocks/video.html b/apps/content/templates/content/blocks/video.html index 791920b1..8c707388 100644 --- a/apps/content/templates/content/blocks/video.html +++ b/apps/content/templates/content/blocks/video.html @@ -15,29 +15,3 @@ - - diff --git a/apps/course/filters.py b/apps/course/filters.py index 6e4e873e..cfecd0a0 100644 --- a/apps/course/filters.py +++ b/apps/course/filters.py @@ -5,7 +5,8 @@ from .models import Course class CourseFilter(django_filters.FilterSet): category = django_filters.CharFilter(field_name='category') + age = django_filters.ChoiceFilter(field_name='age', choices=Course.AGE_CHOICES) class Meta: model = Course - fields = ['category'] + fields = ['category', 'age'] diff --git a/apps/course/migrations/0044_course_age.py b/apps/course/migrations/0044_course_age.py new file mode 100644 index 00000000..a45fd5eb --- /dev/null +++ b/apps/course/migrations/0044_course_age.py @@ -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), + ), + ] 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..c7927600 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 @@ -49,6 +50,23 @@ class Course(BaseModel, DeactivatedMixin): (ARCHIVED, 'Archived'), (DENIED, 'Denied') ) + AGE_LT5 = 1 + AGE_57 = 2 + AGE_79 = 3 + AGE_912 = 4 + AGE_1215 = 5 + AGE_1518 = 6 + AGE_GT18 = 7 + AGE_CHOICES = ( + (0, 'Любой возраст'), + (AGE_LT5, 'до 5'), + (AGE_57, '5-7'), + (AGE_79, '7-9'), + (AGE_912, '9-12'), + (AGE_1215, '12-15'), + (AGE_1518, '15-18'), + (AGE_GT18, 'от 18'), + ) slug = models.SlugField( allow_unicode=True, null=True, blank=True, max_length=100, unique=True, db_index=True, @@ -72,6 +90,7 @@ class Course(BaseModel, DeactivatedMixin): 'Цена курса', help_text='Если цена не выставлена, то курс бесплатный', max_digits=10, decimal_places=2, null=True, blank=True ) + age = models.SmallIntegerField(choices=AGE_CHOICES, default=0) is_infinite = models.BooleanField(default=False) deferred_start_at = models.DateTimeField( 'Отложенный запуск курса', help_text='Заполнить если курс отложенный', @@ -240,6 +259,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 +306,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/_items.html b/apps/course/templates/course/_items.html index b4356f2b..f27bd326 100644 --- a/apps/course/templates/course/_items.html +++ b/apps/course/templates/course/_items.html @@ -54,7 +54,7 @@ {% endif %} {{ course.title }} -
{{ course.short_description }} +
{{ course.short_description | safe | linebreaks | truncatechars_html:300 }}
diff --git a/apps/course/templates/course/course.html b/apps/course/templates/course/course.html index c3a35c4f..2502b9a0 100644 --- a/apps/course/templates/course/course.html +++ b/apps/course/templates/course/course.html @@ -4,14 +4,14 @@ {% load rupluralize from plural %} {% block title %}{{ course.title }} - {{ block.super }}{% endblock title %} {% comment %} seo tags {% endcomment %} -{% block description %}{{ course.short_description }}{% endblock description%} +{% block description %}{{ course.short_description | striptags }}{% endblock description%} {% block twurl %}{{ request.build_absolute_uri }}{% endblock twurl %} {% block ogtitle %}{{ course.title }} - {{ block.super }}{% endblock ogtitle %} {% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %} {% if course.cover and course.cover.image %} {% block ogimage %}http://{{request.META.HTTP_HOST}}{% if course.cover %}{{ course.cover.image.url }}{% else %}{% static 'img/og_courses.jpg' %}{% endif %}{% endblock ogimage %} {% endif %} -{% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %} +{% block ogdescription %}{{ course.short_description | striptags }}{% endblock ogdescription %} {% block content %}
@@ -25,6 +25,9 @@
Вернуться
+ {% if has_full_access %} + Редактировать + {% endif %} {% if course.author != request.user and not paid and course.price %}
{{ course.title }}
-
{{ course.short_description | linebreaks }}
+
{{ course.short_description | safe | linebreaks }}
{% if course.author.photo %} @@ -270,7 +273,7 @@
{{ course.title }}
-
{{ course.short_description | linebreaks }}
+
{{ course.short_description | safe | linebreaks }}
{% if course.author.photo %} @@ -340,38 +343,7 @@
Задавайте вопросы:
-
- {% if request.user.is_authenticated %} -
- -
- -
-
-
- {% else %} -
Только зарегистрированные пользователи могут оставлять комментарии.
- {% endif %} -
- {% include "templates/blocks/comments.html" with object=course %} -
-
+
diff --git a/apps/course/templates/course/course_only_lessons.html b/apps/course/templates/course/course_only_lessons.html index 9efe873c..0e6e601d 100644 --- a/apps/course/templates/course/course_only_lessons.html +++ b/apps/course/templates/course/course_only_lessons.html @@ -4,14 +4,14 @@ {% load rupluralize from plural %} {% block title %}{{ course.title }} - {{ block.super }}{% endblock title %} {% comment %} seo tags {% endcomment %} -{% block description %}{{ course.short_description }}{% endblock description%} +{% block description %}{{ course.short_description | striptags }}{% endblock description%} {% block twurl %}{{ request.build_absolute_uri }}{% endblock twurl %} {% block ogtitle %}{{ course.title }} - {{ block.super }}{% endblock ogtitle %} {% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %} {% if course.cover %} {% block ogimage %}http://{{request.META.HTTP_HOST}}{% if course.cover %}{{ course.cover.image.url }}{% else %}{% static 'img/og_courses.jpg' %}{% endif %}{% endblock ogimage %} {% endif %} -{% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %} +{% block ogdescription %}{{ course.short_description | striptags }}{% endblock ogdescription %} {% block content %}
@@ -25,6 +25,9 @@
Вернуться
+ {% if has_full_access %} + Редактировать + {% endif %} {% if not paid and course.price and not has_full_access %}
{{ course.title }}
-
{{ course.short_description }}
+
{{ course.short_description | safe | linebreaks }}
{% if course.author.photo %} @@ -174,27 +177,21 @@
Содержание курса
@@ -204,31 +201,7 @@
Задавайте вопросы:
-
- {% if user.is_authenticated %} -
- -
- -
-
- -
- -
- -
-
- {% else %} -
Только зарегистрированные пользователи могут оставлять комментарии.
- {% endif %} -
- {% include "templates/blocks/comments.html" with object=course %} -
-
+
diff --git a/apps/course/templates/course/courses.html b/apps/course/templates/course/courses.html index 8f6f57d2..f56279f6 100644 --- a/apps/course/templates/course/courses.html +++ b/apps/course/templates/course/courses.html @@ -23,15 +23,32 @@
Категории
-
-
Все курсы
+
+
Все категории
{% category_items category %}
+
+
+
+
{% if age_name %}{{ age_name }}{% else %}Возраст{% endif %}
+
+
+
Любой возраст
+
+ {% for a in ages %} +
+
{{ a.1 }}
+
+ {% endfor %} +
+
+
+
@@ -40,7 +57,7 @@
{% if page_obj.has_next %} - + {% endif %}
diff --git a/apps/course/templates/course/inclusion/category_items.html b/apps/course/templates/course/inclusion/category_items.html index 5549ffa4..9ebd98ed 100644 --- a/apps/course/templates/course/inclusion/category_items.html +++ b/apps/course/templates/course/inclusion/category_items.html @@ -1,6 +1,6 @@ {% for cat in category_items %} -
+
{{ cat.title }}
-{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/apps/course/templates/course/lesson.html b/apps/course/templates/course/lesson.html index 9c6b1583..547bb402 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 %}
@@ -28,15 +31,9 @@
-
-
- -
-
{{ lesson.title }}
-
{{ lesson.short_description | linebreaks }}
+
{{ lesson.short_description | safe | linebreaks }}
{% if lesson.author.photo %} @@ -57,6 +54,12 @@
+
+
+ +
+
@@ -114,38 +117,7 @@
Задавайте вопросы:
-
- {% if request.user.is_authenticated %} -
- -
- -
-
- -
- -
- -
-
- {% else %} -
Только зарегистрированные пользователи могут оставлять комментарии.
- {% endif %} -
- {% include "templates/blocks/comments.html" with object=lesson %} -
-
+
diff --git a/apps/course/views.py b/apps/course/views.py index 857a57d0..6b578923 100644 --- a/apps/course/views.py +++ b/apps/course/views.py @@ -167,9 +167,8 @@ class CourseEditView(TemplateView): self.object = Course.objects.create( author=request.user, ) - #TODO - #if (request.user != self.object.author and request.user.role < User.AUTHOR_ROLE) or request.user.role != User.ADMIN_ROLE: - # raise Http404 + if request.user != self.object.author and request.user.role != User.ADMIN_ROLE: + raise Http404 return super().get(request) def get_context_data(self): @@ -283,6 +282,14 @@ class CoursesView(ListView): filtered = CourseFilter(self.request.GET) context.update(filtered.data) context['course_items'] = Course.shuffle(context.get('course_items')) + context['ages'] = Course.AGE_CHOICES[1:] + age = context.get('age') + if age and age[0]: + age = int(age[0]) + context['age'] = [age] + context['age_name'] = dict(Course.AGE_CHOICES).get(age, '') + else: + context['age_name'] = '' return context def get_template_names(self): diff --git a/apps/payment/models.py b/apps/payment/models.py index f1613793..4811f489 100644 --- a/apps/payment/models.py +++ b/apps/payment/models.py @@ -116,13 +116,15 @@ class Payment(PolymorphicModel): def add_months(cls, sourcedate, months=1): result = arrow.get(sourcedate, settings.TIME_ZONE).shift(months=months) if months == 1: - if (sourcedate.month == 2 and sourcedate.day >= 28) or (sourcedate.day == 31 and result.day <= 30)\ + if (sourcedate.month == 2 and sourcedate.day >= 28) or (sourcedate.day == 31 and result.day <= 30) \ or (sourcedate.month == 1 and sourcedate.day >= 29 and result.day == 28): result = result.replace(day=1, month=result.month + 1) return result.datetime @classmethod - def calc_amount(cls, payment=None, user=None, course=None, weekdays=None): + def calc_amount(cls, payment=None, user=None, course=None, date_start=None, weekdays=None): + date_start = date_start or now().date() + date_end = Payment.add_months(date_start, 1) if isinstance(payment, CoursePayment): course = payment.course user = payment.user @@ -135,6 +137,7 @@ class Payment(PolymorphicModel): if hasattr(user, 'referral') and not user.referral.payment: referral_bonus = user.referral.bonus referrer_bonus = user.referral.referrer_bonus + date_start = school_payment.date_start discount = 0 if course: price = course.price @@ -142,8 +145,8 @@ class Payment(PolymorphicModel): if user: school_payments = SchoolPayment.objects.filter( user=user, - date_start__lte=now().date(), - date_end__gte=now().date(), + date_start__lte=date_start, + date_end__gte=date_start, status__in=[ Pingback.PINGBACK_TYPE_REGULAR, Pingback.PINGBACK_TYPE_GOODWILL, @@ -162,7 +165,8 @@ class Payment(PolymorphicModel): weekday__in=weekdays, ) if add_days: - weekdays_count = weekdays_in_date_range(now().date(), prev_school_payment.date_end) + date_end = prev_school_payment.date_end + weekdays_count = weekdays_in_date_range(date_start, prev_school_payment.date_end) all_weekdays_count = weekdays_in_date_range(prev_school_payment.date_start, prev_school_payment.date_end) for ss in school_schedules: price += ss.month_price // all_weekdays_count.get(ss.weekday, 0) * weekdays_count.get( @@ -182,6 +186,8 @@ class Payment(PolymorphicModel): 'referral_bonus': referral_bonus, 'referrer_bonus': referrer_bonus, 'discount': discount, + 'date_start': date_start, + 'date_end': date_end, 'weekdays': weekdays, } diff --git a/apps/payment/views.py b/apps/payment/views.py index 2f2f3df0..3e004a20 100644 --- a/apps/payment/views.py +++ b/apps/payment/views.py @@ -102,6 +102,8 @@ class SchoolBuyView(TemplateView): weekdays = set(request.GET.getlist('weekdays', [])) use_bonuses = request.GET.get('use_bonuses') roistat_visit = request.COOKIES.get('roistat_visit', None) + date_start = request.GET.get('date_start') + date_start = date_start and datetime.datetime.strptime(date_start, '%Y-%m-%d') or now().date() if not weekdays: messages.error(request, 'Выберите несколько дней недели.') return redirect('school:school') @@ -112,8 +114,8 @@ class SchoolBuyView(TemplateView): return redirect('school:school') prev_school_payment = SchoolPayment.objects.filter( user=request.user, - date_start__lte=now().date(), - date_end__gte=now().date(), + date_start__lte=date_start, + date_end__gte=date_start, add_days=False, status__in=[ Pingback.PINGBACK_TYPE_REGULAR, @@ -126,7 +128,7 @@ class SchoolBuyView(TemplateView): school_payment = SchoolPayment.objects.create( user=request.user, weekdays=weekdays, - date_start=now().date(), + date_start=date_start, date_end=prev_school_payment.date_end, add_days=True, roistat_visit=roistat_visit, @@ -140,6 +142,8 @@ class SchoolBuyView(TemplateView): user=request.user, weekdays=weekdays, roistat_visit=roistat_visit, + date_start=date_start, + date_end=Payment.add_months(date_start), ) if use_bonuses: if request.user.bonus >= school_payment.amount: @@ -224,30 +228,7 @@ class PaymentwallCallbackView(View): product_type_name, ) - if product_type_name == 'school': - school_payment = SchoolPayment.objects.filter( - user=payment.user, - add_days=False, - date_start__lte=now().date(), - date_end__gte=now().date(), - status__in=[ - Pingback.PINGBACK_TYPE_REGULAR, - Pingback.PINGBACK_TYPE_GOODWILL, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, - ], - ).last() - if school_payment: - if payment.add_days: - date_start = now().date() - date_end = school_payment.date_end - else: - date_start = arrow.get(school_payment.date_end, settings.TIME_ZONE).shift(days=1).datetime - date_end = Payment.add_months(date_start) - else: - date_start = now().date() - date_end = Payment.add_months(date_start) - payment.date_start = date_start - payment.date_end = date_end + if product_type_name == 'course': properties = { 'payment_id': payment.id, diff --git a/apps/school/templates/blocks/online.html b/apps/school/templates/blocks/online.html index 549dad08..bad5cd46 100644 --- a/apps/school/templates/blocks/online.html +++ b/apps/school/templates/blocks/online.html @@ -4,9 +4,6 @@
ВИДЕОУРОКИ
Каждый день в 17.00 (по Мск)
Кроме выходных. Запись эфира доступна по завершению трансляции
-
- - - СМОТРЕТЬ ПРИМЕР ЭФИРА
+
diff --git a/apps/school/templates/school/livelesson_detail.html b/apps/school/templates/school/livelesson_detail.html index cfa77ba3..d3e5c5af 100644 --- a/apps/school/templates/school/livelesson_detail.html +++ b/apps/school/templates/school/livelesson_detail.html @@ -2,19 +2,27 @@ {% 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 %}
{{ livelesson.title }}
-
{{ livelesson.short_description | linebreaks }}
+
{{ livelesson.short_description | safe | linebreaks }}
{% if livelesson.stream_index %} Если видео не загрузилось, - уменьшите качество видео или обновите страницу - + + {% else %} {% if livelesson.cover %} diff --git a/apps/school/templates/school/livelessons_list.html b/apps/school/templates/school/livelessons_list.html index f19f81f0..1c0a1778 100644 --- a/apps/school/templates/school/livelessons_list.html +++ b/apps/school/templates/school/livelessons_list.html @@ -24,7 +24,7 @@
{{ livelesson.date }} // {{ livelesson.title }}
-
{{ livelesson.short_description | linebreaks }}
+
{{ livelesson.short_description | safe | linebreaks | truncatechars_html:800 }}
{% endfor %} diff --git a/apps/school/templates/summer/online.html b/apps/school/templates/summer/online.html index 113965bb..53957d3f 100644 --- a/apps/school/templates/summer/online.html +++ b/apps/school/templates/summer/online.html @@ -3,10 +3,7 @@
ВИДЕОУРОКИ
В 17.00 (по Мск)
-
Каждый день с 1 сентября
-
- - - СМОТРЕТЬ ПРИМЕР ЭФИРА
+
Каждый день
+
diff --git a/apps/school/templates/summer/prolong_btn.html b/apps/school/templates/summer/prolong_btn.html index c4026242..75021093 100644 --- a/apps/school/templates/summer/prolong_btn.html +++ b/apps/school/templates/summer/prolong_btn.html @@ -1,9 +1,5 @@ продлить diff --git a/apps/school/templates/summer/promo.html b/apps/school/templates/summer/promo.html index 7878c9ef..8b1ed12e 100644 --- a/apps/school/templates/summer/promo.html +++ b/apps/school/templates/summer/promo.html @@ -5,7 +5,7 @@ Lil School — первая образовательная онлайн-платформа креативного мышления для детей
- Старт школы - 1 сентября +
Видеоуроки
-
Каждый день с 1 сентября
+
Каждый день
diff --git a/apps/school/templates/summer/schedule_purchased.html b/apps/school/templates/summer/schedule_purchased.html index e22818c7..a2106299 100644 --- a/apps/school/templates/summer/schedule_purchased.html +++ b/apps/school/templates/summer/schedule_purchased.html @@ -5,14 +5,10 @@
diff --git a/apps/school/views.py b/apps/school/views.py index 1728d443..bac97f5a 100644 --- a/apps/school/views.py +++ b/apps/school/views.py @@ -187,6 +187,7 @@ class SchoolView(TemplateView): ll.school_schedule = school_schedules_dict.get(ll.date.isoweekday()) live_lessons_exists = live_lessons.exists() live_lessons = live_lessons or None + subscription_ends = school_payment.filter(add_days=False).last().date_end if school_payment_exists else None context.update({ 'online': online, 'live_lessons': live_lessons, @@ -199,7 +200,9 @@ class SchoolView(TemplateView): 'school_schedules': school_schedules, 'school_schedules_purchased': school_schedules_purchased, 'school_purchased_future': False, - 'subscription_ends': school_payment.filter(add_days=False).first().date_end if school_payment_exists else None, + 'subscription_ends': subscription_ends, + 'prolong_date_start': subscription_ends + timedelta(days=1) if subscription_ends else None, + 'allow_prolong': subscription_ends - date_now <= timedelta(days=14) if subscription_ends else False, }) return context diff --git a/apps/user/templates/user/profile.html b/apps/user/templates/user/profile.html index 9bddf2b0..9ad4d552 100644 --- a/apps/user/templates/user/profile.html +++ b/apps/user/templates/user/profile.html @@ -73,13 +73,14 @@
-
+
{% if is_school_purchased %}
+ style="height: 200px; object-fit: cover;" /> +
Подробнее
{% if is_author %}
-
+
{% if published.exists %} {% include "course/course_items.html" with course_items=published %} diff --git a/project/context_processors.py b/project/context_processors.py index ee33ef54..6e954da7 100644 --- a/project/context_processors.py +++ b/project/context_processors.py @@ -1,3 +1,4 @@ +from django.db.models import Func, F from django.utils.timezone import now from paymentwall.pingback import Pingback from django.contrib.auth import get_user_model @@ -17,7 +18,7 @@ def baner(request): return {'baner': Baner.objects.filter(use=True).first()} -def is_summer_school_purchased(request): +def school_purchased(request): if request.user.is_authenticated: n = now().date() school_payment = SchoolPayment.objects.filter( @@ -30,8 +31,18 @@ def is_summer_school_purchased(request): date_start__lte=n, date_end__gte=n ) - return {'is_summer_school_purchased': school_payment.exists()} - return {'is_summer_school_purchased': False} + school_schedules_purchased = school_payment.annotate( + joined_weekdays=Func(F('weekdays'), function='unnest', ) + ).values_list('joined_weekdays', flat=True).distinct() + return { + 'is_school_purchased': school_payment.exists(), + 'school_schedules_purchased': school_schedules_purchased, + } + return { + 'is_school_purchased': False, + 'school_schedules_purchased': [], + } + def referrer(request): referrer = request.session.get('referrer') 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 cb9ac8d4..8cdb2ed2 100644 --- a/project/settings.py +++ b/project/settings.py @@ -95,7 +95,7 @@ TEMPLATES = [ 'context_processors': [ 'project.context_processors.config', 'project.context_processors.baner', - 'project.context_processors.is_summer_school_purchased', + 'project.context_processors.school_purchased', 'project.context_processors.referrer', 'project.context_processors.settings', 'django.template.context_processors.debug', @@ -200,6 +200,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/about.html b/project/templates/blocks/about.html index de379799..9d285f6c 100644 --- a/project/templates/blocks/about.html +++ b/project/templates/blocks/about.html @@ -13,7 +13,7 @@
Видеоуроки
-
Каждый день с 1 сентября
+
Каждый день
diff --git a/project/templates/blocks/gallery.html b/project/templates/blocks/gallery.html index ccba4448..9a8c634a 100644 --- a/project/templates/blocks/gallery.html +++ b/project/templates/blocks/gallery.html @@ -11,80 +11,56 @@
diff --git a/project/templates/blocks/last_courses.html b/project/templates/blocks/last_courses.html index 9eee94ce..910b4ca3 100644 --- a/project/templates/blocks/last_courses.html +++ b/project/templates/blocks/last_courses.html @@ -3,9 +3,8 @@
Видео-курсы без расписания
Если вам не совсем удобно заниматься с нами каждый день в нашей онлайн-школе, специально для вас мы - делаем отдельные уроки в записи, которые вы можете проходить когда вам будем удобно.
-
-
Учите и развивайте креативное мышление когда и где угодно
+ делаем отдельные уроки в записи, которые вы можете проходить когда вам будем удобно.

+ Учите и развивайте креативное мышление когда и где угодно
diff --git a/project/templates/blocks/lil_store_js.html b/project/templates/blocks/lil_store_js.html index 43fb92bd..8f3197a3 100644 --- a/project/templates/blocks/lil_store_js.html +++ b/project/templates/blocks/lil_store_js.html @@ -1,11 +1,26 @@ {% load static %} +{% load setting from settings %} {% block foot %}{% endblock foot %} + diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index 9d3c8551..7b8204a0 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -56,6 +56,7 @@ powered by LiveChat + + 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..2fd5bbb4 --- /dev/null +++ b/web/src/components/Comments.vue @@ -0,0 +1,134 @@ + + + + diff --git a/web/src/components/ContestRedactor.vue b/web/src/components/ContestRedactor.vue index 41820a66..a0cffc9d 100644 --- a/web/src/components/ContestRedactor.vue +++ b/web/src/components/ContestRedactor.vue @@ -66,45 +66,7 @@
- -
- - - - - -
-
- - +
@@ -113,15 +75,9 @@ diff --git a/web/src/components/CourseRedactor.vue b/web/src/components/CourseRedactor.vue index b3703785..767a4254 100644 --- a/web/src/components/CourseRedactor.vue +++ b/web/src/components/CourseRedactor.vue @@ -37,10 +37,8 @@ v-bind:class="{ error: ($v.course.short_description.$dirty || showErrors) && $v.course.short_description.$invalid }">
{{titles.shortDescription}}
- +
@@ -49,7 +47,7 @@ v-bind:class="{ error: ($v.course.category.$dirty || showErrors) && $v.course.category.$invalid }">
КАТЕГОРИЯ
-
@@ -113,6 +111,13 @@
+
+
ВОЗРАСТ
+
+ +
+
- -
- - - - - -
-
- - +