diff --git a/api/v1/__init__.py b/api/v1/__init__.py index 423698bd..8c95c2f2 100644 --- a/api/v1/__init__.py +++ b/api/v1/__init__.py @@ -1,4 +1,11 @@ -from rest_framework import viewsets +import imghdr +import base64 +import six +import uuid + +from django.core.files.base import ContentFile + +from rest_framework import serializers, viewsets from rest_framework.response import Response # https://gist.github.com/ivlevdenis/a0c8f5b472b6b8550bbb016c6a30e0be @@ -41,3 +48,27 @@ class ExtendViewSet(object): class ExtendedModelViewSet(ExtendViewSet, viewsets.ModelViewSet): pass + + +class Base64ImageField(serializers.ImageField): + + def to_internal_value(self, data): + if isinstance(data, six.string_types): + if 'data:' in data and ';base64,' in data: + header, data = data.split(';base64,') + try: + decoded_file = base64.b64decode(data) + except TypeError: + self.fail('invalid_image') + + file_name = str(uuid.uuid4())[:12] + file_extension = self.get_file_extension( + file_name, decoded_file) + complete_file_name = "%s.%s" % (file_name, file_extension,) + data = ContentFile(decoded_file, name=complete_file_name) + return super().to_internal_value(data) + + def get_file_extension(self, file_name, decoded_file): + extension = imghdr.what(file_name, decoded_file) + extension = "jpg" if extension == "jpeg" else extension + return extension diff --git a/api/v1/permissions.py b/api/v1/permissions.py index e69de29b..b7f6e693 100644 --- a/api/v1/permissions.py +++ b/api/v1/permissions.py @@ -0,0 +1,35 @@ +from django.contrib.auth import get_user_model + +from rest_framework.permissions import BasePermission + +User = get_user_model() + + +class IsAdmin(BasePermission): + def has_permission(self, request, view): + return request.user.is_authenticated and ( + request.user.role == User.ADMIN_ROLE or request.user.is_staff or request.user.is_superuser + ) + + +class IsAdminOrIsSelf(BasePermission): + def has_object_permission(self, request, view, user): + return request.user.is_authenticated and ( + user == request.user or request.user.is_staff or request.user.is_superuser + ) + + +class IsAuthorOrAdmin(BasePermission): + def has_permission(self, request, view): + return request.user.is_authenticated and ( + request.user.role in [ + User.AUTHOR_ROLE, User.ADMIN_ROLE + ] or request.user.is_staff or request.user.is_superuser + ) + + +class IsAuthorObjectOrAdmin(BasePermission): + def has_object_permission(self, request, view, obj): + return request.user.is_authenticated and ( + request.user.role == User.ADMIN_ROLE or request.user.is_staff or request.user.is_superuser + ) and request.user == obj.author diff --git a/api/v1/serializers.py b/api/v1/serializers.py index 60a0f77a..8ff6f44a 100644 --- a/api/v1/serializers.py +++ b/api/v1/serializers.py @@ -1,7 +1,9 @@ from django.contrib.auth import get_user_model from rest_framework import serializers -from apps.course.models import Category, Course, Material, Like +from . import Base64ImageField + +from apps.course.models import Category, Course, Material, Lesson, Like from apps.content.models import ( Image, Text, ImageText, Video, Gallery, GalleryImage, @@ -89,6 +91,29 @@ class CourseSerializer(serializers.ModelSerializer): read_only_fields = ( 'id', + 'cover', + 'created_at', + 'update_at', + ) + + +class LessonSerializer(serializers.ModelSerializer): + + class Meta: + model = Lesson + fields = ( + 'id', + 'title', + 'short_description', + 'course', + 'cover', + 'created_at', + 'update_at', + ) + + read_only_fields = ( + 'id', + 'cover', 'created_at', 'update_at', ) @@ -110,6 +135,7 @@ class ImageSerializer(serializers.ModelSerializer): read_only_fields = ( 'id', + 'img', 'created_at', 'update_at', ) @@ -152,6 +178,7 @@ class ImageTextSerializer(serializers.ModelSerializer): read_only_fields = ( 'id', + 'img', 'created_at', 'update_at', ) @@ -211,6 +238,7 @@ class GalleryImageSerializer(serializers.ModelSerializer): read_only_fields = ( 'id', + 'image', 'created_at', 'update_at', ) @@ -248,8 +276,33 @@ class UserSerializer(serializers.ModelSerializer): read_only_fields = ( 'id', + 'photo', 'date_joined', 'is_staff', 'fb_id', 'fb_data', ) + + +class CoverImageSerializer(serializers.Serializer): + cover = Base64ImageField( + required=False, allow_empty_file=True, allow_null=True + ) + + +class UserPhotoSerializer(serializers.Serializer): + photo = Base64ImageField( + required=False, allow_empty_file=True, allow_null=True + ) + + +class ContentImageSerializer(serializers.Serializer): + img = Base64ImageField( + required=False, allow_empty_file=True, allow_null=True + ) + + +class GalleryImageSerializer(serializers.Serializer): + image = Base64ImageField( + required=False, allow_empty_file=True, allow_null=True + ) diff --git a/api/v1/urls.py b/api/v1/urls.py index 9a7b1957..6483f23d 100644 --- a/api/v1/urls.py +++ b/api/v1/urls.py @@ -12,13 +12,14 @@ from .views import ( ImageViewSet, TextViewSet, ImageTextViewSet, VideoViewSet, GalleryViewSet, GalleryImageViewSet, - UserViewSet, + UserViewSet, LessonViewSet, ) router = DefaultRouter() router.register(r'courses', CourseViewSet, base_name='courses') router.register(r'categories', CategoryViewSet, base_name='categories') 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'images', ImageViewSet, base_name='images') diff --git a/api/v1/views.py b/api/v1/views.py index 9b93f88d..744e7b13 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -1,6 +1,9 @@ from django.contrib.auth import get_user_model +from rest_framework import status from rest_framework import viewsets +from rest_framework.decorators import detail_route, list_route +from rest_framework.response import Response from . import ExtendedModelViewSet from .serializers import ( @@ -9,10 +12,13 @@ from .serializers import ( ImageSerializer, TextSerializer, ImageTextSerializer, VideoSerializer, GallerySerializer, GalleryImageSerializer, - UserSerializer, + UserSerializer, UserPhotoSerializer, + LessonSerializer, ContentImageSerializer, + GalleryImageSerializer, CoverImageSerializer, ) +from .permissions import IsAdmin, IsAdminOrIsSelf, IsAuthorOrAdmin, IsAuthorObjectOrAdmin -from apps.course.models import Category, Course, Material, Like +from apps.course.models import Category, Course, Material, Lesson, Like from apps.content.models import ( Image, Text, ImageText, Video, Gallery, GalleryImage, @@ -24,16 +30,25 @@ User = get_user_model() class MaterialViewSet(ExtendedModelViewSet): queryset = Material.objects.all() serializer_class = MaterialSerializer + search_fields = ('title', 'short_description',) + ordering_fields = ('title', 'created_at', 'update_at',) + # permission_classes = (IsAdmin,) class LikeViewSet(ExtendedModelViewSet): queryset = Like.objects.select_related('user').all() serializer_class = LikeSerializer + search_fields = ('user__email', 'user__firstname', 'user__lastname',) + ordering_fields = ('created_at', 'update_at',) + # permission_classes = (IsAdmin,) class CategoryViewSet(ExtendedModelViewSet): queryset = Category.objects.all() serializer_class = CategorySerializer + search_fields = ('title',) + ordering_fields = ('title',) + # permission_classes = (IsAdmin,) class CourseViewSet(ExtendedModelViewSet): @@ -43,7 +58,57 @@ class CourseViewSet(ExtendedModelViewSet): 'likes', 'materials' ).all() serializer_class = CourseSerializer - filter_fields = ('category',) + serializer_class_map = { + 'upload_photo': CoverImageSerializer, + } + filter_fields = ('category', 'status', 'is_infinite', 'is_featured',) + search_fields = ('author__email', 'title', 'category__title',) + ordering_fields = ('title', 'created_at', 'update_at',) + # permission_classes = (IsAuthorObjectOrAdmin,) + # permission_map = { + # 'create': IsAuthorOrAdmin, + # '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() + serializer_class = LessonSerializer + serializer_class_map = { + 'upload_photo': CoverImageSerializer, + } + filter_fields = ('course',) + search_fields = ('title', 'short_description',) + ordering_fields = ('title', 'created_at', 'update_at',) + # permission_classes = (IsAuthorObjectOrAdmin,) + # permission_map = { + # 'create': IsAuthorOrAdmin, + # '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): @@ -51,6 +116,27 @@ class ImageViewSet(ExtendedModelViewSet): 'course', 'lesson' ).all() serializer_class = ImageSerializer + serializer_class_map = { + 'upload_photo': ContentImageSerializer, + } + search_fields = ('title',) + ordering_fields = ('title', 'created_at', 'update_at', 'position',) + # permission_classes = (IsAuthorOrAdmin,) + # permission_map = { + # '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): @@ -58,6 +144,12 @@ class TextViewSet(ExtendedModelViewSet): 'course', 'lesson' ).all() serializer_class = TextSerializer + search_fields = ('title',) + ordering_fields = ('title', 'created_at', 'update_at', 'position',) + # permission_classes = (IsAuthorOrAdmin,) + # permission_map = { + # 'delete': IsAdmin, + # } class ImageTextViewSet(ExtendedModelViewSet): @@ -65,6 +157,27 @@ class ImageTextViewSet(ExtendedModelViewSet): 'course', 'lesson' ).all() serializer_class = ImageTextSerializer + serializer_class_map = { + 'upload_photo': ContentImageSerializer, + } + search_fields = ('title',) + ordering_fields = ('title', 'created_at', 'update_at', 'position',) + # permission_classes = (IsAuthorOrAdmin,) + # permission_map = { + # '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): @@ -72,18 +185,76 @@ class VideoViewSet(ExtendedModelViewSet): 'course', 'lesson' ).all() serializer_class = VideoSerializer + search_fields = ('title',) + ordering_fields = ('title', 'created_at', 'update_at', 'position',) + # permission_classes = (IsAuthorOrAdmin,) + # permission_map = { + # 'delete': IsAdmin, + # } class GalleryViewSet(ExtendedModelViewSet): queryset = Gallery.objects.select_related('course').all() serializer_class = GallerySerializer + search_fields = ('title',) + ordering_fields = ('title', 'created_at', 'update_at',) + # permission_classes = (IsAuthorOrAdmin,) + # permission_map = { + # 'delete': IsAdmin, + # } class GalleryImageViewSet(ExtendedModelViewSet): queryset = GalleryImage.objects.select_related('gallery').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() serializer_class = UserSerializer + serializer_class_map = { + 'upload_photo': UserPhotoSerializer, + } + filter_fields = ('is_staff', 'is_active', 'role', + 'gender', 'is_email_proved', 'fb_id',) + search_fields = ('email', 'first_name', 'last_name', + 'country', 'city', 'fb_id',) + ordering_fields = ('email', 'first_name', 'last_name', + 'country', 'city', 'date_joined',) + + # permission_classes = (IsAdminOrIsSelf,) + # permission_map = { + # 'delete': IsAdmin, + # } + + @detail_route(methods=['post'], url_path='upload-photo') + def upload_photo(self, request, pk=None): + user = self.get_object() + serializer = self.get_serializer() + serialized_data = serializer(data=request.data) + if serialized_data.is_valid(): + user.photo = serialized_data['photo'] + user.save() + return Response({'success': True}) + else: + return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST) diff --git a/apps/course/migrations/0021_auto_20180206_0632.py b/apps/course/migrations/0021_auto_20180206_0632.py new file mode 100644 index 00000000..f527bc4a --- /dev/null +++ b/apps/course/migrations/0021_auto_20180206_0632.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.2 on 2018-02-06 06:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0020_auto_20180202_1716'), + ] + + operations = [ + migrations.AlterModelOptions( + name='category', + options={'ordering': ['title'], 'verbose_name': 'Категория', 'verbose_name_plural': 'Категории'}, + ), + ] diff --git a/apps/course/models.py b/apps/course/models.py index 954a8160..76c46756 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -86,6 +86,7 @@ class Category(models.Model): class Meta: verbose_name = 'Категория' verbose_name_plural = 'Категории' + ordering = ['title'] class Lesson(models.Model): diff --git a/apps/user/models.py b/apps/user/models.py index d5633f8c..7a59786c 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -5,10 +5,13 @@ from django.utils.translation import gettext_lazy as _ class User(AbstractUser): + USER_ROLE = 0 + AUTHOR_ROLE = 1 + ADMIN_ROLE = 2 ROLE_CHOICES = ( - (0, 'пользователь'), - (1, 'автор'), - (2, 'администратор'), + (USER_ROLE, 'пользователь'), + (AUTHOR_ROLE, 'автор'), + (ADMIN_ROLE, 'администратор'), ) GENDER_CHOICES = ( ('n', 'не указан'), diff --git a/project/settings.py b/project/settings.py index d52eb79d..17c6ec7e 100644 --- a/project/settings.py +++ b/project/settings.py @@ -206,3 +206,9 @@ REST_FRAMEWORK = { if DEBUG: CORS_ORIGIN_ALLOW_ALL = True + +# Swagger doc settings + +SWAGGER_SETTINGS = { + 'DOC_EXPANSION': 'none', +}