Merge branch 'master' of https://gitlab.com/lilcity/backend into hotfix/LIL-601

remotes/origin/hotfix/LIL-691
gzbender 7 years ago
commit 552a04ef34
  1. 1
      .gitlab-ci.yml
  2. 0
      __init__.py
  3. 9
      api/v1/serializers/content.py
  4. 75
      api/v1/serializers/contest.py
  5. 29
      api/v1/serializers/course.py
  6. 4
      api/v1/serializers/mixins.py
  7. 5
      api/v1/serializers/school.py
  8. 1
      api/v1/serializers/user.py
  9. 5
      api/v1/urls.py
  10. 100
      api/v1/views.py
  11. 17
      apps/auth/backend.py
  12. 7
      apps/auth/forms.py
  13. 5
      apps/auth/views.py
  14. 11
      apps/content/admin.py
  15. 48
      apps/content/migrations/0021_auto_20180813_1306.py
  16. 17
      apps/content/migrations/0022_auto_20180815_2129.py
  17. 79
      apps/content/models.py
  18. 64
      apps/content/templates/content/blocks/gallery.html
  19. 20
      apps/content/templates/content/blocks/image.html
  20. 0
      apps/content/templates/content/blocks/imagetext.html
  21. 18
      apps/content/templates/content/blocks/text.html
  22. 6
      apps/content/templates/content/blocks/video.html
  23. 47
      apps/content/templates/content/contest.html
  24. 18
      apps/content/templates/content/contest_edit.html
  25. 118
      apps/content/templates/content/contest_work.html
  26. 112
      apps/content/views.py
  27. 2
      apps/course/filters.py
  28. 18
      apps/course/migrations/0040_course_age.py
  29. 36
      apps/course/migrations/0041_auto_20180813_1306.py
  30. 18
      apps/course/migrations/0042_like_ip.py
  31. 29
      apps/course/migrations/0043_auto_20180824_2132.py
  32. 37
      apps/course/models.py
  33. 2
      apps/course/templates/course/_items.html
  34. 2
      apps/course/templates/course/course.html
  35. 38
      apps/course/templates/course/course_only_lessons.html
  36. 2
      apps/course/templates/course/inclusion/category_menu_items.html
  37. 45
      apps/course/templates/course/lesson.html
  38. 4
      apps/course/templatetags/lilcity_category.py
  39. 3
      apps/course/views.py
  40. 22
      apps/payment/management/commands/payment_fix_amounts.py
  41. 89
      apps/payment/models.py
  42. 2
      apps/payment/templates/payment/payment_success.html
  43. 56
      apps/payment/views.py
  44. 20
      apps/school/migrations/0019_schoolschedule_cover.py
  45. 29
      apps/school/migrations/0020_auto_20180824_2132.py
  46. 18
      apps/school/migrations/0021_schoolschedule_trial_lesson.py
  47. 23
      apps/school/models.py
  48. 6
      apps/school/templates/blocks/day_pay_btn.html
  49. 2
      apps/school/templates/blocks/online.html
  50. 2
      apps/school/templates/blocks/promo.html
  51. 2
      apps/school/templates/blocks/schedule.html
  52. 36
      apps/school/templates/blocks/schedule_item.html
  53. 18
      apps/school/templates/school/livelesson_detail.html
  54. 2
      apps/school/templates/school/livelessons_list.html
  55. 2
      apps/school/templates/summer/day_pay_btn.html
  56. 4
      apps/school/templates/summer/online.html
  57. 14
      apps/school/templates/summer/promo.html
  58. 6
      apps/school/templates/summer/schedule_purchased.html
  59. 3
      apps/school/urls.py
  60. 109
      apps/school/views.py
  61. 2
      apps/user/forms.py
  62. 18
      apps/user/migrations/0023_user_trial_lesson.py
  63. 1
      apps/user/models.py
  64. 13
      apps/user/templates/user/profile-settings.html
  65. 8
      apps/user/templates/user/profile.html
  66. 17
      apps/user/views.py
  67. 2
      project/settings.py
  68. 24
      project/templates/blocks/about.html
  69. 2
      project/templates/blocks/comment.html
  70. 1
      project/templates/blocks/footer.html
  71. 11
      project/templates/blocks/header.html
  72. 2
      project/templates/blocks/last_courses.html
  73. 14
      project/templates/blocks/lil_store_js.html
  74. 4
      project/templates/blocks/live.html
  75. 2
      project/templates/blocks/messages.html
  76. 21
      project/templates/blocks/partners.html
  77. 14
      project/templates/blocks/popup_auth.html
  78. 34
      project/templates/blocks/popup_buy.html
  79. 10
      project/templates/blocks/popup_subscribe.html
  80. 35
      project/templates/blocks/promo.html
  81. 31
      project/templates/blocks/share.html
  82. 17
      project/templates/blocks/teachers.html
  83. 1
      project/templates/lilcity/edit_index.html
  84. 27
      project/templates/lilcity/index.html
  85. 7
      project/urls.py
  86. 5
      project/utils.py
  87. 44
      project/views.py
  88. 2
      requirements.txt
  89. 5
      web/package.json
  90. 361
      web/src/components/ContestRedactor.vue
  91. 139
      web/src/components/ContestWorks.vue
  92. 44
      web/src/components/CourseRedactor.vue
  93. 193
      web/src/components/UploadContestWork.vue
  94. 62
      web/src/components/blocks/ContestWork.vue
  95. 45
      web/src/components/blocks/Image.vue
  96. 81
      web/src/components/blocks/Likes.vue
  97. 2
      web/src/components/redactor/VueRedactor.vue
  98. BIN
      web/src/img/pinkbus.jpg
  99. 31
      web/src/js/app.js
  100. 19
      web/src/js/contest-redactor.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -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

@ -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)

@ -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)

@ -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:

@ -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']:

@ -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 = (

@ -42,6 +42,7 @@ class UserSerializer(serializers.ModelSerializer):
'photo',
'balance',
'show_in_mainpage',
'trial_lesson',
)
read_only_fields = (

@ -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')

@ -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)

@ -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

@ -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()

@ -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', '')

@ -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

@ -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='Конкурс'),
),
]

@ -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': 'Конкурсные работы'},
),
]

@ -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])

@ -1,32 +1,32 @@
{% load thumbnail %}
{% if results %}
<div class="title">Галерея итогов обучения</div>
<div class="examples gallery">
{% for image in course.gallery.gallery_images.all %}
<div class="examples__item">
<a href="{{ image.img.image.url }}">
{% thumbnail image.img.image "165x165" crop="center" as im %}
<img class="examples__pic" src="{{ im.url }}">
{% endthumbnail %}
</a>
</div>
{% endfor %}
</div>
{% else %}
<div class="section section_gradient">
<div class="section__center center center_sm">
<div class="title">{{ content.title }}</div>
<div class="examples gallery">
{% for image in content.gallery_images.all %}
<div class="examples__item">
<a href="{{ image.img.image.url }}">
{% thumbnail image.img.image "165x165" crop="center" as im %}
<img class="examples__pic" src="{{ im.url }}">
{% endthumbnail %}
</a>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% load thumbnail %}
{% if results %}
<div class="title">Галерея итогов обучения</div>
<div class="examples gallery">
{% for image in course.gallery.gallery_images.all %}
<div class="examples__item">
<a href="{{ image.img.image.url }}">
{% thumbnail image.img.image "165x165" crop="center" as im %}
<img class="examples__pic" src="{{ im.url }}">
{% endthumbnail %}
</a>
</div>
{% endfor %}
</div>
{% else %}
<div class="section section_gradient">
<div class="section__center center center_sm">
<div class="title">{{ content.title }}</div>
<div class="examples gallery">
{% for image in content.gallery_images.all %}
<div class="examples__item">
<a href="{{ image.img.image.url }}">
{% thumbnail image.img.image "165x165" crop="center" as im %}
<img class="examples__pic" src="{{ im.url }}">
{% endthumbnail %}
</a>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}

@ -1,10 +1,10 @@
<div class="section section_border">
<div class="section__center center center_sm">
<div class="content-block title">
{{ content.title }}
</div>
<div>
<img class="content-block pic" src="{{ content.img.image.url }}" alt="">
</div>
</div>
</div>
<div class="section section_border">
<div class="section__center center center_sm">
<div class="content-block title">
{{ content.title }}
</div>
<div>
<img class="content-block pic" src="{{ content.img.image.url }}" alt="">
</div>
</div>
</div>

@ -1,10 +1,10 @@
<div class="section section_border">
<div class="section__center center center_sm">
<div class="content-block title">
{{ content.title }}
</div>
<div class="content-block text" style="margin-bottom:0">
{{ content.txt | safe }}
</div>
</div>
<div class="section section_border">
<div class="section__center center center_sm">
<div class="content-block title">
{{ content.title }}
</div>
<div class="content-block text" style="margin-bottom:0">
{{ content.txt | safe }}
</div>
</div>
</div>

@ -3,10 +3,10 @@
<div class="content-block title">
{{ content.title }}
</div>
<div class="iframe__container">
<div class="content-block__video iframe__container">
{% if 'youtube.com' in content.url or 'youtu.be' in content.url %}
<iframe src="https://www.youtube.com/embed/{{ content.video_index }}" frameborder="0" allow="autoplay; encrypted-media"
allowfullscreen></iframe>
webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
{% elif 'vimeo.com' in content.url %}
<iframe src="https://player.vimeo.com/video/{{ content.video_index }}" frameborder="0" webkitallowfullscreen
mozallowfullscreen allowfullscreen>
@ -40,4 +40,4 @@
height: 360px;
}
}
</style>
</style>

@ -0,0 +1,47 @@
{% extends "templates/lilcity/index.html" %}
{% block content %}
<upload-contest-work contest-id="{{ contest.id }}"></upload-contest-work>
<div class="main main_default" {% if contest.cover %}style="background-image: url({{ contest.cover.image.url }});"{% endif %}>
<div class="main__center center">
<div class="main__title">
{{ contest.title }}
</div>
<div class="main__subtitle">
{{ contest.description }}
</div>
<div class="main__actions">
{% if not contest_work_uploaded and not contest.finished %}
<a class="main__btn btn" href=""
{% if request.user.is_authenticated %}data-show-upload-contest-work
{% else %}data-popup=".js-popup-auth"{% endif %}>Загрузить свою работу</a>
{% endif %}
</div>
</div>
</div>
<div style="text-align: center;">
{% for content in contest.content.all %}
{% with template="content/blocks/"|add:content.ctype|add:".html" %}
{% include template %}
{% endwith %}
{% endfor %}
<div class="section">
<div class="section__center center">
<a id="gallery" name="gallery">
<div class="title title_center">Галерея</div>
</a>
<div class="text">
{% if not contest_work_uploaded and not contest.finished %}
<a class="btn" href=""
{% if request.user.is_authenticated %}data-show-upload-contest-work
{% else %}data-popup=".js-popup-auth"{% endif %}>Загрузить свою работу</a>
{% endif %}
</div>
<contest-works contest-id="{{ contest.id }}" autoload="true"></contest-works>
</div>
</div>
</div>
{% endblock content %}

@ -0,0 +1,18 @@
{% extends "templates/lilcity/edit_index.html" %}
{% load static %}
{% block title %}
{% if object %}
Редактирование конкурса {{ object.title }}
{% else %}
Создание конкурса
{% endif %}
{% endblock title %}
{% block content %}
<contest-redactor {% if object and object.id %}:contest-id="{{ object.id }}"{% endif %}></contest-redactor>
{% endblock content %}
{% block foot %}
<script type="text/javascript" src="{% static 'contestRedactor.js' %}"></script>
<link rel="stylesheet" href="{% static 'contestRedactor.css' %}" />
{% endblock foot %}

