Merge branch 'master' of https://gitlab.com/lilcity/backend into feature/lil-583

remotes/origin/hotfix/LIL-691
gzbender 8 years ago
commit 167a6470b3
  1. 1
      .gitlab-ci.yml
  2. 0
      __init__.py
  3. 9
      api/v1/serializers/content.py
  4. 68
      api/v1/serializers/contest.py
  5. 44
      api/v1/serializers/course.py
  6. 4
      api/v1/serializers/mixins.py
  7. 5
      api/v1/urls.py
  8. 63
      api/v1/views.py
  9. 17
      apps/auth/backend.py
  10. 5
      apps/auth/forms.py
  11. 5
      apps/auth/views.py
  12. 48
      apps/content/migrations/0021_auto_20180813_1306.py
  13. 17
      apps/content/migrations/0022_auto_20180815_2129.py
  14. 71
      apps/content/models.py
  15. 64
      apps/content/templates/content/blocks/gallery.html
  16. 20
      apps/content/templates/content/blocks/image.html
  17. 0
      apps/content/templates/content/blocks/imagetext.html
  18. 18
      apps/content/templates/content/blocks/text.html
  19. 84
      apps/content/templates/content/blocks/video.html
  20. 47
      apps/content/templates/content/contest.html
  21. 18
      apps/content/templates/content/contest_edit.html
  22. 112
      apps/content/templates/content/contest_work.html
  23. 110
      apps/content/views.py
  24. 2
      apps/course/filters.py
  25. 18
      apps/course/migrations/0040_course_age.py
  26. 36
      apps/course/migrations/0041_auto_20180813_1306.py
  27. 18
      apps/course/migrations/0042_like_ip.py
  28. 35
      apps/course/models.py
  29. 2
      apps/course/templates/course/_items.html
  30. 2
      apps/course/templates/course/course.html
  31. 15
      apps/course/templates/course/course_only_lessons.html
  32. 4
      apps/course/templates/course/courses.html
  33. 3
      apps/course/templates/course/inclusion/category_items.html
  34. 2
      apps/course/templates/course/inclusion/category_menu_items.html
  35. 50
      apps/course/templates/course/lesson.html
  36. 4
      apps/course/templatetags/lilcity_category.py
  37. 2
      apps/course/views.py
  38. 3
      apps/payment/models.py
  39. 16
      apps/payment/views.py
  40. 2
      apps/school/templates/blocks/open_lesson.html
  41. 16
      apps/school/templates/school/livelesson_detail.html
  42. 2
      apps/user/views.py
  43. 2
      project/settings.py
  44. 3
      project/templates/blocks/header.html
  45. 10
      project/templates/blocks/lil_store_js.html
  46. 3
      project/templates/blocks/partners.html
  47. 2
      project/templates/blocks/popup_auth.html
  48. 2
      project/templates/blocks/share.html
  49. 1
      project/templates/lilcity/edit_index.html
  50. 10
      project/templates/lilcity/index.html
  51. 7
      project/urls.py
  52. 2
      project/views.py
  53. 2
      requirements.txt
  54. 4
      web/package.json
  55. 361
      web/src/components/ContestRedactor.vue
  56. 86
      web/src/components/ContestWorks.vue
  57. 44
      web/src/components/CourseRedactor.vue
  58. 193
      web/src/components/UploadContestWork.vue
  59. 62
      web/src/components/blocks/ContestWork.vue
  60. 43
      web/src/components/blocks/Image.vue
  61. 74
      web/src/components/blocks/Likes.vue
  62. 4
      web/src/components/redactor/VueRedactor.vue
  63. BIN
      web/src/img/pinkbus.jpg
  64. 27
      web/src/js/app.js
  65. 19
      web/src/js/contest-redactor.js
  66. 6
      web/src/js/modules/courses.js
  67. 5
      web/src/js/modules/popup.js
  68. 87
      web/src/sass/_common.sass
  69. 5
      web/webpack.config.js

