LIL-582 Конкурс

remotes/origin/hotfix/LIL-661
gzbender 8 years ago
parent 916deaea6a
commit 0ad60238a9
  1. 0
      __init__.py
  2. 5
      api/v1/serializers/content.py
  3. 45
      api/v1/serializers/contest.py
  4. 4
      api/v1/serializers/mixins.py
  5. 5
      api/v1/urls.py
  6. 21
      api/v1/views.py
  7. 48
      apps/content/migrations/0021_auto_20180813_1306.py
  8. 48
      apps/content/models.py
  9. 3
      apps/content/templates/content/blocks/contest_work.html
  10. 64
      apps/content/templates/content/blocks/gallery.html
  11. 20
      apps/content/templates/content/blocks/image.html
  12. 0
      apps/content/templates/content/blocks/imagetext.html
  13. 18
      apps/content/templates/content/blocks/text.html
  14. 84
      apps/content/templates/content/blocks/video.html
  15. 44
      apps/content/templates/content/contest.html
  16. 18
      apps/content/templates/content/contest_edit.html
  17. 24
      apps/content/views.py
  18. 18
      apps/course/migrations/0040_course_age.py
  19. 36
      apps/course/migrations/0041_auto_20180813_1306.py
  20. 6
      apps/course/models.py
  21. 2
      apps/course/templates/course/course.html
  22. 2
      apps/course/templates/course/lesson.html
  23. 8
      project/templates/blocks/lil_store_js.html
  24. 1
      project/templates/lilcity/edit_index.html
  25. 1
      project/templates/lilcity/index.html
  26. 4
      project/urls.py
  27. 361
      web/src/components/ContestRedactor.vue
  28. 30
      web/src/components/ContestWorks.vue
  29. 38
      web/src/components/blocks/ContestWork
  30. 19
      web/src/js/contest-redactor.js

@ -3,8 +3,7 @@ from rest_framework import serializers
from apps.content.models import ( from apps.content.models import (
Baner, Content, Image, Text, ImageText, Video, Baner, Content, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject, Gallery, GalleryImage, ImageObject,)
)
from . import Base64ImageField from . import Base64ImageField
@ -14,6 +13,7 @@ BASE_CONTENT_FIELDS = (
'uuid', 'uuid',
'course', 'course',
'lesson', 'lesson',
'contest',
'live_lesson', 'live_lesson',
'title', 'title',
'position', 'position',
@ -254,3 +254,4 @@ class ContentSerializer(serializers.ModelSerializer):
elif isinstance(obj, Gallery): elif isinstance(obj, Gallery):
return GallerySerializer(obj, context=self.context).to_representation(obj) return GallerySerializer(obj, context=self.context).to_representation(obj)
return super(ContentSerializer, self).to_representation(obj) return super(ContentSerializer, self).to_representation(obj)

@ -0,0 +1,45 @@
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):
class Meta:
model = ContestWork
fields = '__all__'