@ -0,0 +1,118 @@
{% extends "templates/lilcity/index.html" %}
{% load rupluralize from plural %}
{% load static %}
{% block title %}Конкурс Lil.School{% endblock title %}
{% block description %}Работа {{ contest_work.child_full_name }}, {{ contest_work.age | rupluralize:"год,года,лет" }}{% endblock description%}
{% block ogdescription %}Работа {{ contest_work.child_full_name }}, {{ contest_work.age | rupluralize:"год,года,лет" }}{% endblock ogdescription %}
{% block ogimage %}http://{{request.META.HTTP_HOST}}{{ contest_work.image.image.url }}{% endblock ogimage %}
{% block head %}
<meta property="og:image:height" content="{{ contest_work.image.image.height }}">
<meta property="og:image:width" content="{{ contest_work.image.image.width }}">
{% endblock head %}
{% block content %}
<div class="section" style="padding-bottom: 25px;">
<div class="section__center center center_sm">
<div class="go">
<a class="go__item" href="{% url 'contest' contest_work.contest.slug %}">
<div class="go__arrow">
<svg class="icon icon-arrow-left">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-left"></use>
</svg>
</div>
<div class="go__title">Вернуться к&nbsp;галерее</div>
</a>
</div>
</div>
<div class="contest-work section__center center center_sm">
<div class="contest-work__img-wrap">
<img class="contest-work__img" src="{{ contest_work.image.image.url }}">
</div>
<div class="contest-work__info">
<div class="contest-work__bio">
<div>{{ contest_work.child_full_name }}</div>
<div class="contest-work__age">{{ contest_work.age | rupluralize:"год,года,лет" }}</div>
</div>
<div class="contest-work__likes">
<likes obj-type="contest_work" obj-id="{{ contest_work.id }}"
{% if user_liked %}:user-liked="true"{% endif %} likes="{{ likes_count }}"></likes>
</div>
</div>
</div>
</div>
<div class="section" style="padding: 0;">
<div class="section__center center center_sm">
<div class="go">
{% if prev_contest_work %}
<a class="go__item" href="{% url 'contest_work' prev_contest_work.id %}">
<div class="go__arrow">
<svg class="icon icon-arrow-left">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-left"></use>
</svg>
</div>
<div class="go__title">Предыдущая работа</div>
</a>
{% else %}
<div class="go__item"></div>
{% endif %}
{% if next_contest_work %}
<a class="go__item" href="{% url 'contest_work' next_contest_work.id %}">
<div class="go__title">Следующая работа</div>
<div class="go__arrow">
<svg class="icon icon-arrow-right">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-right"></use>
</svg>
</div>
</a>
{% else %}
<div class="go__item"></div>
{% endif %}
</div>
</div>
</div>
<div class="section">
<div class="section__center center center_sm">
{% include 'templates/blocks/share.html' with share_object_name='работой' %}
</div>
</div>
<div class="section section_gray">
<div class="section__center center center_sm">
<div class="title">Оставьте комментарий:</div>
<div class="questions">
{% if request.user.is_authenticated %}
<form class="questions__form" method="post" action="{% url 'contest_work_comment' contest_work_id=contest_work.id %}">
<input type="hidden" name="reply_id">
<div class="questions__ava ava">
<img
class="ava__pic"
{% if request.user.photo %}
src="{{ request.user.photo.url }}"
{% else %}
src="{% static 'img/user_default.jpg' %}"
{% endif %}
>
</div>
<div class="questions__wrap">
<div class="questions__reply-info">В ответ на
<a href="" class="questions__reply-anchor">этот комментарий</a>.
<a href="#" class="questions__reply-cancel grey-link">Отменить</a>
</div>
<div class="questions__field">
<textarea class="questions__textarea"></textarea>
</div>
<button class="questions__btn btn btn_light">ОТПРАВИТЬ</button>
</div>
</form>
{% else %}
<div>Только зарегистрированные пользователи могут оставлять комментарии.</div>
{% endif %}
<div class="questions__list">
{% include "templates/blocks/comments.html" with object=contest_work %}
</div>
</div>
</div>
</div>
{% endblock content %}

@ -1,2 +1,112 @@
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.template import loader
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView, DetailView
from apps.content.models import Contest, ContestWork
from apps.course.models import ContestWorkComment
@method_decorator(login_required, name='dispatch')
class ContestEditView(TemplateView):
template_name = 'content/contest_edit.html'
query_pk_and_slug = True
def get(self, request, slug=None, lesson=None):
if slug:
self.object = get_object_or_404(Contest, slug=slug)
else:
self.object = Contest()
return super().get(request)
def get_context_data(self):
context = super().get_context_data()
context['object'] = self.object
return context
class ContestView(DetailView):
model = Contest
context_object_name = 'contest'
template_name = 'content/contest.html'
query_pk_and_slug = True
def get_context_data(self, *args, **kwargs):
context = super().get_context_data()
if self.request.user.is_authenticated:
context['contest_work_uploaded'] = ContestWork.objects.filter(user=self.request.user).exists()
return context
class ContestWorkView(DetailView):
model = ContestWork
context_object_name = 'contest_work'
template_name = 'content/contest_work.html'
def get_context_data(self, *args, **kwargs):
context = super().get_context_data()
prev_contest_work = ContestWork.objects.filter(created_at__gt=self.object.created_at)[:1]
if prev_contest_work:
context['prev_contest_work'] = prev_contest_work[0]
next_contest_work = ContestWork.objects.filter(created_at__lt=self.object.created_at)[:1]
if next_contest_work:
context['next_contest_work'] = next_contest_work[0]
context['user_liked'] = self.object.likes.filter(user=self.request.user).exists() \
if self.request.user.is_authenticated else False
context['likes_count'] = self.object.likes.filter(user__is_active=True).count()
return context
@login_required
@csrf_exempt
@require_http_methods(['POST'])
def contest_work_comment(request, contest_work_id):
try:
contest_work = ContestWork.objects.get(id=contest_work_id)
except ContestWork.DoesNotExist:
return JsonResponse({
'success': False,
'errors': ['Contest_work with id f{contest_work_id} not found']
}, status=400)
else:
reply_to = request.POST.get('reply_id', 0)
comment = request.POST.get('comment', '')
if not comment:
return JsonResponse({
'success': False,
'errors': ['Comment can not be empty']
}, status=400)
if not int(reply_to):
contest_work_comment = ContestWorkComment.objects.create(
author=request.user,
content=comment,
contest_work=contest_work,
)
else:
try:
_contest_work_comment = ContestWorkComment.objects.get(id=reply_to)
except ContestWorkComment.DoesNotExist:
return JsonResponse({
'success': False,
'errors': ['LessonComment with id f{reply_to} not found']
}, status=400)
else:
contest_work_comment = ContestWorkComment.objects.create(
author=request.user,
content=comment,
contest_work=contest_work,
parent=_contest_work_comment,
)
ctx = {'node': contest_work_comment, 'user': request.user}
html = loader.render_to_string('templates/blocks/comment.html', ctx)
return JsonResponse({
'success': True,
'comment': html,
})

@ -4,7 +4,7 @@ from .models import Course
class CourseFilter(django_filters.FilterSet):
category = django_filters.CharFilter(field_name='category__title', lookup_expr='iexact')
category = django_filters.CharFilter(field_name='category')
age = django_filters.ChoiceFilter(field_name='age', choices=Course.AGE_CHOICES)
class Meta:

@ -0,0 +1,18 @@
# Generated by Django 2.0.6 on 2018-08-08 01:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0039_lesson_position'),
]
operations = [
migrations.AddField(
model_name='course',
name='age',
field=models.SmallIntegerField(choices=[(0, ''), (1, 'до 5'), (2, '5-7'), (3, '7-9'), (4, '9-12'), (5, '12-15'), (6, '15-18'), (7, 'от 18')], default=0),
),
]

@ -0,0 +1,36 @@
# Generated by Django 2.0.6 on 2018-08-13 13:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('content', '0021_auto_20180813_1306'),
('course', '0040_course_age'),
]
operations = [
migrations.CreateModel(
name='ContestWorkComment',
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')),
('contest_work', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='content.ContestWork')),
],
options={
'ordering': ('tree_id', 'lft'),
'abstract': False,
'base_manager_name': 'objects',
},
bases=('course.comment',),
),
migrations.AlterModelOptions(
name='course',
options={'ordering': ['-is_featured'], 'verbose_name': 'Курс', 'verbose_name_plural': 'Курсы'},
),
migrations.RemoveField(
model_name='course',
name='age',
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.6 on 2018-08-16 16:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0041_auto_20180813_1306'),
]
operations = [
migrations.AddField(
model_name='like',
name='ip',
field=models.GenericIPAddressField(blank=True, null=True),
),
]

@ -0,0 +1,29 @@
# Generated by Django 2.0.6 on 2018-08-24 21:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('course', '0042_like_ip'),
]
operations = [
migrations.AlterField(
model_name='course',
name='cover',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='course_covers', to='content.ImageObject', verbose_name='Обложка курса'),
),
migrations.AlterField(
model_name='lesson',
name='cover',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lesson_covers', to='content.ImageObject', verbose_name='Обложка урока'),
),
migrations.AlterField(
model_name='material',
name='cover',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='material_covers', to='content.ImageObject', verbose_name='Обложка материала'),
),
]

@ -1,9 +1,9 @@
import arrow
from random import shuffle
from uuid import uuid4
from django.db import models
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.utils.text import slugify
from django.utils.timezone import now
from django.contrib.auth import get_user_model
from django.urls import reverse_lazy
@ -12,15 +12,14 @@ from polymorphic_tree.models import PolymorphicMPTTModel, PolymorphicTreeForeign
from project.mixins import BaseModel, DeactivatedMixin
from .manager import CategoryQuerySet
from apps.content.models import ImageObject, Gallery, Video
from apps.content.models import ImageObject, Gallery, Video, ContestWork
User = get_user_model()
class Like(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
ip = models.GenericIPAddressField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
@ -83,7 +82,7 @@ class Course(BaseModel, DeactivatedMixin):
)
cover = models.ForeignKey(
ImageObject, related_name='course_covers',
verbose_name='Обложка курса', on_delete=models.CASCADE,
verbose_name='Обложка курса', on_delete=models.SET_NULL,
null=True, blank=True,
)
price = models.DecimalField(
@ -116,7 +115,7 @@ class Course(BaseModel, DeactivatedMixin):
class Meta:
verbose_name = 'Курс'
verbose_name_plural = 'Курсы'
ordering = ['-created_at']
ordering = ['-is_featured', ]
def __str__(self):
return str(self.id) + ' ' + self.title
@ -167,6 +166,24 @@ class Course(BaseModel, DeactivatedMixin):
def count_videos_in_lessons(self):
return Video.objects.filter(lesson__in=self.lessons.all()).count()
@classmethod
def shuffle(cls, qs, order_is_featured = True):
if order_is_featured:
featured = []
other = []
for c in list(qs):
if c.is_featured:
featured.append(c)
else:
other.append(c)
shuffle(featured)
shuffle(other)
return featured + other
else:
all = list(qs)
shuffle(all)
return all
class Category(models.Model):
title = models.CharField('Название категории', max_length=100)
@ -191,7 +208,7 @@ class Lesson(BaseModel, DeactivatedMixin):
)
cover = models.ForeignKey(
ImageObject, related_name='lesson_covers',
verbose_name='Обложка урока', on_delete=models.CASCADE,
verbose_name='Обложка урока', on_delete=models.SET_NULL,
null=True, blank=True,
)
created_at = models.DateTimeField(auto_now_add=True)
@ -223,7 +240,7 @@ class Material(models.Model):
title = models.CharField('Название материала', max_length=100)
cover = models.ForeignKey(
ImageObject, related_name='material_covers',
verbose_name='Обложка материала', on_delete=models.CASCADE,
verbose_name='Обложка материала', on_delete=models.SET_NULL,
null=True, blank=True,
)
short_description = models.TextField('Краткое описание материала')
@ -283,3 +300,7 @@ class LessonComment(Comment):
class Meta(Comment.Meta):
verbose_name = 'Комментарий урока'
verbose_name_plural = 'Комментарии уроков'
class ContestWorkComment(Comment):
contest_work = models.ForeignKey(ContestWork, on_delete=models.CASCADE, related_name='comments')

@ -48,7 +48,7 @@
</a>
<div class="courses__details">
<a class="courses__theme theme {{ theme_color }}"
href="{% url 'courses' %}?category={{ course.category.title }}">{{ course.category | upper }}</a>
href="{% url 'courses' %}?category={{ course.category.id }}">{{ course.category | upper }}</a>
{% if not course.is_free %}
<div class="courses__price">{{ course.price|floatformat:"-2" }}₽</div>
{% endif %}

@ -227,7 +227,7 @@
</div>
{% for content in course.content.all %}
{% with template="course/content/"|add:content.ctype|add:".html" %}
{% with template="content/blocks/"|add:content.ctype|add:".html" %}
{% include template %}
{% endwith %}

@ -175,16 +175,18 @@
<div class="lessons__list">
{% for lesson in lessons %}
<div class="lessons__item">
<div class="lessons__subtitle subtitle">{{ forloop.counter }}. {{ lesson.title }}</div>
<div class="lessons__row">
{% if lesson.cover %}
<div class="lessons__preview">
<div class="lessons__pic-wrapper">
<img class="lessons__pic" src="{{ lesson.cover.image.url }}">
<img class="lessons__pic"
src="{% if lesson.cover %}{{ lesson.cover.image_thumbnail.url }}{% else %}{% static 'img/no_cover.png' %}{% endif %}">
</div>
</div>
{% endif %}
<div class="lessons__content">{{ lesson.short_description | truncatechars_html:800 | safe | linebreaks }}</div>
<div>
<div class="lessons__subtitle subtitle">{{ forloop.counter }}. {{ lesson.title }}</div>
<div class="lessons__content">{{ lesson.short_description | truncatechars_html:800 | safe }}</div>
<a href="{% url 'lesson' pk=lesson.id %}" class="btn btn_stroke">Перейти к уроку</a>
</div>
</div>
<div class="lessons__row">
{% if course.author == request.user or request.user.role >= request.user.TEACHER_ROLE or paid %}
@ -230,31 +232,7 @@
</div>
<div class="section">
<div class="section__center center center_sm">
<div class="share">
<div class="share__title">Поделиться курсом</div>
<div class="share__list likely">
<a class="share__item facebook" href="#">
<svg class="icon icon-share-facebook">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-facebook"></use>
</svg>
</a>
<a class="share__item twitter" href="#">
<svg class="icon icon-share-twitter">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-twitter"></use>
</svg>
</a>
<a class="share__item gplus" href="#">
<svg class="icon icon-share-google">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-google"></use>
</svg>
</a>
<a class="share__item pinterest" href="#">
<svg class="icon icon-share-pinterest">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-pinterest"></use>
</svg>
</a>
</div>
</div>
{% include 'templates/blocks/share.html' with share_object_name='курсом' %}
</div>
</div>
{% endblock content %}