@ -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,68 @@
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)
class ContestSerializer(serializers.ModelSerializer):
cover = ImageObjectSerializer()
content = ContentSerializer(many=True)
class Meta:
model = Contest
fields = '__all__'
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.count()
def get_user_liked(self, instance):
user = self.context['request'].user
return instance.likes.filter(user=user).exists() if user.is_authenticated 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,41 @@ class LikeSerializer(serializers.ModelSerializer):
)
class LikeCreateSerializer(serializers.ModelSerializer):
OBJ_TYPE_CONTEST_WORK = 'contest_work'
obj_type = serializers.CharField(required=True)
obj_id = serializers.IntegerField(required=True)
class Meta:
model = Like
fields = ['user', 'obj_type', 'obj_id']
def create(self, validated_data):
# FIXME
if validated_data.get('user'):
user = validated_data.get('user')
else:
user = self.context['request'].user
if not user: # FIXME and user.is_authenticated):
return Like()
obj_type = validated_data.pop('obj_type')
obj_id = validated_data.pop('obj_id')
client_ip, is_routable = get_client_ip(self.context['request'])
if obj_type == self.OBJ_TYPE_CONTEST_WORK:
contest_work = ContestWork.objects.get(pk=obj_id)
# FIXME in prod: fixed
if contest_work.user == user or contest_work.likes.filter(user=user).exists():
# if contest_work.likes.filter(user=user).exists():
return Like()
like = Like.objects.create(user=user, ip=client_ip)
contest_work.likes.add(like)
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']:

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

@ -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,)
@ -127,10 +132,16 @@ class MaterialViewSet(ExtendedModelViewSet):
class LikeViewSet(ExtendedModelViewSet):
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 = []
class CategoryViewSet(ExtendedModelViewSet):
@ -419,10 +430,10 @@ 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',
@ -430,6 +441,17 @@ class PaymentViewSet(ExtendedModelViewSet):
)
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')
def calc_amount(self, request, pk=None):
user = request.query_params.get('user')
@ -439,3 +461,34 @@ class PaymentViewSet(ExtendedModelViewSet):
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

@ -6,8 +6,3 @@ class LearnerRegistrationForm(forms.Form):
last_name = forms.CharField()
email = forms.EmailField()
password = forms.CharField()
class LoginForm(forms.Form):
email = forms.CharField()
password = forms.CharField()

@ -35,7 +35,7 @@ class LearnerRegistrationView(FormView):
config = Config.load()
first_name = form.cleaned_data['first_name']
last_name = form.cleaned_data['last_name']
email = form.cleaned_data['email']
email = form.cleaned_data['email'].lower()
password = form.cleaned_data['password']
user, created = User.objects.get_or_create(
@ -179,8 +179,9 @@ class FacebookLoginOrRegistration(View):
"errors": {"email": 'is field required'}
})
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', '')

@ -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,22 @@
from urllib.parse import urlparse
from django.db import models
from django.contrib.auth import get_user_model
from django.urls import reverse_lazy
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 +47,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 +148,57 @@ 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
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>

@ -1,43 +1,43 @@
<div class="section section_border">
<div class="section__center center center_sm">
<div class="content-block title">
{{ content.title }}
</div>
<div class="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>
{% elif 'vimeo.com' in content.url %}
<iframe src="https://player.vimeo.com/video/{{ content.video_index }}" frameborder="0" webkitallowfullscreen
mozallowfullscreen allowfullscreen>
</iframe>
{% endif %}
</div>
</div>
</div>
<style>
@media only screen and (max-width: 639px) {
iframe {
width: 100%;
height: 240px;
}
.iframe__container {
text-align: center;
width: 100%;
padding-bottom: 56.25%;
margin-bottom: -56.25%;
}
}
@media only screen and (min-width: 640px) {
.iframe__container {
text-align: center;
}
iframe {
width: 640px;
height: 360px;
}
}
<div class="section section_border">
<div class="section__center center center_sm">
<div class="content-block title">
{{ content.title }}
</div>
<div class="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>
{% elif 'vimeo.com' in content.url %}
<iframe src="https://player.vimeo.com/video/{{ content.video_index }}" frameborder="0" webkitallowfullscreen
mozallowfullscreen allowfullscreen>
</iframe>
{% endif %}
</div>
</div>
</div>
<style>
@media only screen and (max-width: 639px) {
iframe {
width: 100%;
height: 240px;
}
.iframe__container {
text-align: center;
width: 100%;
padding-bottom: 56.25%;
margin-bottom: -56.25%;
}
}
@media only screen and (min-width: 640px) {
.iframe__container {
text-align: center;
}
iframe {
width: 640px;
height: 360px;
}
}
</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 %}
<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 %}
<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,112 @@
{% extends "templates/lilcity/index.html" %}
{% load static %}
{% block title %}{{ contest_work.child_full_name }}, {{ contest_work.age }} лет{% endblock title %}
{% block ogimage %}http://{{request.META.HTTP_HOST}}{{ contest_work.image.image.url }}{% endblock ogimage %}
{% 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 }} {% if contest_work.age < 5 %}года{% else %}лет{% endif %}</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="{{ contest_work.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,110 @@
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'
def get(self, request, pk=None, lesson=None):
if pk:
self.object = get_object_or_404(Contest, pk=pk)
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
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')
class Meta:
model = Course

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

