diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1d78b5d1..a1a48c4d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -44,6 +44,7 @@ stop-review:
script:
- export REVIEW_HOST=$CI_COMMIT_REF_SLUG-$REVIEW_DOMAIN
- cd docker
+ - cp .env.review .env
- docker-compose -f docker-compose-review.yml -p back$CI_COMMIT_REF_NAME down
- rm -rf /work/data/back_${CI_COMMIT_REF_NAME}/
when: manual
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/api/v1/serializers/content.py b/api/v1/serializers/content.py
index 729368e2..89daefa1 100644
--- a/api/v1/serializers/content.py
+++ b/api/v1/serializers/content.py
@@ -3,8 +3,7 @@ from rest_framework import serializers
from apps.content.models import (
Baner, Content, Image, Text, ImageText, Video,
- Gallery, GalleryImage, ImageObject,
-)
+ Gallery, GalleryImage, ImageObject,)
from . import Base64ImageField
@@ -14,6 +13,7 @@ BASE_CONTENT_FIELDS = (
'uuid',
'course',
'lesson',
+ 'contest',
'live_lesson',
'title',
'position',
@@ -85,12 +85,16 @@ class ImageObjectSerializer(serializers.ModelSerializer):
image = Base64ImageField(
required=True, allow_empty_file=False, allow_null=False, read_only=False,
)
+ image_thumbnail = Base64ImageField(
+ required=False, allow_empty_file=True, allow_null=True, read_only=True,
+ )
class Meta:
model = ImageObject
fields = (
'id',
'image',
+ 'image_thumbnail',
'created_at',
'update_at',
)
@@ -254,3 +258,4 @@ class ContentSerializer(serializers.ModelSerializer):
elif isinstance(obj, Gallery):
return GallerySerializer(obj, context=self.context).to_representation(obj)
return super(ContentSerializer, self).to_representation(obj)
+
diff --git a/api/v1/serializers/contest.py b/api/v1/serializers/contest.py
new file mode 100644
index 00000000..dc5b88fd
--- /dev/null
+++ b/api/v1/serializers/contest.py
@@ -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)
diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py
index bdb1a289..fe06ca53 100644
--- a/api/v1/serializers/course.py
+++ b/api/v1/serializers/course.py
@@ -1,6 +1,10 @@
+from ipware import get_client_ip
+
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
+from django.contrib.auth import get_user_model
+
from apps.course.models import (
Category, Course,
Comment, CourseComment, LessonComment,
@@ -15,12 +19,15 @@ from .content import (
from apps.content.models import (
Content, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject,
-)
+ ContestWork)
from .user import UserSerializer
from .mixins import DispatchContentMixin, DispatchGalleryMixin, DispatchMaterialMixin
+User = get_user_model()
+
+
class MaterialCreateSerializer(serializers.ModelSerializer):
class Meta:
@@ -63,6 +70,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:
diff --git a/api/v1/serializers/mixins.py b/api/v1/serializers/mixins.py
index 0f055f00..40584503 100644
--- a/api/v1/serializers/mixins.py
+++ b/api/v1/serializers/mixins.py
@@ -4,7 +4,7 @@ from apps.school.models import LiveLesson
from apps.content.models import (
Content, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject,
-)
+ Contest)
from .content import (
TextCreateSerializer, ImageCreateSerializer,
ImageTextCreateSerializer, VideoCreateSerializer,
@@ -25,6 +25,8 @@ class DispatchContentMixin(object):
obj_type = 'lesson'
elif isinstance(obj, LiveLesson):
obj_type = 'live_lesson'
+ elif isinstance(obj, Contest):
+ obj_type = 'contest'
cdata[obj_type] = obj.id
if ctype == 'text':
if 'id' in cdata and cdata['id']:
diff --git a/api/v1/urls.py b/api/v1/urls.py
index 409ed79f..51610cb6 100644
--- a/api/v1/urls.py
+++ b/api/v1/urls.py
@@ -18,7 +18,7 @@ from .views import (
UserViewSet, LessonViewSet, ImageObjectViewSet,
SchoolScheduleViewSet, LiveLessonViewSet,
PaymentViewSet,
-)
+ ContestViewSet, ContestWorkViewSet)
router = DefaultRouter()
router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests')
@@ -44,6 +44,9 @@ router.register(r'school-schedules', SchoolScheduleViewSet, base_name='school-sc
router.register(r'users', UserViewSet, base_name='users')
+router.register(r'contests', ContestViewSet, base_name='contests')
+router.register(r'contest-works', ContestWorkViewSet, base_name='contest_works')
+
# router.register(r'configs', ConfigViewSet, base_name='configs')
diff --git a/api/v1/views.py b/api/v1/views.py
index 271d907f..2089f123 100644
--- a/api/v1/views.py
+++ b/api/v1/views.py
@@ -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)
diff --git a/apps/auth/backend.py b/apps/auth/backend.py
new file mode 100644
index 00000000..41568b52
--- /dev/null
+++ b/apps/auth/backend.py
@@ -0,0 +1,17 @@
+from django.contrib.auth.backends import ModelBackend
+from django.contrib.auth import get_user_model
+
+User = get_user_model()
+
+
+class CaseInsensitiveModelBackend(ModelBackend):
+
+ def authenticate(self, request, username=None, password=None, **kwargs):
+ if username is None:
+ username = kwargs.get(User.USERNAME_FIELD)
+ try:
+ user = User.objects.get(**{f'{User.USERNAME_FIELD}__iexact': username})
+ if user.check_password(password) and self.user_can_authenticate(user):
+ return user
+ except User.DoesNotExist:
+ return None
diff --git a/apps/auth/forms.py b/apps/auth/forms.py
index 0baea362..e369a8f9 100644
--- a/apps/auth/forms.py
+++ b/apps/auth/forms.py
@@ -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()
diff --git a/apps/auth/views.py b/apps/auth/views.py
index 6272dbac..f83a9269 100644
--- a/apps/auth/views.py
+++ b/apps/auth/views.py
@@ -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', '')
diff --git a/apps/content/migrations/0021_auto_20180813_1306.py b/apps/content/migrations/0021_auto_20180813_1306.py
new file mode 100644
index 00000000..66033641
--- /dev/null
+++ b/apps/content/migrations/0021_auto_20180813_1306.py
@@ -0,0 +1,48 @@
+# Generated by Django 2.0.6 on 2018-08-13 13:06
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('course', '0040_course_age'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('content', '0020_auto_20180424_1607'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Contest',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=255)),
+ ('description', models.TextField(blank=True, default='', max_length=1000)),
+ ('slug', models.SlugField(allow_unicode=True, blank=True, max_length=100, null=True, unique=True)),
+ ('date_start', models.DateField(blank=True, null=True, verbose_name='Дата начала')),
+ ('date_end', models.DateField(blank=True, null=True, verbose_name='Дата окончания')),
+ ('active', models.BooleanField(default=True)),
+ ('cover', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='contest_covers', to='content.ImageObject', verbose_name='Фоновая картинка')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ContestWork',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('child_full_name', models.CharField(max_length=255)),
+ ('age', models.SmallIntegerField()),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.Contest')),
+ ('image', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contest_work_images', to='content.ImageObject', verbose_name='Работа участника')),
+ ('likes', models.ManyToManyField(blank=True, to='course.Like')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='content',
+ name='contest',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='content', to='content.Contest', verbose_name='Конкурс'),
+ ),
+ ]
diff --git a/apps/content/migrations/0022_auto_20180815_2129.py b/apps/content/migrations/0022_auto_20180815_2129.py
new file mode 100644
index 00000000..92cc567d
--- /dev/null
+++ b/apps/content/migrations/0022_auto_20180815_2129.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.0.6 on 2018-08-15 21:29
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('content', '0021_auto_20180813_1306'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='contestwork',
+ options={'ordering': ('-created_at',), 'verbose_name': 'Конкурсная работа', 'verbose_name_plural': 'Конкурсные работы'},
+ ),
+ ]
diff --git a/apps/content/models.py b/apps/content/models.py
index 468058eb..197e1e7d 100644
--- a/apps/content/models.py
+++ b/apps/content/models.py
@@ -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])
diff --git a/apps/course/templates/course/content/gallery.html b/apps/content/templates/content/blocks/gallery.html
similarity index 97%
rename from apps/course/templates/course/content/gallery.html
rename to apps/content/templates/content/blocks/gallery.html
index 1116322a..69ad0386 100644
--- a/apps/course/templates/course/content/gallery.html
+++ b/apps/content/templates/content/blocks/gallery.html
@@ -1,32 +1,32 @@
-{% load thumbnail %}
-{% if results %}
-
Галерея итогов обучения
-
- {% for image in course.gallery.gallery_images.all %}
-
- {% endfor %}
-
-{% else %}
-
-
-
{{ content.title }}
-
- {% for image in content.gallery_images.all %}
-
- {% endfor %}
-
-
-
-{% endif %}
+{% load thumbnail %}
+{% if results %}
+ Галерея итогов обучения
+
+ {% for image in course.gallery.gallery_images.all %}
+
+ {% endfor %}
+
+{% else %}
+
+
+
{{ content.title }}
+
+ {% for image in content.gallery_images.all %}
+
+ {% endfor %}
+
+
+
+{% endif %}
diff --git a/apps/course/templates/course/content/image.html b/apps/content/templates/content/blocks/image.html
similarity index 96%
rename from apps/course/templates/course/content/image.html
rename to apps/content/templates/content/blocks/image.html
index 4ab24601..5d515430 100644
--- a/apps/course/templates/course/content/image.html
+++ b/apps/content/templates/content/blocks/image.html
@@ -1,10 +1,10 @@
-
-
-
- {{ content.title }}
-
-
-
-
-
-
+
+
+
+ {{ content.title }}
+
+
+
+
+
+
diff --git a/apps/course/templates/course/content/imagetext.html b/apps/content/templates/content/blocks/imagetext.html
similarity index 100%
rename from apps/course/templates/course/content/imagetext.html
rename to apps/content/templates/content/blocks/imagetext.html
diff --git a/apps/course/templates/course/content/text.html b/apps/content/templates/content/blocks/text.html
similarity index 94%
rename from apps/course/templates/course/content/text.html
rename to apps/content/templates/content/blocks/text.html
index 18ce2235..59e46ded 100644
--- a/apps/course/templates/course/content/text.html
+++ b/apps/content/templates/content/blocks/text.html
@@ -1,10 +1,10 @@
-
-
-
- {{ content.title }}
-
-
- {{ content.txt | safe }}
-
-
+
+
+
+ {{ content.title }}
+
+
+ {{ content.txt | safe }}
+
+
\ No newline at end of file
diff --git a/apps/course/templates/course/content/video.html b/apps/content/templates/content/blocks/video.html
similarity index 95%
rename from apps/course/templates/course/content/video.html
rename to apps/content/templates/content/blocks/video.html
index 3527507f..fe741465 100644
--- a/apps/course/templates/course/content/video.html
+++ b/apps/content/templates/content/blocks/video.html
@@ -1,43 +1,43 @@
-
-
-
- {{ content.title }}
-
-
- {% if 'youtube.com' in content.url or 'youtu.be' in content.url %}
- VIDEO
- {% elif 'vimeo.com' in content.url %}
-
- {% endif %}
-
-
-
-
-
\ No newline at end of file
diff --git a/apps/content/templates/content/contest.html b/apps/content/templates/content/contest.html
new file mode 100644
index 00000000..f37f3886
--- /dev/null
+++ b/apps/content/templates/content/contest.html
@@ -0,0 +1,47 @@
+{% extends "templates/lilcity/index.html" %}
+
+
+{% block content %}
+
+
+
+
+ {{ contest.title }}
+
+
+ {{ contest.description }}
+
+
+ {% if not contest_work_uploaded %}
+
Загрузить свою работу
+ {% endif %}
+
+
+
+
+{% for content in contest.content.all %}
+
+ {% with template="content/blocks/"|add:content.ctype|add:".html" %}
+ {% include template %}
+ {% endwith %}
+
+{% endfor %}
+
+
+{% endblock content %}
diff --git a/apps/content/templates/content/contest_edit.html b/apps/content/templates/content/contest_edit.html
new file mode 100644
index 00000000..d1c26b62
--- /dev/null
+++ b/apps/content/templates/content/contest_edit.html
@@ -0,0 +1,18 @@
+{% extends "templates/lilcity/edit_index.html" %}
+{% load static %}
+
+{% block title %}
+ {% if object %}
+ Редактирование конкурса {{ object.title }}
+ {% else %}
+ Создание конкурса
+ {% endif %}
+{% endblock title %}
+
+{% block content %}
+
+{% endblock content %}
+{% block foot %}
+
+
+{% endblock foot %}
diff --git a/apps/content/templates/content/contest_work.html b/apps/content/templates/content/contest_work.html
new file mode 100644
index 00000000..67e2e287
--- /dev/null
+++ b/apps/content/templates/content/contest_work.html
@@ -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 %}
+
+
+
+
+
+
+
+
+
{{ contest_work.child_full_name }}
+
{{ contest_work.age }} {% if contest_work.age < 5 %}года{% else %}лет{% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include 'templates/blocks/share.html' with share_object_name='работой' %}
+
+
+
+
+
+
Оставьте комментарий:
+
+ {% if request.user.is_authenticated %}
+
+ {% else %}
+
Только зарегистрированные пользователи могут оставлять комментарии.
+ {% endif %}
+
+ {% include "templates/blocks/comments.html" with object=contest_work %}
+
+
+
+
+{% endblock content %}
diff --git a/apps/content/views.py b/apps/content/views.py
index 28002783..ff3a4deb 100644
--- a/apps/content/views.py
+++ b/apps/content/views.py
@@ -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,
+ })
diff --git a/apps/course/filters.py b/apps/course/filters.py
index 827079e6..6e4e873e 100644
--- a/apps/course/filters.py
+++ b/apps/course/filters.py
@@ -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
diff --git a/apps/course/migrations/0040_course_age.py b/apps/course/migrations/0040_course_age.py
new file mode 100644
index 00000000..f8426fd7
--- /dev/null
+++ b/apps/course/migrations/0040_course_age.py
@@ -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),
+ ),
+ ]
diff --git a/apps/course/migrations/0041_auto_20180813_1306.py b/apps/course/migrations/0041_auto_20180813_1306.py
new file mode 100644
index 00000000..d2596544
--- /dev/null
+++ b/apps/course/migrations/0041_auto_20180813_1306.py
@@ -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',
+ ),
+ ]
diff --git a/apps/course/migrations/0042_like_ip.py b/apps/course/migrations/0042_like_ip.py
new file mode 100644
index 00000000..388ab03f
--- /dev/null
+++ b/apps/course/migrations/0042_like_ip.py
@@ -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),
+ ),
+ ]
diff --git a/apps/course/models.py b/apps/course/models.py
index 942a0593..39eed28e 100644
--- a/apps/course/models.py
+++ b/apps/course/models.py
@@ -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')
diff --git a/apps/course/templates/course/_items.html b/apps/course/templates/course/_items.html
index 42757ddc..b4356f2b 100644
--- a/apps/course/templates/course/_items.html
+++ b/apps/course/templates/course/_items.html
@@ -48,7 +48,7 @@
{{ course.category | upper }}
+ href="{% url 'courses' %}?category={{ course.category.id }}">{{ course.category | upper }}
{% if not course.is_free %}
{{ course.price|floatformat:"-2" }}₽
{% endif %}
diff --git a/apps/course/templates/course/course.html b/apps/course/templates/course/course.html
index 53a56abc..c3a35c4f 100644
--- a/apps/course/templates/course/course.html
+++ b/apps/course/templates/course/course.html
@@ -227,7 +227,7 @@
{% 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 %}
diff --git a/apps/course/templates/course/course_only_lessons.html b/apps/course/templates/course/course_only_lessons.html
index 74a5bf47..b987475c 100644
--- a/apps/course/templates/course/course_only_lessons.html
+++ b/apps/course/templates/course/course_only_lessons.html
@@ -180,19 +180,18 @@
{% endif %}
-
{{ forloop.counter }}. {{ lesson.title }}
- {% if lesson.cover %}
-
+
- {% endif %}
-
{{ lesson.short_description | truncatechars_html:800 | safe | linebreaks }}
-
-
diff --git a/apps/course/templates/course/courses.html b/apps/course/templates/course/courses.html
index a3277174..8f6f57d2 100644
--- a/apps/course/templates/course/courses.html
+++ b/apps/course/templates/course/courses.html
@@ -20,8 +20,8 @@
-
-
{% if category %}{{ category.0 }}{% else %}Категории{% endif %}
+
+
Категории
diff --git a/apps/course/templates/course/inclusion/category_items.html b/apps/course/templates/course/inclusion/category_items.html
index 5e6aeef5..5549ffa4 100644
--- a/apps/course/templates/course/inclusion/category_items.html
+++ b/apps/course/templates/course/inclusion/category_items.html
@@ -1,5 +1,6 @@
{% for cat in category_items %}
-
+
{% endfor %}
\ No newline at end of file
diff --git a/apps/course/templates/course/inclusion/category_menu_items.html b/apps/course/templates/course/inclusion/category_menu_items.html
index a01342a9..ecb83298 100644
--- a/apps/course/templates/course/inclusion/category_menu_items.html
+++ b/apps/course/templates/course/inclusion/category_menu_items.html
@@ -2,7 +2,7 @@
{% for cat in category_items %}
-
{% endfor %}
diff --git a/apps/course/templates/course/lesson.html b/apps/course/templates/course/lesson.html
index 662a4dd5..94d6b89b 100644
--- a/apps/course/templates/course/lesson.html
+++ b/apps/course/templates/course/lesson.html
@@ -27,24 +27,43 @@
{% endif %}
{% 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 @@
{% if request.user.is_authenticated %}
+
+
diff --git a/apps/course/templatetags/lilcity_category.py b/apps/course/templatetags/lilcity_category.py
index 957e50c2..956bbab4 100644
--- a/apps/course/templatetags/lilcity_category.py
+++ b/apps/course/templatetags/lilcity_category.py
@@ -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,
}
diff --git a/apps/course/views.py b/apps/course/views.py
index 3ecd6bd0..857a57d0 100644
--- a/apps/course/views.py
+++ b/apps/course/views.py
@@ -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):
diff --git a/apps/payment/models.py b/apps/payment/models.py
index 17c75bb1..ec3936e2 100644
--- a/apps/payment/models.py
+++ b/apps/payment/models.py
@@ -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')
diff --git a/apps/payment/views.py b/apps/payment/views.py
index 2f70df36..afe2b8ac 100644
--- a/apps/payment/views.py
+++ b/apps/payment/views.py
@@ -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:
diff --git a/apps/school/templates/blocks/open_lesson.html b/apps/school/templates/blocks/open_lesson.html
index 46999c7f..abb8a9a2 100644
--- a/apps/school/templates/blocks/open_lesson.html
+++ b/apps/school/templates/blocks/open_lesson.html
@@ -1,4 +1,4 @@
смотреть урок
+>подробнее
diff --git a/apps/school/templates/school/livelesson_detail.html b/apps/school/templates/school/livelesson_detail.html
index 1bcb3227..26e785f9 100644
--- a/apps/school/templates/school/livelesson_detail.html
+++ b/apps/school/templates/school/livelesson_detail.html
@@ -8,27 +8,25 @@
{{ livelesson.title }}
{{ livelesson.short_description }}
-
+
{% for content in livelesson.content.all %}
- {% with template="course/content/"|add:content.ctype|add:".html" %}
+ {% with template="content/blocks/"|add:content.ctype|add:".html" %}
{% include template %}
{% endwith %}
diff --git a/apps/user/views.py b/apps/user/views.py
index da1245ae..309fb0e0 100644
--- a/apps/user/views.py
+++ b/apps/user/views.py
@@ -1,5 +1,3 @@
-import arrow
-
from io import BytesIO
from PIL import Image
from uuid import uuid4
diff --git a/project/settings.py b/project/settings.py
index b4fc9a6f..1228211c 100644
--- a/project/settings.py
+++ b/project/settings.py
@@ -56,6 +56,7 @@ INSTALLED_APPS = [
'sorl.thumbnail',
'raven.contrib.django.raven_compat',
'django_user_agents',
+ 'imagekit',
] + [
'apps.auth.apps',
'apps.user',
@@ -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/
diff --git a/project/templates/blocks/header.html b/project/templates/blocks/header.html
index 5be001d8..38d314bf 100644
--- a/project/templates/blocks/header.html
+++ b/project/templates/blocks/header.html
@@ -50,6 +50,9 @@
+
{% include 'templates/blocks/user_menu.html' %}
diff --git a/project/templates/blocks/lil_store_js.html b/project/templates/blocks/lil_store_js.html
new file mode 100644
index 00000000..cb5eb6f8
--- /dev/null
+++ b/project/templates/blocks/lil_store_js.html
@@ -0,0 +1,10 @@
+{% load static %}
+
diff --git a/project/templates/blocks/partners.html b/project/templates/blocks/partners.html
index ee16423d..194daf54 100644
--- a/project/templates/blocks/partners.html
+++ b/project/templates/blocks/partners.html
@@ -11,6 +11,9 @@
+
+
+
diff --git a/project/templates/blocks/popup_auth.html b/project/templates/blocks/popup_auth.html
index 058a2205..1a527a90 100644
--- a/project/templates/blocks/popup_auth.html
+++ b/project/templates/blocks/popup_auth.html
@@ -3,7 +3,7 @@