@ -2,7 +2,7 @@
<div class="header__title">Все курсы</div>
</a>
{% for cat in category_items %}
<a class="header__link{% if category.0 == cat.title %} active{% endif %}" data-category-name="{{ cat.title }}" href="{% url 'courses' %}?category={{ cat.title }}">
<a class="header__link{% if category == cat.id %} active{% endif %}" data-category-name="{{ cat.title }}" href="{% url 'courses' %}?category={{ cat.id }}">
<div class="header__title">{{ cat.title }}</div>
</a>
{% endfor %}

@ -27,24 +27,43 @@
{% endif %}
</div>
<div class="lesson">
<div class="lesson__subtitle subtitle">{{ lesson.title }}</div>
<div class="lesson__content">{{ lesson.short_description | safe | linebreaks }}</div>
{% comment %} <a class="lesson__video video" href="#">
{% if lesson.cover %}
<img class="video__pic" src="{{ lesson.cover.image.url }}"/>
{% else %}
{% endif %}
<svg class="icon icon-play">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-play"></use>
</svg>
</a> {% endcomment %}
<div class="lesson__row">
<div class="lesson__preview">
<div class="lesson__pic-wrapper">
<img class="lesson__pic"
src="{% if lesson.cover %}{{ lesson.cover.image_thumbnail.url }}{% else %}{% static 'img/no_cover.png' %}{% endif %}">
</div>
</div>
<div>
<div class="lesson__subtitle subtitle">{{ lesson.title }}</div>
<div class="lesson__content">{{ lesson.short_description | linebreaks }}</div>
<a href="{% url 'user' lesson.author.id %}">
<div class="lesson__user user">
{% if lesson.author.photo %}
<div class="user__ava ava">
<img class="ava__pic" src="{{ lesson.author.photo.url }}">
</div>
{% else %}
<div class="user__ava ava">
<img class="ava__pic" src="{% static 'img/user_default.jpg' %}">
</div>
{% endif %}
<div class="user__info">
<div class="user__name">{{ lesson.author.get_full_name }}</div>
<div class="user__meta">
<div class="user__date">{{ lesson.created_at_humanize }}</div>
</div>
</div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
{% for content in lesson.content.all %}
{% with template="course/content/"|add:content.ctype|add:".html" %}
{% with template="content/blocks/"|add:content.ctype|add:".html" %}
{% include template %}
{% endwith %}

@ -9,7 +9,7 @@ register = template.Library()
def category_items(category=None):
return {
'category_items': Category.objects.filter(courses__status=Course.PUBLISHED).exclude(courses=None).distinct(),
'category': category,
'category': int(category[0]) if category and category[0] else None,
}
@ -17,5 +17,5 @@ def category_items(category=None):
def category_menu_items(category=None):
return {
'category_items': Category.objects.filter(courses__status=Course.PUBLISHED).exclude(courses=None).distinct(),
'category': category,
'category': int(category[0]) if category and category[0] else None,
}

@ -241,7 +241,6 @@ class CoursesView(ListView):
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
allow_empty = self.get_allow_empty()
if request.is_ajax():
context = self.get_context_data()
template_name = self.get_template_names()
@ -283,8 +282,8 @@ class CoursesView(ListView):
context = super().get_context_data()
filtered = CourseFilter(self.request.GET)
context.update(filtered.data)
context['course_items'] = Course.shuffle(context.get('course_items'))
context['ages'] = Course.AGE_CHOICES[1:]
# context['age'] = list(map(int, context.get('age', [])))
age = context.get('age')
if age and age[0]:
age = int(age[0])

@ -0,0 +1,22 @@
from decimal import Decimal
from django.core.management.base import BaseCommand
from django.db.models import F
from apps.payment.models import Payment, AuthorBalance
class Command(BaseCommand):
help = 'Fix payment and author balance amount based on payment.data.effective_price_amount'
def handle(self, *args, **options):
for payment in Payment.objects.exclude(data__effective_price_amount=''):
if payment.data.get('effective_price_amount'):
payment.amount = Decimal(payment.data.get('effective_price_amount'))
payment.save()
for ab in AuthorBalance.objects.exclude(payment__amount=F('amount')).select_related('payment'):
ab.amount = ab.payment.amount
ab.save()

@ -1,4 +1,5 @@
import arrow
from django.db.models import Func, F
from paymentwall import Pingback
from polymorphic.models import PolymorphicModel
@ -11,7 +12,7 @@ from django.core.validators import RegexValidator
from django.utils.timezone import now
from django.conf import settings
from project.utils import weekday_in_date_range
from project.utils import weekdays_in_date_range
from apps.course.models import Course
from apps.config.models import Config
@ -109,6 +110,61 @@ class Payment(PolymorphicModel):
verbose_name_plural = 'Платежи'
ordering = ('created_at',)
@classmethod
def calc_amount(cls, course_payment=None, school_payment=None, user=None, course=None, weekdays=None):
if course_payment:
course = course_payment.course
user = course_payment.user
if school_payment:
user = school_payment.user
weekdays = school_payment.weekdays
discount = 0
price = 0
if course:
price = course.price
else:
if user:
school_payments = SchoolPayment.objects.filter(
user=user,
date_start__lte=now().date(),
date_end__gte=now().date(),
add_days=False,
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
)
school_schedules_purchased = school_payments.annotate(
joined_weekdays=Func(F('weekdays'), function='unnest', )
).values_list('joined_weekdays', flat=True).distinct()
weekdays = set(map(int, weekdays)) - set(school_schedules_purchased)
prev_school_payment = school_payments.last()
add_days = bool(prev_school_payment)
else:
add_days = False
school_schedules = SchoolSchedule.objects.filter(
weekday__in=weekdays,
)
if add_days:
weekdays_count = weekdays_in_date_range(now().date(), 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(
ss.weekday, 0)
else:
price = school_schedules.aggregate(
models.Sum('month_price'),
).get('month_price__sum', 0)
if not (school_payment and school_payment.id) and price >= config.SERVICE_DISCOUNT_MIN_AMOUNT:
discount = config.SERVICE_DISCOUNT
amount = price - discount
return {
'price': price,
'amount': amount,
'discount': discount,
}
def calc_commission(self):
return self.amount * config.SERVICE_COMMISSION / 100
@ -137,7 +193,8 @@ class CoursePayment(Payment):
verbose_name_plural = 'Платежи за курсы'
def save(self, *args, **kwargs):
self.amount = self.course.price
amount_data = Payment.calc_amount(course_payment=self)
self.amount = amount_data.get('amount')
super().save(*args, **kwargs)
author_balance = getattr(self, 'authorbalance', None)
if not author_balance:
@ -169,32 +226,8 @@ class SchoolPayment(Payment):
return days
def save(self, *args, **kwargs):
aggregate = SchoolSchedule.objects.filter(
weekday__in=self.weekdays,
).aggregate(
models.Sum('month_price'),
)
if self.add_days:
_school_payment = SchoolPayment.objects.filter(
add_days=False,
date_start__lte=self.date_start,
date_end__gte=self.date_end,
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
).last()
weekday_count = weekday_in_date_range(self.date_start, self.date_end, self.weekdays[0])
all_weekday_count = weekday_in_date_range(_school_payment.date_start, _school_payment.date_end, self.weekdays[0])
month_price_sum = aggregate.get('month_price__sum', 0) * weekday_count // all_weekday_count
else:
month_price_sum = aggregate.get('month_price__sum', 0)
if month_price_sum >= config.SERVICE_DISCOUNT_MIN_AMOUNT:
discount = config.SERVICE_DISCOUNT
else:
discount = 0
self.amount = month_price_sum - discount
amount_data = Payment.calc_amount(school_payment=self)
self.amount = amount_data.get('amount')
super().save(*args, **kwargs)
@property

@ -5,7 +5,7 @@
{% if school %}
<div class="done__title title">Вы успешно приобрели доступ к урокам онлайн-школы!</div>
<div class="done__foot">
<a class="done__btn btn btn_md btn_stroke" href="{% url 'user' request.user.id %}">ПЕРЕЙТИ К ШКОЛЕ</a>
<a class="done__btn btn btn_md btn_stroke" href="{% url 'school:school' %}">ПЕРЕЙТИ К ШКОЛЕ</a>
</div>
{% else %}
<div class="done__title title">Вы успешно приобрели курс!</div>

@ -98,7 +98,6 @@ class SchoolBuyView(TemplateView):
host = urlsplit(self.request.META.get('HTTP_REFERER'))
host = str(host[0]) + '://' + str(host[1])
weekdays = set(request.GET.getlist('weekdays', []))
add_days = 'add_days' in request.GET
roistat_visit = request.COOKIES.get('roistat_visit', None)
if not weekdays:
messages.error(request, 'Выберите несколько дней недели.')
@ -108,24 +107,30 @@ class SchoolBuyView(TemplateView):
except ValueError:
messages.error(request, 'Ошибка выбора дней недели.')
return redirect('school:summer-school')
prev_school_payment = SchoolPayment.objects.filter(
user=request.user,
date_start__lte=now().date(),
date_end__gte=now().date(),
add_days=False,
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
).first() # ??? first?
add_days = bool(prev_school_payment)
if add_days:
_school_payment = SchoolPayment.objects.get(
user=request.user,
date_start__lte=now().date(),
date_end__gte=now().date(),
add_days=False,
)
school_payment = SchoolPayment.objects.create(
user=request.user,
weekdays=weekdays,
date_start=now().date(),
date_end=_school_payment.date_end,
date_end=prev_school_payment.date_end,
add_days=True,
roistat_visit=roistat_visit,
)
if school_payment.amount <= 0:
messages.error(request, 'Выбранные дни отсутствуют в оставшемся периоде подписки')
return redirect(reverse_lazy('school:summer-school'))
return redirect(reverse_lazy('school:school'))
else:
school_payment = SchoolPayment.objects.create(
user=request.user,
@ -157,12 +162,13 @@ class SchoolBuyView(TemplateView):
@method_decorator(csrf_exempt, name='dispatch')
class PaymentwallCallbackView(View):
def add_months(self, sourcedate, months):
month = sourcedate.month - 1 + months
year = sourcedate.year + month // 12
month = month % 12 + 1
day = min(sourcedate.day, calendar.monthrange(year, month)[1])
return datetime.date(year, month, day)
def add_months(self, 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)\
or (sourcedate.month == 1 and sourcedate.day >= 29 and result.day == 28):
result = result.replace(day=1, month=result.month + 1)
return result.datetime
def get_request_ip(self):
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
@ -197,12 +203,18 @@ class PaymentwallCallbackView(View):
payment.status = pingback.get_type()
payment.data = payment_raw_data
if pingback.is_deliverable():
effective_amount = payment_raw_data.get('effective_price_amount')
if effective_amount:
payment.amount = effective_amount
transaction_to_mixpanel.delay(
payment.user.id,
payment.amount,
now().strftime('%Y-%m-%dT%H:%M:%S'),
product_type_name,
)
if product_type_name == 'school':
school_payment = SchoolPayment.objects.filter(
user=payment.user,
@ -217,17 +229,14 @@ class PaymentwallCallbackView(View):
).last()
if school_payment:
if payment.add_days:
date_start = self.add_months(sourcedate=now().replace(hour=0, minute=0, day=1), months=1)
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 = arrow.get(date_start, settings.TIME_ZONE).shift(months=1).datetime
date_end = self.add_months(date_start)
else:
#month = 0 if now().day >= 1 and now().day <= 10 else 1
# Логика июльского лагеря: до конца июля приобретаем только на текущий месяц
month = 0
date_start = self.add_months(sourcedate=now().replace(hour=0, minute=0, day=1), months=month)
date_end = arrow.get(date_start, settings.TIME_ZONE).shift(months=1, minutes=-1).datetime
date_start = now().date()
date_end = self.add_months(date_start)
payment.date_start = date_start
payment.date_end = date_end
if product_type_name == 'course':
@ -252,6 +261,7 @@ class PaymentwallCallbackView(View):
'update_at': payment.update_at,
}
payment.save()
product_payment_to_mixpanel.delay(
payment.user.id,
f'{product_type_name.title()} payment',
@ -269,6 +279,7 @@ class PaymentwallCallbackView(View):
product_type_name,
payment.roistat_visit,
)
author_balance = getattr(payment, 'author_balance', None)
if author_balance and author_balance.type == AuthorBalance.IN:
if pingback.is_deliverable():
@ -277,7 +288,6 @@ class PaymentwallCallbackView(View):
payment.author_balance.status = AuthorBalance.PENDING
else:
payment.author_balance.status = AuthorBalance.DECLINED
payment.author_balance.save()
return HttpResponse('OK')
else:

@ -0,0 +1,20 @@
# Generated by Django 2.0.6 on 2018-08-24 19:56
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('content', '0022_auto_20180815_2129'),
('school', '0018_auto_20180629_1501'),
]
operations = [
migrations.AddField(
model_name='schoolschedule',
name='cover',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='schoolschedule_cover', to='content.ImageObject', verbose_name='Обложка дня'),
),
]