@ -1,4 +1,5 @@
import arrow
from random import shuffle
from uuid import uuid4
from django.db import models
from django.core.exceptions import ValidationError
@ -7,19 +8,21 @@ 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
from django.conf import settings
from polymorphic_tree.models import PolymorphicMPTTModel, PolymorphicTreeForeignKey
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)
@ -97,7 +100,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
@ -129,11 +132,11 @@ class Course(BaseModel, DeactivatedMixin):
@property
def deferred_start_at_humanize(self):
return arrow.get(self.deferred_start_at).humanize(locale='ru')
return arrow.get(self.deferred_start_at, settings.TIME_ZONE).humanize(locale='ru')
@property
def created_at_humanize(self):
return arrow.get(self.created_at).humanize(locale='ru')
return arrow.get(self.created_at, settings.TIME_ZONE).humanize(locale='ru')
@property
def is_deferred_start(self):
@ -148,6 +151,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)
@ -234,7 +255,7 @@ class Comment(PolymorphicMPTTModel, DeactivatedMixin):
@property
def created_at_humanize(self):
return arrow.get(self.created_at).humanize(locale='ru')
return arrow.get(self.created_at, settings.TIME_ZONE).humanize(locale='ru')
def __str__(self):
return self.content
@ -264,3 +285,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 %}

@ -180,19 +180,18 @@
<a href="#">
{% endif %}
<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.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__row">
<a href="{% url 'lesson' pk=lesson.id %}" class="btn btn_stroke">Перейти к уроку</a>
<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>
</a>

@ -20,8 +20,8 @@
<div class="head__right">
<div class="head__field field">
<div class="field__wrap">
<div class="field__select select js-select{% if category %} selected{% endif %}">
<div class="select__head js-select-head">{% if category %}{{ category.0 }}{% else %}Категории{% endif %}</div>
<div class="field__select select js-select{% if category %} selected{% endif %}" data-category-select>
<div class="select__head js-select-head">Категории</div>
<div class="select__drop js-select-drop">
<div class="select__option js-select-option{% if not category %} active{% endif %}"
data-category-option data-category-url="{% url 'courses' %}">

@ -1,5 +1,6 @@
{% for cat in category_items %}
<div class="select__option js-select-option{% if category and category.0 == cat.title %} active{% endif %}" data-category-option data-category-name="{{ cat.title }}" data-category-url="{% url 'courses' %}?category={{ cat.title }}">
<div class="select__option js-select-option{% if category == cat.id %} active{% endif %}"
data-category-option data-category-name="{{ cat.title }}" data-category-url="{% url 'courses' %}?category={{ cat.id }}">
<div class="select__title">{{ cat.title }}</div>
</div>
{% endfor %}

@ -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 }}</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.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 }}</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 %}
@ -98,6 +117,7 @@
<div class="questions">
{% if request.user.is_authenticated %}
<form class="questions__form" method="post" action="{% url 'lessoncomment' lesson_id=lesson.id %}">
<input type="hidden" name="reply_id">
<div class="questions__ava ava">
<img
class="ava__pic"
@ -109,6 +129,10 @@
>
</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" placeholder="Спросите автора курса интересующие вас вопросы"></textarea>
</div>

@ -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,6 +282,7 @@ 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'))
return context
def get_template_names(self):

@ -9,6 +9,7 @@ from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import ArrayField, JSONField
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
@ -245,4 +246,4 @@ class SchoolPayment(Payment):
@property
def date_end_humanize(self):
return arrow.get(self.date_end).humanize(locale='ru')
return arrow.get(self.date_end, settings.TIME_ZONE).humanize(locale='ru')

@ -17,6 +17,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.timezone import now
from django.conf import settings
from paymentwall import Pingback, Product, Widget
@ -196,12 +197,18 @@ class PaymentwallCallbackView(View):
payment.status = pingback.get_type()
payment.data = payment_raw_data
if pingback.is_deliverable():
effective_amount = pingback.get_parameter('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,
@ -219,14 +226,14 @@ class PaymentwallCallbackView(View):
date_start = self.add_months(sourcedate=now().replace(hour=0, minute=0, day=1), months=1)
date_end = school_payment.date_end
else:
date_start = arrow.get(school_payment.date_end).shift(days=1).datetime
date_end = arrow.get(date_start).shift(months=1).datetime
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
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).shift(months=1, minutes=-1).datetime
date_end = arrow.get(date_start, settings.TIME_ZONE).shift(months=1, minutes=-1).datetime
payment.date_start = date_start
payment.date_end = date_end
if product_type_name == 'course':
@ -251,6 +258,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',
@ -268,6 +276,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():
@ -276,7 +285,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:

