diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1d78b5d1..a1a48c4d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -44,6 +44,7 @@ stop-review: script: - export REVIEW_HOST=$CI_COMMIT_REF_SLUG-$REVIEW_DOMAIN - cd docker + - cp .env.review .env - docker-compose -f docker-compose-review.yml -p back$CI_COMMIT_REF_NAME down - rm -rf /work/data/back_${CI_COMMIT_REF_NAME}/ when: manual diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/v1/serializers/content.py b/api/v1/serializers/content.py index 729368e2..89daefa1 100644 --- a/api/v1/serializers/content.py +++ b/api/v1/serializers/content.py @@ -3,8 +3,7 @@ from rest_framework import serializers from apps.content.models import ( Baner, Content, Image, Text, ImageText, Video, - Gallery, GalleryImage, ImageObject, -) + Gallery, GalleryImage, ImageObject,) from . import Base64ImageField @@ -14,6 +13,7 @@ BASE_CONTENT_FIELDS = ( 'uuid', 'course', 'lesson', + 'contest', 'live_lesson', 'title', 'position', @@ -85,12 +85,16 @@ class ImageObjectSerializer(serializers.ModelSerializer): image = Base64ImageField( required=True, allow_empty_file=False, allow_null=False, read_only=False, ) + image_thumbnail = Base64ImageField( + required=False, allow_empty_file=True, allow_null=True, read_only=True, + ) class Meta: model = ImageObject fields = ( 'id', 'image', + 'image_thumbnail', 'created_at', 'update_at', ) @@ -254,3 +258,4 @@ class ContentSerializer(serializers.ModelSerializer): elif isinstance(obj, Gallery): return GallerySerializer(obj, context=self.context).to_representation(obj) return super(ContentSerializer, self).to_representation(obj) + diff --git a/api/v1/serializers/contest.py b/api/v1/serializers/contest.py new file mode 100644 index 00000000..9d2e63c4 --- /dev/null +++ b/api/v1/serializers/contest.py @@ -0,0 +1,75 @@ +from django.contrib.auth import get_user_model +from rest_framework import serializers + +from api.v1.serializers.content import ContentSerializer, ContentCreateSerializer, ImageObjectSerializer +from api.v1.serializers.mixins import DispatchContentMixin + +from apps.content.models import (Contest, ContestWork) + +User = get_user_model() + + +class ContestSerializer(serializers.ModelSerializer): + cover = ImageObjectSerializer() + content = ContentSerializer(many=True) + + class Meta: + model = Contest + fields = ['title', 'description', 'slug', 'cover', + 'date_start', 'date_end', 'active', 'content', 'finished'] + + +class ContestCreateSerializer(DispatchContentMixin, serializers.ModelSerializer): + content = serializers.ListSerializer( + child=ContentCreateSerializer(), + required=False, + ) + + class Meta: + model = Contest + fields = '__all__' + + def create(self, validated_data): + content = validated_data.pop('content', []) + contest = super().create(validated_data) + self.dispatch_content(contest, content) + return contest + + def update(self, instance, validated_data): + content = validated_data.pop('content', []) + contest = super().update(instance, validated_data) + self.dispatch_content(contest, content) + return contest + + def to_representation(self, instance): + return ContestSerializer(instance=instance, context=self.context).to_representation(instance) + + +class ContestWorkSerializer(serializers.ModelSerializer): + image = ImageObjectSerializer() + likes = serializers.SerializerMethodField() + user_liked = serializers.SerializerMethodField() + + class Meta: + model = ContestWork + fields = ['id', 'user', 'contest', 'image', 'child_full_name', 'age', + 'created_at', 'likes', 'user_liked', 'img_width', 'img_height'] + + def get_likes(self, instance): + return instance.likes.filter(user__is_active=True).count() + + def get_user_liked(self, instance): + # FIXME + user = self.context['request'].query_params.get('current_user') + if user: + user = User.objects.get(pk=user) + return instance.likes.filter(user=user).exists() if user else False + + +class ContestWorkCreateSerializer(serializers.ModelSerializer): + class Meta: + model = ContestWork + fields = '__all__' + + def to_representation(self, instance): + return ContestWorkSerializer(instance=instance, context=self.context).to_representation(instance) diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index 4e558f3a..2b7d2806 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -1,6 +1,10 @@ +from ipware import get_client_ip + from rest_framework import serializers from rest_framework.validators import UniqueValidator +from django.contrib.auth import get_user_model + from apps.course.models import ( Category, Course, Comment, CourseComment, LessonComment, @@ -15,12 +19,15 @@ from .content import ( from apps.content.models import ( Content, Image, Text, ImageText, Video, Gallery, GalleryImage, ImageObject, -) + ContestWork) from .user import UserSerializer from .mixins import DispatchContentMixin, DispatchGalleryMixin, DispatchMaterialMixin +User = get_user_model() + + class MaterialCreateSerializer(serializers.ModelSerializer): class Meta: @@ -63,6 +70,26 @@ class LikeSerializer(serializers.ModelSerializer): ) +class LikeCreateSerializer(serializers.ModelSerializer): + + class Meta: + model = Like + fields = ['user'] + + def create(self, validated_data): + # FIXME + if validated_data.get('user'): + user = validated_data.get('user') + else: + user = self.context['request'].user + client_ip, is_routable = get_client_ip(self.context['request']) + like = Like.objects.create(user=user, ip=client_ip) + return like + + def to_representation(self, instance): + return LikeSerializer(instance, context=self.context).to_representation(instance) + + class CategorySerializer(serializers.ModelSerializer): class Meta: diff --git a/api/v1/serializers/mixins.py b/api/v1/serializers/mixins.py index 0f055f00..40584503 100644 --- a/api/v1/serializers/mixins.py +++ b/api/v1/serializers/mixins.py @@ -4,7 +4,7 @@ from apps.school.models import LiveLesson from apps.content.models import ( Content, Image, Text, ImageText, Video, Gallery, GalleryImage, ImageObject, -) + Contest) from .content import ( TextCreateSerializer, ImageCreateSerializer, ImageTextCreateSerializer, VideoCreateSerializer, @@ -25,6 +25,8 @@ class DispatchContentMixin(object): obj_type = 'lesson' elif isinstance(obj, LiveLesson): obj_type = 'live_lesson' + elif isinstance(obj, Contest): + obj_type = 'contest' cdata[obj_type] = obj.id if ctype == 'text': if 'id' in cdata and cdata['id']: diff --git a/api/v1/serializers/school.py b/api/v1/serializers/school.py index 751bb6c2..4f8cd318 100644 --- a/api/v1/serializers/school.py +++ b/api/v1/serializers/school.py @@ -54,6 +54,8 @@ class SchoolScheduleSerializer(serializers.ModelSerializer): 'day_discount', 'start_at', 'schoolschedule_images', + 'cover', + 'trial_lesson', ) read_only_fields = ( @@ -96,6 +98,7 @@ class SchoolScheduleSerializerImg(serializers.ModelSerializer): child=GalleryImageSerializer(), required=False, ) + cover = ImageObjectSerializer() class Meta: model = SchoolSchedule @@ -111,6 +114,8 @@ class SchoolScheduleSerializerImg(serializers.ModelSerializer): 'day_discount', 'start_at', 'schoolschedule_images', + 'cover', + 'trial_lesson', ) read_only_fields = ( diff --git a/api/v1/serializers/user.py b/api/v1/serializers/user.py index 888806d0..bbb5e6b5 100644 --- a/api/v1/serializers/user.py +++ b/api/v1/serializers/user.py @@ -42,6 +42,7 @@ class UserSerializer(serializers.ModelSerializer): 'photo', 'balance', 'show_in_mainpage', + 'trial_lesson', ) read_only_fields = ( diff --git a/api/v1/urls.py b/api/v1/urls.py index 409ed79f..51610cb6 100644 --- a/api/v1/urls.py +++ b/api/v1/urls.py @@ -18,7 +18,7 @@ from .views import ( UserViewSet, LessonViewSet, ImageObjectViewSet, SchoolScheduleViewSet, LiveLessonViewSet, PaymentViewSet, -) + ContestViewSet, ContestWorkViewSet) router = DefaultRouter() router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests') @@ -44,6 +44,9 @@ router.register(r'school-schedules', SchoolScheduleViewSet, base_name='school-sc router.register(r'users', UserViewSet, base_name='users') +router.register(r'contests', ContestViewSet, base_name='contests') +router.register(r'contest-works', ContestWorkViewSet, base_name='contest_works') + # router.register(r'configs', ConfigViewSet, base_name='configs') diff --git a/api/v1/views.py b/api/v1/views.py index 41cd881a..7620244b 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -1,7 +1,7 @@ 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 +from rest_framework.decorators import detail_route, list_route, action from rest_framework.response import Response from . import ExtendedModelViewSet @@ -14,7 +14,7 @@ from .serializers.course import ( CommentSerializer, MaterialSerializer, MaterialCreateSerializer, LessonSerializer, LessonCreateSerializer, -) + LikeCreateSerializer) from .serializers.content import ( BanerSerializer, ImageSerializer, ImageCreateSerializer, @@ -39,6 +39,9 @@ from .serializers.user import ( AuthorRequestSerializer, UserSerializer, UserPhotoSerializer, ) +from .serializers.contest import ( + ContestCreateSerializer, ContestSerializer, ContestWorkSerializer, ContestWorkCreateSerializer +) from .permissions import ( IsAdmin, IsAdminOrIsSelf, @@ -56,7 +59,7 @@ from apps.config.models import Config from apps.content.models import ( Baner, Image, Text, ImageText, Video, Gallery, GalleryImage, ImageObject, -) + Contest, ContestWork) from apps.payment.models import ( AuthorBalance, Payment, CoursePayment, SchoolPayment, @@ -110,6 +113,8 @@ class BanerViewSet(ExtendedModelViewSet): class ImageObjectViewSet(ExtendedModelViewSet): queryset = ImageObject.objects.all() serializer_class = ImageObjectSerializer + # FIXME + authentication_classes = [] # permission_classes = (IsAuthorOrAdmin,) @@ -126,11 +131,41 @@ class MaterialViewSet(ExtendedModelViewSet): class LikeViewSet(ExtendedModelViewSet): + OBJ_TYPE_CONTEST_WORK = 'contest_work' + queryset = Like.objects.select_related('user').all() - serializer_class = LikeSerializer + serializer_class = LikeCreateSerializer + serializer_class_map = { + 'list': LikeSerializer, + 'retrieve': LikeSerializer, + } search_fields = ('user__email', 'user__firstname', 'user__lastname',) ordering_fields = ('created_at', 'update_at',) # permission_classes = (IsAdmin,) + # FIXME + authentication_classes = [] + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + obj_type = request.data.get('obj_type') + obj_id = request.data.get('obj_id') + user = serializer.validated_data.get('user') + if not user.is_active: # FIXME and user.is_authenticated): + return Response(status=status.HTTP_403_FORBIDDEN) + if obj_type == self.OBJ_TYPE_CONTEST_WORK: + contest_work = ContestWork.objects.get(pk=obj_id) + if contest_work.user == user: + return Response({'error': u'Нельзя голосовать за свою работу'}, status=status.HTTP_400_BAD_REQUEST) + if contest_work.likes.filter(user=user).exists(): + return Response({'error': u'Вы уже голосовали за эту работу'}, status=status.HTTP_400_BAD_REQUEST) + if contest_work.contest.finished: + return Response({'error': u'Голосование закончено'}, status=status.HTTP_400_BAD_REQUEST) + instance = serializer.save() + if obj_type == self.OBJ_TYPE_CONTEST_WORK: + contest_work.likes.add(instance) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) class CategoryViewSet(ExtendedModelViewSet): @@ -419,13 +454,66 @@ class AuthorRequestViewSet(ExtendedModelViewSet): class PaymentViewSet(ExtendedModelViewSet): - queryset = Payment.objects.filter(status__isnull=False).order_by('-created_at') + queryset = Payment.objects.all() serializer_class = PaymentSerializer permission_classes = (IsAdmin,) - filter_fields = ('status',) + filter_fields = ('status', 'user',) ordering_fields = ( 'id', 'user__email', 'user__first_name', 'user__last_name', 'amount', 'created_at', ) search_fields = ('user__email', 'user__first_name', 'user__last_name',) + + def get_queryset(self): + queryset = self.queryset + course = self.request.query_params.get('course') + weekdays = self.request.query_params.getlist('weekdays[]') + if course: + queryset = CoursePayment.objects.filter(course=course) + if weekdays: + queryset = SchoolPayment.objects.filter(weekdays__overlap=weekdays) + + return queryset.filter(status__isnull=False).order_by('-created_at') + + @action(methods=['get'], detail=False, url_path='calc-amount', authentication_classes=[], permission_classes=[]) + def calc_amount(self, request, pk=None): + user = request.query_params.get('user') + course = request.query_params.get('course') + weekdays = request.query_params.getlist('weekdays[]') + user = user and User.objects.get(pk=user) + course = course and Course.objects.get(pk=course) + + return Response(Payment.calc_amount(user=user, course=course, weekdays=weekdays)) + + +class ContestViewSet(ExtendedModelViewSet): + queryset = Contest.objects.all() + serializer_class = ContestCreateSerializer + serializer_class_map = { + 'list': ContestSerializer, + 'retrieve': ContestSerializer, + } + filter_fields = ('active',) + search_fields = ('description', 'title', 'slug',) + ordering_fields = ('id', 'title', 'active', 'date_start', 'date_end',) + permission_classes = (IsAdmin,) + + +class ContestWorkViewSet(ExtendedModelViewSet): + queryset = ContestWork.objects.all() + serializer_class = ContestWorkCreateSerializer + serializer_class_map = { + 'list': ContestWorkSerializer, + 'retrieve': ContestWorkSerializer, + } + filter_fields = ('contest',) + # FIXME + authentication_classes = [] + + def create(self, request, *args, **kwargs): + # FIXME + user = User.objects.get(pk=request.data.get('user')) + if ContestWork.objects.filter(user=user).exists(): + return Response(status=status.HTTP_400_BAD_REQUEST) + return super().create(request, *args, **kwargs) diff --git a/apps/auth/backend.py b/apps/auth/backend.py new file mode 100644 index 00000000..41568b52 --- /dev/null +++ b/apps/auth/backend.py @@ -0,0 +1,17 @@ +from django.contrib.auth.backends import ModelBackend +from django.contrib.auth import get_user_model + +User = get_user_model() + + +class CaseInsensitiveModelBackend(ModelBackend): + + def authenticate(self, request, username=None, password=None, **kwargs): + if username is None: + username = kwargs.get(User.USERNAME_FIELD) + try: + user = User.objects.get(**{f'{User.USERNAME_FIELD}__iexact': username}) + if user.check_password(password) and self.user_can_authenticate(user): + return user + except User.DoesNotExist: + return None diff --git a/apps/auth/forms.py b/apps/auth/forms.py index 6977a3c2..e369a8f9 100644 --- a/apps/auth/forms.py +++ b/apps/auth/forms.py @@ -1,5 +1,4 @@ from django import forms -from django.contrib.auth.forms import AuthenticationForm class LearnerRegistrationForm(forms.Form): @@ -7,9 +6,3 @@ class LearnerRegistrationForm(forms.Form): last_name = forms.CharField() email = forms.EmailField() password = forms.CharField() - - -class AuthenticationForm(AuthenticationForm): - - def clean_username(self): - return self.cleaned_data.get('username', '').lower() diff --git a/apps/auth/views.py b/apps/auth/views.py index acb7712d..379ab0a5 100644 --- a/apps/auth/views.py +++ b/apps/auth/views.py @@ -7,6 +7,7 @@ from facepy import GraphAPI from facepy.exceptions import FacepyError from django.contrib.auth import get_user_model, logout, login, views +from django.contrib.auth.forms import AuthenticationForm from django.core.files.base import ContentFile from django.http import JsonResponse from django.urls import reverse_lazy @@ -19,7 +20,7 @@ from django.shortcuts import redirect from apps.notification.utils import send_email from apps.config.models import Config -from .forms import LearnerRegistrationForm, AuthenticationForm +from .forms import LearnerRegistrationForm from .tokens import verification_email_token User = get_user_model() @@ -171,7 +172,7 @@ class FacebookLoginOrRegistration(View): else: email = email.lower() try: - user = User.objects.get(email=email) + user = User.objects.get(email__iexact=email) except User.DoesNotExist: first_name = data.get('first_name', '') last_name = data.get('last_name', '') diff --git a/apps/content/admin.py b/apps/content/admin.py index 1c92c012..c025f198 100644 --- a/apps/content/admin.py +++ b/apps/content/admin.py @@ -8,6 +8,7 @@ from polymorphic.admin import ( from apps.content.models import ( Baner, Content, Image, Text, ImageText, Video, Gallery, GalleryImage, ImageObject, + Contest,ContestWork, ) @@ -79,3 +80,13 @@ class ContentAdmin(PolymorphicParentModelAdmin): @admin.register(GalleryImage) class GalleryImageAdmin(admin.ModelAdmin): pass + + +@admin.register(Contest) +class ContestAdmin(admin.ModelAdmin): + base_model = Contest + + +@admin.register(ContestWork) +class ContestWorkAdmin(admin.ModelAdmin): + base_model = ContestWork diff --git a/apps/content/migrations/0021_auto_20180813_1306.py b/apps/content/migrations/0021_auto_20180813_1306.py new file mode 100644 index 00000000..66033641 --- /dev/null +++ b/apps/content/migrations/0021_auto_20180813_1306.py @@ -0,0 +1,48 @@ +# Generated by Django 2.0.6 on 2018-08-13 13:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0040_course_age'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('content', '0020_auto_20180424_1607'), + ] + + operations = [ + migrations.CreateModel( + name='Contest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('description', models.TextField(blank=True, default='', max_length=1000)), + ('slug', models.SlugField(allow_unicode=True, blank=True, max_length=100, null=True, unique=True)), + ('date_start', models.DateField(blank=True, null=True, verbose_name='Дата начала')), + ('date_end', models.DateField(blank=True, null=True, verbose_name='Дата окончания')), + ('active', models.BooleanField(default=True)), + ('cover', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='contest_covers', to='content.ImageObject', verbose_name='Фоновая картинка')), + ], + ), + migrations.CreateModel( + name='ContestWork', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('child_full_name', models.CharField(max_length=255)), + ('age', models.SmallIntegerField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.Contest')), + ('image', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contest_work_images', to='content.ImageObject', verbose_name='Работа участника')), + ('likes', models.ManyToManyField(blank=True, to='course.Like')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='content', + name='contest', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='content', to='content.Contest', verbose_name='Конкурс'), + ), + ] diff --git a/apps/content/migrations/0022_auto_20180815_2129.py b/apps/content/migrations/0022_auto_20180815_2129.py new file mode 100644 index 00000000..92cc567d --- /dev/null +++ b/apps/content/migrations/0022_auto_20180815_2129.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.6 on 2018-08-15 21:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0021_auto_20180813_1306'), + ] + + operations = [ + migrations.AlterModelOptions( + name='contestwork', + options={'ordering': ('-created_at',), 'verbose_name': 'Конкурсная работа', 'verbose_name_plural': 'Конкурсные работы'}, + ), + ] diff --git a/apps/content/models.py b/apps/content/models.py index 468058eb..0c445958 100644 --- a/apps/content/models.py +++ b/apps/content/models.py @@ -1,11 +1,25 @@ +from datetime import datetime, time from urllib.parse import urlparse +from django.conf import settings from django.db import models +from django.contrib.auth import get_user_model +from django.urls import reverse_lazy +from django.utils import timezone +from imagekit.models import ImageSpecField +from imagekit.processors import ResizeToCover from polymorphic.models import PolymorphicModel +User = get_user_model() + + class ImageObject(models.Model): image = models.ImageField('Изображение', upload_to='content/imageobject') + image_thumbnail = ImageSpecField(source='image', + processors=[ResizeToCover(300, 200, False)], + format='JPEG', + options={'quality': 85}) created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) @@ -36,6 +50,12 @@ class Content(PolymorphicModel): verbose_name='Урок онлайн школы', related_name='content', ) + contest = models.ForeignKey( + 'Contest', on_delete=models.CASCADE, + null=True, blank=True, + verbose_name='Конкурс', + related_name='content', + ) title = models.CharField('Заголовок', max_length=100, default='') position = models.PositiveSmallIntegerField( 'Положение на странице', @@ -131,3 +151,62 @@ class Baner(models.Model): if self.use: Baner.objects.filter(use=True).update(use=False) return super().save(*args, **kwargs) + + +class Contest(models.Model): + title = models.CharField(max_length=255) + description = models.TextField(max_length=1000, blank=True, default='') + slug = models.SlugField( + allow_unicode=True, null=True, blank=True, + max_length=100, unique=True, db_index=True, + ) + cover = models.ForeignKey( + ImageObject, related_name='contest_covers', + verbose_name='Фоновая картинка', on_delete=models.CASCADE, + null=True, blank=True, + ) + date_start = models.DateField('Дата начала', null=True, blank=True) + date_end = models.DateField('Дата окончания', null=True, blank=True) + active = models.BooleanField(default=True) + # TODO? baner + + @property + def finished(self): + # FIXME + return datetime(2018, 8, 29, 21) < timezone.now() + + def save(self, *args, **kwargs): + if self.active: + Contest.objects.filter(active=True).update(active=False) + return super().save(*args, **kwargs) + + +class ContestWork(models.Model): + user = models.ForeignKey( + User, on_delete=models.CASCADE + ) + contest = models.ForeignKey(Contest, on_delete=models.CASCADE) + image = models.ForeignKey( + ImageObject, related_name='contest_work_images', + verbose_name='Работа участника', on_delete=models.CASCADE, + ) + child_full_name = models.CharField(max_length=255) + age = models.SmallIntegerField() + created_at = models.DateTimeField(auto_now_add=True) + likes = models.ManyToManyField('course.Like', blank=True) + + class Meta: + verbose_name = 'Конкурсная работа' + verbose_name_plural = 'Конкурсные работы' + ordering = ('-created_at',) + + @property + def img_width(self): + return self.image.image.width if self.image and self.image.image else None + + @property + def img_height(self): + return self.image.image.height if self.image and self.image.image else None + + def get_absolute_url(self): + return reverse_lazy('contest_work', args=[self.id]) diff --git a/apps/course/templates/course/content/gallery.html b/apps/content/templates/content/blocks/gallery.html similarity index 97% rename from apps/course/templates/course/content/gallery.html rename to apps/content/templates/content/blocks/gallery.html index 1116322a..69ad0386 100644 --- a/apps/course/templates/course/content/gallery.html +++ b/apps/content/templates/content/blocks/gallery.html @@ -1,32 +1,32 @@ -{% load thumbnail %} -{% if results %} -
Галерея итогов обучения
- -{% else %} -
-
-
{{ content.title }}
- -
-
-{% endif %} +{% load thumbnail %} +{% if results %} +
Галерея итогов обучения
+ +{% else %} +
+
+
{{ content.title }}
+ +
+
+{% endif %} diff --git a/apps/course/templates/course/content/image.html b/apps/content/templates/content/blocks/image.html similarity index 96% rename from apps/course/templates/course/content/image.html rename to apps/content/templates/content/blocks/image.html index 4ab24601..5d515430 100644 --- a/apps/course/templates/course/content/image.html +++ b/apps/content/templates/content/blocks/image.html @@ -1,10 +1,10 @@ -
-
-
- {{ content.title }} -
-
- -
-
-
+
+
+
+ {{ content.title }} +
+
+ +
+
+
diff --git a/apps/course/templates/course/content/imagetext.html b/apps/content/templates/content/blocks/imagetext.html similarity index 100% rename from apps/course/templates/course/content/imagetext.html rename to apps/content/templates/content/blocks/imagetext.html diff --git a/apps/course/templates/course/content/text.html b/apps/content/templates/content/blocks/text.html similarity index 94% rename from apps/course/templates/course/content/text.html rename to apps/content/templates/content/blocks/text.html index 18ce2235..59e46ded 100644 --- a/apps/course/templates/course/content/text.html +++ b/apps/content/templates/content/blocks/text.html @@ -1,10 +1,10 @@ -
-
-
- {{ content.title }} -
-
- {{ content.txt | safe }} -
-
+
+
+
+ {{ content.title }} +
+
+ {{ content.txt | safe }} +
+
\ No newline at end of file diff --git a/apps/course/templates/course/content/video.html b/apps/content/templates/content/blocks/video.html similarity index 88% rename from apps/course/templates/course/content/video.html rename to apps/content/templates/content/blocks/video.html index 3527507f..791920b1 100644 --- a/apps/course/templates/course/content/video.html +++ b/apps/content/templates/content/blocks/video.html @@ -3,10 +3,10 @@
{{ content.title }}
-
+
{% if 'youtube.com' in content.url or 'youtu.be' in content.url %} + webkitallowfullscreen mozallowfullscreen allowfullscreen> {% elif 'vimeo.com' in content.url %} - Если видео не загрузилось обновите страницу + Если видео не загрузилось, - уменьшите качество видео или обновите страницу {% else %} - {% if livelesson.cover %} - - {% else %} - - {% endif %} + {% if livelesson.cover %} + + {% endif %} {% endif %} - +
{% for content in livelesson.content.all %} - {% with template="course/content/"|add:content.ctype|add:".html" %} + {% with template="content/blocks/"|add:content.ctype|add:".html" %} {% include template %} {% endwith %} diff --git a/apps/school/templates/school/livelessons_list.html b/apps/school/templates/school/livelessons_list.html index b2099cbe..f19f81f0 100644 --- a/apps/school/templates/school/livelessons_list.html +++ b/apps/school/templates/school/livelessons_list.html @@ -9,7 +9,7 @@
{% for livelesson in livelesson_list %} -
+
diff --git a/apps/school/templates/summer/day_pay_btn.html b/apps/school/templates/summer/day_pay_btn.html index 155b8827..f44bee84 100644 --- a/apps/school/templates/summer/day_pay_btn.html +++ b/apps/school/templates/summer/day_pay_btn.html @@ -3,5 +3,5 @@ data-popup=".js-popup-auth" {% endif %} class="timing__btn btn" - href="{% url 'school-checkout' %}?weekdays={{ school_schedule.weekday }}&add_days=true" + href="{% url 'school-checkout' %}?weekdays={{ school_schedule.weekday }}" >купить diff --git a/apps/school/templates/summer/online.html b/apps/school/templates/summer/online.html index 3599afcc..113965bb 100644 --- a/apps/school/templates/summer/online.html +++ b/apps/school/templates/summer/online.html @@ -1,9 +1,9 @@ {% load static %}
@@ -50,7 +46,7 @@
-
12 уроков
+
7 дисциплин
В разных техниках
diff --git a/apps/school/templates/summer/schedule_purchased.html b/apps/school/templates/summer/schedule_purchased.html index b6e183a1..e22818c7 100644 --- a/apps/school/templates/summer/schedule_purchased.html +++ b/apps/school/templates/summer/schedule_purchased.html @@ -26,14 +26,12 @@
Новые уроки
{% endif %} - {% comment %} - {% endcomment %}
{% endif %}
diff --git a/apps/school/urls.py b/apps/school/urls.py index 47361f96..59973c6d 100644 --- a/apps/school/urls.py +++ b/apps/school/urls.py @@ -3,12 +3,11 @@ from django.urls import path, include from .views import ( LiveLessonsView, LiveLessonEditView, LiveLessonsDetailView, SchoolView, - SchoolSchedulesPrintView, SummerSchoolView, + SchoolSchedulesPrintView, ) urlpatterns = [ path('', SchoolView.as_view(), name='school'), - path('summer', SummerSchoolView.as_view(), name='summer-school'), path('schedules/print', SchoolSchedulesPrintView.as_view(), name='school_schedules-print'), path('lessons/', LiveLessonsView.as_view(), name='lessons'), path('lessons/create', LiveLessonEditView.as_view(), name='lessons-create'), diff --git a/apps/school/views.py b/apps/school/views.py index 3f269067..3816c0eb 100644 --- a/apps/school/views.py +++ b/apps/school/views.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date from paymentwall import Pingback from django.contrib.auth import get_user_model @@ -34,19 +34,22 @@ class LiveLessonsView(ListView): template_name = 'school/livelessons_list.html' def get_queryset(self): + september2018 = date(2018, 9, 1) + date_start = (now() - timedelta(days=7)).date() + if date_start < september2018: + date_start = september2018 date_range = Q( date__range=[ - (now() - timedelta(days=7)).date(), - (now() + timedelta(days=10)).date(), + date_start, + date_start + timedelta(days=17), ] ) queryset = LiveLesson.objects.filter(date_range) if queryset.count() < 17: - start_date = now() - timedelta(days=7) for i in range(18): try: LiveLesson.objects.create( - date=(start_date + timedelta(days=i)).date(), + date=date_start + timedelta(days=i), ) except IntegrityError: pass @@ -61,6 +64,7 @@ class LiveLessonsDetailView(DetailView): def get(self, request, pk=None): response = super().get(request, pk=pk) + # ??? где проверка? #try: # school_payment = SchoolPayment.objects.get( # user=request.user, @@ -107,75 +111,8 @@ class LiveLessonEditView(TemplateView): class SchoolView(TemplateView): - template_name = 'school/school.html' - - def get_context_data(self): - context = super().get_context_data() - is_previous = 'is_previous' in self.request.GET - date_now = now().date() - now_time = now() - try: - school_schedule = SchoolSchedule.objects.get(weekday=now_time.isoweekday()) - except SchoolSchedule.DoesNotExist: - online = False - else: - end_at = datetime.combine(now_time.today(), school_schedule.start_at) - online = ( - school_schedule.start_at <= now_time.time() and - (end_at + timedelta(hours=1)).time() >= now_time.time() and - school_schedule.current_live_lesson() - ) - if self.request.user.is_authenticated: - school_payment = 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__lte=date_now, - date_end__gte=date_now - ) - school_payment_exists = school_payment.exists() - school_schedules_purchased = school_payment.annotate( - joined_weekdays=Func(F('weekdays'), function='unnest',) - ).values_list('joined_weekdays', flat=True).distinct() - else: - school_payment_exists = False - school_schedules_purchased = [] - if school_payment_exists and is_previous: - live_lessons = LiveLesson.objects.filter( - date__gte=school_payment.last().date_start, - date__range=[(now_time - timedelta(days=8)).date(), (now_time - timedelta(days=1)).date()], - deactivated_at__isnull=True, - ) - live_lessons_exists = live_lessons.exists() - else: - live_lessons = None - live_lessons_exists = False - context.update({ - 'online': online, - 'live_lessons': live_lessons, - 'live_lessons_exists': live_lessons_exists, - 'is_previous': is_previous, - 'course_items': Course.objects.filter(status=Course.PUBLISHED)[:6], - 'is_purchased': school_payment_exists, - 'min_school_price': SchoolSchedule.objects.aggregate(Min('month_price'))['month_price__min'], - 'school_schedules': SchoolSchedule.objects.all(), - 'school_schedules_purchased': school_schedules_purchased, - 'subscription_ends': school_payment.filter(add_days=False).first().date_end if school_payment_exists else None, - }) - return context - -class SummerSchoolView(TemplateView): template_name = 'school/summer_school.html' - def get(self, request, *args, **kwargs): - context = self.get_context_data(**kwargs) - if not context.get('is_purchased'): - return redirect('/') - return self.render_to_response(context) - def get_context_data(self): context = super().get_context_data() is_previous = 'is_previous' in self.request.GET @@ -193,10 +130,14 @@ class SummerSchoolView(TemplateView): online = ( school_schedule.start_at <= now_time.time() and (end_at + timedelta(hours=1)).time() >= now_time.time() and - school_schedule.current_live_lesson() + school_schedule.current_live_lesson ) school_schedules = SchoolSchedule.objects.all() + try: + school_schedules = sorted(school_schedules, key=lambda ss: ss.current_live_lesson and ss.current_live_lesson.date) + except Exception: + pass school_schedules_dict = {ss.weekday: ss for ss in school_schedules} school_schedules_dict[0] = school_schedules_dict.get(7) all_schedules_purchased = [] @@ -226,31 +167,15 @@ class SummerSchoolView(TemplateView): ) school_payment_exists = school_payment.exists() - school_payment_future = 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__gte=date_now, - date_end__gte=date_now - ) - - school_payment_exists_future = school_payment_future.exists() - school_purchased_future = school_payment_future.last() - school_schedules_purchased = school_payment.annotate( joined_weekdays=Func(F('weekdays'), function='unnest',) ).values_list('joined_weekdays', flat=True).distinct() else: school_payment_exists = False - school_payment_exists_future = False - school_purchased_future = False school_schedules_purchased = [] if all_schedules_purchased and is_previous: live_lessons = LiveLesson.objects.filter( - date__range=[month_start, yesterday], + date__range=[yesterday - timedelta(days=7), yesterday], deactivated_at__isnull=True, date__week_day__in=all_schedules_purchased, ).order_by('-date') @@ -267,11 +192,11 @@ class SummerSchoolView(TemplateView): 'is_previous': is_previous, 'course_items': Course.objects.filter(status=Course.PUBLISHED)[:6], 'is_purchased': school_payment_exists, - 'is_purchased_future': school_payment_exists_future, + 'is_purchased_future': False, 'min_school_price': SchoolSchedule.objects.aggregate(Min('month_price'))['month_price__min'], 'school_schedules': school_schedules, 'school_schedules_purchased': school_schedules_purchased, - 'school_purchased_future': school_purchased_future, + 'school_purchased_future': False, 'subscription_ends': school_payment.filter(add_days=False).first().date_end if school_payment_exists else None, }) return context diff --git a/apps/user/forms.py b/apps/user/forms.py index 2445d522..d20ee0ea 100644 --- a/apps/user/forms.py +++ b/apps/user/forms.py @@ -18,6 +18,7 @@ class UserEditForm(forms.ModelForm): # gender = forms.ChoiceField(choices=User.GENDER_CHOICES, required=False) gender = forms.CharField(required=False) # about = forms.CharField() + trial_lesson = forms.URLField(required=False) old_password = forms.CharField(required=False) new_password1 = forms.CharField(required=False) new_password2 = forms.CharField(required=False) @@ -41,6 +42,7 @@ class UserEditForm(forms.ModelForm): 'birthday', 'gender', 'about', + 'trial_lesson', 'old_password', 'new_password1', 'new_password2', diff --git a/apps/user/migrations/0023_user_trial_lesson.py b/apps/user/migrations/0023_user_trial_lesson.py new file mode 100644 index 00000000..33a0aab5 --- /dev/null +++ b/apps/user/migrations/0023_user_trial_lesson.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.6 on 2018-08-22 12:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0022_user_instagram_hashtag'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='trial_lesson', + field=models.URLField(blank=True, default='', null=True), + ), + ] diff --git a/apps/user/models.py b/apps/user/models.py index 7391b0c8..0df7cd01 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -75,6 +75,7 @@ class User(AbstractUser): ) photo = models.ImageField('Фото', null=True, blank=True, upload_to='users') show_in_mainpage = models.BooleanField('Показывать на главной странице', default=False) + trial_lesson = models.URLField(default='', null=True, blank=True) objects = UserManager() diff --git a/apps/user/templates/user/profile-settings.html b/apps/user/templates/user/profile-settings.html index 5502c21d..90470b96 100644 --- a/apps/user/templates/user/profile-settings.html +++ b/apps/user/templates/user/profile-settings.html @@ -150,7 +150,18 @@ {% for error in form.about.errors %}
{{ error }}
{% endfor %} -
+
+ {% if is_teacher %} +
+
Пробный урок
+
+ +
+ {% for error in form.trial_lesson.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endif %}
Пароль
diff --git a/apps/user/templates/user/profile.html b/apps/user/templates/user/profile.html index fd279635..e8a81d33 100644 --- a/apps/user/templates/user/profile.html +++ b/apps/user/templates/user/profile.html @@ -64,7 +64,7 @@
- {# #} + @@ -75,7 +75,6 @@ {% endif %}
- {% comment %}
{% if is_purchased_future %}
@@ -89,7 +88,7 @@ {% else %}
-
Вы не подписаны на лагерь!
+
Вы не подписаны на онлайн-школу!
- {% endcomment %} -
+
{% if paid.exists %} diff --git a/apps/user/views.py b/apps/user/views.py index 309fb0e0..41a18cb0 100644 --- a/apps/user/views.py +++ b/apps/user/views.py @@ -73,7 +73,7 @@ class ProfileView(TemplateView): school_payment = SchoolPayment.objects.filter( user=self.object, date_start__lte=now(), - date_end__gt=now(), + date_end__gte=now(), status__in=[ Pingback.PINGBACK_TYPE_REGULAR, Pingback.PINGBACK_TYPE_GOODWILL, @@ -94,14 +94,8 @@ class ProfileView(TemplateView): ).all() context['all_school_schedules'] = SchoolSchedule.objects.all() - school_payment_future = SchoolPayment.objects.filter( - user=self.object, - date_start__gte=now(), - date_end__gte=now() - ) - - context['is_purchased_future'] = school_payment_future.exists() - context['school_purchased_future'] = school_payment_future.last() + context['is_purchased_future'] = False + context['school_purchased_future'] = False return context @@ -210,6 +204,11 @@ class ProfileEditView(UpdateView): self.object = self.get_object() return super().dispatch(request, *args, **kwargs) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['is_teacher'] = self.object.role == User.TEACHER_ROLE + return context + def post(self, request, *args, **kwargs): # it's magic *-*-*-*-* if 'photo' in request.FILES: diff --git a/project/settings.py b/project/settings.py index 1aecdb8c..b9232bcd 100644 --- a/project/settings.py +++ b/project/settings.py @@ -56,6 +56,7 @@ INSTALLED_APPS = [ 'sorl.thumbnail', 'raven.contrib.django.raven_compat', 'django_user_agents', + 'imagekit', ] + [ 'apps.auth.apps', 'apps.user', @@ -142,6 +143,7 @@ AUTH_PASSWORD_VALIDATORS = [ AUTH_USER_MODEL = 'user.User' +AUTHENTICATION_BACKENDS = ['apps.auth.backend.CaseInsensitiveModelBackend'] # Internationalization # https://docs.djangoproject.com/en/2.0/topics/i18n/ diff --git a/project/templates/blocks/about.html b/project/templates/blocks/about.html index 5fefa4ca..de379799 100644 --- a/project/templates/blocks/about.html +++ b/project/templates/blocks/about.html @@ -12,8 +12,8 @@
-
Прямой эфир
-
Понедельник, среда, пятница
+
Видеоуроки
+
Каждый день с 1 сентября
@@ -30,7 +30,7 @@
-
12 уроков
+
7 дисциплин
В разных техниках
@@ -43,22 +43,6 @@
Хранится 7 дней
-
+
diff --git a/project/templates/blocks/comment.html b/project/templates/blocks/comment.html index be3234e9..d9e4822a 100644 --- a/project/templates/blocks/comment.html +++ b/project/templates/blocks/comment.html @@ -1,5 +1,5 @@ {% load static %} -{% if not node.deactivated_at %} +{% if not node.deactivated_at and node.author.is_active %}
{% if node.author.photo %} diff --git a/project/templates/blocks/footer.html b/project/templates/blocks/footer.html index b586ffc9..14ebdebb 100644 --- a/project/templates/blocks/footer.html +++ b/project/templates/blocks/footer.html @@ -19,7 +19,6 @@
{% if not is_purchased %}Получить доступ{% else %}Смотреть урок{% endif %} + >{% if not school_schedule.weekday in school_schedules_purchased %}Получить доступ{% else %}Смотреть урок{% endif %}
{% else %}
- {# Присоединяйтесь в Рисовальный лагерь #} - Приглашаем вас на месяц открытых дверей в Lil School + Приглашаем вас присоединиться к онлайн-школе с 1 сентября!
- {% comment %} {% if not is_purchased and not is_purchased_future %} - Получить доступ + купить доступ от {{ min_school_price }} руб./месяц + {% else %} + Подробнее {% endif %} - {% endcomment %} - {# О лагере #} - Подробнее
{% endif %}
diff --git a/project/templates/blocks/share.html b/project/templates/blocks/share.html index c895fe32..85fa1ebb 100644 --- a/project/templates/blocks/share.html +++ b/project/templates/blocks/share.html @@ -1,27 +1,14 @@ {% load static %} +
- {{ teacher.get_full_name }}{% if teacher.instagram_hashtag %}, - - {{ teacher.instagram_hashtag }} - +
+ {{ teacher.get_full_name }}{% if teacher.instagram_hashtag %}, + + {{ teacher.instagram_hashtag }} + + {% endif %} +
+ {% if teacher.trial_lesson %} + ПРОБНЫЙ УРОК {% endif %}
+ + {% include 'templates/blocks/lil_store_js.html' %} + + diff --git a/web/src/components/ContestWorks.vue b/web/src/components/ContestWorks.vue new file mode 100644 index 00000000..b0d6f493 --- /dev/null +++ b/web/src/components/ContestWorks.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/web/src/components/CourseRedactor.vue b/web/src/components/CourseRedactor.vue index 9eab29b4..7888d9cc 100644 --- a/web/src/components/CourseRedactor.vue +++ b/web/src/components/CourseRedactor.vue @@ -1017,28 +1017,28 @@ }); } - // let user = api.getCurrentUser(this.accessToken); - // promises.push(user); - // - // user.then((response) => { - // if (response.data) { - // this.me = response.data; - // - // if(this.me.role == ROLE_ADMIN) { - // api.getUsers({role: [ROLE_AUTHOR,ROLE_ADMIN], page_size: 1000}, this.accessToken) - // .then((usersResponse) => { - // if (usersResponse.data) { - // this.users = usersResponse.data.results.map((user) => { - // return { - // title: `${user.first_name} ${user.last_name}`, - // value: user.id - // } - // }); - // } - // }); - // } - // } - // }); + let user = api.getCurrentUser(this.accessToken); + promises.push(user); + + user.then((response) => { + if (response.data) { + this.me = response.data; + + /* if(this.me.role == ROLE_ADMIN) { + api.getUser(this.me.id, {role: [ROLE_AUTHOR, ROLE_ADMIN], page_size: 1000}, this.accessToken) + .then((usersResponse) => { + if (usersResponse.data) { + this.users = usersResponse.data.results.map((user) => { + return { + title: `${user.first_name} ${user.last_name}`, + value: user.id + } + }); + } + }); + } */ + } + }); // if (this.courseId) { // this.loadCourse().then(()=>{this.updateViewSection(window.location, 'load')}).catch(()=>{ diff --git a/web/src/components/UploadContestWork.vue b/web/src/components/UploadContestWork.vue new file mode 100644 index 00000000..326c05c4 --- /dev/null +++ b/web/src/components/UploadContestWork.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/web/src/components/blocks/ContestWork.vue b/web/src/components/blocks/ContestWork.vue new file mode 100644 index 00000000..617b9fab --- /dev/null +++ b/web/src/components/blocks/ContestWork.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/web/src/components/blocks/Image.vue b/web/src/components/blocks/Image.vue index 9006500d..527df9f1 100644 --- a/web/src/components/blocks/Image.vue +++ b/web/src/components/blocks/Image.vue @@ -1,5 +1,5 @@