@ -0,0 +1,29 @@
# Generated by Django 2.0.6 on 2018-08-24 21:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('school', '0019_schoolschedule_cover'),
]
operations = [
migrations.AlterField(
model_name='livelesson',
name='cover',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='livelesson_covers', to='content.ImageObject', verbose_name='Обложка урока школы'),
),
migrations.AlterField(
model_name='schoolschedule',
name='cover',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schoolschedule_cover', to='content.ImageObject', verbose_name='Обложка дня'),
),
migrations.AlterField(
model_name='schoolscheduleimage',
name='img',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='schoolschedule_images', to='content.ImageObject', verbose_name='Объект изображения'),
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.6 on 2018-08-27 21:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('school', '0020_auto_20180824_2132'),
]
operations = [
migrations.AddField(
model_name='schoolschedule',
name='trial_lesson',
field=models.URLField(blank=True, default=''),
),
]

@ -1,9 +1,10 @@
import arrow
from datetime import datetime, timedelta
from datetime import datetime, timedelta, date
from django.conf import settings
from django.db import models
from django.urls import reverse_lazy
from django.utils.functional import cached_property
from django.utils.timezone import now
from project.mixins import BaseModel, DeactivatedMixin
@ -37,6 +38,15 @@ class SchoolSchedule(models.Model):
'Скидка, в валюте', max_digits=8, decimal_places=2, default=0
)
start_at = models.TimeField('Начало урока', null=True)
cover = models.ForeignKey(
ImageObject,
related_name='schoolschedule_cover',
verbose_name='Обложка дня',
on_delete=models.SET_NULL,
null=True,
blank=True,
)
trial_lesson = models.URLField(default='', blank=True)
class Meta:
ordering = ('weekday',)
@ -50,15 +60,18 @@ class SchoolSchedule(models.Model):
end_at = datetime.combine(now().today(), self.start_at) + timedelta(hours=1)
return self.start_at <= now().time() and end_at.time() >= now().time() and self.weekday == now().isoweekday()
@cached_property
def current_live_lesson(self):
now_time = now()
september2018 = date(2018, 9, 1)
date_start = max(september2018, now().date())
live_lesson = LiveLesson.objects.filter(
date__week_day=self.weekday % 7 + 1,
date__range=[now_time.date(), (now_time + timedelta(days=6)).date()],
date__range=[date_start, date_start + timedelta(days=6)],
deactivated_at__isnull=True,
).first()
return live_lesson
@cached_property
def previous_live_lesson(self):
now_time = now()
live_lesson = LiveLesson.objects.filter(
@ -84,7 +97,7 @@ class SchoolScheduleImage(models.Model):
ImageObject,
related_name='schoolschedule_images',
verbose_name='Объект изображения',
on_delete=models.CASCADE,
on_delete=models.SET_NULL,
null=True,
blank=True,
)
@ -107,7 +120,7 @@ class LiveLesson(BaseModel, DeactivatedMixin):
ImageObject,
related_name='livelesson_covers',
verbose_name='Обложка урока школы',
on_delete=models.CASCADE,
on_delete=models.SET_NULL,
null=True,
blank=True,
)

@ -1,7 +1,5 @@
<a
{% if not user.is_authenticated %}
data-popup=".js-popup-auth"
{% endif %}
data-popup=".js-popup-buy"
class="timing__btn btn"
href="{% url 'school-checkout' %}?weekdays={{ school_schedule.weekday }}&add_days=true"
data-day="{{ school_schedule.weekday }}"
>купить</a>

@ -1,7 +1,7 @@
{% load static %}
<a class="online" target="_blank" href="https://www.youtube.com/watch?v=PhZ8qQbIej0" style="background-image: url({% static 'img/video-1.jpg' %});">
<div class="online__center center">
<div class="online__type">ПРЯМОЙ ЭФИР</div>
<div class="online__type">ВИДЕОУРОКИ</div>
<div class="online__title">Каждый день в 17.00 (по Мск) </div>
<div class="online__text text">Кроме выходных. Запись эфира доступна по завершению трансляции</div>
<div class="online__action">

@ -17,7 +17,7 @@
<use xlink:href="{% static 'img/sprite.svg' %}#icon-online"></use>
</svg>
</div>
<div class="school__title">Прямой эфир</div>
<div class="school__title">Видеоуроки</div>
<div class="school__text">С понедельника по&nbsp;пятницу кроме выходных</div>
</div>
<div class="school__col">

@ -10,7 +10,7 @@
{% endfor %}
</div>
<div class="timing__foot">
<!--{% include './pay_btn.html' %}-->
{# include './pay_btn.html' #}
<a target="_blank" class="timing__print" href="{% url 'school:school_schedules-print' %}">Распечатать расписание
<span class="bold">чтобы не забыть</span>
<svg class="icon icon-print">

@ -1,23 +1,27 @@
{% load static %} {% load thumbnail %}
<div class="timing__item{% if school_schedule.weekday > 5 %} timing__item_bg{% endif %} js-timing-item js-timing-toggle {% if print %}open{% endif %} {% if purchased and not school_schedule.weekday in school_schedules_purchased %} disable{% endif %}">
<div class="timing__item{% if school_schedule.weekday > 5 %} timing__item_bg{% endif %} js-timing-item js-timing-toggle {% if print %}open{% endif %} {% if is_purchased and not school_schedule.weekday in school_schedules_purchased %} disable{% endif %}">
<div class="timing__cell timing__cell--info">
<div class="timing__info">
<div class="timing__day{% if school_schedule.is_online %} active{% endif %}">
{{ school_schedule }}
{% if request.user_agent.is_mobile and school_schedule.trial_lesson %}
<a class="timing__trial-lesson js-video-modal" href="#" data-video-url="{{ school_schedule.trial_lesson }}">Пробный урок</a>
{% endif %}
</div>
{% if purchased and live_lesson %}
<div class="timing__date">{{ live_lesson.date }}</div>
{% if is_purchased and live_lesson %}
<div class="timing__date">{% if request.user_agent.is_mobile %}{{ live_lesson.date|date:"j b" }}{% else %}{{ live_lesson.date|date:"j E" }}{% endif %}</div>
{% endif %}
<div class="timing__time">{{ school_schedule.start_at }} (МСК)</div>
<div class="timing__buy">
{% if purchased %}
{% if school_schedule.weekday in school_schedules_purchased %}
{% if live_lesson and live_lesson.title %}
{% include './open_lesson.html' %}
{% endif %}
{% else %}
{% include './day_pay_btn.html' %}
{% if is_purchased and school_schedule.weekday in school_schedules_purchased %}
{% if live_lesson and live_lesson.title %}
{% include './open_lesson.html' %}
{% endif %}
{% else %}
{% include './day_pay_btn.html' %}
{% endif %}
{% if not request.user_agent.is_mobile and school_schedule.trial_lesson %}
<a class="timing__trial-lesson js-video-modal" href="#" data-video-url="{{ school_schedule.trial_lesson }}">Пробный урок</a>
{% endif %}
</div>
</div>
@ -31,11 +35,15 @@
<div style="display: flex; width: 100%;">
<div class="timing__cell timing__cell--preview">
<div class="timing__preview">
{% comment %}
{% thumbnail live_lesson.cover.image "70x70" crop="center" as im %}
<img class="timing__pic" src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}" />
{% empty %}
<img class="timing__pic" src="{% static 'img/no_cover.png' %}" style="width: 70px; height: 70px;" />
{% endthumbnail %}
{% endcomment %}
<img class="timing__pic"
src="{% if school_schedule.cover %}{{ school_schedule.cover.image_thumbnail.url }}{% else %}{% static 'img/no_cover.png' %}{% endif %}"
style="width: 70px; height: 70px;" />
{% comment %}{% endthumbnail %}{% endcomment %}
</div>
</div>
<div class="timing__cell timing__cell--content" {% if print %}style="flex: 1 0 0;"{% endif %}>
@ -44,11 +52,13 @@
{% endif %}
</div>
<div class="timing__content">
{% comment %}
{% if live_lesson and live_lesson.short_description %}
{{ live_lesson.short_description }}
{% else %}
{% endcomment %}
{{ school_schedule.description }}
{% endif %}
{% comment %}{% endif %}{% endcomment %}
</div>
<div class="timing__more">
<div class="timing__head">Материалы</div>

@ -7,28 +7,26 @@
<div class="section__center center center_sm">
<div class="lesson">
<div class="lesson__subtitle subtitle">{{ livelesson.title }}</div>
<div class="lesson__content">{{ livelesson.short_description }}</div>
<a class="lesson__video video" href="#">
<div class="lesson__content">{{ livelesson.short_description | linebreaks }}</div>
<div class="lesson__video video">
{% if livelesson.stream_index %}
<iframe class="lesson__video_frame" src="https://player.vimeo.com/video/{{ livelesson.stream_index }}?autoplay=1" frameborder="0" webkitallowfullscreen
mozallowfullscreen allowfullscreen>
</iframe>
<a href="#" onclick="location.reload();">Если видео не загрузилось обновите страницу</a>
<span>Если видео не загрузилось, - уменьшите качество видео или <a href="#" onclick="location.reload();">обновите страницу</a></span>
<iframe class="lesson__chat_frame" src="https://vimeo.com/live-chat/{{ livelesson.stream_index }}" frameborder="0"></iframe>
{% else %}
{% if livelesson.cover %}
<img class="video__pic" src="{{ livelesson.cover.image.url }}"/>
{% else %}
{% endif %}
{% if livelesson.cover %}
<img class="video__pic" src="{{ livelesson.cover.image.url }}"/>
{% endif %}
{% endif %}
</a>
</div>
</div>
</div>
</div>
{% 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 %}

@ -9,7 +9,7 @@
<div class="kit__body">
<div class="lessons__list">
{% for livelesson in livelesson_list %}
<div class="lessons__item" v-for="(lesson, index) in lessons">
<div class="lessons__item">
<div class="lessons__actions lessons__actions__no-hover">
<a target="_blank" class="lessons__action" href="{% url 'school:lesson-detail' livelesson.id %}">
<svg class="icon icon-eye">

@ -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 }}"
>купить</a>

@ -1,9 +1,9 @@
{% load static %}
<a class="online" target="_blank" href="https://www.youtube.com/watch?v=PhZ8qQbIej0" style="background-image: url({% static 'img/video-1.jpg' %});">
<div class="online__center center">
<div class="online__type">ПРЯМОЙ ЭФИР</div>
<div class="online__type">ВИДЕОУРОКИ</div>
<div class="online__title">В 17.00 (по Мск) </div>
<div class="online__text text">Понедельник, среда, пятница.</div>
<div class="online__text text">Каждый день с 1 сентября</div>
<div class="online__action">
<svg class="icon icon-play">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-play"></use>

@ -5,21 +5,17 @@
<span class="main__bold">Lil School</span> — первая образовательная онлайн-платформа креативного мышления для детей
</div>
<div class="main__subtitle">
Присоединяйтесь в Рисовальный лагерь
Старт школы - 1 сентября
</div>
<div class="main__actions">
<a
{% if not is_purchased_future %}
{% if not user.is_authenticated %}
data-popup=".js-popup-auth"
{% else %}
data-popup=".js-popup-buy"
{% endif %}
{% endif %}
class="main__btn btn"
href="#"
>
{% if not is_purchased and not is_purchased_future %}Получить доступ{% endif %}
{% if not is_purchased and not is_purchased_future %}Купить доступ от {{ min_school_price }} руб./месяц{% endif %}
{% if is_purchased_future and not is_purchased %}ваша подписка начинается {{school_purchased_future.date_start}}{% endif %}
{% if is_purchased %}ваша подписка истекает {{ subscription_ends_humanize }}<br/>перейти к оплате{% endif %}
</a>
@ -32,8 +28,8 @@
<use xlink:href="{% static 'img/sprite.svg' %}#icon-online"></use>
</svg>
</div>
<div class="school__title">Прямой эфир</div>
<div class="school__text">Понедельник, среда, пятница</div>
<div class="school__title">Видеоуроки</div>
<div class="school__text">Каждый день с 1 сентября</div>
</div>
<div class="school__col">
<div class="school__preview">
@ -50,7 +46,7 @@
<use xlink:href="{% static 'img/sprite.svg' %}#icon-subjects"></use>
</svg>
</div>
<div class="school__title">12 уроков</div>
<div class="school__title">7 дисциплин</div>
<div class="school__text">В разных техниках</div>
</div>
<div class="school__col">

