diff --git a/api/v1/serializers.py b/api/v1/serializers.py index 8ff6f44a..f0955f50 100644 --- a/api/v1/serializers.py +++ b/api/v1/serializers.py @@ -6,12 +6,71 @@ from . import Base64ImageField from apps.course.models import Category, Course, Material, Lesson, Like from apps.content.models import ( Image, Text, ImageText, Video, - Gallery, GalleryImage, + Gallery, GalleryImage, ImageObject, ) User = get_user_model() +class ImageObjectSerializer(serializers.ModelSerializer): + image = Base64ImageField( + required=True, allow_empty_file=False, allow_null=False, read_only=False, + ) + + class Meta: + model = ImageObject + fields = ( + 'id', + 'image', + 'created_at', + 'update_at', + ) + + read_only_fields = ( + 'id', + 'created_at', + 'update_at', + ) + + +class GallerySerializer(serializers.ModelSerializer): + + class Meta: + model = Gallery + fields = ( + 'id', + 'title', + 'created_at', + 'update_at', + ) + + read_only_fields = ( + 'id', + 'created_at', + 'update_at', + ) + + +class GalleryImageSerializer(serializers.ModelSerializer): + img = ImageObjectSerializer() + + class Meta: + model = GalleryImage + fields = ( + 'id', + 'gallery', + 'img', + 'created_at', + 'update_at', + ) + + read_only_fields = ( + 'id', + 'created_at', + 'update_at', + ) + + class MaterialSerializer(serializers.ModelSerializer): class Meta: @@ -27,6 +86,7 @@ class MaterialSerializer(serializers.ModelSerializer): read_only_fields = ( 'id', + 'cover', 'created_at', 'update_at', ) @@ -59,12 +119,14 @@ class CategorySerializer(serializers.ModelSerializer): 'title', ) - read_only_fields = ( - 'id', - ) + read_only_fields = ( + 'id', + ) class CourseSerializer(serializers.ModelSerializer): + cover = ImageObjectSerializer() + gallery = GallerySerializer() class Meta: model = Course @@ -87,17 +149,27 @@ class CourseSerializer(serializers.ModelSerializer): 'materials', 'created_at', 'update_at', + 'content', + 'gallery', ) read_only_fields = ( 'id', - 'cover', + 'content', 'created_at', 'update_at', ) +class CourseRetrieveSerializer(CourseSerializer): + category = CategorySerializer() + materials = MaterialSerializer(many=True) + cover = ImageObjectSerializer() + gallery = GallerySerializer() + + class LessonSerializer(serializers.ModelSerializer): + cover = ImageObjectSerializer() class Meta: model = Lesson @@ -107,6 +179,7 @@ class LessonSerializer(serializers.ModelSerializer): 'short_description', 'course', 'cover', + 'content', 'created_at', 'update_at', ) @@ -114,12 +187,14 @@ class LessonSerializer(serializers.ModelSerializer): read_only_fields = ( 'id', 'cover', + 'content', 'created_at', 'update_at', ) class ImageSerializer(serializers.ModelSerializer): + img = ImageObjectSerializer() class Meta: model = Image @@ -129,13 +204,13 @@ class ImageSerializer(serializers.ModelSerializer): 'lesson', 'title', 'position', + 'img', 'created_at', 'update_at', - ) + ('img',) + ) read_only_fields = ( 'id', - 'img', 'created_at', 'update_at', ) @@ -163,6 +238,7 @@ class TextSerializer(serializers.ModelSerializer): class ImageTextSerializer(serializers.ModelSerializer): + img = ImageObjectSerializer() class Meta: model = ImageText @@ -172,13 +248,14 @@ class ImageTextSerializer(serializers.ModelSerializer): 'lesson', 'title', 'position', + 'img', + 'txt', 'created_at', 'update_at', - ) + ('img', 'txt',) + ) read_only_fields = ( 'id', - 'img', 'created_at', 'update_at', ) @@ -205,45 +282,6 @@ class VideoSerializer(serializers.ModelSerializer): ) -class GallerySerializer(serializers.ModelSerializer): - - class Meta: - model = Gallery - fields = ( - 'id', - 'course', - 'title', - 'created_at', - 'update_at', - ) - - read_only_fields = ( - 'id', - 'created_at', - 'update_at', - ) - - -class GalleryImageSerializer(serializers.ModelSerializer): - - class Meta: - model = GalleryImage - fields = ( - 'id', - 'gallery', - 'image', - 'created_at', - 'update_at', - ) - - read_only_fields = ( - 'id', - 'image', - 'created_at', - 'update_at', - ) - - class UserSerializer(serializers.ModelSerializer): class Meta: diff --git a/api/v1/urls.py b/api/v1/urls.py index 6483f23d..846af2ae 100644 --- a/api/v1/urls.py +++ b/api/v1/urls.py @@ -12,7 +12,7 @@ from .views import ( ImageViewSet, TextViewSet, ImageTextViewSet, VideoViewSet, GalleryViewSet, GalleryImageViewSet, - UserViewSet, LessonViewSet, + UserViewSet, LessonViewSet, ImageObjectViewSet, ) router = DefaultRouter() @@ -22,6 +22,7 @@ router.register(r'materials', MaterialViewSet, base_name='materials') router.register(r'lessons', LessonViewSet, base_name='lessons') router.register(r'likes', LikeViewSet, base_name='likes') +router.register(r'image-objects', ImageObjectViewSet, base_name='image-objects') router.register(r'images', ImageViewSet, base_name='images') router.register(r'texts', TextViewSet, base_name='texts') router.register(r'image-texts', ImageTextViewSet, base_name='image-texts') diff --git a/api/v1/views.py b/api/v1/views.py index 744e7b13..651f1024 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -15,18 +15,25 @@ from .serializers import ( UserSerializer, UserPhotoSerializer, LessonSerializer, ContentImageSerializer, GalleryImageSerializer, CoverImageSerializer, + CourseRetrieveSerializer, ImageObjectSerializer, ) from .permissions import IsAdmin, IsAdminOrIsSelf, IsAuthorOrAdmin, IsAuthorObjectOrAdmin from apps.course.models import Category, Course, Material, Lesson, Like from apps.content.models import ( Image, Text, ImageText, Video, - Gallery, GalleryImage, + Gallery, GalleryImage, ImageObject, ) User = get_user_model() +class ImageObjectViewSet(ExtendedModelViewSet): + queryset = ImageObject.objects.all() + serializer_class = ImageObjectSerializer + # permission_classes = (IsAuthorOrAdmin,) + + class MaterialViewSet(ExtendedModelViewSet): queryset = Material.objects.all() serializer_class = MaterialSerializer @@ -53,13 +60,14 @@ class CategoryViewSet(ExtendedModelViewSet): class CourseViewSet(ExtendedModelViewSet): queryset = Course.objects.select_related( - 'author', 'category' + 'author', 'category', 'cover', ).prefetch_related( - 'likes', 'materials' + 'likes', 'materials', 'content', ).all() serializer_class = CourseSerializer serializer_class_map = { - 'upload_photo': CoverImageSerializer, + 'list': CourseRetrieveSerializer, + 'retrieve': CourseRetrieveSerializer, } filter_fields = ('category', 'status', 'is_infinite', 'is_featured',) search_fields = ('author__email', 'title', 'category__title',) @@ -70,25 +78,12 @@ class CourseViewSet(ExtendedModelViewSet): # 'delete': IsAdmin, # } - @detail_route(methods=['post'], url_path='upload-photo') - def upload_photo(self, request, pk=None): - course = self.get_object() - serializer = self.get_serializer() - serialized_data = serializer(data=request.data) - if serialized_data.is_valid(): - course.cover = serialized_data['cover'] - course.save() - return Response({'success': True}) - else: - return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST) - class LessonViewSet(ExtendedModelViewSet): - queryset = Lesson.objects.select_related('course').all() + queryset = Lesson.objects.select_related( + 'course', 'cover' + ).prefetch_related('content').all() serializer_class = LessonSerializer - serializer_class_map = { - 'upload_photo': CoverImageSerializer, - } filter_fields = ('course',) search_fields = ('title', 'short_description',) ordering_fields = ('title', 'created_at', 'update_at',) @@ -98,27 +93,12 @@ class LessonViewSet(ExtendedModelViewSet): # 'delete': IsAdmin, # } - @detail_route(methods=['post'], url_path='upload-photo') - def upload_photo(self, request, pk=None): - lesson = self.get_object() - serializer = self.get_serializer() - serialized_data = serializer(data=request.data) - if serialized_data.is_valid(): - lesson.cover = serialized_data['cover'] - lesson.save() - return Response({'success': True}) - else: - return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST) - class ImageViewSet(ExtendedModelViewSet): queryset = Image.objects.select_related( - 'course', 'lesson' + 'course', 'lesson', 'img', ).all() serializer_class = ImageSerializer - serializer_class_map = { - 'upload_photo': ContentImageSerializer, - } search_fields = ('title',) ordering_fields = ('title', 'created_at', 'update_at', 'position',) # permission_classes = (IsAuthorOrAdmin,) @@ -126,18 +106,6 @@ class ImageViewSet(ExtendedModelViewSet): # 'delete': IsAdmin, # } - @detail_route(methods=['post'], url_path='upload-photo') - def upload_photo(self, request, pk=None): - image = self.get_object() - serializer = self.get_serializer() - serialized_data = serializer(data=request.data) - if serialized_data.is_valid(): - image.img = serialized_data['img'] - image.save() - return Response({'success': True}) - else: - return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST) - class TextViewSet(ExtendedModelViewSet): queryset = Text.objects.select_related( @@ -154,12 +122,9 @@ class TextViewSet(ExtendedModelViewSet): class ImageTextViewSet(ExtendedModelViewSet): queryset = ImageText.objects.select_related( - 'course', 'lesson' + 'course', 'lesson', 'img' ).all() serializer_class = ImageTextSerializer - serializer_class_map = { - 'upload_photo': ContentImageSerializer, - } search_fields = ('title',) ordering_fields = ('title', 'created_at', 'update_at', 'position',) # permission_classes = (IsAuthorOrAdmin,) @@ -167,18 +132,6 @@ class ImageTextViewSet(ExtendedModelViewSet): # 'delete': IsAdmin, # } - @detail_route(methods=['post'], url_path='upload-photo') - def upload_photo(self, request, pk=None): - image_text = self.get_object() - serializer = self.get_serializer() - serialized_data = serializer(data=request.data) - if serialized_data.is_valid(): - image_text.img = serialized_data['img'] - image_text.save() - return Response({'success': True}) - else: - return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST) - class VideoViewSet(ExtendedModelViewSet): queryset = Video.objects.select_related( @@ -194,7 +147,7 @@ class VideoViewSet(ExtendedModelViewSet): class GalleryViewSet(ExtendedModelViewSet): - queryset = Gallery.objects.select_related('course').all() + queryset = Gallery.objects.all() serializer_class = GallerySerializer search_fields = ('title',) ordering_fields = ('title', 'created_at', 'update_at',) @@ -205,29 +158,16 @@ class GalleryViewSet(ExtendedModelViewSet): class GalleryImageViewSet(ExtendedModelViewSet): - queryset = GalleryImage.objects.select_related('gallery').all() + queryset = GalleryImage.objects.select_related( + 'gallery', 'img', + ).all() serializer_class = GalleryImageSerializer - serializer_class_map = { - 'upload_photo': GalleryImageSerializer, - } search_fields = ('gallery__title',) # permission_classes = (IsAuthorOrAdmin,) # permission_map = { # 'delete': IsAdmin, # } - @detail_route(methods=['post'], url_path='upload-photo') - def upload_photo(self, request, pk=None): - gallery_image = self.get_object() - serializer = self.get_serializer() - serialized_data = serializer(data=request.data) - if serialized_data.is_valid(): - gallery_image.image = serialized_data['image'] - gallery_image.save() - return Response({'success': True}) - else: - return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST) - class UserViewSet(ExtendedModelViewSet): queryset = User.objects.all() diff --git a/apps/content/admin.py b/apps/content/admin.py index 53619829..bb935e08 100644 --- a/apps/content/admin.py +++ b/apps/content/admin.py @@ -7,10 +7,20 @@ from polymorphic.admin import ( from apps.content.models import ( Content, Image, Text, ImageText, Video, - Gallery, GalleryImage, + Gallery, GalleryImage, ImageObject, ) +@admin.register(ImageObject) +class ImageObjectAdmin(admin.ModelAdmin): + list_display = ( + 'id', + 'image', + 'created_at', + 'update_at', + ) + + class ContentChildAdmin(PolymorphicChildModelAdmin): base_model = Content diff --git a/apps/content/migrations/0005_auto_20180208_0520.py b/apps/content/migrations/0005_auto_20180208_0520.py new file mode 100644 index 00000000..a739f51f --- /dev/null +++ b/apps/content/migrations/0005_auto_20180208_0520.py @@ -0,0 +1,31 @@ +# Generated by Django 2.0.2 on 2018-02-08 05:20 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0004_gallery_galleryimage'), + ] + + operations = [ + migrations.CreateModel( + name='ImageObject', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(upload_to='content/imageobject', verbose_name='Изображение')), + ], + ), + migrations.AlterField( + model_name='content', + name='course', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='content', to='course.Course', verbose_name='Курс'), + ), + migrations.AlterField( + model_name='content', + name='lesson', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='content', to='course.Lesson', verbose_name='Урок'), + ), + ] diff --git a/apps/content/migrations/0006_auto_20180208_0551.py b/apps/content/migrations/0006_auto_20180208_0551.py new file mode 100644 index 00000000..361a2d49 --- /dev/null +++ b/apps/content/migrations/0006_auto_20180208_0551.py @@ -0,0 +1,29 @@ +# Generated by Django 2.0.2 on 2018-02-08 05:51 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0005_auto_20180208_0520'), + ] + + operations = [ + migrations.AlterModelOptions( + name='imageobject', + options={'ordering': ('-created_at',), 'verbose_name': 'Объект изображения', 'verbose_name_plural': 'Объекты изображения'}, + ), + migrations.AddField( + model_name='imageobject', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='imageobject', + name='update_at', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/apps/content/migrations/0007_auto_20180208_0626.py b/apps/content/migrations/0007_auto_20180208_0626.py new file mode 100644 index 00000000..acba3e98 --- /dev/null +++ b/apps/content/migrations/0007_auto_20180208_0626.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.2 on 2018-02-08 06:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0006_auto_20180208_0551'), + ] + + operations = [ + migrations.AlterField( + model_name='image', + name='img', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='content_images', to='content.ImageObject', verbose_name='Объект изображения'), + ), + ] diff --git a/apps/content/migrations/0008_auto_20180208_0631.py b/apps/content/migrations/0008_auto_20180208_0631.py new file mode 100644 index 00000000..3f8b023e --- /dev/null +++ b/apps/content/migrations/0008_auto_20180208_0631.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.2 on 2018-02-08 06:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0007_auto_20180208_0626'), + ] + + operations = [ + migrations.AlterField( + model_name='imagetext', + name='img', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='content_imagetexts', to='content.ImageObject', verbose_name='Объект изображения'), + ), + ] diff --git a/apps/content/migrations/0009_auto_20180208_0637.py b/apps/content/migrations/0009_auto_20180208_0637.py new file mode 100644 index 00000000..f96f55f6 --- /dev/null +++ b/apps/content/migrations/0009_auto_20180208_0637.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.2 on 2018-02-08 06:37 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0008_auto_20180208_0631'), + ] + + operations = [ + migrations.RemoveField( + model_name='galleryimage', + name='image', + ), + migrations.AddField( + model_name='galleryimage', + name='img', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='gallery_images', to='content.ImageObject', verbose_name='Объект изображения'), + ), + ] diff --git a/apps/content/migrations/0010_remove_gallery_course.py b/apps/content/migrations/0010_remove_gallery_course.py new file mode 100644 index 00000000..06537707 --- /dev/null +++ b/apps/content/migrations/0010_remove_gallery_course.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.2 on 2018-02-08 08:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0009_auto_20180208_0637'), + ] + + operations = [ + migrations.RemoveField( + model_name='gallery', + name='course', + ), + ] diff --git a/apps/content/models.py b/apps/content/models.py index aa5e87a8..1bd06dff 100644 --- a/apps/content/models.py +++ b/apps/content/models.py @@ -2,19 +2,31 @@ from django.db import models from polymorphic.models import PolymorphicModel -from apps.course.models import Course, Lesson + +class ImageObject(models.Model): + image = models.ImageField('Изображение', upload_to='content/imageobject') + + created_at = models.DateTimeField(auto_now_add=True) + update_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = 'Объект изображения' + verbose_name_plural = 'Объекты изображения' + ordering = ('-created_at',) class Content(PolymorphicModel): course = models.ForeignKey( - Course, on_delete=models.CASCADE, + 'course.Course', on_delete=models.CASCADE, null=True, blank=True, - verbose_name='Курс' + verbose_name='Курс', + related_name='content', ) lesson = models.ForeignKey( - Lesson, on_delete=models.CASCADE, + 'course.Lesson', on_delete=models.CASCADE, null=True, blank=True, - verbose_name='Урок' + verbose_name='Урок', + related_name='content', ) title = models.CharField('Заголовок', max_length=100, default='') position = models.PositiveSmallIntegerField( @@ -32,7 +44,10 @@ class Content(PolymorphicModel): class Image(Content): - img = models.ImageField('Изображение', upload_to='content/images') + img = models.ForeignKey( + ImageObject, related_name='content_images', + verbose_name='Объект изображения', on_delete=models.CASCADE, + ) class Text(Content): @@ -40,7 +55,10 @@ class Text(Content): class ImageText(Content): - img = models.ImageField('Изображение', upload_to='content/images') + img = models.ForeignKey( + ImageObject, related_name='content_imagetexts', + verbose_name='Объект изображения', on_delete=models.CASCADE, + ) txt = models.TextField('Текст', default='') @@ -49,11 +67,6 @@ class Video(Content): class Gallery(models.Model): - course = models.ForeignKey( - Course, on_delete=models.CASCADE, - null=True, blank=True, - verbose_name='Курс' - ) title = models.CharField('Заголовок', max_length=100, default='') created_at = models.DateTimeField(auto_now_add=True) @@ -70,8 +83,10 @@ class GalleryImage(models.Model): Gallery, on_delete=models.CASCADE, verbose_name='Галерея' ) - image = models.ImageField( - 'Изображение', upload_to='content/gallery_images' + img = models.ForeignKey( + ImageObject, related_name='gallery_images', + verbose_name='Объект изображения', on_delete=models.CASCADE, + null=True, blank=True, ) created_at = models.DateTimeField(auto_now_add=True) diff --git a/apps/course/fixtures/course.json b/apps/course/fixtures/course.json index 52f5bba3..cc3d2a0f 100644 --- a/apps/course/fixtures/course.json +++ b/apps/course/fixtures/course.json @@ -7,7 +7,6 @@ "title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438", "short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.", "from_author": "", - "cover": "courses/pic-1_sTaZawQ.jpg", "price": "1500.00", "is_infinite": false, "deferred_start_at": null, @@ -30,7 +29,6 @@ "title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438", "short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.", "from_author": "", - "cover": "courses/pic-1_sTaZawQ.jpg", "price": "1900.00", "is_infinite": false, "deferred_start_at": null, @@ -53,7 +51,6 @@ "title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438", "short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.", "from_author": "", - "cover": "courses/pic-1_sTaZawQ.jpg", "price": "100.00", "is_infinite": false, "deferred_start_at": null, @@ -76,7 +73,6 @@ "title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438", "short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.", "from_author": "", - "cover": "courses/pic-1_sTaZawQ.jpg", "price": "400.00", "is_infinite": false, "deferred_start_at": null, @@ -99,7 +95,6 @@ "title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438", "short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.", "from_author": "", - "cover": "courses/pic-1_sTaZawQ.jpg", "price": "1800.00", "is_infinite": false, "deferred_start_at": null, @@ -122,7 +117,6 @@ "title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438", "short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.", "from_author": "", - "cover": "courses/pic-1_sTaZawQ.jpg", "price": "100.00", "is_infinite": false, "deferred_start_at": null, @@ -145,7 +139,6 @@ "title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438", "short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.", "from_author": "", - "cover": "courses/pic-1_sTaZawQ.jpg", "price": "1600.00", "is_infinite": false, "deferred_start_at": null, @@ -168,7 +161,6 @@ "title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438", "short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.", "from_author": "", - "cover": "courses/pic-1_sTaZawQ.jpg", "price": "1900.00", "is_infinite": false, "deferred_start_at": null, @@ -191,7 +183,6 @@ "title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438", "short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.", "from_author": "", - "cover": "courses/pic-1_sTaZawQ.jpg", "price": "200.00", "is_infinite": false, "deferred_start_at": null, @@ -214,7 +205,6 @@ "title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438", "short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.", "from_author": "", - "cover": "courses/pic-1_sTaZawQ.jpg", "price": "800.00", "is_infinite": false, "deferred_start_at": null, @@ -241,7 +231,6 @@ "title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438", "short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.", "from_author": "", - "cover": "courses/pic-1_sTaZawQ.jpg", "price": "100.00", "is_infinite": false, "deferred_start_at": "2018-02-28T12:00:00Z", @@ -330,7 +319,6 @@ "title": "1 \u0423\u0420\u041e\u041a", "short_description": "\u0412\u044b\u0431\u0438\u0440\u0430\u0435\u043c \u0441\u044e\u0436\u0435\u0442, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043d\u0430 \u043a\u0443\u0440\u0441\u0435 \u0438 \u0433\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u0433\u0435\u0440\u043e\u044f \u0432\u0430\u0448\u0435\u0439 \u0438\u0441\u0442\u043e\u0440\u0438\u0438. \u0421 \u044d\u0442\u0438\u043c \u0433\u0435\u0440\u043e\u0435\u043c \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043d\u0430 \u043f\u0440\u043e\u0442\u044f\u0436\u0435\u043d\u0438\u0438 \u0432\u0441\u0435\u0433\u043e \u043a\u0443\u0440\u0441\u0430.", "course": 11, - "cover": "lessons/kat-watercolor.jpg", "created_at": "2018-01-31T15:06:14.830Z", "update_at": "2018-01-31T15:06:14.830Z" } @@ -342,7 +330,6 @@ "title": "2 \u0423\u0420\u041e\u041a", "short_description": "\u0421\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b \u0438 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0438\u0440\u0443\u0435\u043c \u0441 \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u0433\u0435\u0440\u043e\u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u043d\u044b\u0445 \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e\u0441\u0442\u0435\u0439 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0430. \u041f\u043e \u043a\u0430\u0436\u0434\u043e\u043c\u0443 \u043f\u0440\u0438\u0437\u043d\u0430\u043a\u0443 \u043d\u0443\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u00ab\u0440\u0435\u0444\u0435\u0440\u0435\u043d\u0441\u044b\u00bb. \u0420\u0438\u0441\u0443\u0435\u043c \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u043e\u0431\u0440\u0430\u0437\u0430 \u0432 \u0441\u0432\u043e\u0435\u043c \u0441\u0442\u0438\u043b\u0435.\r\n\r\n\u0421\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u0438\u0437 \u043d\u0438\u0445 \u043d\u0430\u0448\u0435\u0433\u043e \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0430. \u0412\u044b\u0431\u0438\u0440\u0430\u0435\u043c \u0441\u0430\u043c\u044b\u0435 \u0443\u0434\u0430\u0447\u043d\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b, \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0435\u043c \u0438\u0445 \u0432 \u043e\u0434\u043d\u043e\u043c \u043d\u0430\u0431\u0440\u043e\u0441\u043a\u0435.", "course": 11, - "cover": "lessons/kat-watercolor_SA9juHa.jpg", "created_at": "2018-01-31T15:06:46.772Z", "update_at": "2018-01-31T15:06:46.772Z" } @@ -354,7 +341,6 @@ "title": "3 \u0423\u0420\u041e\u041a", "short_description": "\u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0435\u043c \u043e\u0436\u0438\u0432\u0438\u0442\u044c \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0430. \u0412\u044b\u0431\u0438\u0440\u0430\u0435\u043c 5 \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u043d\u044b\u0445 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0434\u043b\u044f \u0432\u0430\u0448\u0435\u0433\u043e \u0433\u0435\u0440\u043e\u044f \u0438 \u0442\u043e\u0433\u043e \u0441\u044e\u0436\u0435\u0442\u0430, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043e\u043d \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0443\u0447\u0430\u0441\u0442\u0438\u0435, \u0440\u0438\u0441\u0443\u0435\u043c \u044d\u0441\u043a\u0438\u0437\u044b \u0432\u0430\u0448\u0435\u0433\u043e \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0430 \u0432 \u0440\u0430\u0437\u043d\u044b\u0445 \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u043d\u044b\u0445 \u0434\u043b\u044f \u043d\u0435\u0433\u043e \u043f\u043e\u0437\u0430\u0445 \u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0438.\r\n\r\n\u0412\u044b\u0434\u0435\u043b\u044f\u0435\u043c 5 \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u043d\u044b\u0445 \u044d\u043c\u043e\u0446\u0438\u0439 \u0434\u043b\u044f \u0433\u0435\u0440\u043e\u044f, \u043d\u0430\u0434 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u043c. \u041f\u043e \u044d\u043c\u043e\u0446\u0438\u044f\u043c \u043f\u043e\u0434\u0431\u0438\u0440\u0430\u0435\u043c \u0440\u0435\u0444\u0435\u0440\u0435\u043d\u0441\u044b \u0438 \u0441\u0442\u0438\u043b\u0438\u0437\u0443\u0435\u043c \u0438\u0445 \u0432 \u0441\u0432\u043e\u0435\u043c \u0441\u0442\u0438\u043b\u0435.", "course": 11, - "cover": "lessons/kat-watercolor_QYFi9sq.jpg", "created_at": "2018-01-31T15:07:08.979Z", "update_at": "2018-01-31T15:07:08.979Z" } @@ -364,7 +350,6 @@ "pk": 1, "fields": { "title": "\u0411\u0443\u043c\u0430\u0433\u0430 \u0430\u043a\u0432\u0430\u0440\u0435\u043b\u044c\u043d\u0430\u044f", - "cover": "materials/kat-watercolor.jpg", "short_description": "\u0411\u0443\u043c\u0430\u0433\u0430 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0430\u043a\u0432\u0430\u0440\u0435\u043b\u044c\u044e \u0438\u043c\u0435\u0435\u0442 \u0431\u043e\u043b\u044c\u0448\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435. \u042d\u0442\u043e \u043e\u0431\u044a\u044f\u0441\u043d\u044f\u0435\u0442\u0441\u044f \u0442\u0435\u043c, \u0447\u0442\u043e \u0430\u043a\u0432\u0430\u0440\u0435\u043b\u044c \u2014 \u043a\u0440\u0430\u0441\u043a\u0430 \u043f\u0440\u043e\u0437\u0440\u0430\u0447\u043d\u0430\u044f, \u0430 \u0437\u043d\u0430\u0447\u0438\u0442 \u0444\u0430\u043a\u0442\u0443\u0440\u0430 \u0431\u0443\u043c\u0430\u0433\u0438 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0441\u0438\u043b\u044c\u043d\u043e \u0432\u043b\u0438\u044f\u0442\u044c \u043d\u0430 \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0432\u0438\u0434 \u043a\u0440\u0430\u0441\u043e\u0447\u043d\u043e\u0433\u043e \u0441\u043b\u043e\u044f.", "created_at": "2018-01-31T14:55:48.394Z", "update_at": "2018-01-31T14:55:48.394Z" @@ -375,7 +360,6 @@ "pk": 2, "fields": { "title": "\u041a\u0438\u0441\u0442\u043e\u0447\u043a\u0438 \u0434\u043b\u044f \u0440\u0438\u0441\u043e\u0432\u0430\u043d\u0438\u044f", - "cover": "materials/shutterstock_125323070-700x861.jpg", "short_description": "\u041a\u0438\u0441\u0442\u044c \u2014 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442 \u0434\u043b\u044f \u043f\u043e\u043a\u0440\u0430\u0441\u043a\u0438 \u0438 \u0436\u0438\u0432\u043e\u043f\u0438\u0441\u0438. \u041a\u0438\u0441\u0442\u0438 \u0434\u0435\u043b\u0430\u044e\u0442\u0441\u044f \u0438\u0437 \u0449\u0435\u0442\u0438\u043d\u044b \u0438 \u0445\u0432\u043e\u0441\u0442\u043e\u0432\u044b\u0445 \u0432\u043e\u043b\u043e\u0441\u043a\u043e\u0432 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u0436\u0438\u0432\u043e\u0442\u043d\u044b\u0445.", "created_at": "2018-01-31T14:57:37.751Z", "update_at": "2018-01-31T14:57:37.751Z" @@ -386,7 +370,6 @@ "pk": 3, "fields": { "title": "\u041a\u0440\u0430\u0441\u043a\u0438 \u0430\u043a\u0432\u0430\u0440\u0435\u043b\u044c\u043d\u044b\u0435", - "cover": "materials/\u043a\u0440\u0430\u0441\u043a\u0438.jpeg", "short_description": "\u0417\u0430\u0432\u043e\u0434 \u0445\u0443\u0434\u043e\u0436\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0445 \u043a\u0440\u0430\u0441\u043e\u043a \u00ab\u041d\u0435\u0432\u0441\u043a\u0430\u044f \u043f\u0430\u043b\u0438\u0442\u0440\u0430\u00bb \u0432\u044b\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u0430\u043a\u0432\u0430\u0440\u0435\u043b\u044c 80 \u043b\u0435\u0442, \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044f \u0442\u0440\u0430\u0434\u0438\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0441\u0442\u0432\u0430 \u043f\u0440\u043e\u0434\u0443\u043a\u0446\u0438\u0438 \u0432\u044b\u0441\u043e\u0447\u0430\u0439\u0448\u0435\u0433\u043e \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430. \u041f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u044b\u0435 \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c \u0440\u0435\u0446\u0435\u043f\u0442\u0443\u0440\u044b, \u043e\u0442\u043b\u0430\u0436\u0435\u043d\u043d\u0430\u044f \u0442\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u044f \u0438\u0437\u0433\u043e\u0442\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0434\u0435\u043b\u0430\u043b\u0438 \u0430\u043a\u0432\u0430\u0440\u0435\u043b\u044c\u043d\u044b\u0435 \u043a\u0440\u0430\u0441\u043a\u0438 \u0432\u0438\u0437\u0438\u0442\u043d\u043e\u0439 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u043e\u0439 \u043f\u0440\u0435\u0434\u043f\u0440\u0438\u044f\u0442\u0438\u044f \u0432 \u0420\u043e\u0441\u0441\u0438\u0438", "created_at": "2018-01-31T14:58:46.209Z", "update_at": "2018-01-31T14:58:46.209Z" diff --git a/apps/course/migrations/0022_auto_20180208_0647.py b/apps/course/migrations/0022_auto_20180208_0647.py new file mode 100644 index 00000000..c95cd3fc --- /dev/null +++ b/apps/course/migrations/0022_auto_20180208_0647.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.2 on 2018-02-08 06:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0021_auto_20180206_0632'), + ] + + operations = [ + migrations.AlterField( + model_name='course', + name='cover', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_covers', to='content.ImageObject', verbose_name='Обложка курса'), + ), + ] diff --git a/apps/course/migrations/0023_auto_20180208_0714.py b/apps/course/migrations/0023_auto_20180208_0714.py new file mode 100644 index 00000000..797d9539 --- /dev/null +++ b/apps/course/migrations/0023_auto_20180208_0714.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.2 on 2018-02-08 07:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0022_auto_20180208_0647'), + ] + + operations = [ + migrations.AlterField( + model_name='course', + name='cover', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='course_covers', to='content.ImageObject', verbose_name='Обложка курса'), + ), + ] diff --git a/apps/course/migrations/0024_auto_20180208_0824.py b/apps/course/migrations/0024_auto_20180208_0824.py new file mode 100644 index 00000000..4205d492 --- /dev/null +++ b/apps/course/migrations/0024_auto_20180208_0824.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.2 on 2018-02-08 08:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0023_auto_20180208_0714'), + ] + + operations = [ + migrations.AlterField( + model_name='lesson', + name='cover', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='lesson_covers', to='content.ImageObject', verbose_name='Обложка урока'), + ), + ] diff --git a/apps/course/migrations/0025_course_gallery.py b/apps/course/migrations/0025_course_gallery.py new file mode 100644 index 00000000..b20d83cb --- /dev/null +++ b/apps/course/migrations/0025_course_gallery.py @@ -0,0 +1,20 @@ +# Generated by Django 2.0.2 on 2018-02-08 08:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0010_remove_gallery_course'), + ('course', '0024_auto_20180208_0824'), + ] + + operations = [ + migrations.AddField( + model_name='course', + name='gallery', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='content.Gallery', verbose_name='Галерея работ'), + ), + ] diff --git a/apps/course/models.py b/apps/course/models.py index 76c46756..6ad7866f 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -7,6 +7,8 @@ from polymorphic_tree.models import PolymorphicMPTTModel, PolymorphicTreeForeign from .manager import CategoryQuerySet +from apps.content.models import ImageObject, Gallery + User = get_user_model() @@ -30,10 +32,20 @@ class Course(models.Model): title = models.CharField('Название курса', max_length=100, db_index=True) short_description = models.TextField('Краткое описание курса', db_index=True) from_author = models.TextField('От автора', default='', null=True, blank=True) - cover = models.ImageField('Фон курса', upload_to='courses') - price = models.DecimalField('Цена курса', help_text='Если цены нету, то курс бесплатный', max_digits=10, decimal_places=2, null=True, blank=True) + cover = models.ForeignKey( + ImageObject, related_name='course_covers', + verbose_name='Обложка курса', on_delete=models.CASCADE, + null=True, blank=True, + ) + price = models.DecimalField( + 'Цена курса', help_text='Если цены нету, то курс бесплатный', + max_digits=10, decimal_places=2, null=True, blank=True + ) is_infinite = models.BooleanField(default=False) - deferred_start_at = models.DateTimeField('Отложенный запуск курса', help_text='Заполнить если курс отложенный', null=True, blank=True) + deferred_start_at = models.DateTimeField( + 'Отложенный запуск курса', help_text='Заполнить если курс отложенный', + null=True, blank=True + ) category = models.ForeignKey('Category', on_delete=models.PROTECT) duration = models.IntegerField('Продолжительность курса', default=0) is_featured = models.BooleanField(default=False) @@ -41,6 +53,10 @@ class Course(models.Model): status = models.PositiveSmallIntegerField('Статус', default=0, choices=STATUS_CHOICES) likes = models.ManyToManyField(Like, blank=True) materials = models.ManyToManyField('Material', blank=True) + gallery = models.ForeignKey( + Gallery, verbose_name='Галерея работ', + on_delete=models.CASCADE, null=True, blank=True, + ) created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) @@ -93,8 +109,11 @@ class Lesson(models.Model): title = models.CharField('Название урока', max_length=100) short_description = models.TextField('Краткое описание урока') course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='lessons') - cover = models.ImageField('Фон урока', upload_to='lessons') - + cover = models.ForeignKey( + ImageObject, related_name='lesson_covers', + verbose_name='Обложка урока', on_delete=models.CASCADE, + null=True, blank=True, + ) created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) @@ -127,7 +146,10 @@ class Material(models.Model): class Comment(PolymorphicMPTTModel): content = models.TextField('Текст комментария', default='') author = models.ForeignKey(User, on_delete=models.CASCADE) - parent = PolymorphicTreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.PROTECT) + parent = PolymorphicTreeForeignKey( + 'self', null=True, blank=True, related_name='children', + db_index=True, on_delete=models.PROTECT + ) created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) diff --git a/apps/course/templates/course/_items.html b/apps/course/templates/course/_items.html index 13de9f80..fe9a5a3d 100644 --- a/apps/course/templates/course/_items.html +++ b/apps/course/templates/course/_items.html @@ -7,7 +7,11 @@ {% if course.is_deferred_start %}data-future-course data-future-course-time={{ course.deferred_start_at.timestamp }}{% endif %} > + {% if course.cover %} + {% else %} + + {% endif %}
Подробнее
{% if course.is_featured %}
diff --git a/apps/course/views.py b/apps/course/views.py index c3f6f05b..cd5fd64a 100644 --- a/apps/course/views.py +++ b/apps/course/views.py @@ -177,7 +177,11 @@ class CoursesView(ListView): return super().get(request, args, kwargs) def get_queryset(self): - queryset = super().get_queryset() + queryset = super().get_queryset().select_related( + 'author', 'category' + ).prefetch_related( + 'likes', 'materials', 'content', + ) filtered = CourseFilter(self.request.GET, queryset=queryset) return filtered.qs diff --git a/apps/user/forms.py b/apps/user/forms.py new file mode 100644 index 00000000..697d3910 --- /dev/null +++ b/apps/user/forms.py @@ -0,0 +1,49 @@ +from django import forms +from django.contrib.auth import get_user_model + +User = get_user_model() + + +class UserEditForm(forms.ModelForm): + # first_name = forms.CharField() + # last_name = forms.CharField() + # email = forms.CharField() + # city = forms.CharField() + # country = forms.CharField() + birthday = forms.DateField(input_formats=['%d.%m.%Y']) + # gender = forms.ChoiceField(choices=User.GENDER_CHOICES, required=False) + gender = forms.CharField(required=False) + # about = forms.CharField() + old_password = forms.CharField(required=False) + new_password1 = forms.CharField(required=False) + new_password2 = forms.CharField(required=False) + instagram = forms.URLField(required=False) + facebook = forms.URLField(required=False) + twitter = forms.URLField(required=False) + pinterest = forms.URLField(required=False) + youtube = forms.URLField(required=False) + vkontakte = forms.URLField(required=False) + photo = forms.ImageField(required=False) + + class Meta: + model = User + fields = ( + 'first_name', + 'last_name', + 'email', + 'city', + 'country', + 'birthday', + 'gender', + 'about', + 'old_password', + 'new_password1', + 'new_password2', + 'instagram', + 'facebook', + 'twitter', + 'pinterest', + 'youtube', + 'vkontakte', + 'photo', + ) diff --git a/apps/user/migrations/0005_user_birthday.py b/apps/user/migrations/0005_user_birthday.py new file mode 100644 index 00000000..479a9294 --- /dev/null +++ b/apps/user/migrations/0005_user_birthday.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.2 on 2018-02-06 13:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0004_auto_20180129_1259'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='birthday', + field=models.DateField(blank=True, null=True, verbose_name='День рождения'), + ), + ] diff --git a/apps/user/migrations/0006_auto_20180206_1352.py b/apps/user/migrations/0006_auto_20180206_1352.py new file mode 100644 index 00000000..b65c014a --- /dev/null +++ b/apps/user/migrations/0006_auto_20180206_1352.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.2 on 2018-02-06 13:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0005_user_birthday'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='about', + field=models.CharField(blank=True, max_length=1000, null=True, verbose_name='О себе'), + ), + ] diff --git a/apps/user/migrations/0007_auto_20180207_0808.py b/apps/user/migrations/0007_auto_20180207_0808.py new file mode 100644 index 00000000..98dd1616 --- /dev/null +++ b/apps/user/migrations/0007_auto_20180207_0808.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.2 on 2018-02-07 08:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0006_auto_20180206_1352'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='city', + field=models.CharField(blank=True, max_length=85, null=True, verbose_name='Город'), + ), + migrations.AlterField( + model_name='user', + name='country', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Страна'), + ), + ] diff --git a/apps/user/models.py b/apps/user/models.py index 7a59786c..141073ce 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -13,17 +13,21 @@ class User(AbstractUser): (AUTHOR_ROLE, 'автор'), (ADMIN_ROLE, 'администратор'), ) + NOT_DEFINED = 'n' + MALE = 'm' + FEMALE = 'f' GENDER_CHOICES = ( - ('n', 'не указан'), - ('m', 'Мужчина'), - ('f', 'Женщина'), + (NOT_DEFINED, 'не указан'), + (MALE, 'Мужчина'), + (FEMALE, 'Женщина'), ) email = models.EmailField(_('email address'), unique=True) role = models.PositiveSmallIntegerField('Роль', default=0, choices=ROLE_CHOICES) gender = models.CharField('Пол', max_length=1, default='n', choices=GENDER_CHOICES) - country = models.CharField('Страна', max_length=50, default='') - city = models.CharField('Город', max_length=85, default='') - about = models.CharField('О себе', max_length=1000, default='', blank=True) + birthday = models.DateField('День рождения', null=True, blank=True) + country = models.CharField('Страна', max_length=50, null=True, blank=True) + city = models.CharField('Город', max_length=85, null=True, blank=True) + about = models.CharField('О себе', max_length=1000, null=True, blank=True) instagram = models.URLField(default='', null=True, blank=True) facebook = models.URLField(default='', null=True, blank=True) twitter = models.URLField(default='', null=True, blank=True) diff --git a/apps/user/templates/user/profile-settings.html b/apps/user/templates/user/profile-settings.html new file mode 100644 index 00000000..4af4e6ba --- /dev/null +++ b/apps/user/templates/user/profile-settings.html @@ -0,0 +1,209 @@ +{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %} +
+
+ +
+
+{% comment %} + +{% endcomment %} +{% if messages %} +
+
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+
+{% endif %} +{{form.errors}} +
+
+
+
+ {% csrf_token %} +
+
Личные данные
+
+ {% if user.photo %} + + {% else %} + + {% endif %} + +
+ + + +
+
+
+
+
ИМЯ
+
+ +
+
+
+
ФАМИЛИЯ
+
+ +
+
+
+
+
Почта
+
+ +
+
+
+
+
ГОРОД
+
+ +
+
+
+
СТРАНА
+
+ +
+
+
+
+
+
ДАТА РОЖДЕНИЯ
+
+ +
+
+
+
ПОЛ
+
+
+
+ {% if user.gender == 'f' %}Ж{% elif user.gender == 'm' %}M{% else %}М / Ж{% endif %} +
+
+
+
М
+
+
+
Ж
+
+
+ +
+
+
+
+
+
О себе
+
+ +
+
+
+
+
Пароль
+
+
ТЕКУЩИЙ ПАРОЛЬ
+
+ +
+
+
+
НОВЫЙ ПАРОЛЬ
+
+ +
+
+
+
ПОДТВЕРДИТЬ НОВЫЙ ПАРОЛЬ
+
+ +
+
+
+
+
Соцсети
+
+
INSTAGRAM
+
+ +
+
+
+
FACEBOOK
+
+ +
+
+
+
TWITTER
+
+ +
+
+
+
PINTEREST
+
+ +
+
+
+
YOUTUBE
+
+ +
+
+
+
VKONTAKTE
+
+ +
+
+
+
+ +
+
+
+
+
+ +{% endblock content %} \ No newline at end of file diff --git a/apps/user/templates/user/profile.html b/apps/user/templates/user/profile.html index 2142dca5..c4085f29 100644 --- a/apps/user/templates/user/profile.html +++ b/apps/user/templates/user/profile.html @@ -2,7 +2,7 @@
- Редактировать + Редактировать {% if user.photo %}
diff --git a/apps/user/views.py b/apps/user/views.py index 18d7c9ce..3b3f1daa 100644 --- a/apps/user/views.py +++ b/apps/user/views.py @@ -1,9 +1,20 @@ -from django.shortcuts import render -from django.views.generic import DetailView +from io import BytesIO +from PIL import Image +from os.path import splitext +from django.contrib.auth import login +from django.shortcuts import render, reverse +from django.views.generic import DetailView, UpdateView +from django.contrib import messages from django.contrib.auth import get_user_model +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.hashers import check_password, make_password +from django.http import Http404 +from django.utils.decorators import method_decorator from apps.course.models import Course +from .forms import UserEditForm + User = get_user_model() @@ -13,6 +24,65 @@ class UserView(DetailView): def get_context_data(self, object): context = super().get_context_data() - context['published'] = Course.objects.filter(author=self.object, status=Course.PUBLISHED) + context['published'] = Course.objects.filter( + author=self.object, status=Course.PUBLISHED + ) context['paid'] = Course.objects.none() return context + + +class UserEditView(UpdateView): + model = User + template_name = 'user/profile-settings.html' + form_class = UserEditForm + + @method_decorator(login_required) + def dispatch(self, request, *args, **kwargs): + self.object = self.get_object() + if request.user != self.object: + raise Http404() + return super().dispatch(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + # it's magic *-*-*-*-* + if 'photo' in request.FILES: + photo_fp = request.FILES.pop('photo')[0] + fname = photo_fp.name + photo = Image.open(photo_fp) + lowest_side = min(photo.size) + horizontal_padding = (lowest_side - photo.size[0]) / 2 + vertical_padding = (lowest_side - photo.size[1]) / 2 + photo = photo.crop( + ( + -horizontal_padding, + -vertical_padding, + photo.size[0] + horizontal_padding, + photo.size[1] + vertical_padding + ) + ) + if photo.size[0] > 512: + photo = photo.resize((512, 512,)) + buffer = BytesIO() + ext = splitext(fname)[1][1:].upper() + if ext == 'JPG': + ext = 'JPEG' + photo.save(buffer, ext) + self.object.photo.save(fname, buffer) + buffer.close() + if not request.POST._mutable: + request.POST._mutable = True + old_password = request.POST.pop('old_password')[0] + new_password1 = request.POST.pop('new_password1')[0] + new_password2 = request.POST.pop('new_password2')[0] + if old_password: + if request.user.check_password(old_password) and new_password1 == new_password2: + request.user.set_password(new_password1) + request.user.save() + login(request, request.user) + else: + messages.error(request, 'Неверный пароль.') + messages.info(request, 'Данные сохранены.') + return super().post(request, *args, **kwargs) + + def get_success_url(self): + return reverse('user-edit', args=[self.object.id]) diff --git a/docker-compose.yml b/docker-compose.yml index 9465b2a4..f42e5b6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: restart: always volumes: - .:/lilcity - command: bash -c "python manage.py migrate && python manage.py loaddata /lilcity/apps/*/fixtures/*.json && python manage.py runserver 0.0.0.0:8000 && celery worker -A project -Q web" + command: bash -c "python manage.py migrate && python manage.py loaddata /lilcity/apps/*/fixtures/*.json && python manage.py runserver 0.0.0.0:8000 && celery worker -A project" environment: - DJANGO_SETTINGS_MODULE=project.settings - DATABASE_SERVICE_HOST=db diff --git a/project/settings.py b/project/settings.py index 17c6ec7e..a450d5b2 100644 --- a/project/settings.py +++ b/project/settings.py @@ -162,7 +162,7 @@ STATICFILES_DIRS = [ MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, "media") - +LOGIN_URL = '/' # Email # https://github.com/anymail/django-anymail diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index 94ac3aca..41e72cad 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -33,7 +33,7 @@ - +