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..aff24dd5 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', @@ -254,3 +254,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..3d76244d --- /dev/null +++ b/api/v1/serializers/contest.py @@ -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__' 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..0e8cab3d 100644 --- a/api/v1/urls.py +++ b/api/v1/urls.py @@ -18,7 +18,7 @@ from .views import ( UserViewSet, LessonViewSet, ImageObjectViewSet, SchoolScheduleViewSet, LiveLessonViewSet, PaymentViewSet, -) + ContestViewSet, ContestWorkViewSet) router = DefaultRouter() router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests') @@ -44,6 +44,9 @@ router.register(r'school-schedules', SchoolScheduleViewSet, base_name='school-sc router.register(r'users', UserViewSet, base_name='users') +router.register(r'contests', ContestViewSet, base_name='contests') +router.register(r'contest_works', ContestWorkViewSet, base_name='contest_works') + # router.register(r'configs', ConfigViewSet, base_name='configs') diff --git a/api/v1/views.py b/api/v1/views.py index 41cd881a..ac9d70fe 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -39,6 +39,7 @@ from .serializers.user import ( AuthorRequestSerializer, UserSerializer, UserPhotoSerializer, ) +from .serializers.contest import ContestCreateSerializer, ContestSerializer, ContestWorkSerializer from .permissions import ( IsAdmin, IsAdminOrIsSelf, @@ -56,7 +57,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, @@ -429,3 +430,21 @@ class PaymentViewSet(ExtendedModelViewSet): 'amount', 'created_at', ) 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 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/models.py b/apps/content/models.py index 468058eb..3f4992ea 100644 --- a/apps/content/models.py +++ b/apps/content/models.py @@ -1,9 +1,13 @@ from urllib.parse import urlparse from django.db import models +from django.contrib.auth import get_user_model from polymorphic.models import PolymorphicModel +User = get_user_model() + + class ImageObject(models.Model): image = models.ImageField('Изображение', upload_to='content/imageobject') @@ -36,6 +40,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 +141,41 @@ 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) diff --git a/apps/content/templates/content/blocks/contest_work.html b/apps/content/templates/content/blocks/contest_work.html new file mode 100644 index 00000000..b80e6e06 --- /dev/null +++ b/apps/content/templates/content/blocks/contest_work.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/apps/course/templates/course/content/gallery.html b/apps/content/templates/content/blocks/gallery.html similarity index 97% rename from apps/course/templates/course/content/gallery.html rename to apps/content/templates/content/blocks/gallery.html index 1116322a..69ad0386 100644 --- a/apps/course/templates/course/content/gallery.html +++ b/apps/content/templates/content/blocks/gallery.html @@ -1,32 +1,32 @@ -{% load thumbnail %} -{% if results %} -
Галерея итогов обучения
- -{% else %} -
-
-
{{ content.title }}
- -
-
-{% endif %} +{% load thumbnail %} +{% if results %} +
Галерея итогов обучения
+ +{% else %} +
+
+
{{ content.title }}
+ +
+
+{% endif %} diff --git a/apps/course/templates/course/content/image.html b/apps/content/templates/content/blocks/image.html similarity index 96% rename from apps/course/templates/course/content/image.html rename to apps/content/templates/content/blocks/image.html index 4ab24601..5d515430 100644 --- a/apps/course/templates/course/content/image.html +++ b/apps/content/templates/content/blocks/image.html @@ -1,10 +1,10 @@ -
-
-
- {{ content.title }} -
-
- -
-
-
+
+
+
+ {{ content.title }} +
+
+ +
+
+
diff --git a/apps/course/templates/course/content/imagetext.html b/apps/content/templates/content/blocks/imagetext.html similarity index 100% rename from apps/course/templates/course/content/imagetext.html rename to apps/content/templates/content/blocks/imagetext.html diff --git a/apps/course/templates/course/content/text.html b/apps/content/templates/content/blocks/text.html similarity index 94% rename from apps/course/templates/course/content/text.html rename to apps/content/templates/content/blocks/text.html index 18ce2235..59e46ded 100644 --- a/apps/course/templates/course/content/text.html +++ b/apps/content/templates/content/blocks/text.html @@ -1,10 +1,10 @@ -
-
-
- {{ content.title }} -
-
- {{ content.txt | safe }} -
-
+
+
+
+ {{ content.title }} +
+
+ {{ content.txt | safe }} +
+
\ No newline at end of file diff --git a/apps/course/templates/course/content/video.html b/apps/content/templates/content/blocks/video.html similarity index 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 %} - - {% 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..2ca0a7d6 --- /dev/null +++ b/apps/content/templates/content/contest.html @@ -0,0 +1,44 @@ +{% extends "templates/lilcity/index.html" %} + + +{% block content %} +
+
+
+ Lil School — первая образовательная онлайн-платформа креативного мышления для детей +
+
+ Приглашаем вас на месяц открытых дверей в Lil School +
+ +
+
+{% for content in contest.content.all %} + + {% with template="content/blocks/"|add:content.ctype|add:".html" %} + {% include template %} + {% endwith %} + +{% endfor %} +
+
+ +
Галерея
+
+
+

Тысячи шедевров уже созданы благодаря Lil School. Более 10000 работ можно + увидеть в Инстаграм

+ +
+ +
+
+{% 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/views.py b/apps/content/views.py index 28002783..f4335009 100644 --- a/apps/content/views.py +++ b/apps/content/views.py @@ -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 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/models.py b/apps/course/models.py index d4966b3c..6492b999 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -15,7 +15,7 @@ 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() @@ -284,3 +284,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/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/lesson.html b/apps/course/templates/course/lesson.html index 1152d092..cf40b447 100644 --- a/apps/course/templates/course/lesson.html +++ b/apps/course/templates/course/lesson.html @@ -44,7 +44,7 @@ {% 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 %} diff --git a/project/templates/blocks/lil_store_js.html b/project/templates/blocks/lil_store_js.html new file mode 100644 index 00000000..d7d6bb9e --- /dev/null +++ b/project/templates/blocks/lil_store_js.html @@ -0,0 +1,8 @@ + diff --git a/project/templates/lilcity/edit_index.html b/project/templates/lilcity/edit_index.html index f6029dbb..18be67aa 100644 --- a/project/templates/lilcity/edit_index.html +++ b/project/templates/lilcity/edit_index.html @@ -315,6 +315,7 @@ +{% include 'templates/blocks/lil_store_js.html' %} + + diff --git a/web/src/components/ContestWorks.vue b/web/src/components/ContestWorks.vue new file mode 100644 index 00000000..40f35f21 --- /dev/null +++ b/web/src/components/ContestWorks.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/web/src/components/blocks/ContestWork b/web/src/components/blocks/ContestWork new file mode 100644 index 00000000..a59389f9 --- /dev/null +++ b/web/src/components/blocks/ContestWork @@ -0,0 +1,38 @@ + + + + + diff --git a/web/src/js/contest-redactor.js b/web/src/js/contest-redactor.js new file mode 100644 index 00000000..a485cac6 --- /dev/null +++ b/web/src/js/contest-redactor.js @@ -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, + } +});