commit
bbccc9996d
58 changed files with 1942 additions and 193 deletions
@ -0,0 +1,74 @@ |
|||||||
|
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 |
||||||
|
|
||||||
|
|
||||||
|
class ExtendViewSet(object): |
||||||
|
""" |
||||||
|
This viewset mixin class with extended options list. |
||||||
|
""" |
||||||
|
permission_map = {} |
||||||
|
throttle_scope_map = {} |
||||||
|
serializer_class_map = {} |
||||||
|
|
||||||
|
def get_serializer_class(self): |
||||||
|
ser = self.serializer_class_map.get(self.action, None) |
||||||
|
self.serializer_class = ser or self.serializer_class |
||||||
|
return super().get_serializer_class() |
||||||
|
|
||||||
|
def initialize_request(self, request, *args, **kwargs): |
||||||
|
request = super().initialize_request(request, *args, **kwargs) |
||||||
|
throttle_scope = self.throttle_scope_map.get(self.action, None) |
||||||
|
cls_throttle_scope = getattr(self, 'throttle_scope', None) |
||||||
|
self.throttle_scope = throttle_scope or cls_throttle_scope or '' |
||||||
|
return request |
||||||
|
|
||||||
|
def get_permissions(self): |
||||||
|
perms = self.permission_map.get(self.action, None) |
||||||
|
if perms and not isinstance(perms, (tuple, list)): |
||||||
|
perms = [perms, ] |
||||||
|
self.permission_classes = perms or self.permission_classes |
||||||
|
return super().get_permissions() |
||||||
|
|
||||||
|
def options(self, request, *args, **kwargs): |
||||||
|
if self.metadata_class is None: |
||||||
|
return self.http_method_not_allowed(request, *args, **kwargs) |
||||||
|
data = self.metadata_class().determine_metadata(request, self) |
||||||
|
data['actions']['GET'] = self.query_metadata |
||||||
|
return Response(data, status=status.HTTP_200_OK) |
||||||
|
|
||||||
|
|
||||||
|
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 |
||||||
@ -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 |
||||||
@ -0,0 +1,337 @@ |
|||||||
|
from django.contrib.auth import get_user_model |
||||||
|
from rest_framework import serializers |
||||||
|
|
||||||
|
from . import Base64ImageField |
||||||
|
|
||||||
|
from apps.course.models import Category, Course, Material, Lesson, Like |
||||||
|
from apps.content.models import ( |
||||||
|
Image, Text, ImageText, Video, |
||||||
|
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 MaterialSerializer(serializers.ModelSerializer): |
||||||
|
|
||||||
|
class Meta: |
||||||
|
model = Material |
||||||
|
fields = ( |
||||||
|
'id', |
||||||
|
'title', |
||||||
|
'cover', |
||||||
|
'short_description', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) |
||||||
|
|
||||||
|
read_only_fields = ( |
||||||
|
'id', |
||||||
|
'cover', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class LikeSerializer(serializers.ModelSerializer): |
||||||
|
|
||||||
|
class Meta: |
||||||
|
model = Like |
||||||
|
fields = ( |
||||||
|
'id', |
||||||
|
'user', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) |
||||||
|
|
||||||
|
read_only_fields = ( |
||||||
|
'id', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class CategorySerializer(serializers.ModelSerializer): |
||||||
|
|
||||||
|
class Meta: |
||||||
|
model = Category |
||||||
|
fields = ( |
||||||
|
'id', |
||||||
|
'title', |
||||||
|
) |
||||||
|
|
||||||
|
read_only_fields = ( |
||||||
|
'id', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class CourseSerializer(serializers.ModelSerializer): |
||||||
|
|
||||||
|
class Meta: |
||||||
|
model = Course |
||||||
|
fields = ( |
||||||
|
'id', |
||||||
|
'author', |
||||||
|
'title', |
||||||
|
'short_description', |
||||||
|
'from_author', |
||||||
|
'cover', |
||||||
|
'price', |
||||||
|
'is_infinite', |
||||||
|
'deferred_start_at', |
||||||
|
'category', |
||||||
|
'duration', |
||||||
|
'is_featured', |
||||||
|
'url', |
||||||
|
'status', |
||||||
|
'likes', |
||||||
|
'materials', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
'content', |
||||||
|
) |
||||||
|
|
||||||
|
read_only_fields = ( |
||||||
|
'id', |
||||||
|
'cover', |
||||||
|
'content', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class CourseRetrieveSerializer(CourseSerializer): |
||||||
|
category = CategorySerializer() |
||||||
|
materials = MaterialSerializer(many=True) |
||||||
|
|
||||||
|
|
||||||
|
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', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class ImageSerializer(serializers.ModelSerializer): |
||||||
|
|
||||||
|
class Meta: |
||||||
|
model = Image |
||||||
|
fields = ( |
||||||
|
'id', |
||||||
|
'course', |
||||||
|
'lesson', |
||||||
|
'title', |
||||||
|
'position', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) + ('img',) |
||||||
|
|
||||||
|
read_only_fields = ( |
||||||
|
'id', |
||||||
|
'img', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class TextSerializer(serializers.ModelSerializer): |
||||||
|
|
||||||
|
class Meta: |
||||||
|
model = Text |
||||||
|
fields = ( |
||||||
|
'id', |
||||||
|
'course', |
||||||
|
'lesson', |
||||||
|
'title', |
||||||
|
'position', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) + ('txt',) |
||||||
|
|
||||||
|
read_only_fields = ( |
||||||
|
'id', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class ImageTextSerializer(serializers.ModelSerializer): |
||||||
|
|
||||||
|
class Meta: |
||||||
|
model = ImageText |
||||||
|
fields = ( |
||||||
|
'id', |
||||||
|
'course', |
||||||
|
'lesson', |
||||||
|
'title', |
||||||
|
'position', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) + ('img', 'txt',) |
||||||
|
|
||||||
|
read_only_fields = ( |
||||||
|
'id', |
||||||
|
'img', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class VideoSerializer(serializers.ModelSerializer): |
||||||
|
|
||||||
|
class Meta: |
||||||
|
model = Video |
||||||
|
fields = ( |
||||||
|
'id', |
||||||
|
'course', |
||||||
|
'lesson', |
||||||
|
'title', |
||||||
|
'position', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) + ('url',) |
||||||
|
|
||||||
|
read_only_fields = ( |
||||||
|
'id', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
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: |
||||||
|
model = User |
||||||
|
fields = ( |
||||||
|
'id', |
||||||
|
'username', |
||||||
|
'email', |
||||||
|
'first_name', |
||||||
|
'last_name', |
||||||
|
'is_staff', |
||||||
|
'is_active', |
||||||
|
'date_joined', |
||||||
|
'role', |
||||||
|
'gender', |
||||||
|
'country', |
||||||
|
'city', |
||||||
|
'about', |
||||||
|
'instagram', |
||||||
|
'facebook', |
||||||
|
'twitter', |
||||||
|
'pinterest', |
||||||
|
'youtube', |
||||||
|
'vkontakte', |
||||||
|
'fb_id', |
||||||
|
'fb_data', |
||||||
|
'is_email_proved', |
||||||
|
'photo', |
||||||
|
) |
||||||
|
|
||||||
|
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 |
||||||
|
) |
||||||
@ -0,0 +1,51 @@ |
|||||||
|
from django.urls import path, include |
||||||
|
|
||||||
|
from rest_framework import permissions |
||||||
|
from rest_framework.routers import DefaultRouter |
||||||
|
|
||||||
|
from drf_yasg.views import get_schema_view |
||||||
|
from drf_yasg import openapi |
||||||
|
|
||||||
|
from .views import ( |
||||||
|
CategoryViewSet, CourseViewSet, |
||||||
|
MaterialViewSet, LikeViewSet, |
||||||
|
ImageViewSet, TextViewSet, |
||||||
|
ImageTextViewSet, VideoViewSet, |
||||||
|
GalleryViewSet, GalleryImageViewSet, |
||||||
|
UserViewSet, LessonViewSet, ImageObjectViewSet, |
||||||
|
) |
||||||
|
|
||||||
|
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'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') |
||||||
|
router.register(r'videos', VideoViewSet, base_name='videos') |
||||||
|
router.register(r'galleries', GalleryViewSet, base_name='galleries') |
||||||
|
router.register(r'gallery-images', GalleryImageViewSet, base_name='gallery-images') |
||||||
|
router.register(r'users', UserViewSet, base_name='users') |
||||||
|
|
||||||
|
|
||||||
|
schema_view = get_schema_view( |
||||||
|
openapi.Info( |
||||||
|
title="Lil Sity API", |
||||||
|
default_version='v1', |
||||||
|
description="Routes of Lil City project", |
||||||
|
), |
||||||
|
validators=['flex', 'ssv'], |
||||||
|
public=False, |
||||||
|
permission_classes=(permissions.AllowAny,), |
||||||
|
) |
||||||
|
|
||||||
|
urlpatterns = [ |
||||||
|
path('swagger(<str:format>.json|.yaml)', schema_view.without_ui(cache_timeout=None), name='schema-json'), |
||||||
|
path('swagger/', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'), |
||||||
|
path('redoc/', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'), |
||||||
|
path('', include((router.urls, 'api-root')), name='api-root') |
||||||
|
] |
||||||
@ -0,0 +1,269 @@ |
|||||||
|
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 ( |
||||||
|
CategorySerializer, CourseSerializer, |
||||||
|
MaterialSerializer, LikeSerializer, |
||||||
|
ImageSerializer, TextSerializer, |
||||||
|
ImageTextSerializer, VideoSerializer, |
||||||
|
GallerySerializer, GalleryImageSerializer, |
||||||
|
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, 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 |
||||||
|
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): |
||||||
|
queryset = Course.objects.select_related( |
||||||
|
'author', 'category' |
||||||
|
).prefetch_related( |
||||||
|
'likes', 'materials', 'content', |
||||||
|
).all() |
||||||
|
serializer_class = CourseSerializer |
||||||
|
serializer_class_map = { |
||||||
|
'list': CourseRetrieveSerializer, |
||||||
|
'retrieve': CourseRetrieveSerializer, |
||||||
|
'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): |
||||||
|
queryset = Image.objects.select_related( |
||||||
|
'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): |
||||||
|
queryset = Text.objects.select_related( |
||||||
|
'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): |
||||||
|
queryset = ImageText.objects.select_related( |
||||||
|
'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): |
||||||
|
queryset = Video.objects.select_related( |
||||||
|
'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) |
||||||
@ -0,0 +1,66 @@ |
|||||||
|
from django.contrib import admin |
||||||
|
from polymorphic.admin import ( |
||||||
|
PolymorphicParentModelAdmin, |
||||||
|
PolymorphicChildModelAdmin, |
||||||
|
PolymorphicChildModelFilter, |
||||||
|
) |
||||||
|
|
||||||
|
from apps.content.models import ( |
||||||
|
Content, Image, Text, ImageText, Video, |
||||||
|
Gallery, GalleryImage, ImageObject, |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ImageObject) |
||||||
|
class ImageObjectAdmin(admin.ModelAdmin): |
||||||
|
list_display = ( |
||||||
|
'id', |
||||||
|
'image', |
||||||
|
'created_at', |
||||||
|
'update_at', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class ContentChildAdmin(PolymorphicChildModelAdmin): |
||||||
|
base_model = Content |
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Image) |
||||||
|
class ImageAdmin(ContentChildAdmin): |
||||||
|
base_model = Image |
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Text) |
||||||
|
class TextAdmin(ContentChildAdmin): |
||||||
|
base_model = Text |
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ImageText) |
||||||
|
class ImageTextAdmin(ContentChildAdmin): |
||||||
|
base_model = ImageText |
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Video) |
||||||
|
class VideoAdmin(ContentChildAdmin): |
||||||
|
base_model = Video |
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Content) |
||||||
|
class ContentAdmin(PolymorphicParentModelAdmin): |
||||||
|
base_model = Content |
||||||
|
child_models = ( |
||||||
|
Image, |
||||||
|
Text, |
||||||
|
ImageText, |
||||||
|
Video |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Gallery) |
||||||
|
class GalleryAdmin(admin.ModelAdmin): |
||||||
|
pass |
||||||
|
|
||||||
|
|
||||||
|
@admin.register(GalleryImage) |
||||||
|
class GalleryImageAdmin(admin.ModelAdmin): |
||||||
|
pass |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
from django.apps import AppConfig |
||||||
|
|
||||||
|
|
||||||
|
class ContentConfig(AppConfig): |
||||||
|
name = 'content' |
||||||
|
verbose_name = 'Контент' |
||||||
@ -0,0 +1,93 @@ |
|||||||
|
# Generated by Django 2.0.2 on 2018-02-05 12:05 |
||||||
|
|
||||||
|
from django.db import migrations, models |
||||||
|
import django.db.models.deletion |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
initial = True |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('course', '0020_auto_20180202_1716'), |
||||||
|
('contenttypes', '0002_remove_content_type_name'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.CreateModel( |
||||||
|
name='Content', |
||||||
|
fields=[ |
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||||
|
('title', models.CharField(default='', max_length=100, verbose_name='Заголовок')), |
||||||
|
('position', models.PositiveSmallIntegerField(default=1, unique=True, verbose_name='Положение на странице')), |
||||||
|
], |
||||||
|
options={ |
||||||
|
'abstract': False, |
||||||
|
'base_manager_name': 'objects', |
||||||
|
}, |
||||||
|
), |
||||||
|
migrations.CreateModel( |
||||||
|
name='Image', |
||||||
|
fields=[ |
||||||
|
('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='content.Content')), |
||||||
|
('img', models.ImageField(upload_to='content/images', verbose_name='Изображение')), |
||||||
|
], |
||||||
|
options={ |
||||||
|
'abstract': False, |
||||||
|
'base_manager_name': 'objects', |
||||||
|
}, |
||||||
|
bases=('content.content',), |
||||||
|
), |
||||||
|
migrations.CreateModel( |
||||||
|
name='ImageText', |
||||||
|
fields=[ |
||||||
|
('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='content.Content')), |
||||||
|
('img', models.ImageField(upload_to='content/images', verbose_name='Изображение')), |
||||||
|
('txt', models.TextField(default='', verbose_name='Текст')), |
||||||
|
], |
||||||
|
options={ |
||||||
|
'abstract': False, |
||||||
|
'base_manager_name': 'objects', |
||||||
|
}, |
||||||
|
bases=('content.content',), |
||||||
|
), |
||||||
|
migrations.CreateModel( |
||||||
|
name='Text', |
||||||
|
fields=[ |
||||||
|
('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='content.Content')), |
||||||
|
('txt', models.TextField(default='', verbose_name='Текст')), |
||||||
|
], |
||||||
|
options={ |
||||||
|
'abstract': False, |
||||||
|
'base_manager_name': 'objects', |
||||||
|
}, |
||||||
|
bases=('content.content',), |
||||||
|
), |
||||||
|
migrations.CreateModel( |
||||||
|
name='Video', |
||||||
|
fields=[ |
||||||
|
('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='content.Content')), |
||||||
|
('url', models.URLField(verbose_name='Ссылка')), |
||||||
|
], |
||||||
|
options={ |
||||||
|
'abstract': False, |
||||||
|
'base_manager_name': 'objects', |
||||||
|
}, |
||||||
|
bases=('content.content',), |
||||||
|
), |
||||||
|
migrations.AddField( |
||||||
|
model_name='content', |
||||||
|
name='course', |
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='course.Course', verbose_name='Курс'), |
||||||
|
), |
||||||
|
migrations.AddField( |
||||||
|
model_name='content', |
||||||
|
name='lesson', |
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='course.Lesson', verbose_name='Урок'), |
||||||
|
), |
||||||
|
migrations.AddField( |
||||||
|
model_name='content', |
||||||
|
name='polymorphic_ctype', |
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_content.content_set+', to='contenttypes.ContentType'), |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,29 @@ |
|||||||
|
# Generated by Django 2.0.2 on 2018-02-05 12:12 |
||||||
|
|
||||||
|
from django.db import migrations, models |
||||||
|
import django.utils.timezone |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('content', '0001_initial'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.AlterModelOptions( |
||||||
|
name='content', |
||||||
|
options={'verbose_name': 'Контент', 'verbose_name_plural': 'Контент'}, |
||||||
|
), |
||||||
|
migrations.AddField( |
||||||
|
model_name='content', |
||||||
|
name='created_at', |
||||||
|
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), |
||||||
|
preserve_default=False, |
||||||
|
), |
||||||
|
migrations.AddField( |
||||||
|
model_name='content', |
||||||
|
name='update_at', |
||||||
|
field=models.DateTimeField(auto_now=True), |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
# Generated by Django 2.0.2 on 2018-02-05 12:46 |
||||||
|
|
||||||
|
from django.db import migrations |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('content', '0002_auto_20180205_1212'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.AlterModelOptions( |
||||||
|
name='content', |
||||||
|
options={'ordering': ('-created_at',), 'verbose_name': 'Контент', 'verbose_name_plural': 'Контент'}, |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,45 @@ |
|||||||
|
# Generated by Django 2.0.2 on 2018-02-05 13:09 |
||||||
|
|
||||||
|
from django.db import migrations, models |
||||||
|
import django.db.models.deletion |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('course', '0020_auto_20180202_1716'), |
||||||
|
('content', '0003_auto_20180205_1246'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.CreateModel( |
||||||
|
name='Gallery', |
||||||
|
fields=[ |
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||||
|
('title', models.CharField(default='', max_length=100, verbose_name='Заголовок')), |
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)), |
||||||
|
('update_at', models.DateTimeField(auto_now=True)), |
||||||
|
('course', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='course.Course', verbose_name='Курс')), |
||||||
|
], |
||||||
|
options={ |
||||||
|
'verbose_name': 'Галерея', |
||||||
|
'verbose_name_plural': 'Галереи', |
||||||
|
'ordering': ('-created_at',), |
||||||
|
}, |
||||||
|
), |
||||||
|
migrations.CreateModel( |
||||||
|
name='GalleryImage', |
||||||
|
fields=[ |
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||||
|
('image', models.ImageField(upload_to='content/gallery_images', verbose_name='Изображение')), |
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)), |
||||||
|
('update_at', models.DateTimeField(auto_now=True)), |
||||||
|
('gallery', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.Gallery', verbose_name='Галерея')), |
||||||
|
], |
||||||
|
options={ |
||||||
|
'verbose_name': 'Изображение в галерее', |
||||||
|
'verbose_name_plural': 'Изображения в галерее', |
||||||
|
'ordering': ('-created_at',), |
||||||
|
}, |
||||||
|
), |
||||||
|
] |
||||||
@ -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='Урок'), |
||||||
|
), |
||||||
|
] |
||||||
@ -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), |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,97 @@ |
|||||||
|
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, |
||||||
|
null=True, blank=True, |
||||||
|
verbose_name='Курс', |
||||||
|
related_name='content', |
||||||
|
) |
||||||
|
lesson = models.ForeignKey( |
||||||
|
Lesson, on_delete=models.CASCADE, |
||||||
|
null=True, blank=True, |
||||||
|
verbose_name='Урок', |
||||||
|
related_name='content', |
||||||
|
) |
||||||
|
title = models.CharField('Заголовок', max_length=100, default='') |
||||||
|
position = models.PositiveSmallIntegerField( |
||||||
|
'Положение на странице', |
||||||
|
default=1, unique=True |
||||||
|
) |
||||||
|
|
||||||
|
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 Image(Content): |
||||||
|
img = models.ImageField('Изображение', upload_to='content/images') |
||||||
|
|
||||||
|
|
||||||
|
class Text(Content): |
||||||
|
txt = models.TextField('Текст', default='') |
||||||
|
|
||||||
|
|
||||||
|
class ImageText(Content): |
||||||
|
img = models.ImageField('Изображение', upload_to='content/images') |
||||||
|
txt = models.TextField('Текст', default='') |
||||||
|
|
||||||
|
|
||||||
|
class Video(Content): |
||||||
|
url = models.URLField('Ссылка') |
||||||
|
|
||||||
|
|
||||||
|
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) |
||||||
|
update_at = models.DateTimeField(auto_now=True) |
||||||
|
|
||||||
|
class Meta: |
||||||
|
verbose_name = 'Галерея' |
||||||
|
verbose_name_plural = 'Галереи' |
||||||
|
ordering = ('-created_at',) |
||||||
|
|
||||||
|
|
||||||
|
class GalleryImage(models.Model): |
||||||
|
gallery = models.ForeignKey( |
||||||
|
Gallery, on_delete=models.CASCADE, |
||||||
|
verbose_name='Галерея' |
||||||
|
) |
||||||
|
image = models.ImageField( |
||||||
|
'Изображение', upload_to='content/gallery_images' |
||||||
|
) |
||||||
|
|
||||||
|
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',) |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
from django.test import TestCase |
||||||
|
|
||||||
|
# Create your tests here. |
||||||
@ -0,0 +1,2 @@ |
|||||||
|
from django.shortcuts import render |
||||||
|
|
||||||
@ -0,0 +1,48 @@ |
|||||||
|
# Generated by Django 2.0.2 on 2018-02-02 17:16 |
||||||
|
|
||||||
|
from django.conf import settings |
||||||
|
from django.db import migrations, models |
||||||
|
import django.db.models.deletion |
||||||
|
import polymorphic_tree.models |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('contenttypes', '0002_remove_content_type_name'), |
||||||
|
('course', '0019_auto_20180130_1630'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.AlterModelOptions( |
||||||
|
name='coursecomment', |
||||||
|
options={'base_manager_name': 'objects', 'ordering': ('tree_id', 'lft'), 'verbose_name': 'Комментарий курса', 'verbose_name_plural': 'Комментарии курсов'}, |
||||||
|
), |
||||||
|
migrations.AlterModelOptions( |
||||||
|
name='lessoncomment', |
||||||
|
options={'base_manager_name': 'objects', 'ordering': ('tree_id', 'lft'), 'verbose_name': 'Комментарий урока', 'verbose_name_plural': 'Комментарии уроков'}, |
||||||
|
), |
||||||
|
migrations.RemoveField( |
||||||
|
model_name='coursecomment', |
||||||
|
name='parent', |
||||||
|
), |
||||||
|
migrations.RemoveField( |
||||||
|
model_name='lessoncomment', |
||||||
|
name='parent', |
||||||
|
), |
||||||
|
migrations.AddField( |
||||||
|
model_name='comment', |
||||||
|
name='parent', |
||||||
|
field=polymorphic_tree.models.PolymorphicTreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='children', to='course.Comment'), |
||||||
|
), |
||||||
|
migrations.AddField( |
||||||
|
model_name='comment', |
||||||
|
name='polymorphic_ctype', |
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_course.comment_set+', to='contenttypes.ContentType'), |
||||||
|
), |
||||||
|
migrations.AlterField( |
||||||
|
model_name='comment', |
||||||
|
name='author', |
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), |
||||||
|
), |
||||||
|
] |
||||||
@ -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': 'Категории'}, |
||||||
|
), |
||||||
|
] |
||||||
@ -1,5 +1,5 @@ |
|||||||
{% for cat in category_items %} |
{% for cat in category_items %} |
||||||
<div class="select__option js-select-option{% if category and category.0 == cat.title %} active{% endif %}" data-category-option data-category-url="{% url 'courses' %}?category={{ cat.title }}"> |
<div class="select__option js-select-option{% if category and category.0 == cat.title %} active{% endif %}" data-category-option data-category-name="{{ cat.title }}" data-category-url="{% url 'courses' %}?category={{ cat.title }}"> |
||||||
<div class="select__title">{{ cat.title }}</div> |
<div class="select__title">{{ cat.title }}</div> |
||||||
</div> |
</div> |
||||||
{% endfor %} |
{% endfor %} |
||||||
@ -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', |
||||||
|
) |
||||||
@ -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='День рождения'), |
||||||
|
), |
||||||
|
] |
||||||
@ -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='О себе'), |
||||||
|
), |
||||||
|
] |
||||||
@ -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='Страна'), |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,209 @@ |
|||||||
|
{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %} |
||||||
|
<div class="section section_gray section_menu"> |
||||||
|
<div class="section__center center center_xs"> |
||||||
|
<div class="menu"> |
||||||
|
<a class="menu__link active" href="{% url 'user-edit' user.id %}">Профиль</a> |
||||||
|
<a class="menu__link" href="#">Уведомления</a> |
||||||
|
<a class="menu__link" href="#">Платежи</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% comment %} |
||||||
|
<!-- <div class="section section_confirm"> |
||||||
|
<div class="section__center center center_xs"> |
||||||
|
<div class="confirm"> |
||||||
|
<div class="confirm__title title">Подтверждение почты</div> |
||||||
|
<div class="confirm__content">На электронный адрес |
||||||
|
<strong>sasha@lil.city</strong> отправлено письмо с кодом подтверждения. Введите код, чтобы подтвердить почту.</div> |
||||||
|
<div class="confirm__form"> |
||||||
|
<div class="confirm__field field field_code"> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input class="field__input" type="text" placeholder="Введите код подтверждения"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<button class="confirm__btn btn btn_dark">ПОДТВЕРДИТЬ</button> |
||||||
|
</div> |
||||||
|
<div class="confirm__content">Если у вас нет кода или письмо где-то затерялось, вы можете получить новый код подтверждения. Отправить новый код?</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> --> |
||||||
|
{% endcomment %} |
||||||
|
{% if messages %} |
||||||
|
<div class="section section_gray section_menu"> |
||||||
|
<div class="section__center center center_xs"> |
||||||
|
{% for message in messages %} |
||||||
|
<div class="message message_{{ message.tags }}">{{ message }}</div> |
||||||
|
{% endfor %} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
{{form.errors}} |
||||||
|
<div class="section section_gray"> |
||||||
|
<div class="section__center center center_xs"> |
||||||
|
<div class="form"> |
||||||
|
<form action="" method="POST" enctype="multipart/form-data"> |
||||||
|
{% csrf_token %} |
||||||
|
<div class="form__group"> |
||||||
|
<div class="form__title">Личные данные</div> |
||||||
|
<div class="form__ava ava"> |
||||||
|
{% if user.photo %} |
||||||
|
<img id="photo" class="ava__pic" src="{{user.photo.url}}"> |
||||||
|
{% else %} |
||||||
|
<img id="photo" class="ava__pic" src="{% static 'img/user.jpg' %}"> |
||||||
|
{% endif %} |
||||||
|
<input name="photo" class="ava__input" type="file" accept='image/*' onchange='openFile(event)'> |
||||||
|
<div class="ava__icon"> |
||||||
|
<svg class="icon icon-photo"> |
||||||
|
<use xlink:href="{% static 'img/sprite.svg' %}#icon-photo"></use> |
||||||
|
</svg> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__fieldset"> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">ИМЯ</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='first_name' class="field__input" type="text" placeholder="Имя" value="{{ user.first_name }}"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">ФАМИЛИЯ</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='last_name' class="field__input" type="text" placeholder="Фамилия" value="{{ user.last_name }}"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">Почта</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='email' class="field__input" type="email" placeholder="Почта" value="{{ user.email }}"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__fieldset"> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">ГОРОД</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='city' class="field__input" type="text" placeholder="Город" value="{% if user.city %}{{ user.city }}{% endif %}"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">СТРАНА</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='country' class="field__input" type="text" placeholder="Страна" value="{% if user.country %}{{ user.country }}{% endif %}"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__fieldset"> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">ДАТА РОЖДЕНИЯ</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='birthday' class="field__input" type="text" placeholder="dd.mm.yyyy" value="{% if user.birthday %}{{ user.birthday | date:'d.m.Y' }}{% endif %}"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">ПОЛ</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<div class="field__select select js-select{% if user.gender and user.gender != 'n' %} selected{% endif %}"> |
||||||
|
<div class="select__head js-select-head"> |
||||||
|
{% if user.gender == 'f' %}Ж{% elif user.gender == 'm' %}M{% else %}М / Ж{% endif %} |
||||||
|
</div> |
||||||
|
<div class="select__drop js-select-drop"> |
||||||
|
<div class="select__option js-select-option" data-gender-option data-gender="m"> |
||||||
|
<div class="select__title">М</div> |
||||||
|
</div> |
||||||
|
<div class="select__option js-select-option" data-gender-option data-gender="f"> |
||||||
|
<div class="select__title">Ж</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<input id="gender" name='gender' class="select__input" type="hidden"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">О себе</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<textarea name='about' class="field__textarea" placeholder="Расскажите о себе и своем опыте">{% if user.about %}{{ user.about }}{% endif %}</textarea> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__group"> |
||||||
|
<div class="form__title">Пароль</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">ТЕКУЩИЙ ПАРОЛЬ</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='old_password' class="field__input" type="password" placeholder="Введите текущий пароль"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">НОВЫЙ ПАРОЛЬ</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='new_password1' class="field__input" type="password" placeholder="Введите новый пароль"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">ПОДТВЕРДИТЬ НОВЫЙ ПАРОЛЬ</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='new_password2' class="field__input" type="password" placeholder="Подтвердите новый пароль"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__group"> |
||||||
|
<div class="form__title">Соцсети</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">INSTAGRAM</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='instagram' class="field__input" type="text" placeholder="https://instagram.com/school.lil.city" value="{{ user.instagram }}"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">FACEBOOK</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='facebook' class="field__input" type="text" placeholder="https://facebook.com/lilcitycompany" value="{{ user.facebook }}"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">TWITTER</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='twitter' class="field__input" type="text" placeholder="https://twitter.com/lilcitycompany" value="{{ user.twitter }}"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">PINTEREST</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='pinterest' class="field__input" type="text" placeholder="https://pinterest.com/lilcitycompany" value="{{ user.pinterest }}"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">YOUTUBE</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='youtube' class="field__input" type="text" placeholder="https://youtube.com/lilcitycompany" value="{{ user.youtube }}"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__field field"> |
||||||
|
<div class="field__label">VKONTAKTE</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input name='vkontakte' class="field__input" type="text" placeholder="https://vk.com/lilcitycompany" value="{{ user.vkontakte }}"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form__foot"> |
||||||
|
<button type="submit" class="form__btn btn btn_md">СОХРАНИТЬ</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<script> |
||||||
|
var openFile = function(file) { |
||||||
|
var input = file.target; |
||||||
|
|
||||||
|
var reader = new FileReader(); |
||||||
|
reader.onload = function(){ |
||||||
|
var dataURL = reader.result; |
||||||
|
var output = document.getElementById('photo'); |
||||||
|
output.src = dataURL; |
||||||
|
}; |
||||||
|
reader.readAsDataURL(input.files[0]); |
||||||
|
}; |
||||||
|
</script> |
||||||
|
{% endblock content %} |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
import os |
||||||
|
from celery import Celery |
||||||
|
|
||||||
|
# set the default Django settings module for the 'celery' program. |
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') |
||||||
|
|
||||||
|
app = Celery('project') |
||||||
|
|
||||||
|
# Using a string here means the worker don't have to serialize |
||||||
|
# the configuration object to child processes. |
||||||
|
# - namespace='CELERY' means all celery-related configuration keys |
||||||
|
# should have a `CELERY_` prefix. |
||||||
|
app.config_from_object('project.celery_settings') |
||||||
|
|
||||||
|
# Load task modules from all registered Django app configs. |
||||||
|
app.autodiscover_tasks() |
||||||
|
|
||||||
|
|
||||||
|
@app.task(bind=True) |
||||||
|
def debug_task(self): |
||||||
|
return f'Request: {self.request}' |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
broker_url = 'redis://redis:6379/0' |
||||||
|
result_backend = 'redis://redis:6379/1' |
||||||
|
task_serializer = 'json' |
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 793 B |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 793 B |
@ -0,0 +1,14 @@ |
|||||||
|
import $ from 'jquery'; |
||||||
|
|
||||||
|
$(document).ready(function () { |
||||||
|
// Обработчик выбора пола
|
||||||
|
let genderInput = $('#gender') |
||||||
|
|
||||||
|
$('div.js-select-option[data-gender-option]').on('click', function (e) { |
||||||
|
e.preventDefault(); |
||||||
|
const currentGender = $(this).attr('data-gender'); |
||||||
|
$('[data-gender]').removeClass('active'); |
||||||
|
$(`[data-gender=${currentGender}]`).addClass('active'); |
||||||
|
genderInput.val(currentGender) |
||||||
|
}); |
||||||
|
}) |
||||||
Loading…
Reference in new issue