@ -1,4 +1,4 @@
<a
class="timing__btn btn btn_light"
href="{% url 'school:lesson-detail' live_lesson.id %}"
>смотреть урок</a>
>подробнее</a>

@ -8,27 +8,25 @@
<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__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 %}

@ -1,5 +1,3 @@
import arrow
from io import BytesIO
from PIL import Image
from uuid import uuid4

@ -56,6 +56,7 @@ INSTALLED_APPS = [
'sorl.thumbnail',
'raven.contrib.django.raven_compat',
'django_user_agents',
'imagekit',
] + [
'apps.auth.apps',
'apps.user',
@ -143,6 +144,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/

@ -50,6 +50,9 @@
<div class="header__group">
<a class="header__section" href="https://blog.lil.school">БЛОГ</a>
</div>
<div class="header__group">
<a class="header__section" href="{% url 'contest' 'august' %}">КОНКУРС</a>
</div>
</nav>
</div>
{% include 'templates/blocks/user_menu.html' %}

@ -0,0 +1,10 @@
{% load static %}
<script>
window.LIL_STORE = {
staticUrl: '{% static "" %}',
accessToken: '{{ request.user.auth_token }}',
user: {
id: {{ request.user.id|default:'null' }},
}
};
</script>

@ -11,6 +11,9 @@
<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="http://www.pinkbus.ru/">
<img class="partners__pic" src="{% static 'img/pinkbus.jpg' %}">
</a>
</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">

@ -1,7 +1,7 @@
{% load static %}
<div class="share">
<div class="share__title">Поделиться {% if livelesson or lesson %}уроком{% else %}курсом{% endif %}</div>
<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">
<a class="share__item facebook" href="#" data-url="http://{{request.META.HTTP_HOST}}{{object.get_absolute_url}}">
<svg class="icon icon-share-facebook">

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

@ -130,14 +130,8 @@
{% include "templates/blocks/popup_course_lock.html" %}
{% include "templates/blocks/popup_subscribe.html" %}
</div>
<script>
window.STORE = {
accessToken: '{{ request.user.auth_token }}',
user: {
id: '{{ request.user.id }}',
}
};
</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/<int:pk>/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'),
]

@ -87,7 +87,7 @@ class IndexView(TemplateView):
'online': online,
'online_coming_soon': online_coming_soon,
'school_schedule': school_schedule,
'course_items': Course.objects.filter(status=Course.PUBLISHED)[:6],
'course_items': Course.shuffle(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(),

@ -28,5 +28,7 @@ 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
short_url

@ -34,9 +34,13 @@
"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",

@ -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,86 @@
<template>
<div class="contest-works">
<div class="contest-works__works">
<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();
}
});
}
},
methods: {
load() {
this.loading = true;
api.get(`/api/v1/contest-works/?contest=${this.contestId}&page=${this.page}`)
.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 {
column-width: 300px;
column-gap: 20px;
text-align: left;
}
.contest-works__loader {
width: 100%;
height: 30px;
position: relative;
}
.contest-works__no-works {
text-align: center;
width: 100%;
}
</style>

@ -992,28 +992,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,6 +10,7 @@
<script>
import {api} from "../../js/modules/api";
import downscale from 'downscale';
export default {
name: "lil-image",
@ -22,19 +23,37 @@
methods: {
onImageAdded(event) {
this.loading = true;
const maxSize = 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 = maxSize;
h = 0;
}
else {
w = 0;
h = maxSize;
}
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,74 @@
<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";
export default {
name: 'likes',
props: ['likes', 'userLiked', 'objType', 'objId'],
data() {
return {
likesProp: +this.likes || 0,
userLikedProp: this.userLiked || false,
}
},
methods: {
addLike(event) {
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');
}
});
}
}
}
}
</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>

@ -18,7 +18,7 @@
const me = this;
$(me.$refs.input).redactor({
air: true,
buttonsHide: ['image', 'link', 'format'],
buttonsHide: ['image', 'format'],
lang: 'ru',
placeholder: this.placeholder,
callbacks: {
@ -40,4 +40,4 @@
<style scoped>
</style>
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@ -17,3 +17,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,
}
});