@ -26,14 +26,12 @@
<div class="casing__title title">Новые уроки</div>
{% endif %}
{% comment %}
<label class="casing__switcher switcher">
<span class="switcher__wrap">
<a href="{% url 'school:summer-school' %}?is_previous=true" class="switcher__item{% if is_previous %} active{% endif %}">запись уроков</a>
<a href="{% url 'school:summer-school' %}" class="switcher__item{% if not is_previous %} active{% endif %}">новые уроки</a>
<a href="{% url 'school:school' %}?is_previous=true" class="switcher__item{% if is_previous %} active{% endif %}">запись уроков</a>
<a href="{% url 'school:school' %}" class="switcher__item{% if not is_previous %} active{% endif %}">новые уроки</a>
</span>
</label>
{% endcomment %}
</div>
{% endif %}
<div class="casing__timing timing js-timing">

@ -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'),

@ -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

@ -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',

@ -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),
),
]

@ -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()

@ -150,7 +150,18 @@
{% for error in form.about.errors %}
<div class="field__error">{{ error }}</div>
{% endfor %}
</div>
</div>
{% if is_teacher %}
<div class="form__field field{% if form.trial_lesson.errors %} error{% endif %}">
<div class="field__label">Пробный урок</div>
<div class="field__wrap">
<input name='trial_lesson' class="field__input" type="url" value="{{ user.trial_lesson }}">
</div>
{% for error in form.trial_lesson.errors %}
<div class="field__error">{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
<div class="form__group">
<div class="form__title">Пароль</div>

@ -64,7 +64,7 @@
<div class="section__center center">
<div class="tabs js-tabs">
<div class="tabs__nav">
{# <button class="tabs__btn js-tabs-btn active">ЛАГЕРЬ</button> #}
<button class="tabs__btn js-tabs-btn active">ОНЛАЙН-ШКОЛА</button>
<button class="tabs__btn js-tabs-btn">ПРИОБРЕТЕННЫЕ
<span class="mobile-hide">КУРСЫ</span>
</button>
@ -75,7 +75,6 @@
{% endif %}
</div>
<div class="tabs__container">
{% comment %}
<div class="tabs__item js-tabs-item" style="display: block;">
{% if is_purchased_future %}
<div class="center center_xs">
@ -89,7 +88,7 @@
{% else %}
<div class="center center_xs">
<div class="done">
<div class="done__title title">Вы не подписаны на лагерь!</div>
<div class="done__title title">Вы не подписаны на онлайн-школу!</div>
<div class="done__foot">
<a
{% if not user.is_authenticated %}
@ -106,8 +105,7 @@
{% endif %}
{% endif %}
</div>
{% endcomment %}
<div class="tabs__item js-tabs-item" style="display: block;">
<div class="tabs__item js-tabs-item">
<div class="courses courses_scroll">
<div class="courses__list">
{% if paid.exists %}

@ -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:

@ -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/

@ -12,8 +12,8 @@
<use xlink:href="{% static 'img/sprite.svg' %}#icon-online"></use>
</svg>
</div>
<div class="school__title">Прямой эфир</div>
<div class="school__text">Понедельник, среда, пятница</div>
<div class="school__title">Видеоуроки</div>
<div class="school__text">Каждый день с 1 сентября</div>
</div>
<div class="school__col">
<div class="school__preview">
@ -30,7 +30,7 @@
<use xlink:href="{% static 'img/sprite.svg' %}#icon-subjects"></use>
</svg>
</div>
<div class="school__title">12 уроков</div>
<div class="school__title">7 дисциплин</div>
<div class="school__text">В разных техниках</div>
</div>
<div class="school__col">
@ -43,22 +43,6 @@
<div class="school__text">Хранится 7 дней</div>
</div>
</div>
<div class="letsgo">
{% comment %}
{% if not is_purchased and not is_purchased_future %}
<a
{% if not user.is_authenticated %}
data-popup=".js-popup-auth"
{% else %}
data-popup=".js-popup-buy"
{% endif %}
class="main__btn btn"
href="#"
>
купить доступ от {{ min_school_price }} руб./месяц
</a>
{% endif %}
{% endcomment %}
</div>
<div class="letsgo"></div>
</div>
</div>

@ -1,5 +1,5 @@
{% load static %}
{% if not node.deactivated_at %}
{% if not node.deactivated_at and node.author.is_active %}
<a class="questions__anchor" id="question__{{ node.id }}"></a>
<div id="question__replyto__{{ node.id }}" class="questions__item {% if node.is_child_node %}questions__item_reply{% endif %}">
{% if node.author.photo %}

@ -19,7 +19,6 @@
<div class="footer__col">
<div class="footer__title">Программы</div>
<nav class="footer__nav">
<a class="footer__link" href="{% url 'school:summer-school' %}">Лагерь</a>
<a class="footer__link" href="{% url 'school:school' %}">Онлайн-школа</a>
<a class="footer__link" href="{% url 'courses' %}">Онлайн-курсы</a>
<a class="footer__link" href="{% url 'author_request' %}">Стать автором</a>

@ -25,19 +25,10 @@
</form>
</div>
<nav class="header__nav">
{% if is_summer_school_purchased %}
<div class="header__group">
<a class="header__section {% active_link 'school:summer-school' %}" href="{% url 'school:summer-school' %}?is_previous=true">
ЛАГЕРЬ {% if online or livelesson.is_online %}
<div class="header__dot"></div>
{% endif %}
</a>
</div>
{% endif %}
<div class="header__group">
<a class="header__section {% active_link 'school:school' %}" href="{% url 'school:school' %}">
ОНЛАЙН-ШКОЛА {% if online or livelesson.is_online %}
<!--<div class="header__dot"></div>-->
<div class="header__dot"></div>
{% endif %}
</a>
</div>

@ -2,7 +2,7 @@
<div class="section section_courses">
<div class="section__center center">
<div class="title title_center">Видео-курсы без расписания</div>
<div class="text text_courses">Если вам не совсем удобно заниматься с нами в прямом эфире каждый день как в нашей онлайн-школе, специально для вас мы
<div class="text text_courses">Если вам не совсем удобно заниматься с нами каждый день в нашей онлайн-школе, специально для вас мы
делаем отдельные уроки в записи, которые вы можете проходить когда вам будем удобно.<br><br>
Учите и развивайте креативное мышление когда и где угодно
</div>

@ -0,0 +1,14 @@
{% load static %}
<script>
window.LIL_STORE = {
staticUrl: '{% static "" %}',
accessToken: '{{ request.user.auth_token }}',
isMobile: {{ request.user_agent.is_mobile|yesno:"true,false" }},
user: {
id: '{{ request.user.id|default:'' }}',
},
urls: {
courses: "{% url 'courses' %}"
}
};
</script>

@ -1,9 +1,9 @@
{% load static %}
<a class="online" target="_blank" href="https://www.youtube.com/watch?v=PhZ8qQbIej0" style="background-image: url({% static 'img/video-1.jpg' %});">
<div class="online__center center">
<div class="online__type">ПРЯМОЙ ЭФИР</div>
<div class="online__type">ВИДЕОУРОКИ</div>
<div class="online__title">В 17.00 (по Мск) </div>
<div class="online__text text">Понедельник, среда, пятница.</div>
<div class="online__text text">Каждый день с 1 сентября</div>
<div class="online__action">
<svg class="icon icon-play">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-play"></use>

@ -1,5 +1,5 @@
{% if messages %}
<div class="section section_gray section_menu">
<div class="section section_gray section_menu" style="margin-bottom: 20px;">
<div class="section__center center center_xs">
{% for message in messages %}
<div class="message message_{{ message.tags }}">{{ message }}</div>

@ -4,13 +4,30 @@
<a name="partners">
<div class="title title_center">Наши партнеры</div>
</a>
<div class="text">
Скидка 15% по промокоду у наших партнеров
</div>
<div class="partners">
<a target="_blank" class="partners__item" href="https://www.mann-ivanov-ferber.ru/tag/sasha-kru/">
<img class="partners__pic" src="{% static 'img/mif.jpg' %}">
</a>
<a target="_blank" class="partners__item" href="https://artkvartal.ru">
<img class="partners__pic" src="{% static 'img/artkvartal.png' %}">
</a>
<a target="_blank" class="partners__item" href="https://www.mann-ivanov-ferber.ru/tag/sasha-kru/">
<img class="partners__pic" src="{% static 'img/mif.jpg' %}">
<a target="_blank" class="partners__item" href="http://www.pinkbus.ru/">
<img class="partners__pic" src="{% static 'img/pinkbus.jpg' %}">
</a>
</div>
<div class="partners partners_promocodes">
<div class="partners__item">
<div class="partners__item-promocode">LIL</div>
</div>
<div class="partners__item">
<div class="partners__item-promocode">LILCITY</div>
</div>
<div class="partners__item">
<div class="partners__item-promocode">LILCITY</div>
</div>
</div>
</div>
</div>

@ -3,7 +3,7 @@
<div class="popup__wrap js-popup-wrap">
<button class="popup__close js-popup-close">
<svg class="icon icon-close">
<use xlink:href={% static "img/sprite.svg" %}#icon-close></use>
<use xlink:href="{% static 'img/sprite.svg' %}#icon-close"></use>
</svg>
</button>
<div class="popup__body">
@ -37,10 +37,10 @@
<div class="field__password-show">
<button class="password-toggle" type="button">
<svg class="icon icon-password-eye">
<use xlink:href={% static "img/sprite.svg" %}#icon-password-eye></use>
<use xlink:href="{% static 'img/sprite.svg' %}#icon-password-eye"></use>
</svg>
<svg class="icon icon-password-hidden-eye">
<use xlink:href={% static "img/sprite.svg" %}#icon-password-hidden-eye></use>
<use xlink:href="{% static 'img/sprite.svg' %}#icon-password-hidden-eye"></use>
</svg>
</button>
</div>
@ -53,7 +53,7 @@
<div class="auth__or">или</div>
<button type="button" class="auth__btn btn btn_fb">
<svg class="icon icon-facebook">
<use xlink:href={% static "img/sprite.svg" %}#icon-facebook></use>
<use xlink:href="{% static 'img/sprite.svg' %}#icon-facebook"></use>
</svg>
<span class="btn__title">ЧЕРЕЗ FACEBOOK</span>
</button>
@ -96,10 +96,10 @@
<div class="field__password-show">
<button class="password-toggle" type="button">
<svg class="icon icon-password-eye">
<use xlink:href={% static "img/sprite.svg" %}#icon-password-eye></use>
<use xlink:href="{% static 'img/sprite.svg' %}#icon-password-eye"></use>
</svg>
<svg class="icon icon-password-hidden-eye">
<use xlink:href={% static "img/sprite.svg" %}#icon-password-hidden-eye></use>
<use xlink:href="{% static 'img/sprite.svg' %}#icon-password-hidden-eye"></use>
</svg>
</button>
</div>
@ -122,7 +122,7 @@
<div class="auth__or">или</div>
<button type="button" class="auth__btn btn btn_fb">
<svg class="icon icon-facebook">
<use xlink:href={% static "img/sprite.svg" %}#icon-facebook></use>
<use xlink:href="{% static 'img/sprite.svg' %}#icon-facebook"></use>
</svg>
<span class="btn__title">ЧЕРЕЗ FACEBOOK</span>
</button>

@ -29,7 +29,10 @@
data-day="{{school_schedule.weekday}}"
data-price="{{school_schedule.month_price}}"
autocomplete="off"
{% if school_schedule.weekday in school_schedules_purchased or not is_purchased %}
{% if school_schedule.weekday in school_schedules_purchased %}
disabled
{% endif %}
{% if not is_purchased %}
checked
{% endif %}>
<span class="switch__content">
@ -37,6 +40,15 @@
{% comment %} dont delete {% endcomment %}
<span class="switch__cell"></span>
<span class="switch__cell">{{ school_schedule.title }}</span>
<span class="buy__trial-lesson switch__cell">
{% if school_schedule.weekday in school_schedules_purchased %}
Куплено
{% else %}
{% if school_schedule.trial_lesson %}
<a class="js-video-modal" data-video-url="{{ school_schedule.trial_lesson }}" href="">Пробный урок</a>
{% endif %}
{% endif %}
</span>
<span class="switch__cell">{{school_schedule.month_price}}р в мес.</span>
</span>
</label>
@ -50,7 +62,10 @@
data-day="{{school_schedule.weekday}}"
data-price="{{school_schedule.month_price}}"
autocomplete="off"
{% if school_schedule.weekday in school_schedules_purchased or not is_purchased %}
{% if school_schedule.weekday in school_schedules_purchased %}
disabled
{% endif %}
{% if not is_purchased %}
checked
{% endif %}>
<span class="switch__content">
@ -58,6 +73,15 @@
{% comment %} dont delete {% endcomment %}
<span class="switch__cell"></span>
<span class="switch__cell">{{ school_schedule.title }}</span>
<span class="buy__trial-lesson switch__cell">
{% if school_schedule.weekday in school_schedules_purchased %}
Куплено
{% else %}
{% if school_schedule.trial_lesson %}
<a class="js-video-modal" data-video-url="{{ school_schedule.trial_lesson }}" href="">Пробный урок</a>
{% endif %}
{% endif %}
</span>
<span class="switch__cell">{{school_schedule.month_price}}р в мес.</span>
</span>
</label>
@ -78,18 +102,18 @@
</div>
<div class="order__info">
<div class="order__label">ШКОЛА</div>
<div class="order__days">Вторник, Четверг, Воскресенье</div>
<div class="order__days"></div>
</div>
<div class="order__foot">
<div class="order__subtitle">Итого, за месяц:</div>
<div class="order__total order_price_text">1800р.</div>
<div class="order__total order_price_text"></div>
</div>
</div>
</div>
</div>
</div>
<div class="buy__foot">
<a class="buy__btn btn btn_md but_btn_popup" data-link="{% url 'school-checkout' %}" href="{% url 'school-checkout' %}?weekdays=1&weekdays=2">ПЕРЕЙТИ К ОПЛАТЕ</a>
<a class="buy__btn btn btn_md but_btn_popup" data-link="{% url 'school-checkout' %}">ПЕРЕЙТИ К ОПЛАТЕ</a>
</div>
</div>
</div>

@ -10,20 +10,20 @@
<div class="auth js-auth" style="padding:20px 20px 25px;">
<div class="auth__login js-auth-login">
<div class="auth__body">
<form id="learner-auth-form" method="post" action="{% url 'subscribe' %}">
<form {# COMMENTED id="learner-auth-form" #} method="post" action="{% url 'subscribe' %}">
{% csrf_token %}
<div class="auth__tab js-auth-tab" style="display: block;">
<div class="auth__enter js-auth-enter">
<div id="learner-auth-field__username" class="auth__field field learner-auth-form__field">
<div {# COMMENTED id="learner-auth-field__username" #} class="auth__field field learner-auth-form__field">
<div class="field__label">ПОЧТА</div>
<div class="field__wrap"><input id="learner-auth-form__email" class="field__input" type="email"
<div class="field__wrap"><input {# COMMENTED id="learner-auth-form__email" #} class="field__input" type="email"
name="email"
placeholder="name@website.com" tabindex="1"></div>
<div id="learner-auth-field-error__username"
class="field__error learner-auth-form__field-error"></div>
</div>
<div class="auth__foot">
<button id="learner-auth__button" class="auth__btn btn btn_light" tabindex="3">Подписаться</button>
<button {# COMMENTED id="learner-auth__button" #} class="auth__btn btn btn_light" tabindex="3">Подписаться</button>
</div>
</div>
</div>
@ -33,4 +33,4 @@
</div>
</div>
</div>
</div>
</div>

@ -11,22 +11,23 @@
<div class="main__title">
<span class="main__bold">Lil School</span> — первая образовательная онлайн-платформа креативного мышления для детей
</div>
{% if False and user.is_authenticated and online %}
{% if user.is_authenticated and online %}
<div class="main__content">
Сейчас идёт прямой эфир урока «{{ school_schedule.title }}, {{ school_schedule.current_live_lesson.title }}»
</div>
<div class="main__actions">
<a
{% if not is_purchased %}
{% if not school_schedule.weekday in school_schedules_purchased %}
data-popup=".js-popup-buy"
data-day="{{ school_schedule.weekday }}"
href='#'
{% else %}
href="{{ school_schedule.current_live_lesson.get_absolute_url }}"
href="{% url 'school:school' %}"
{% endif %}
class="main__btn btn"
>{% if not is_purchased %}Получить доступ{% else %}Смотреть урок{% endif %}</a>
>{% if not school_schedule.weekday in school_schedules_purchased %}Получить доступ{% else %}Смотреть урок{% endif %}</a>
</div>
{% elif False and user.is_authenticated and online_coming_soon and school_schedule and school_schedule.start_at_humanize %}
{% elif user.is_authenticated and online_coming_soon and school_schedule and school_schedule.start_at_humanize %}
<div class="">
Урок «{{ school_schedule.title }}, {{ school_schedule.current_live_lesson.title }}» начнётся
</div>
@ -35,40 +36,32 @@
</div>
<div class="main__actions">
<a
{% if not is_purchased %}
{% if not school_schedule.weekday in school_schedules_purchased %}
data-popup=".js-popup-buy"
data-day="{{ school_schedule.weekday }}"
href='#'
{% else %}
href="{{ school_schedule.current_live_lesson.get_absolute_url }}"
href="{% url 'school:school' %}"
{% endif %}
class="main__btn btn"
>{% if not is_purchased %}Получить доступ{% else %}Смотреть урок{% endif %}</a>
>{% if not school_schedule.weekday in school_schedules_purchased %}Получить доступ{% else %}Смотреть урок{% endif %}</a>
</div>
{% else %}
<div class="main__subtitle">
{# Присоединяйтесь в Рисовальный лагерь #}
Приглашаем вас на месяц открытых дверей в Lil School
Приглашаем вас присоединиться к онлайн-школе с 1 сентября!
</div>
<div class="main__actions">
{% comment %}
{% if not is_purchased and not is_purchased_future %}
<a
{% if not is_purchased_future %}
{% if not user.is_authenticated %}
data-popup=".js-popup-auth"
{% else %}
data-popup=".js-popup-buy"
{% endif %}
{% endif %}
class="main__btn btn"
href="#"
>
Получить доступ
купить доступ от {{ min_school_price }} руб./месяц
</a>
{% else %}
<a class="main__btn btn btn_white" href="{% url 'school:school' %}">Подробнее</a>
{% endif %}
{% endcomment %}
{# <a class="main__btn btn btn_white" href="{% url 'school:summer-school' %}">О лагере</a> #}
<a class="main__btn btn btn_white" href="{% url 'course' pk=50 %}">Подробнее</a>
</div>
{% endif %}
</div>

@ -1,27 +1,14 @@
{% load static %}
<div class="share">
<div class="share__title">Поделиться {% if livelesson or lesson %}уроком{% else %}курсом{% endif %}</div>
<div class="share__list likely">
<a class="share__item facebook" href="#" data-url="http://{{request.META.HTTP_HOST}}{{object.get_absolute_url}}">
<svg class="icon icon-share-facebook">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-facebook"></use>
</svg>
</a>
<a class="share__item twitter" href="#" data-url="http://{{request.META.HTTP_HOST}}{{object.get_absolute_url}}">
<svg class="icon icon-share-twitter">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-twitter"></use>
</svg>
</a>
<a class="share__item gplus" href="#" data-url="http://{{request.META.HTTP_HOST}}{{object.get_absolute_url}}">
<svg class="icon icon-share-google">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-google"></use>
</svg>
</a>
<a class="share__item pinterest" href="#" data-url="http://{{request.META.HTTP_HOST}}{{object.get_absolute_url}}">
<svg class="icon icon-share-pinterest">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-pinterest"></use>
</svg>
</a>
<div class="share__title">Поделиться {% if share_object_name %}{{ share_object_name }}{% else %}{% if livelesson or lesson %}уроком{% else %}курсом{% endif %}{% endif %}</div>
<div class="share__list likely likely-big" data-url="http://{{request.META.HTTP_HOST}}{{object.get_absolute_url}}">
<div class="twitter"></div>
<div class="facebook"></div>
<div class="gplus"></div>
<div class="vkontakte"></div>
<div class="telegram"></div>
<div class="pinterest"></div>
</div>
</div>

@ -17,10 +17,15 @@
</div>
<div class="teachers__wrap">
<div class="teachers__title">
<a href="{% url 'user' teacher.id %}">{{ teacher.get_full_name }}</a>{% if teacher.instagram_hashtag %},
<a href='https://www.instagram.com/explore/tags/{{ teacher.instagram_hashtag }}/' target="_blank">
{{ teacher.instagram_hashtag }}
</a>
<div class="teachers__title-name">
<a href="{% url 'user' teacher.id %}">{{ teacher.get_full_name }}</a>{% if teacher.instagram_hashtag %},
<a href='https://www.instagram.com/explore/tags/{{ teacher.instagram_hashtag }}/' target="_blank">
{{ teacher.instagram_hashtag }}
</a>
{% endif %}
</div>
{% if teacher.trial_lesson %}
<a data-video-url="{{ teacher.trial_lesson }}" href="#" class="btn btn_light js-video-modal">ПРОБНЫЙ УРОК</a>
{% endif %}
</div>
<div class="teachers__social">
@ -30,13 +35,13 @@
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-facebook"></use>
</svg>
</a>
{% endif %} {% if teacher.instagram %}
{% endif %} {# if teacher.instagram #}
<a class="social__item" href="{{ teacher.instagram }}" target="_blank">
<svg class="icon icon-share-instagram">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-instagram"></use>
</svg>
</a>
{% endif %} {% if teacher.twitter %}
{# endif #} {% if teacher.twitter %}
<a class="social__item" href="{{ teacher.twitter }}" target="_blank">
<svg class="icon icon-share-twitter">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-share-twitter"></use>

@ -315,6 +315,7 @@
</div>
</div>
</div>
{% include 'templates/blocks/lil_store_js.html' %}
<script type="text/javascript" src={% static "app.js" %}></script>
<script>
var schoolDiscount = parseFloat({{ config.SERVICE_DISCOUNT }});

@ -38,6 +38,24 @@
{% endcompress %}
<link rel="shortcut icon" type="image/png" href="{% static 'img/favicon.png' %}"/>
<!-- Start of LiveChat (www.livechatinc.com) code -->
<script type="text/javascript">
window.__lc = window.__lc || {};
window.__lc.license = 9917240;
{% if request.user.is_authenticated %}
window.__lc.visitor = { name: '{{ request.user.get_full_name }}', email: '{{ request.user.email }}' };
{% endif %}
(function() {
var lc = document.createElement('script'); lc.type = 'text/javascript'; lc.async = true;
lc.src = ('https:' == document.location.protocol ? 'https://' : 'http://') + 'cdn.livechatinc.com/tracking.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(lc, s);
})();
</script>
<noscript>
<a href="https://www.livechatinc.com/chat-with/9917240/">Напишите нам</a>,
powered by <a href="https://www.livechatinc.com/?welcome" rel="noopener" target="_blank">LiveChat</a>
</noscript>
<!-- End of LiveChat code -->
<script>
var viewportmeta = document.querySelector('meta[name="viewport"]');
if (viewportmeta) {
@ -115,6 +133,7 @@
<noscript><div><img src="https://mc.yandex.ru/watch/49354039" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
{% include "templates/blocks/mixpanel.html" %}
{% block head %}{% endblock head %}
</head>
<body>
{% include "templates/blocks/social.html" %}
@ -130,13 +149,7 @@
{% include "templates/blocks/popup_course_lock.html" %}
{% include "templates/blocks/popup_subscribe.html" %}
</div>
<script>
window.LIL_STORE = {
urls: {
courses: "{% url 'courses' %}"
}
};
</script>
{% include 'templates/blocks/lil_store_js.html' %}
<script type="text/javascript" src={% static "app.js" %}></script>
<script>
var schoolDiscount = parseFloat({{ config.SERVICE_DISCOUNT }});

@ -18,7 +18,7 @@ from django.contrib import admin
from django.views.generic import TemplateView
from django.urls import path, include
from apps.content.views import ContestEditView, ContestView, ContestWorkView, contest_work_comment
from apps.course.views import (
CoursesView, likes, coursecomment,
CourseView, LessonView, SearchView,
@ -83,6 +83,11 @@ urlpatterns = [
path('api/v1/', include(('api.v1.urls', 'api_v1'))),
path('school/', include(('apps.school.urls', 'school'))),
path('test', TemplateView.as_view(template_name='templates/lilcity/test.html'), name='test'),
path('contest/create', ContestEditView.as_view(), name='contest_create'),
path('contest/<str:slug>/edit', ContestEditView.as_view(), name='contest_edit'),
path('contest/<str:slug>/', ContestView.as_view(), name='contest'),
path('contest-work/<int:pk>/', ContestWorkView.as_view(), name='contest_work'),
path('contest-work/<int:contest_work_id>/comment', contest_work_comment, name='contest_work_comment'),
]

@ -13,6 +13,5 @@ def date_range(start, end):
return
def weekday_in_date_range(start, end, weekday):
counter = Counter([d.isoweekday() for d in date_range(start, end)])
return counter.get(weekday, 0)
def weekdays_in_date_range(start, end):
return Counter([d.isoweekday() for d in date_range(start, end)])

@ -1,9 +1,10 @@
from datetime import datetime, timedelta
from django.db.models import Min
from django.db.models import Min, Func, F
from django.contrib.auth import get_user_model
from django.views.generic import TemplateView
from django.utils.timezone import now
from paymentwall.pingback import Pingback
from apps.course.models import Course
from apps.school.models import SchoolSchedule
@ -34,57 +35,50 @@ class IndexView(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
)
online_coming_soon = (
school_schedule.start_at > now_time.time() and
(
datetime.combine(datetime.today(), school_schedule.start_at) - timedelta(hours=12)
).time() <= now_time.time() and
school_schedule.current_live_lesson()
datetime.combine(datetime.today(), school_schedule.start_at) - timedelta(hours=12)
<= datetime.combine(datetime.today(), now_time.time()) and
school_schedule.current_live_lesson
)
date_now = now_time.date()
if self.request.user.is_authenticated:
school_payment = SchoolPayment.objects.filter(
user=self.request.user,
date_start__lte=date_now,
date_end__gte=date_now
date_end__gte=date_now,
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
)
school_payment_exists = school_payment.exists()
school_schedules_purchased = school_payment.values_list('weekdays', flat=True)
school_schedules_purchased = school_schedules_purchased[0] if school_schedules_purchased else []
school_payment_future = SchoolPayment.objects.filter(
user=self.request.user,
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 = []
context.update({
'online': online,
'online_coming_soon': online_coming_soon,
'school_schedule': school_schedule,
'course_items': Course.objects.filter(status=Course.PUBLISHED)[:3],
'course_items': Course.shuffle(Course.objects.filter(status=Course.PUBLISHED)[:3]),
'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,
'school_schedules_purchased': set(school_schedules_purchased),
'teachers': User.objects.filter(role=User.TEACHER_ROLE, show_in_mainpage=True),
'subscription_ends': school_payment.filter(add_days=False).first().date_end if school_payment_exists else None,
'subscription_ends_humanize': school_payment.filter(add_days=False).first().date_end_humanize if school_payment_exists else None,
'school_purchased_future': school_purchased_future,
'is_purchased_future': school_payment_exists_future,
'school_purchased_future': False,
'is_purchased_future': False,
})
return context

@ -28,4 +28,6 @@ git+https://github.com/ivlevdenis/python-instagram.git
django-user-agents==0.3.2
user-agents==1.1.0
ua-parser==0.8.0
django-ipware
django-imagekit

@ -34,14 +34,19 @@
"webpack": "^3.10.0"
},
"dependencies": {
"autosize": "^4.0.2",
"autosize-input": "^1.0.2",
"axios": "^0.17.1",
"babel-polyfill": "^6.26.0",
"baguettebox.js": "^1.10.0",
"downscale": "^1.0.4",
"glob": "^7.1.2",
"history": "^4.7.2",
"ilyabirman-likely": "^2.3.0",
"inputmask": "^3.3.11",
"jquery": "^3.3.1",
"lodash.debounce": "^4.0.8",
"modal-video": "^2.4.2",
"moment": "^2.20.1",
"owl.carousel": "^2.2.0",
"slugify": "^1.2.9",

@ -0,0 +1,361 @@
<template>
<form @submit.prevent="save">
<div class="info">
<div class="info__section" :style="coverBackgroundStyle">
<div class="info__main">
<div class="info__head">
<div class="info__upload upload">
Загрузить фон
<input type="file" class="upload__file" @change="onCoverImageSelected">
</div>
</div>
<div class="info__title">
<div class="info__field field field_info"
v-bind:class="{ error: $v.contest.title.$dirty && $v.contest.title.$invalid }">
<div class="field__label">Название</div>
<div class="field__wrap">
<textarea class="field__textarea"
rows="1"
v-autosize="contest.title"
@change="onTitleInput"
v-model="contest.title"></textarea>
</div>
</div>
<div class="info__field field field_info field_short_description">
<div class="field__label">Описание</div>
<div class="field__wrap">
<textarea class="field__textarea"
v-autosize="contest.description"
v-model="contest.description"></textarea>
</div>
</div>
</div>
<div class="info__foot">
<div class="info__field field">
<div class="field__label field__label_gray">ССЫЛКА</div>
<div class="field__wrap">
<input type="text" class="field__input" v-model="contest.slug" @input="slugChanged = true">
</div>
<div class="field__wrap field__wrap--additional">{{ contestFullUrl }}</div>
</div>
<div class="info__field field">
<div class="field__label">ДАТА НАЧАЛА</div>
<div class="field__wrap">
<vue-datepicker input-class="field__input" v-model="contest.date_start" language="ru" format="dd/MM/yyyy"/>
</div>
</div>
<div class="info__field field">
<div class="field__label">ДАТА ОКОНЧАНИЯ</div>
<div class="field__wrap">
<vue-datepicker input-class="field__input" v-model="contest.date_end" language="ru" format="dd/MM/yyyy"/>
</div>
</div>
<button type="submit">Save</button>
</div>
</div>
</div>
<div class="info__sidebar">
<div class="info__wrap">
<div class="info__fieldset">
</div>
</div>
</div>
</div>
<div class="section">
<div class="section__center center">
<div class="kit">
<div class="kit__body">
<vue-draggable v-model="contest.content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }">
<div v-for="(block, index) in contest.content" :key="block.data.id ? block.data.id : block.data.guid">
<block-text v-if="block.type === 'text'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-image-text v-if="block.type === 'image-text'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
:image-id.sync="block.data.image_id"
:image-url.sync="block.data.image_url"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-image v-if="block.type === 'image'"
:index="index"
:title.sync="block.data.title"
:image-id.sync="block.data.image_id"
:image-url.sync="block.data.image_url"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-images v-if="block.type === 'images'"
:index="index"
:title.sync="block.data.title"
:text.sync="block.data.text"
:images.sync="block.data.images"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-video v-if="block.type === 'video'"
:index="index"
:title.sync="block.data.title"
v-on:remove="onBlockRemoved"
:video-url.sync="block.data.video_url"/>
</div>
</vue-draggable>
<block-add v-on:added="onBlockAdded"/>
</div>
</div>
</div>
</div>
</form>
</template>
<script>
import BlockText from './blocks/BlockText'
import BlockImage from './blocks/BlockImage'
import BlockImages from './blocks/BlockImages'
import BlockImageText from './blocks/BlockImageText'
import BlockVideo from './blocks/BlockVideo'
import BlockAdd from "./blocks/BlockAdd";
import {api} from "../js/modules/api";
import DatePicker from 'vuejs-datepicker';
import Draggable from 'vuedraggable';
import slugify from 'slugify';
import {required, minValue, numeric, url } from 'vuelidate/lib/validators'
import _ from 'lodash';
import moment from 'moment'
export default {
name: 'contest-redactor',
props: ["contestId"],
data() {
return {
loading: false,
slugChanged: false,
contest: {
coverImage: '',
coverImageId: null,
title: '',
description: '',
content: [],
date_start: '',
date_end: '',
slug: '',
active: true,
}
}
},
computed: {
accessToken() {
return window.LIL_STORE.accessToken;
},
coverBackgroundStyle() {
return this.contest.coverImage ? `background-image: url(${this.contest.coverImage});` : '';
},
contestFullUrl() {
return `https://lil.city/contest/${this.contest.slug}`;
},
},
validations() {
return {
contest: {
title: {
required
},
},
};
},
mounted() {
if (this.contestId) {
this.load();
}
},
methods: {
onBlockRemoved(blockIndex) {
const blockToRemove = this.contest.content[blockIndex];
// Удаляем блок из Vue
this.contest.content.splice(blockIndex, 1);
// Если блок уже был записан в БД, отправляем запрос на сервер на удаление блока из БД
if (blockToRemove.data.id) {
api.removeContentBlock(blockToRemove, this.accessToken);
}
},
onBlockAdded(blockData) {
this.contest.content.push(blockData);
},
onTitleInput() {
this.$v.contest.title.$touch();
if (!this.slugChanged) {
this.contest.slug = slugify(this.contest.title);
}
},
onCoverImageSelected(event) {
let file = event.target.files[0];
let reader = new FileReader();
reader.onload = () => {
this.$set(this.contest, 'coverImage', reader.result);
api.uploadImage(reader.result, this.accessToken)
.then((response) => {
this.contest.coverImageId = response.data.id;
})
.catch((error) => {
//console.log('error', error);
});
};
if (file) {
reader.readAsDataURL(file);
}
},
processContestJson(data) {
this.contest = {
coverImage: data.cover && data.cover.image || null,
coverImageId: data.cover && data.cover.id || null,
id: data.id,
title: data.title,
description: data.description,
content: api.convertContentResponse(data.content),
date_start: data.date_start,
date_end: data.date_end,
slug: data.slug,
active: data.active,
};
},
load() {
this.loading = true;
const request = api.get(`/api/v1/contests/${this.contestId}/`, {
headers: {
'Authorization': `Token ${window.LIL_STORE.accessToken}`,
}
});
request
.then((response) => {
this.processContestJson(response.data);
this.$nextTick(() => {
this.loading = false;
});
})
.catch((err) => {
this.loading = false;
});
return request;
},
save() {
let data = _.pick(this.contest, ['title', 'description', 'slug', 'active']);
data.date_start = data.date_start ? moment(data.date_start).format('MM-DD-YYYY') : null;
data.date_end = data.date_end ? moment(data.date_end).format('MM-DD-YYYY') : null;
data.cover = this.contest.coverImageId || '';
data.content = this.contest.content.map((block, index) => {
if (block.type === 'text') {
return {
'type': 'text',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'txt': block.data.text,
}
}
} else if (block.type === 'image') {
return {
'type': 'image',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'img': block.data.image_id,
}
}
} else if (block.type === 'image-text') {
return {
'type': 'image-text',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'img': block.data.image_id,
'txt': block.data.text,
}
}
} else if (block.type === 'images') {
return {
'type': 'images',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'images': block.data.images.map((galleryImage) => {
return {
'id': galleryImage.id ? galleryImage.id : null,
'img': galleryImage.img,
}
}),
}
}
} else if (block.type === 'video') {
return {
'type': 'video',
'data': {
'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'url': block.data.video_url,
}
}
}
});
const request = this.contest.id
? api.put(`/api/v1/contests/${this.contest.id}/`, data, {
headers: {
'Authorization': `Token ${window.LIL_STORE.accessToken}`,
}
})
: api.post('/api/v1/contests/', data, {
headers: {
'Authorization': `Token ${window.LIL_STORE.accessToken}`,
}
});
request.then((response) => {
if(this.contest.id) {
this.contest = this.processContestJson(response.data);
}
else {
window.location.href = `/contest/${response.data.id}/edit`;
}
})
.catch((err) => {
//this.contestSaving = false;
});
}
},
components: {
BlockAdd,
'vue-datepicker': DatePicker,
'block-text': BlockText,
'block-image': BlockImage,
'block-image-text': BlockImageText,
'block-images': BlockImages,
'block-video': BlockVideo,
'vue-draggable': Draggable,
}
};
</script>
<style>
</style>

@ -0,0 +1,139 @@
<template>
<div class="contest-works">
<div class="contest-works__works" v-if="! $root.store.isMobile">
<div class="contest-works__column">
<contest-work v-for="contestWork in columns[0]" :key="contestWork.id" :contest-work="contestWork"></contest-work>
</div>
<div class="contest-works__column">
<contest-work v-for="contestWork in columns[1]" :key="contestWork.id" :contest-work="contestWork"></contest-work>
</div>
<div class="contest-works__column">
<contest-work v-for="contestWork in columns[2]" :key="contestWork.id" :contest-work="contestWork"></contest-work>
</div>
</div>
<div class="contest-works__works" v-if="$root.store.isMobile" style="display: block;">
<contest-work v-for="contestWork in contestWorks" :key="contestWork.id" :contest-work="contestWork"></contest-work>
</div>
<div v-show="loading" class="contest-works__loader"><div class="loading-loader"></div></div>
<div v-if="loaded && !contestWorks.length" class="contest-works__no-works">Здесь вы сможете увидеть работы участников после их добавления</div>
</div>
</template>
<script>
import {api} from "../js/modules/api";
import $ from 'jquery';
import ContestWork from "./blocks/ContestWork.vue";
export default {
name: "contest-works",
props: ['contestId', 'autoload'],
data(){
return {
page: 1,
lastPage: null,
loading: false,
loaded: false,
contestWorks: [],
};
},
mounted() {
this.load();
if(this.autoload) {
$(window).scroll(() => {
const pos = $(this.$el).offset().top + this.$el.clientHeight - 100;
if($(window).scrollTop() + $(window).height() >= pos
&& !this.loading && !this.lastPage) {
this.page += 1;
this.load();
}
});
}
},
computed: {
columns() {
const columnWidth = 300;
const heights = [0, 0, 0];
const first = [];
const second = [];
const third = [];
let index = 0;
for(let i=0; i < this.contestWorks.length; i++) {
const work = this.contestWorks[i];
let column = index % 3;
const workHeight = work.img_height / work.img_width * columnWidth + 50;
let minHeight = 0;
if(i > 3){
for(let j=0; j < 3; j++){
let col = j % 3;
if(! minHeight || heights[j] < minHeight){
minHeight = heights[j];
if(heights[column] - heights[j] > workHeight * 0.1){
column = j;
index += j - column;
}
}
}
}
heights[column] += workHeight;
[first, second, third][column].push(work);
index++;
}
return [first, second, third];
}
},
methods: {
load() {
this.loading = true;
api.get(`/api/v1/contest-works/?contest=${this.contestId}&page=${this.page}&current_user=${this.$root.store.user.id}`, {
headers: {
'Authorization': `Token ${this.$root.store.accessToken}`,
}
})
.then(response => {
this.loading = false;
this.loaded = true;
if(this.page > 1){
this.contestWorks = this.contestWorks.concat(response.data.results);
}
else{
this.contestWorks = response.data.results;
}
})
.catch((response) => {
this.loading = false;
this.loaded = true;
if(response.response.status == 404){
this.lastPage = this.page;
}
});
}
},
components: {ContestWork},
}
</script>
<style>
.contest-works {
width: 100%;
}
.contest-works__works {
text-align: left;
display: flex;
}
.contest-works__column {
display: flex;
flex-direction: column;
margin-right: 20px;
width: 300px;
}
.contest-works__loader {
width: 100%;
height: 30px;
position: relative;
}
.contest-works__no-works {
text-align: center;
width: 100%;
}
</style>

@ -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(()=>{

@ -0,0 +1,193 @@
<template>
<div ref="popup" class="upload-contest-work popup" @click.prevent="hide">
<div class="popup__wrap popup__wrap_md" @click.stop>
<button class="popup__close" @click.prevent="hide">
<svg class="icon icon-close">
<use v-bind="{'xlink:href': $root.store.staticUrl + 'img/sprite.svg' + '#icon-close' }"></use>
</svg>
</button>
<div class="popup__body">
<form class="form">
<div class="title">
Чтобы принять участие<br>в конкурсе, заполните поля<br>и прикрепите изображение
<img src="/static/img/curve-1.svg" class="text__curve">
</div>
<div class="field"
v-bind:class="{ error: $v.contestWork.child_full_name.$dirty && $v.contestWork.child_full_name.$invalid }">
<div class="field__label">ИМЯ И ФАМИЛИЯ РЕБЕНКА</div>
<div class="field__wrap">
<input class="field__input" type="text" v-model="contestWork.child_full_name">
</div>
<div class="field__error"></div>
</div>
<div class="field"
v-bind:class="{ error: $v.contestWork.age.$dirty && $v.contestWork.age.$invalid }">
<div class="field__label">ВОЗРАСТ</div>
<div class="field__wrap">
<input class="field__input" type="number" v-model="contestWork.age" style="width: 100px;">
</div>
<div class="field__error"></div>
</div>
<div class="field">
<lil-image :image-id.sync="contestWork.imageId" :image-url.sync="contestWork.imageUrl"
v-on:update:imageId="onUpdateImageId" :access-token="$root.store.accessToken" />
</div>
<div class="field" style="text-align: center;">
<button class="btn" tabindex="3" @click.prevent="save">Отправить на конкурс</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import {api} from "../js/modules/api";
import LilImage from './blocks/Image';
import {showNotification} from "../js/modules/notification";
import {required, minValue, numeric, url } from 'vuelidate/lib/validators';
import $ from 'jquery';
export default {
name: 'upload-contest-work',
props: ['contestId'],
data() {
return {
fields: {
image: "Изображение",
child_full_name: "Имя и фамилия ребенка",
age: 'Возраст',
},
contestWork: {
contest: this.contestId,
image: null,
imageId: null,
imageUrl: '',
child_full_name: '',
age: '',
}
}
},
validations() {
return {
contestWork: {
image: {
required
},
child_full_name: {
required
},
age: {
required, numeric
},
},
};
},
mounted() {
$('[data-show-upload-contest-work]').click((e) => {
e.preventDefault();
e.stopPropagation();
this.clear();
this.show();
});
},
methods: {
clear() {
Object.assign(this.$data, this.$options.data.apply(this))
},
onUpdateImageId(imageId) {
this.contestWork.image = imageId;
},
show() {
const $popup = $(this.$refs.popup)
$('body').addClass('no-scroll');
$popup.addClass('visible');
setTimeout(() => {
$popup.addClass('open');
}, 300);
},
hide() {
const $popup = $(this.$refs.popup)
$('body').removeClass('no-scroll');
$popup.removeClass('visible');
setTimeout(() => {
$popup.removeClass('open');
}, 300);
},
validate() {
if (this.$v.contestWork.$invalid) {
for(let i in this.$v.contestWork) {
if(this.$v.contestWork[i].$invalid) {
showNotification("error", "Ошибка валидации поля " + this.fields[i]);
}
}
return false;
}
return true;
},
save() {
if(! this.validate()) {
return false;
}
let data = this.contestWork;
data.contest = this.contestId;
data.user = this.$root.store.user.id;
const request = api.post(`/api/v1/contest-works/`, data, {
headers: {
'Authorization': `Token ${this.$root.store.accessToken}`,
}
});
request.then((response) => {
if(+response.data.id){
this.$emit('add:contest-work', response.data);
this.hide();
window.location.reload();
}
});
}
},
components: {
'lil-image': LilImage,
}
}
</script>
<style lang="scss">
.upload-contest-work {
.popup__wrap {
padding: 35px 35px 0;
}
.title {
text-align: center; font-size: 24px;
.text__curve {
right: 55px;
width: 170px;
bottom: -40px;
}
}
.kit__photo {
height: 400px;
}
.kit__photo.has-image {
border: none;
}
.kit__photo-image {
max-height: 400px;
height: auto;
width: auto;
}
.kit__file {
bottom: 0;
}
}
</style>

@ -0,0 +1,62 @@
<template>
<div class="contest-work-item">
<a :href="`/contest-work/${contestWork.id}/`">
<img class="contest-work-item__img" :src="contestWork.image.image_thumbnail" />
</a>
<div class="contest-work-item__info">
<div class="contest-work-item__bio">
<div>{{ contestWork.child_full_name }}</div>
<div class="contest-work-item__age">{{ contestWork.age }} {{ contestWork.age < 5 ? 'года' : 'лет' }}</div>
</div>
<div class="contest-work-item__likes">
<likes obj-type="contest_work" :obj-id="contestWork.id" :user-liked="contestWork.user_liked"
:likes="contestWork.likes"></likes>
</div>
</div>
</div>
</template>
<script>
import Likes from './Likes.vue';
export default {
name: "contest-work",
props: ['contestWork'],
components: {Likes},
}
</script>
<style lang="scss">
.contest-work-item {
break-inside: avoid;
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
transition: opacity .4s ease-in-out;
text-transform: uppercase;
font-weight: bold;
color: black;
border: 1px solid #ececec;
display: block;
}
.contest-work-item__img {
width: 100%;
height: auto;
}
.contest-work-item__info {
display: flex;
padding: 5px 10px;
}
.contest-work-item__age {
color: #919191;
}
.contest-work-item__bio {
flex: calc(100% - 70px);
}
@media only screen and (min-width: 1023px) {
.contest-works:hover .contest-work-item:not(:hover) {
opacity: 0.4;
}
}
</style>

@ -1,5 +1,5 @@
<template>
<div class="kit__photo">
<div class="kit__photo" :class="{ 'loading': loading, 'has-image': !! imageUrl }">
<svg class="icon icon-add-plus" v-if="!imageUrl">
<use xlink:href="/static/img/sprite.svg#icon-add-plus"></use>
</svg>
@ -10,10 +10,11 @@
<script>
import {api} from "../../js/modules/api";
import downscale from 'downscale';
export default {
name: "lil-image",
props: ["imageId", "imageUrl", "accessToken"],
props: ["imageId", "imageUrl", "accessToken", "longSide"],
data() {
return {
loading: false,
@ -22,19 +23,37 @@
methods: {
onImageAdded(event) {
this.loading = true;
const longSide = +this.longSide || 1600;
let file = event.target.files[0];
let reader = new FileReader();
const reader = new FileReader();
reader.onload = () => {
api.uploadImage(reader.result, this.accessToken)
.then((response) => {
this.loading = false;
this.$emit('update:imageId', response.data.id);
this.$emit('update:imageUrl', response.data.image);
})
.catch((error) => {
this.loading = false;
console.log('error', error);
let img = document.createElement('img');
img.onload = () => {
let w = 0;
let h = 0;
if(img.width > img.height) {
w = longSide;
h = 0;
}
else {
w = 0;
h = longSide;
}
downscale(img.src, w, h).then((dataURL) => {
img = null;
api.uploadImage(dataURL, this.accessToken)
.then((response) => {
this.loading = false;
this.$emit('update:imageId', response.data.id);
this.$emit('update:imageUrl', response.data.image);
})
.catch((error) => {
this.loading = false;
console.log('error', error);
});
});
}
img.src = reader.result;
};
if (file) {
reader.readAsDataURL(file);
@ -51,4 +70,4 @@
display: block;
object-fit: contain;
}
</style>
</style>

@ -0,0 +1,81 @@
<template>
<div class="likes" :class="{ 'likes_liked': userLikedProp }">
<span>{{ likesProp }}</span><span class="likes__like" @click="addLike"
v-bind="{ 'data-popup': $root.store.user.id ? '' : '.js-popup-auth' }">
<svg class="likes__icon icon icon-like">
<use v-bind="{'xlink:href': $root.store.staticUrl + 'img/sprite.svg' + '#icon-like' }"></use>
</svg></span>
</div>
</template>
<script>
import {api} from "../../js/modules/api";
import {showNotification} from "../../js/modules/notification";
export default {
name: 'likes',
props: ['likes', 'userLiked', 'objType', 'objId'],
data() {
return {
likesProp: +this.likes || 0,
userLikedProp: this.userLiked || false,
}
},
methods: {
addLike(event) {
this.$emit('like');
if(this.userLikedProp){
return;
}
if(this.$root.store.user.id) {
event.stopPropagation();
api.post('/api/v1/likes/', {
user: this.$root.store.user.id, // FIXME
obj_type: this.objType,
obj_id: this.objId,
}, {
headers: {
'Authorization': `Token ${this.$root.store.accessToken}`,
}
})
.then(response => {
if (response.data && response.data.id) {
this.userLikedProp = true;
this.likesProp += 1;
this.$emit('liked');
}
})
.catch(({response}) => {
if(response.status == 400 && response.data.error){
showNotification('error', response.data.error);
}
});
}
}
}
}
</script>
<style>
.likes {
font-weight: bold;
text-align: right;
}
.likes__like {
cursor: pointer;
margin-left: 5px;
}
.likes_liked .likes__like {
cursor: default;
}
.likes__icon {
margin-bottom: -3px;
}
.likes_liked .likes__icon {
fill: #d40700;
}
</style>

@ -40,4 +40,4 @@
<style scoped>
</style>
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@ -1,6 +1,10 @@
/**
* Входная точка клиентского приложения.
*/
import 'ilyabirman-likely/release/likely.js';
import 'ilyabirman-likely/release/likely.css';
import "modal-video/js/jquery-modal-video.min.js";
import "modal-video/css/modal-video.min.css";
import "./modules/common";
import "./modules/header";
import "./modules/search";
@ -17,3 +21,30 @@ import "./modules/notification";
import "./modules/mixpanel";
import "../sass/app.sass";
import Vue from 'vue';
import Vuelidate from 'vuelidate';
import UploadContestWork from '../components/UploadContestWork.vue';
import ContestWorks from '../components/ContestWorks.vue';
import Likes from '../components/blocks/Likes.vue';
Vue.use(Vuelidate);
if (process.env.NODE_ENV === 'development') {
// Enable vue-devtools
Vue.config.devtools = true;
}
const app = new Vue({
el: '#lilcity-vue-app',
data() {
return {
store: window.LIL_STORE,
}
},
components: {
UploadContestWork,
ContestWorks,
Likes,
}
});

@ -0,0 +1,19 @@
import Vue from 'vue'
import Vuelidate from 'vuelidate'
import VueAutosize from '../components/directives/autosize'
import ContestRedactor from '../components/ContestRedactor.vue'
if (process.env.NODE_ENV === 'development') {
// Enable vue-devtools
Vue.config.devtools = true;
}
Vue.use(VueAutosize);
Vue.use(Vuelidate);
let app = new Vue({
el: '#lilcity-vue-app',
components: {
'contest-redactor': ContestRedactor,
}
});

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save