@ -4,7 +4,7 @@ from apps.school.models import LiveLesson
from apps.content.models import ( from apps.content.models import (
Content, Image, Text, ImageText, Video, Content, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject, Gallery, GalleryImage, ImageObject,
) Contest)
from .content import ( from .content import (
TextCreateSerializer, ImageCreateSerializer, TextCreateSerializer, ImageCreateSerializer,
ImageTextCreateSerializer, VideoCreateSerializer, ImageTextCreateSerializer, VideoCreateSerializer,
@ -25,6 +25,8 @@ class DispatchContentMixin(object):
obj_type = 'lesson' obj_type = 'lesson'
elif isinstance(obj, LiveLesson): elif isinstance(obj, LiveLesson):
obj_type = 'live_lesson' obj_type = 'live_lesson'
elif isinstance(obj, Contest):
obj_type = 'contest'
cdata[obj_type] = obj.id cdata[obj_type] = obj.id
if ctype == 'text': if ctype == 'text':
if 'id' in cdata and cdata['id']: if 'id' in cdata and cdata['id']:

@ -18,7 +18,7 @@ from .views import (
UserViewSet, LessonViewSet, ImageObjectViewSet, UserViewSet, LessonViewSet, ImageObjectViewSet,
SchoolScheduleViewSet, LiveLessonViewSet, SchoolScheduleViewSet, LiveLessonViewSet,
PaymentViewSet, PaymentViewSet,
) ContestViewSet, ContestWorkViewSet)
router = DefaultRouter() router = DefaultRouter()
router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests') 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'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') # router.register(r'configs', ConfigViewSet, base_name='configs')

@ -39,6 +39,7 @@ from .serializers.user import (
AuthorRequestSerializer, AuthorRequestSerializer,
UserSerializer, UserPhotoSerializer, UserSerializer, UserPhotoSerializer,
) )
from .serializers.contest import ContestCreateSerializer, ContestSerializer, ContestWorkSerializer
from .permissions import ( from .permissions import (
IsAdmin, IsAdminOrIsSelf, IsAdmin, IsAdminOrIsSelf,
@ -56,7 +57,7 @@ from apps.config.models import Config
from apps.content.models import ( from apps.content.models import (
Baner, Image, Text, ImageText, Video, Baner, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject, Gallery, GalleryImage, ImageObject,
) Contest, ContestWork)
from apps.payment.models import ( from apps.payment.models import (
AuthorBalance, Payment, AuthorBalance, Payment,
CoursePayment, SchoolPayment, CoursePayment, SchoolPayment,
@ -429,3 +430,21 @@ class PaymentViewSet(ExtendedModelViewSet):
'amount', 'created_at', 'amount', 'created_at',
) )
search_fields = ('user__email', 'user__first_name', 'user__last_name',) search_fields = ('user__email', 'user__first_name', 'user__last_name',)
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 = ContestWorkSerializer

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

@ -1,9 +1,13 @@
from urllib.parse import urlparse from urllib.parse import urlparse
from django.db import models from django.db import models
from django.contrib.auth import get_user_model
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
User = get_user_model()
class ImageObject(models.Model): class ImageObject(models.Model):
image = models.ImageField('Изображение', upload_to='content/imageobject') image = models.ImageField('Изображение', upload_to='content/imageobject')
@ -36,6 +40,12 @@ class Content(PolymorphicModel):
verbose_name='Урок онлайн школы', verbose_name='Урок онлайн школы',
related_name='content', 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='') title = models.CharField('Заголовок', max_length=100, default='')
position = models.PositiveSmallIntegerField( position = models.PositiveSmallIntegerField(
'Положение на странице', 'Положение на странице',
@ -131,3 +141,41 @@ class Baner(models.Model):
if self.use: if self.use:
Baner.objects.filter(use=True).update(use=False) Baner.objects.filter(use=True).update(use=False)
return super().save(*args, **kwargs) 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)

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

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

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

@ -1,43 +1,43 @@
<div class="section section_border"> <div class="section section_border">
<div class="section__center center center_sm"> <div class="section__center center center_sm">
<div class="content-block title"> <div class="content-block title">
{{ content.title }} {{ content.title }}
</div> </div>
<div class="iframe__container"> <div class="iframe__container">
{% if 'youtube.com' in content.url or 'youtu.be' in content.url %} {% 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" <iframe src="https://www.youtube.com/embed/{{ content.video_index }}" frameborder="0" allow="autoplay; encrypted-media"
allowfullscreen></iframe> allowfullscreen></iframe>
{% elif 'vimeo.com' in content.url %} {% elif 'vimeo.com' in content.url %}
<iframe src="https://player.vimeo.com/video/{{ content.video_index }}" frameborder="0" webkitallowfullscreen <iframe src="https://player.vimeo.com/video/{{ content.video_index }}" frameborder="0" webkitallowfullscreen
mozallowfullscreen allowfullscreen> mozallowfullscreen allowfullscreen>
</iframe> </iframe>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
<style> <style>
@media only screen and (max-width: 639px) { @media only screen and (max-width: 639px) {
iframe { iframe {
width: 100%; width: 100%;
height: 240px; height: 240px;
} }
.iframe__container { .iframe__container {
text-align: center; text-align: center;
width: 100%; width: 100%;
padding-bottom: 56.25%; padding-bottom: 56.25%;
margin-bottom: -56.25%; margin-bottom: -56.25%;
} }
} }
@media only screen and (min-width: 640px) { @media only screen and (min-width: 640px) {
.iframe__container { .iframe__container {
text-align: center; text-align: center;
} }
iframe { iframe {
width: 640px; width: 640px;
height: 360px; height: 360px;
} }
} }
</style> </style>

@ -0,0 +1,44 @@
{% extends "templates/lilcity/index.html" %}
{% block content %}
<div class="main main_default">
<div class="main__center center">
<div class="main__title">
<span class="main__bold">Lil School</span> — первая образовательная онлайн-платформа креативного мышления для детей
</div>
<div class="main__subtitle">
Приглашаем вас на месяц открытых дверей в Lil School
</div>
<div class="main__actions">
<a class="main__btn btn btn_white" href="#">Загрузить свою работу</a>
</div>
</div>
</div>
{% 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">
<p>Тысячи шедевров уже созданы благодаря Lil School. Более 10000 работ можно
<a target="_blank" href='{{ config.SERVICE_INSTAGRAM_URL }}'>увидеть</a> в Инстаграм</p>
<img class="text__curve text__curve_three" src="{% static 'img/curve-3.svg' %}">
</div>
<div class="gallery">
<div class="gallery__grid">
{% for contest_work in contest_works %}
{% include '' %}
{% endfor %}
</div>
</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 %}

@ -1,2 +1,24 @@
from django.shortcuts import render from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView
from apps.content.models import Contest
@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

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

@ -15,7 +15,7 @@ from project.mixins import BaseModel, DeactivatedMixin
from .manager import CategoryQuerySet 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() User = get_user_model()
@ -284,3 +284,7 @@ class LessonComment(Comment):
class Meta(Comment.Meta): class Meta(Comment.Meta):
verbose_name = 'Комментарий урока' verbose_name = 'Комментарий урока'
verbose_name_plural = 'Комментарии уроков' verbose_name_plural = 'Комментарии уроков'
class ContestWorkComment(Comment):
contest_work = models.ForeignKey(ContestWork, on_delete=models.CASCADE, related_name='comments')

@ -227,7 +227,7 @@
</div> </div>
{% for content in course.content.all %} {% 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 %} {% include template %}
{% endwith %} {% endwith %}

@ -44,7 +44,7 @@
</div> </div>
{% for content in lesson.content.all %} {% 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 %} {% include template %}
{% endwith %} {% endwith %}

@ -0,0 +1,8 @@
<script>
window.LIL_STORE = {
accessToken: '{{ request.user.auth_token }}',
user: {
id: '{{ request.user.id }}',
}
};
</script>

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

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

@ -18,7 +18,7 @@ from django.contrib import admin
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.urls import path, include from django.urls import path, include
from apps.content.views import ContestEditView
from apps.course.views import ( from apps.course.views import (
CoursesView, likes, coursecomment, CoursesView, likes, coursecomment,
CourseView, LessonView, SearchView, CourseView, LessonView, SearchView,
@ -83,6 +83,8 @@ urlpatterns = [
path('api/v1/', include(('api.v1.urls', 'api_v1'))), path('api/v1/', include(('api.v1.urls', 'api_v1'))),
path('school/', include(('apps.school.urls', 'school'))), path('school/', include(('apps.school.urls', 'school'))),
path('test', TemplateView.as_view(template_name='templates/lilcity/test.html'), name='test'), 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'),
] ]

@ -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,30 @@
<template>
<div class="contest-works"></div>
</template>
<script>
import {api} from "../js/modules/api";
import ContestWork from './blocks/ContestWork';
export default {
name: "contest-work",
prop: ['contestId'],
data(){
return {
};
},
mounted() {
api.getContestWorks(this.contestId).then((response) => {
});
}
}
</script>
<style>
.contest-works::after {
content ' '
flex-grow 99999999
}
</style>

@ -0,0 +1,38 @@
<template>
<div class="contest-work" :style="style">
<i :style="{paddingBottom: imgHeight / imgWidth * 100 + '%'}"></i>
<img class="contest-work__img" :src="contestWork.image" />
</div>
</template>
<script>
export default {
name: "contest-work",
props: ['contestWork'],
computed: {
imgHeight() {
return contestWork.imgHeight;
},
imgWidth() {
return contestWork.imgWidth;
},
style() {
const aspectRatio = this.imgWidth / this.imgHeight;
return {
width: `${this.averageFileCardHeight * aspectRatio}px`,
flexGrow: this.averageFileCardHeight * aspectRatio,
};
}
}
}
</script>
<style>
.contest-work__img {
position: absolute;
max-height 100%;
max-width 100%;
width auto;
}
</style>

@ -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,
}
});
Loading…
Cancel
Save