@ -12,6 +12,10 @@ moment.locale('ru');
const history = createHistory();
$(document).ready(function () {
const currentCategory = $('div.js-select-option.active[data-category-option]').attr('data-category-name');
if(currentCategory) {
$('.js-select[data-category-select] .js-select-head').text(currentCategory);
}
// Обработчик отложенных курсов
setInterval(() => {
$('div[data-future-course]').each((_, element) => {
@ -129,4 +133,4 @@ function load_courses(coursesUrl, fromStart) {
buttonElement.removeClass('loading');
}
});
}
}

@ -7,10 +7,13 @@ $(document).ready(function () {
popup = $('.popup.visible.open');
body.on('click', '[data-popup]', function(e){
let data = $(this).data('popup');
if(! data) {
return true;
}
e.preventDefault();
e.stopPropagation();
let data = $(this).data('popup');
popup = $(data);
showPopup();

@ -175,7 +175,7 @@ button
top: 1px
left: 1px
right: 1px
bottom: 1px
bottom: 2px
background: white
border-radius: 2px
transition: opacity .2s
@ -812,12 +812,10 @@ a[name]
+m
margin-bottom: 30px
p
+t
display: inline
&:not(:last-child)
margin-bottom: 35px
+t
margin: 0
margin: 5px 5px 15px 5px
&__curve
position: absolute
pointer-events: none
@ -2654,10 +2652,9 @@ a.grey-link
border-radius: 50%;
overflow: hidden;
&__pic
top: 50%;
position: relative;
transform: translateY(-50%);
object-fit: cover
width: 100%
height: 100%
&__content
flex: 0 0 calc(100% - 165px)
&__actions
@ -2700,7 +2697,7 @@ a.grey-link
margin-bottom: 10px
color: #191919
&__content
margin-bottom: 30px
margin-bottom: 15px
color: #191919
&__video_frame
width: 100%
@ -2708,6 +2705,26 @@ a.grey-link
&__chat_frame
width: 100%
height: 600px
&__row
display: flex
+m
display: block
&__preview
margin-right: 25px
flex: 0 0 140px
+m
display: none
&__pic-wrapper
width: 130px;
height: 130px;
border-radius: 50%;
overflow: hidden;
&__pic
object-fit: cover
width: 100%
height: 100%
&__user
margin-bottom: 30px
.lessons
@ -3463,6 +3480,24 @@ a.grey-link
.icon
font-size: 20px
fill: #B5B5B5
&.loading &-image
visibility: hidden
&.loading
.icon
visibility: hidden
&:after
content: ''
position: absolute
top: 50%
left: 50%
width: 24px
height: 24px
margin: -12px 0 0 -12px
border: 3px solid #B5B5B5
border-left: 3px solid transparent
border-radius: 50%
animation: loading .6s infinite linear
&__file
position: absolute
top: 0
@ -3869,7 +3904,7 @@ a.grey-link
position: relative
padding-left: 17px
&:before
content: ''
content: '-'
position: absolute
top: 0
left: 0
@ -4080,3 +4115,37 @@ a
.anchor
padding-top: 100px
margin-top: -100px
.contest-work
text-transform: uppercase;
font-weight: bold;
&__img-wrap
width: 100%;
text-align: center;
&__img
max-width: 100%;
&__info
display: flex;
padding: 5px 10px;
&__age
color: #919191;
&__bio
flex: calc(100% - 70px);
.loading-loader
content: ''
position: absolute
top: 50%
left: 50%
width: 24px
height: 24px
margin: -12px 0 0 -12px
border: 3px solid #B5B5B5
border-left: 3px solid transparent
border-radius: 50%
animation: loading .6s infinite linear

@ -9,6 +9,7 @@ module.exports = {
entry: {
app: "./src/js/app.js",
courseRedactor: "./src/js/course-redactor.js",
contestRedactor: "./src/js/contest-redactor.js",
mixpanel: "./src/js/third_party/mixpanel-2-latest.js",
sprite: glob('./src/icons/*.svg'),
images: glob('./src/img/*'),
@ -116,6 +117,10 @@ module.exports = {
},
watch: NODE_ENV === 'development',
watchOptions: {
ignored: 'node_modules',
poll: 2000,
},
devtool: NODE_ENV === 'development' ? 'source-map' : false
};

Loading…
Cancel
Save