commit
e631905c60
124 changed files with 18670 additions and 35422 deletions
@ -0,0 +1,45 @@ |
||||
|
||||
|
||||
from rest_framework import 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 |
||||
@ -0,0 +1,45 @@ |
||||
from django.contrib.auth import get_user_model |
||||
from django.utils.translation import ugettext_lazy as _ |
||||
|
||||
from rest_framework import serializers |
||||
from rest_framework.authtoken.views import ObtainAuthToken |
||||
from rest_framework.compat import authenticate |
||||
|
||||
User = get_user_model() |
||||
|
||||
|
||||
class AuthTokenSerializer(serializers.Serializer): |
||||
email = serializers.CharField(label=_("Email")) |
||||
password = serializers.CharField( |
||||
label=_("Password"), |
||||
style={'input_type': 'password'}, |
||||
trim_whitespace=False |
||||
) |
||||
|
||||
def validate(self, attrs): |
||||
email = attrs.get('email') |
||||
password = attrs.get('password') |
||||
|
||||
if email and password: |
||||
user = authenticate(request=self.context.get('request'), |
||||
email=email, password=password) |
||||
|
||||
# The authenticate call simply returns None for is_active=False |
||||
# users. (Assuming the default ModelBackend authentication |
||||
# backend.) |
||||
if not user: |
||||
msg = _('Unable to log in with provided credentials.') |
||||
raise serializers.ValidationError(msg, code='authorization') |
||||
elif user.role != User.ADMIN_ROLE: |
||||
msg = _('Only admin have permission to login admin page.') |
||||
raise serializers.ValidationError(msg, code='authorization') |
||||
else: |
||||
msg = _('Must include "email" and "password".') |
||||
raise serializers.ValidationError(msg, code='authorization') |
||||
|
||||
attrs['user'] = user |
||||
return attrs |
||||
|
||||
|
||||
class ObtainToken(ObtainAuthToken): |
||||
serializer_class = AuthTokenSerializer |
||||
@ -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,32 @@ |
||||
import imghdr |
||||
import base64 |
||||
import six |
||||
import uuid |
||||
|
||||
from django.core.files.base import ContentFile |
||||
|
||||
from rest_framework import serializers |
||||
|
||||
|
||||
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,261 @@ |
||||
from rest_framework import serializers |
||||
|
||||
from apps.content.models import ( |
||||
Content, Image, Text, ImageText, Video, |
||||
Gallery, GalleryImage, ImageObject, |
||||
) |
||||
|
||||
from . import Base64ImageField |
||||
|
||||
|
||||
class ContentCreateSerializer(serializers.Serializer): |
||||
TYPE_CHOICES = ( |
||||
'text', |
||||
'image', |
||||
'image-text', |
||||
'images', |
||||
'video', |
||||
) |
||||
type = serializers.ChoiceField(choices=TYPE_CHOICES) |
||||
data = serializers.JSONField() |
||||
|
||||
def to_representation(self, obj): |
||||
if isinstance(obj, Image): |
||||
return ImageSerializer(obj, context=self.context).to_representation(obj) |
||||
elif isinstance(obj, Text): |
||||
return TextSerializer(obj, context=self.context).to_representation(obj) |
||||
elif isinstance(obj, ImageText): |
||||
return ImageTextSerializer(obj, context=self.context).to_representation(obj) |
||||
elif isinstance(obj, Video): |
||||
return VideoSerializer(obj, context=self.context).to_representation(obj) |
||||
elif isinstance(obj, Gallery): |
||||
return GallerySerializer(obj, context=self.context).to_representation(obj) |
||||
return super(ContentSerializer, self).to_representation(obj) |
||||
|
||||
|
||||
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 ImageCreateSerializer(serializers.ModelSerializer): |
||||
type = serializers.SerializerMethodField() |
||||
|
||||
class Meta: |
||||
model = Image |
||||
fields = ( |
||||
'id', |
||||
'course', |
||||
'lesson', |
||||
'title', |
||||
'position', |
||||
'img', |
||||
'type', |
||||
'created_at', |
||||
'update_at', |
||||
) |
||||
|
||||
read_only_fields = ( |
||||
'id', |
||||
'type', |
||||
'created_at', |
||||
'update_at', |
||||
) |
||||
|
||||
def get_type(self, object): |
||||
return 'image' |
||||
|
||||
|
||||
class ImageSerializer(ImageCreateSerializer): |
||||
img = ImageObjectSerializer() |
||||
|
||||
|
||||
class TextCreateSerializer(serializers.ModelSerializer): |
||||
type = serializers.SerializerMethodField() |
||||
|
||||
class Meta: |
||||
model = Text |
||||
fields = ( |
||||
'id', |
||||
'course', |
||||
'lesson', |
||||
'title', |
||||
'position', |
||||
'type', |
||||
'created_at', |
||||
'update_at', |
||||
) + ('txt',) |
||||
|
||||
read_only_fields = ( |
||||
'id', |
||||
'type', |
||||
'created_at', |
||||
'update_at', |
||||
) |
||||
|
||||
def get_type(self, object): |
||||
return 'text' |
||||
|
||||
|
||||
class TextSerializer(TextCreateSerializer): |
||||
pass |
||||
|
||||
|
||||
class ImageTextCreateSerializer(serializers.ModelSerializer): |
||||
type = serializers.SerializerMethodField() |
||||
|
||||
class Meta: |
||||
model = ImageText |
||||
fields = ( |
||||
'id', |
||||
'course', |
||||
'lesson', |
||||
'title', |
||||
'position', |
||||
'img', |
||||
'txt', |
||||
'type', |
||||
'created_at', |
||||
'update_at', |
||||
) |
||||
|
||||
read_only_fields = ( |
||||
'id', |
||||
'type', |
||||
'created_at', |
||||
'update_at', |
||||
) |
||||
|
||||
def get_type(self, object): |
||||
return 'image-text' |
||||
|
||||
|
||||
class ImageTextSerializer(ImageTextCreateSerializer): |
||||
img = ImageObjectSerializer() |
||||
|
||||
|
||||
class VideoCreateSerializer(serializers.ModelSerializer): |
||||
type = serializers.SerializerMethodField() |
||||
|
||||
class Meta: |
||||
model = Video |
||||
fields = ( |
||||
'id', |
||||
'course', |
||||
'lesson', |
||||
'title', |
||||
'position', |
||||
'type', |
||||
'created_at', |
||||
'update_at', |
||||
) + ('url',) |
||||
|
||||
read_only_fields = ( |
||||
'id', |
||||
'type', |
||||
'created_at', |
||||
'update_at', |
||||
) |
||||
|
||||
def get_type(self, object): |
||||
return 'video' |
||||
|
||||
|
||||
class VideoSerializer(VideoCreateSerializer): |
||||
pass |
||||
|
||||
|
||||
class GalleryImageCreateSerializer(serializers.ModelSerializer): |
||||
|
||||
class Meta: |
||||
model = GalleryImage |
||||
fields = ( |
||||
'id', |
||||
'gallery', |
||||
'img', |
||||
'created_at', |
||||
'update_at', |
||||
) |
||||
|
||||
read_only_fields = ( |
||||
'id', |
||||
'created_at', |
||||
'update_at', |
||||
) |
||||
|
||||
|
||||
class GalleryImageSerializer(GalleryImageCreateSerializer): |
||||
img = ImageObjectSerializer() |
||||
|
||||
|
||||
class GallerySerializer(serializers.ModelSerializer): |
||||
type = serializers.SerializerMethodField() |
||||
gallery_images = GalleryImageSerializer(many=True) |
||||
|
||||
class Meta: |
||||
model = Gallery |
||||
fields = ( |
||||
'id', |
||||
'course', |
||||
'lesson', |
||||
'title', |
||||
'position', |
||||
'gallery_images', |
||||
'type', |
||||
'created_at', |
||||
'update_at', |
||||
) |
||||
|
||||
read_only_fields = ( |
||||
'id', |
||||
'type', |
||||
'created_at', |
||||
'update_at', |
||||
) |
||||
|
||||
def get_type(self, object): |
||||
return 'images' |
||||
|
||||
|
||||
class ContentSerializer(serializers.ModelSerializer): |
||||
|
||||
class Meta: |
||||
model = Content |
||||
fields = ( |
||||
'id', |
||||
'course', |
||||
'lesson', |
||||
'title', |
||||
'position', |
||||
'created_at', |
||||
'update_at', |
||||
) |
||||
|
||||
def to_representation(self, obj): |
||||
if isinstance(obj, Image): |
||||
return ImageSerializer(obj, context=self.context).to_representation(obj) |
||||
elif isinstance(obj, Text): |
||||
return TextSerializer(obj, context=self.context).to_representation(obj) |
||||
elif isinstance(obj, ImageText): |
||||
return ImageTextSerializer(obj, context=self.context).to_representation(obj) |
||||
elif isinstance(obj, Video): |
||||
return VideoSerializer(obj, context=self.context).to_representation(obj) |
||||
elif isinstance(obj, Gallery): |
||||
return GallerySerializer(obj, context=self.context).to_representation(obj) |
||||
return super(ContentSerializer, self).to_representation(obj) |
||||
@ -0,0 +1,291 @@ |
||||
from rest_framework import serializers |
||||
|
||||
from apps.course.models import Category, Course, Material, Lesson, Like |
||||
|
||||
from .content import ( |
||||
ImageObjectSerializer, ContentSerializer, ContentCreateSerializer, |
||||
GallerySerializer, GalleryImageSerializer, |
||||
) |
||||
|
||||
from apps.content.models import ( |
||||
Content, Image, Text, ImageText, Video, |
||||
Gallery, GalleryImage, ImageObject, |
||||
) |
||||
|
||||
from .mixins import DispatchContentMixin, DispatchGalleryMixin, DispatchMaterialMixin |
||||
|
||||
|
||||
class MaterialCreateSerializer(serializers.ModelSerializer): |
||||
|
||||
class Meta: |
||||
model = Material |
||||
fields = ( |
||||
'id', |
||||
'title', |
||||
'cover', |
||||
'short_description', |
||||
'created_at', |
||||
'update_at', |
||||
) |
||||
|
||||
read_only_fields = ( |
||||
'id', |
||||
'created_at', |
||||
'update_at', |
||||
) |
||||
|
||||
|
||||
class MaterialSerializer(MaterialCreateSerializer): |
||||
cover = ImageObjectSerializer(allow_null=True) |
||||
|
||||
|
||||
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 CourseCreateSerializer(DispatchContentMixin, |
||||
DispatchGalleryMixin, |
||||
DispatchMaterialMixin, |
||||
serializers.ModelSerializer |
||||
): |
||||
slug = serializers.SlugField(allow_unicode=True, required=False) |
||||
content = serializers.ListSerializer( |
||||
child=ContentCreateSerializer(), |
||||
required=False, |
||||
) |
||||
materials = MaterialSerializer(many=True, required=False) |
||||
gallery = GallerySerializer() |
||||
|
||||
class Meta: |
||||
model = Course |
||||
fields = ( |
||||
'id', |
||||
'slug', |
||||
'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', |
||||
'deactivated_at', |
||||
'content', |
||||
'gallery', |
||||
) |
||||
|
||||
read_only_fields = ( |
||||
'id', |
||||
'url', |
||||
'created_at', |
||||
'update_at', |
||||
'deactivated_at', |
||||
) |
||||
|
||||
def create(self, validated_data): |
||||
content = validated_data.pop('content', []) |
||||
materials = validated_data.pop('materials', []) |
||||
gallery = validated_data.pop('gallery', {}) |
||||
course = super().create(validated_data) |
||||
self.dispatch_content(course, content) |
||||
self.dispatch_materials(course, materials) |
||||
self.dispatch_gallery(course, gallery) |
||||
return course |
||||
|
||||
def update(self, instance, validated_data): |
||||
content = validated_data.pop('content', []) |
||||
materials = validated_data.pop('materials', []) |
||||
gallery = validated_data.pop('gallery', {}) |
||||
course = super().update(instance, validated_data) |
||||
self.dispatch_materials(course, materials) |
||||
self.dispatch_content(course, content) |
||||
self.dispatch_gallery(course, gallery) |
||||
return course |
||||
|
||||
def to_representation(self, instance): |
||||
return CourseSerializer(instance, context=self.context).to_representation(instance) |
||||
|
||||
|
||||
class CourseSerializer(CourseCreateSerializer): |
||||
category = CategorySerializer() |
||||
materials = MaterialSerializer(many=True) |
||||
cover = ImageObjectSerializer() |
||||
gallery = GallerySerializer() |
||||
content = ContentSerializer(many=True) |
||||
|
||||
|
||||
class LessonCreateSerializer(serializers.ModelSerializer): |
||||
content = serializers.ListSerializer( |
||||
child=ContentCreateSerializer(), |
||||
required=False, |
||||
) |
||||
|
||||
class Meta: |
||||
model = Lesson |
||||
fields = ( |
||||
'id', |
||||
'title', |
||||
'short_description', |
||||
'course', |
||||
'cover', |
||||
'content', |
||||
'created_at', |
||||
'update_at', |
||||
'deactivated_at', |
||||
) |
||||
|
||||
read_only_fields = ( |
||||
'id', |
||||
'created_at', |
||||
'update_at', |
||||
'deactivated_at', |
||||
) |
||||
|
||||
def dispatch_content(self, lesson, content): |
||||
for c in content: |
||||
if 'type' not in c or not c['type'] or 'data' not in c or not c['data']: |
||||
continue |
||||
ctype = c['type'] |
||||
cdata = c['data'] |
||||
if ctype == 'text': |
||||
if 'id' in cdata and cdata['id']: |
||||
t = Text.objects.get(id=cdata['id']) |
||||
t.position = cdata['position'] |
||||
t.title = cdata['title'] |
||||
t.lesson = lesson |
||||
t.txt = cdata['txt'] |
||||
t.save() |
||||
else: |
||||
t = Text.objects.create( |
||||
position=cdata['position'], |
||||
title=cdata['title'], |
||||
lesson=lesson, |
||||
txt=cdata['txt'], |
||||
) |
||||
elif ctype == 'image': |
||||
if 'id' in cdata and cdata['id']: |
||||
image = Image.objects.get(id=cdata['id']) |
||||
image.position = cdata['position'] |
||||
image.title = cdata['title'] |
||||
image.lesson = lesson |
||||
image.img = ImageObject.objects.get(id=cdata['img']) |
||||
image.save() |
||||
else: |
||||
image = Image.objects.create( |
||||
position=cdata['position'], |
||||
title=cdata['title'], |
||||
lesson=lesson, |
||||
img=ImageObject.objects.get(id=cdata['img']), |
||||
) |
||||
elif ctype == 'image-text': |
||||
if 'id' in cdata and cdata['id']: |
||||
it = ImageText.objects.get(id=cdata['id']) |
||||
it.position = cdata['position'] |
||||
it.title = cdata['title'] |
||||
it.lesson = lesson |
||||
it.img = ImageObject.objects.get(id=cdata['img']) |
||||
it.txt = cdata['txt'] |
||||
it.save() |
||||
else: |
||||
it = ImageText.objects.create( |
||||
position=cdata['position'], |
||||
title=cdata['title'], |
||||
lesson=lesson, |
||||
img=ImageObject.objects.get(id=cdata['img']), |
||||
txt=cdata['txt'], |
||||
) |
||||
elif ctype == 'video': |
||||
if 'id' in cdata and cdata['id']: |
||||
v = Video.objects.get(id=cdata['id']) |
||||
v.position = cdata['position'] |
||||
v.title = cdata['title'] |
||||
v.lesson = lesson |
||||
v.url = cdata['url'] |
||||
v.save() |
||||
else: |
||||
v = Video.objects.create( |
||||
position=cdata['position'], |
||||
title=cdata['title'], |
||||
lesson=lesson, |
||||
url=cdata['url'], |
||||
) |
||||
elif ctype == 'images': |
||||
if 'id' in cdata and cdata['id']: |
||||
g = Gallery.objects.get(id=cdata['id']) |
||||
g.position = cdata['position'] |
||||
g.title = cdata['title'] |
||||
g.lesson = lesson |
||||
g.save() |
||||
if 'images' in cdata: |
||||
for image in cdata['images']: |
||||
gi = GalleryImage.objects.create( |
||||
gallery=g, |
||||
img=ImageObject.objects.get(id=image['img']), |
||||
) |
||||
else: |
||||
g = Gallery.objects.create( |
||||
lesson=lesson, |
||||
position=cdata['position'], |
||||
title=cdata['title'], |
||||
) |
||||
if 'images' in cdata: |
||||
for image in cdata['images']: |
||||
gi = GalleryImage.objects.create( |
||||
gallery=g, |
||||
img=ImageObject.objects.get(id=image['img']), |
||||
) |
||||
|
||||
def create(self, validated_data): |
||||
content = validated_data.pop('content', []) |
||||
lesson = super().create(validated_data) |
||||
self.dispatch_content(lesson, content) |
||||
return lesson |
||||
|
||||
def update(self, instance, validated_data): |
||||
content = validated_data.pop('content', []) |
||||
lesson = super().update(instance, validated_data) |
||||
self.dispatch_content(lesson, content) |
||||
return lesson |
||||
|
||||
|
||||
class LessonSerializer(LessonCreateSerializer): |
||||
course = CourseSerializer() |
||||
cover = ImageObjectSerializer() |
||||
content = ContentSerializer(many=True) |
||||
@ -0,0 +1,152 @@ |
||||
from apps.course.models import Category, Course, Material, Lesson, Like |
||||
|
||||
from apps.content.models import ( |
||||
Content, Image, Text, ImageText, Video, |
||||
Gallery, GalleryImage, ImageObject, |
||||
) |
||||
|
||||
|
||||
class DispatchContentMixin(object): |
||||
|
||||
def dispatch_content(self, course, content): |
||||
for c in content: |
||||
if 'type' not in c or not c['type'] or 'data' not in c or not c['data']: |
||||
continue |
||||
ctype = c['type'] |
||||
cdata = c['data'] |
||||
if ctype == 'text': |
||||
if 'id' in cdata and cdata['id']: |
||||
t = Text.objects.get(id=cdata['id']) |
||||
t.position = cdata['position'] |
||||
t.title = cdata['title'] |
||||
t.course = course |
||||
t.txt = cdata['txt'] |
||||
t.save() |
||||
else: |
||||
t = Text.objects.create( |
||||
position=cdata['position'], |
||||
title=cdata['title'], |
||||
course=course, |
||||
txt=cdata['txt'], |
||||
) |
||||
elif ctype == 'image': |
||||
if 'id' in cdata and cdata['id']: |
||||
image = Image.objects.get(id=cdata['id']) |
||||
image.position = cdata['position'] |
||||
image.title = cdata['title'] |
||||
image.course = course |
||||
image.img = ImageObject.objects.get(id=cdata['img']) |
||||
image.save() |
||||
else: |
||||
image = Image.objects.create( |
||||
position=cdata['position'], |
||||
title=cdata['title'], |
||||
course=course, |
||||
img=ImageObject.objects.get(id=cdata['img']), |
||||
) |
||||
elif ctype == 'image-text': |
||||
if 'id' in cdata and cdata['id']: |
||||
it = ImageText.objects.get(id=cdata['id']) |
||||
it.position = cdata['position'] |
||||
it.title = cdata['title'] |
||||
it.course = course |
||||
it.img = ImageObject.objects.get(id=cdata['img']) |
||||
it.txt = cdata['txt'] |
||||
it.save() |
||||
else: |
||||
it = ImageText.objects.create( |
||||
position=cdata['position'], |
||||
title=cdata['title'], |
||||
course=course, |
||||
img=ImageObject.objects.get(id=cdata['img']), |
||||
txt=cdata['txt'], |
||||
) |
||||
elif ctype == 'video': |
||||
if 'id' in cdata and cdata['id']: |
||||
v = Video.objects.get(id=cdata['id']) |
||||
v.position = cdata['position'] |
||||
v.title = cdata['title'] |
||||
v.course = course |
||||
v.url = cdata['url'] |
||||
v.save() |
||||
else: |
||||
v = Video.objects.create( |
||||
position=cdata['position'], |
||||
title=cdata['title'], |
||||
course=course, |
||||
url=cdata['url'], |
||||
) |
||||
elif ctype == 'images': |
||||
if 'id' in cdata and cdata['id']: |
||||
g = Gallery.objects.get(id=cdata['id']) |
||||
g.course = course |
||||
g.position = cdata['position'] |
||||
g.title = cdata['title'] |
||||
g.save() |
||||
if 'images' in cdata: |
||||
for image in cdata['images']: |
||||
gi = GalleryImage.objects.create( |
||||
gallery=g, |
||||
img=ImageObject.objects.get(id=image['img']) |
||||
) |
||||
else: |
||||
g = Gallery.objects.create( |
||||
course=course, |
||||
position=cdata['position'], |
||||
title=cdata['title'], |
||||
) |
||||
if 'images' in cdata: |
||||
for image in cdata['images']: |
||||
gi = GalleryImage.objects.create( |
||||
gallery=g, |
||||
img=ImageObject.objects.get(id=image['img']), |
||||
) |
||||
|
||||
|
||||
class DispatchMaterialMixin(object): |
||||
|
||||
def dispatch_materials(self, course, materials): |
||||
for material in materials: |
||||
if 'id' in material and material['id']: |
||||
m = Material.objects.get(id=material['id']) |
||||
m.title = material['title'] |
||||
m.cover = ImageObject.objects.get(id=material['cover']) |
||||
m.short_description = material['short_description'] |
||||
m.save() |
||||
else: |
||||
m = Material.objects.create( |
||||
title=material['title'], |
||||
cover=ImageObject.objects.get(id=material['cover']), |
||||
short_description=material['short_description'], |
||||
) |
||||
course.materials.add(m) |
||||
|
||||
|
||||
class DispatchGalleryMixin(object): |
||||
|
||||
def dispatch_gallery(self, course, gallery): |
||||
if gallery: |
||||
if 'id' in gallery and gallery['id']: |
||||
g = Gallery.objects.get(id=gallery['id']) |
||||
g.title = gallery.get('title', g.title) |
||||
g.position = 0 |
||||
g.save() |
||||
else: |
||||
g = Gallery.objects.create( |
||||
title=gallery.get('title', ''), |
||||
position=0, |
||||
) |
||||
if 'images' in gallery: |
||||
for image in gallery['images']: |
||||
if 'id' in image and image['id']: |
||||
gi = GalleryImage.objects.get(id=image['id']) |
||||
gi.gallery = g |
||||
gi.img = image['img'] |
||||
gi.save() |
||||
else: |
||||
gi = GalleryImage.objects.create( |
||||
gallery=g, |
||||
img=image['img'], |
||||
) |
||||
course.gallery = g |
||||
course.save() |
||||
@ -0,0 +1,53 @@ |
||||
from django.contrib.auth import get_user_model |
||||
from rest_framework import serializers |
||||
|
||||
from . import Base64ImageField |
||||
|
||||
|
||||
User = get_user_model() |
||||
|
||||
|
||||
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 UserPhotoSerializer(serializers.Serializer): |
||||
photo = Base64ImageField( |
||||
required=False, allow_empty_file=True, allow_null=True |
||||
) |
||||
@ -0,0 +1,53 @@ |
||||
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 .auth import ObtainToken |
||||
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('api-token-auth/', ObtainToken.as_view(), name='api-token-auth'), |
||||
path('', include((router.urls, 'api-root')), name='api-root'), |
||||
] |
||||
@ -0,0 +1,243 @@ |
||||
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.course import ( |
||||
CategorySerializer, LikeSerializer, |
||||
CourseSerializer, CourseCreateSerializer, |
||||
MaterialSerializer, MaterialCreateSerializer, |
||||
LessonSerializer, LessonCreateSerializer, |
||||
) |
||||
from .serializers.content import ( |
||||
ImageSerializer, ImageCreateSerializer, |
||||
TextSerializer, TextCreateSerializer, |
||||
ImageTextSerializer, ImageTextCreateSerializer, |
||||
VideoSerializer, VideoCreateSerializer, |
||||
GallerySerializer, |
||||
GalleryImageSerializer, GalleryImageCreateSerializer, |
||||
ImageObjectSerializer, |
||||
) |
||||
from .serializers.user import ( |
||||
UserSerializer, UserPhotoSerializer, |
||||
) |
||||
|
||||
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 = MaterialCreateSerializer |
||||
serializer_class_map = { |
||||
'list': MaterialSerializer, |
||||
'retrieve': 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', 'cover', 'gallery', |
||||
).prefetch_related( |
||||
'likes', 'materials', 'content', |
||||
).all() |
||||
serializer_class = CourseCreateSerializer |
||||
serializer_class_map = { |
||||
'list': CourseSerializer, |
||||
'retrieve': CourseSerializer, |
||||
} |
||||
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, |
||||
# } |
||||
|
||||
|
||||
class LessonViewSet(ExtendedModelViewSet): |
||||
queryset = Lesson.objects.select_related( |
||||
'course', 'cover' |
||||
).prefetch_related('content').all() |
||||
serializer_class = LessonCreateSerializer |
||||
serializer_class_map = { |
||||
'list': LessonSerializer, |
||||
'retrieve': LessonSerializer, |
||||
} |
||||
filter_fields = ('course',) |
||||
search_fields = ('title', 'short_description',) |
||||
ordering_fields = ('title', 'created_at', 'update_at',) |
||||
# permission_classes = (IsAuthorObjectOrAdmin,) |
||||
# permission_map = { |
||||
# 'create': IsAuthorOrAdmin, |
||||
# 'delete': IsAdmin, |
||||
# } |
||||
|
||||
|
||||
class ImageViewSet(ExtendedModelViewSet): |
||||
queryset = Image.objects.select_related( |
||||
'course', 'lesson', 'img', |
||||
).all() |
||||
serializer_class = ImageCreateSerializer |
||||
serializer_class_map = { |
||||
'list': ImageSerializer, |
||||
'retrieve': ImageSerializer, |
||||
} |
||||
search_fields = ('title',) |
||||
ordering_fields = ('title', 'created_at', 'update_at', 'position',) |
||||
# permission_classes = (IsAuthorOrAdmin,) |
||||
# permission_map = { |
||||
# 'delete': IsAdmin, |
||||
# } |
||||
|
||||
|
||||
class TextViewSet(ExtendedModelViewSet): |
||||
queryset = Text.objects.select_related( |
||||
'course', 'lesson' |
||||
).all() |
||||
serializer_class = TextCreateSerializer |
||||
serializer_class_map = { |
||||
'list': TextSerializer, |
||||
'retrieve': 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', 'img' |
||||
).all() |
||||
serializer_class = ImageTextCreateSerializer |
||||
serializer_class_map = { |
||||
'list': ImageTextSerializer, |
||||
'retrieve': ImageTextSerializer, |
||||
} |
||||
search_fields = ('title',) |
||||
ordering_fields = ('title', 'created_at', 'update_at', 'position',) |
||||
# permission_classes = (IsAuthorOrAdmin,) |
||||
# permission_map = { |
||||
# 'delete': IsAdmin, |
||||
# } |
||||
|
||||
|
||||
class VideoViewSet(ExtendedModelViewSet): |
||||
queryset = Video.objects.select_related( |
||||
'course', 'lesson' |
||||
).all() |
||||
serializer_class = VideoCreateSerializer |
||||
serializer_class_map = { |
||||
'list': VideoSerializer, |
||||
'retrieve': 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.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', 'img', |
||||
).all() |
||||
serializer_class = GalleryImageCreateSerializer |
||||
search_fields = ('gallery__title',) |
||||
serializer_class_map = { |
||||
'list': GalleryImageSerializer, |
||||
'retrieve': GalleryImageSerializer, |
||||
} |
||||
# permission_classes = (IsAuthorOrAdmin,) |
||||
# permission_map = { |
||||
# 'delete': IsAdmin, |
||||
# } |
||||
|
||||
|
||||
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, |
||||
# } |
||||
|
||||
@list_route(methods=['get']) |
||||
def me(self, request): |
||||
serializer = self.get_serializer_class() |
||||
serialized_data = serializer(instance=request.user) |
||||
return Response(serialized_data.data) |
||||
|
||||
@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,72 @@ |
||||
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 |
||||
show_in_index = True |
||||
base_fieldsets = ( |
||||
(None, {'fields': ('course', 'lesson', 'title', 'position',)}), |
||||
) |
||||
|
||||
|
||||
@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(Gallery) |
||||
class GalleryAdmin(ContentChildAdmin): |
||||
base_model = Gallery |
||||
|
||||
|
||||
@admin.register(Content) |
||||
class ContentAdmin(PolymorphicParentModelAdmin): |
||||
base_model = Content |
||||
polymorphic_list = True |
||||
child_models = ( |
||||
Image, |
||||
Text, |
||||
ImageText, |
||||
Video, |
||||
# GalleryAdmin, |
||||
) |
||||
|
||||
|
||||
@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,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='Объект изображения'), |
||||
), |
||||
] |
||||
@ -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='Объект изображения'), |
||||
), |
||||
] |
||||
@ -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='Объект изображения'), |
||||
), |
||||
] |
||||
@ -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', |
||||
), |
||||
] |
||||
@ -0,0 +1,18 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-09 15:49 |
||||
|
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('content', '0010_remove_gallery_course'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='content', |
||||
name='position', |
||||
field=models.PositiveSmallIntegerField(default=1, verbose_name='Положение на странице'), |
||||
), |
||||
] |
||||
@ -0,0 +1,19 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-09 18:47 |
||||
|
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('content', '0011_auto_20180209_1549'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='galleryimage', |
||||
name='gallery', |
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gallery_images', to='content.Gallery', verbose_name='Галерея'), |
||||
), |
||||
] |
||||
@ -0,0 +1,40 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-12 05:37 |
||||
|
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('content', '0012_auto_20180209_1847'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterModelOptions( |
||||
name='gallery', |
||||
options={'base_manager_name': 'objects'}, |
||||
), |
||||
migrations.RemoveField( |
||||
model_name='gallery', |
||||
name='created_at', |
||||
), |
||||
migrations.RemoveField( |
||||
model_name='gallery', |
||||
name='id', |
||||
), |
||||
migrations.RemoveField( |
||||
model_name='gallery', |
||||
name='title', |
||||
), |
||||
migrations.RemoveField( |
||||
model_name='gallery', |
||||
name='update_at', |
||||
), |
||||
migrations.AddField( |
||||
model_name='gallery', |
||||
name='content_ptr', |
||||
field=models.OneToOneField(auto_created=True, default=1, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='content.Content'), |
||||
preserve_default=False, |
||||
), |
||||
] |
||||
@ -0,0 +1,96 @@ |
||||
from django.db import models |
||||
|
||||
from polymorphic.models import PolymorphicModel |
||||
|
||||
|
||||
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.Course', on_delete=models.CASCADE, |
||||
null=True, blank=True, |
||||
verbose_name='Курс', |
||||
related_name='content', |
||||
) |
||||
lesson = models.ForeignKey( |
||||
'course.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, |
||||
) |
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True) |
||||
update_at = models.DateTimeField(auto_now=True) |
||||
|
||||
class Meta: |
||||
verbose_name = 'Контент' |
||||
verbose_name_plural = 'Контент' |
||||
ordering = ('position', '-created_at',) |
||||
|
||||
def ctype(self): |
||||
return self.__class__.__name__.lower() |
||||
|
||||
|
||||
class Image(Content): |
||||
img = models.ForeignKey( |
||||
ImageObject, related_name='content_images', |
||||
verbose_name='Объект изображения', on_delete=models.CASCADE, |
||||
) |
||||
|
||||
|
||||
class Text(Content): |
||||
txt = models.TextField('Текст', default='') |
||||
|
||||
|
||||
class ImageText(Content): |
||||
img = models.ForeignKey( |
||||
ImageObject, related_name='content_imagetexts', |
||||
verbose_name='Объект изображения', on_delete=models.CASCADE, |
||||
) |
||||
txt = models.TextField('Текст', default='') |
||||
|
||||
|
||||
class Video(Content): |
||||
url = models.URLField('Ссылка') |
||||
|
||||
def video_index(self): |
||||
return self.url.split('/')[-1] |
||||
|
||||
|
||||
class Gallery(Content): |
||||
pass |
||||
|
||||
|
||||
class GalleryImage(models.Model): |
||||
gallery = models.ForeignKey( |
||||
Gallery, on_delete=models.CASCADE, |
||||
verbose_name='Галерея', related_name='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) |
||||
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 |
||||
|
||||
@ -1,35 +0,0 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-05 15:59 |
||||
|
||||
from django.db import migrations |
||||
from django.contrib.contenttypes.models import ContentType |
||||
|
||||
|
||||
def fwrd_func(apps, schema_editor): |
||||
CourseComment = apps.get_model('course', 'CourseComment') |
||||
LessonComment = apps.get_model('course', 'LessonComment') |
||||
db_alias = schema_editor.connection.alias |
||||
if CourseComment.objects.exists(): |
||||
coursecomment_content_type = ContentType.objects.get( |
||||
app_label='course', model='coursecomment', |
||||
) |
||||
CourseComment.objects.using(db_alias).all().update( |
||||
polymorphic_ctype=coursecomment_content_type, |
||||
) |
||||
if LessonComment.objects.exists(): |
||||
lessoncomment_content_type = ContentType.objects.get( |
||||
app_label='course', model='lessoncomment', |
||||
) |
||||
LessonComment.objects.using(db_alias).all().update( |
||||
polymorphic_ctype=lessoncomment_content_type, |
||||
) |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('course', '0020_auto_20180202_1716'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.RunPython(fwrd_func) |
||||
] |
||||
@ -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,35 +0,0 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-05 16:15 |
||||
|
||||
from django.db import migrations |
||||
from django.contrib.contenttypes.models import ContentType |
||||
|
||||
|
||||
def fwrd_func(apps, schema_editor): |
||||
CourseComment = apps.get_model('course', 'CourseComment') |
||||
LessonComment = apps.get_model('course', 'LessonComment') |
||||
db_alias = schema_editor.connection.alias |
||||
if CourseComment.objects.using(db_alias).all().exists(): |
||||
coursecomment_content_type = ContentType.objects.get( |
||||
app_label='course', model='coursecomment', |
||||
) |
||||
CourseComment.objects.using(db_alias).all().update( |
||||
polymorphic_ctype=coursecomment_content_type, |
||||
) |
||||
if LessonComment.objects.using(db_alias).all().exists(): |
||||
lessoncomment_content_type = ContentType.objects.get( |
||||
app_label='course', model='lessoncomment', |
||||
) |
||||
LessonComment.objects.using(db_alias).all().update( |
||||
polymorphic_ctype=lessoncomment_content_type, |
||||
) |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('course', '0021_auto_20180205_1559'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.RunPython(fwrd_func) |
||||
] |
||||
@ -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='Обложка курса'), |
||||
), |
||||
] |
||||
@ -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='Обложка курса'), |
||||
), |
||||
] |
||||
@ -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='Обложка урока'), |
||||
), |
||||
] |
||||
@ -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='Галерея работ'), |
||||
), |
||||
] |
||||
@ -0,0 +1,24 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-08 10:53 |
||||
|
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('course', '0025_course_gallery'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='course', |
||||
name='price', |
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Если цена не выставлена, то курс бесплатный', max_digits=10, null=True, verbose_name='Цена курса'), |
||||
), |
||||
migrations.AlterField( |
||||
model_name='material', |
||||
name='cover', |
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='material_covers', to='content.ImageObject', verbose_name='Обложка материала'), |
||||
), |
||||
] |
||||
@ -0,0 +1,17 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-09 08:16 |
||||
|
||||
from django.db import migrations |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('course', '0026_auto_20180208_1053'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.RemoveField( |
||||
model_name='course', |
||||
name='url', |
||||
), |
||||
] |
||||
@ -0,0 +1,19 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-09 08:59 |
||||
from uuid import uuid4 |
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('course', '0027_remove_course_url'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AddField( |
||||
model_name='course', |
||||
name='slug', |
||||
field=models.SlugField(allow_unicode=True, default=str(uuid4()), max_length=100, unique=True), |
||||
preserve_default=False, |
||||
), |
||||
] |
||||
@ -0,0 +1,19 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-09 09:11 |
||||
|
||||
import apps.course.models |
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('course', '0028_course_slug'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='course', |
||||
name='slug', |
||||
field=models.SlugField(allow_unicode=True, default=apps.course.models.default_slug, max_length=100, unique=True), |
||||
), |
||||
] |
||||
@ -0,0 +1,19 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-12 05:37 |
||||
|
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('course', '0029_auto_20180209_0911'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='course', |
||||
name='gallery', |
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='results_gallery', to='content.Gallery', verbose_name='Галерея работ'), |
||||
), |
||||
] |
||||
@ -0,0 +1,23 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-13 09:06 |
||||
|
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('course', '0030_auto_20180212_0537'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AddField( |
||||
model_name='course', |
||||
name='deactivated_at', |
||||
field=models.DateTimeField(blank=True, default=None, null=True), |
||||
), |
||||
migrations.AddField( |
||||
model_name='lesson', |
||||
name='deactivated_at', |
||||
field=models.DateTimeField(blank=True, default=None, null=True), |
||||
), |
||||
] |
||||
@ -0,0 +1,19 @@ |
||||
{% if results %} |
||||
<div class="title">Галерея итогов обучения</div> |
||||
<div class="examples"> |
||||
{% for image in course.gallery.gallery_images.all %} |
||||
<div class="examples__item"> |
||||
<img class="examples__pic" src="{{ image.img.image.url }}"> |
||||
</div> |
||||
{% endfor %} |
||||
</div> |
||||
{% else %} |
||||
<div class="content-block title">{{ content.title }}</div> |
||||
<div class="examples"> |
||||
{% for image in content.gallery_images.all %} |
||||
<div class="examples__item"> |
||||
<img class="examples__pic" src="{{ image.img.image.url }}"> |
||||
</div> |
||||
{% endfor %} |
||||
</div> |
||||
{% endif %} |
||||
@ -0,0 +1,6 @@ |
||||
<div class="content-block title"> |
||||
{{ content.title }} |
||||
</div> |
||||
<div> |
||||
<img class="content-block pic" src="{{ content.img.image.url }}" alt=""> |
||||
</div> |
||||
@ -0,0 +1,9 @@ |
||||
<div class="content-block title"> |
||||
{{ content.title }} |
||||
</div> |
||||
<div class="content-block text"> |
||||
{{ content.txt }} |
||||
</div> |
||||
<div> |
||||
<img class="content-block pic" src="{{ content.img.image.url }}" alt=""> |
||||
</div> |
||||
@ -0,0 +1,6 @@ |
||||
<div class="content-block title"> |
||||
{{ content.title }} |
||||
</div> |
||||
<div class="content-block text"> |
||||
{{ content.txt }} |
||||
</div> |
||||
@ -0,0 +1,13 @@ |
||||
<div class="content-block title"> |
||||
{{ content.title }} |
||||
</div> |
||||
<div> |
||||
{% if 'youtube.com' in content.url or 'youtu.be' in content.url %} |
||||
<iframe width="640" height="360" src="https://www.youtube.com/embed/{{ content.video_index }}" frameborder="0" allow="autoplay; encrypted-media" |
||||
allowfullscreen></iframe> |
||||
{% elif 'vimeo.com' in content.url %} |
||||
<iframe src="https://player.vimeo.com/video/{{ content.video_index }}" width="640" height="360" frameborder="0" webkitallowfullscreen |
||||
mozallowfullscreen allowfullscreen> |
||||
</iframe> |
||||
{% endif %} |
||||
</div> |
||||
@ -0,0 +1,12 @@ |
||||
{% extends "templates/lilcity/index.html" %} |
||||
{% load static %} |
||||
{% block content %} |
||||
<course-redactor author-picture="{% if request.user.photo %}{{ request.user.photo.url }}{% else %}{% static 'img/user.jpg' %}{% endif %}" |
||||
author-name="{{ request.user.first_name }} {{ request.user.last_name }}" |
||||
access-token="{{ request.user.auth_token }}" |
||||
{% if course and course.id %}:course-id="{{ course.id }}"{% endif %}></course-redactor> |
||||
{% endblock content %} |
||||
{% block foot %} |
||||
<script type="text/javascript" src={% static "courseRedactor.js" %}></script> |
||||
<link rel="stylesheet" href={% static "courseRedactor.css" %}/> |
||||
{% endblock foot %} |
||||
@ -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,17 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-12 07:50 |
||||
|
||||
from django.db import migrations |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('user', '0007_auto_20180207_0808'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterModelOptions( |
||||
name='user', |
||||
options={'ordering': ('-date_joined',), 'verbose_name': 'user', 'verbose_name_plural': 'users'}, |
||||
), |
||||
] |
||||
@ -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,38 @@ |
||||
from django.db import models |
||||
from django.utils import timezone |
||||
|
||||
|
||||
class BaseModel(models.Model): |
||||
|
||||
class Meta: |
||||
abstract = True |
||||
|
||||
|
||||
class DeactivatedQueryset(models.Manager): |
||||
|
||||
def allow_delete(self): |
||||
super().delete() |
||||
|
||||
def delete(self): |
||||
count = self.count() |
||||
self.filter(deactivated_at__isnull=True).update( |
||||
deactivated_at=timezone.now() |
||||
) |
||||
return (count, None) |
||||
|
||||
|
||||
class DeactivatedMixin(models.Model): |
||||
deactivated_at = models.DateTimeField(null=True, blank=True, default=None) |
||||
|
||||
objects = DeactivatedQueryset() |
||||
|
||||
class Meta: |
||||
abstract = True |
||||
|
||||
def allow_delete(self, using=None, keep_parents=False): |
||||
super().delete(using=using, keep_parents=keep_parents) |
||||
|
||||
def delete(self, using=None, keep_parents=False): |
||||
if not self.deactivated_at: |
||||
self.deactivated_at = timezone.now() |
||||
self.save() |
||||
@ -0,0 +1,11 @@ |
||||
{% extends "templates/lilcity/index.html" %} {% load static %} |
||||
|
||||
{% block title %}School LIL.CITY{% endblock title %} |
||||
{% block content %} |
||||
{{ request.user.auth_token }} |
||||
<course-redactor author-picture="http://localhost:8000/static/img/user.jpg" author-name="Vitaly Baev" access-token="1fac76972542b0f6492076e45e4b8cb39b5c422a" :course-id="71"></course-redactor> |
||||
{% endblock content %} |
||||
{% block foot %} |
||||
<script type="text/javascript" src={% static "courseRedactor.js" %}></script> |
||||
<link rel="stylesheet" href={% static "courseRedactor.css" %}></link> |
||||
{% endblock foot %} |
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 793 B |
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,582 @@ |
||||
<template> |
||||
<div> |
||||
<div v-if="!courseLoading"> |
||||
<form v-if="viewSection !== 'lessons-edit'" @submit.prevent="onSubmit"> |
||||
<div class="info"> |
||||
<div class="info__section" :style="coverBackgroundStyle"> |
||||
<div class="info__main"> |
||||
<div class="info__head"> |
||||
<div class="info__user"> |
||||
<div class="info__ava ava"> |
||||
<img :src="authorPicture" alt="Аватар" class="ava__pic"> |
||||
</div> |
||||
<div class="info__group"> |
||||
<div class="info__label">АВТОР</div> |
||||
<div class="info__value">{{ authorName }}</div> |
||||
</div> |
||||
</div> |
||||
<div class="info__upload upload"> |
||||
Загрузить фон |
||||
<input type="file" class="upload__file" @change="onCoverImageSelected"> |
||||
</div> |
||||
</div> |
||||
<div class="info__title"> |
||||
<div class="info__field field field_info"> |
||||
<div class="field__label">НАЗВАНИЕ КУРСА</div> |
||||
<div class="field__wrap"> |
||||
<textarea class="field__textarea field__textarea_lg" title="Название курса" |
||||
v-model="course.title"></textarea> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="info__foot"> |
||||
<div class="info__field field field_info"> |
||||
<div class="field__label field__label_gray">КАТЕГОРИЯ</div> |
||||
<div class="field__wrap"> |
||||
<lil-select :value.sync="categorySelect" :options="categoryOptions" |
||||
placeholder="Выберите категорию"/> |
||||
</div> |
||||
</div> |
||||
<div class="info__field field field_info"> |
||||
<div class="field__label field__label_gray">ПРОДОЛЖИТЕЛЬНОСТЬ</div> |
||||
<div class="field__wrap"> |
||||
<input type="text" class="field__input" v-model.number="course.duration"> |
||||
</div> |
||||
</div> |
||||
<div class="info__field field field_info"> |
||||
<div class="field__label field__label_gray">СТОИМОСТЬ</div> |
||||
<div class="field__wrap"> |
||||
<input type="text" class="field__input" v-model="displayPrice" :disabled="!course.is_paid"> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="info__sidebar"> |
||||
<div class="info__wrap"> |
||||
<div class="info__fieldset"> |
||||
<!--<div class="info__field field"> |
||||
<div class="field__label field__label_gray">ССЫЛКА</div> |
||||
<div class="field__wrap"> |
||||
<input type="text" class="field__input" v-model="course.url"> |
||||
</div> |
||||
<div class="field__wrap field__wrap--additional">{{ courseFullUrl }}</div> |
||||
</div>--> |
||||
<div class="info__field field"> |
||||
<div class="field__label field__label_gray">ДОСТУП</div> |
||||
<div class="field__wrap"> |
||||
<label class="field__switch switch switch_lg switch_circle"> |
||||
<input type="radio" :value="true" class="switch__input" v-model="course.is_paid"> |
||||
<span class="switch__content">Платный</span> |
||||
</label> |
||||
<label class="field__switch switch switch_lg switch_circle"> |
||||
<input type="radio" :value="false" class="switch__input" v-model="course.is_paid"> |
||||
<span class="switch__content">Бесплатный</span> |
||||
</label> |
||||
</div> |
||||
</div> |
||||
<label class="info__switch switch switch_lg"> |
||||
<input type="checkbox" class="switch__input" v-model="course.is_featured"> |
||||
<span class="switch__content">Выделить</span> |
||||
</label> |
||||
</div> |
||||
<div class="info__fieldset"> |
||||
<div class="info__field field"> |
||||
<div class="field__label field__label_gray">ЗАПУСК</div> |
||||
<div class="field__wrap"> |
||||
<label class="field__switch switch switch_lg switch_circle"> |
||||
<input type="radio" :value="false" class="switch__input" v-model="course.is_deferred"> |
||||
<span class="switch__content">Мгновенный</span> |
||||
</label> |
||||
<label class="field__switch switch switch_lg switch_circle"> |
||||
<input type="radio" :value="true" class="switch__input" v-model="course.is_deferred"> |
||||
<span class="switch__content">Отложенный</span> |
||||
</label> |
||||
</div> |
||||
</div> |
||||
<div class="info__field field" v-show="course.is_deferred"> |
||||
<div class="field__label">ДАТА</div> |
||||
<div class="field__wrap"> |
||||
<vue-datepicker input-class="field__input" v-model="course.date" language="ru" format="dd/MM/yyyy"/> |
||||
</div> |
||||
</div> |
||||
<div class="info__field field" v-show="course.is_deferred"> |
||||
<div class="field__label">ВРЕМЯ</div> |
||||
<div class="field__wrap"> |
||||
<lil-select :value.sync="course.time" :options="timeOptions" placeholder="Выберите время"/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="section"> |
||||
<div class="section__center center"> |
||||
<div class="kit"> |
||||
<div class="kit__section"> |
||||
<div class="kit__field field"> |
||||
<div class="field__wrap"> |
||||
<input type="text" class="field__input" placeholder="Кратко о курсе" |
||||
v-model="course.short_description"> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="kit__nav"> |
||||
<button class="kit__btn btn btn_lg" |
||||
v-bind:class="{ 'btn_stroke': viewSection === 'course', 'btn_gray': viewSection !== 'course' }" |
||||
type="button" @click="viewSection = 'course'">Описание |
||||
курса |
||||
</button> |
||||
<button class="kit__btn btn btn_lg" |
||||
v-bind:class="{ 'btn_stroke': viewSection === 'lessons', 'btn_gray': viewSection !== 'lessons' }" |
||||
type="button" |
||||
@click="showLessons" |
||||
:disabled="!course.id"> |
||||
Уроки |
||||
</button> |
||||
</div> |
||||
<div v-if="viewSection === 'course'" class="kit__body"> |
||||
<div v-for="(block, index) in course.content"> |
||||
<block-text v-if="block.type === 'text'" |
||||
:index="index" |
||||
:title.sync="block.data.title" |
||||
:text.sync="block.data.text" |
||||
v-on:remove="onBlockRemoved" |
||||
:access-token="accessToken"/> |
||||
<block-image-text v-if="block.type === 'image-text'" |
||||
:index="index" |
||||
:title.sync="block.data.title" |
||||
:text.sync="block.data.text" |
||||
:image-id.sync="block.data.image_id" |
||||
:image-url.sync="block.data.image_url" |
||||
v-on:remove="onBlockRemoved" |
||||
:access-token="accessToken"/> |
||||
<block-image v-if="block.type === 'image'" |
||||
:index="index" |
||||
:title.sync="block.data.title" |
||||
:image-id.sync="block.data.image_id" |
||||
:image-url.sync="block.data.image_url" |
||||
v-on:remove="onBlockRemoved" |
||||
:access-token="accessToken"/> |
||||
<block-images v-if="block.type === 'images'" |
||||
:index="index" |
||||
:title.sync="block.data.title" |
||||
:text.sync="block.data.text" |
||||
:images.sync="block.data.images" |
||||
v-on:remove="onBlockRemoved" |
||||
:access-token="accessToken"/> |
||||
<block-video v-if="block.type === 'video'" |
||||
:index="index" |
||||
:title.sync="block.data.title" |
||||
v-on:remove="onBlockRemoved" |
||||
:video-url.sync="block.data.video_url"/> |
||||
</div> |
||||
|
||||
<block-add v-on:added="onBlockAdded"/> |
||||
|
||||
<div class="kit__foot"> |
||||
<button type="submit" class="kit__submit btn btn_md" v-bind:class="{ loading: courseSaving }"> |
||||
Сохранить |
||||
</button> |
||||
</div> |
||||
</div> |
||||
<div v-if="viewSection === 'lessons'" class="kit__body"> |
||||
<div class="lessons__title title">Содержание курса</div> |
||||
<div v-if="!lessonsLoading" class="lessons__list"> |
||||
<div class="lessons__item" v-for="(lesson, index) in lessons"> |
||||
<div class="lessons__actions"> |
||||
<button type="button" class="lessons__action" @click="removeLesson(index)"> |
||||
<svg class="icon icon-delete"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-delete"></use> |
||||
</svg> |
||||
</button> |
||||
<button type="button" class="lessons__action" @click="editLesson(index)"> |
||||
<svg class="icon icon-edit"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-edit"></use> |
||||
</svg> |
||||
</button> |
||||
</div> |
||||
<div class="lessons__subtitle subtitle">{{ lesson.title }}</div> |
||||
<div class="lessons__row"> |
||||
<div class="lessons__content">{{ lesson.short_description }}</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div v-if="lessonsLoading">Загрузка...</div> |
||||
<div class="lessons__foot"> |
||||
<button type="button" class="lessons__btn btn btn_md" @click="addLesson">СОЗДАТЬ УРОК</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</form> |
||||
<form v-if="viewSection === 'lessons-edit'" @submit.prevent="onLessonSubmit"> |
||||
<lesson-redactor :lesson.sync="currentLesson" :saving="lessonSaving" :access-token="accessToken" |
||||
v-on:back="goToLessons"/> |
||||
</form> |
||||
</div> |
||||
<div v-else> |
||||
<div class="section"> |
||||
<div class="section__center center"> |
||||
Загрузка... |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import LinkInput from './inputs/LinkInput' |
||||
import DatePicker from 'vuejs-datepicker' |
||||
import BlockText from './blocks/BlockText' |
||||
import BlockImage from './blocks/BlockImage' |
||||
import BlockImages from './blocks/BlockImages' |
||||
import BlockImageText from './blocks/BlockImageText' |
||||
import BlockVideo from './blocks/BlockVideo' |
||||
import LilSelect from "./inputs/LilSelect"; |
||||
import LessonRedactor from "./LessonRedactor"; |
||||
import {api} from "../js/modules/api"; |
||||
import BlockAdd from "./blocks/BlockAdd"; |
||||
|
||||
export default { |
||||
name: "course-redactor", |
||||
props: ["authorName", "authorPicture", "accessToken", "courseId"], |
||||
data() { |
||||
return { |
||||
viewSection: 'course', |
||||
course: { |
||||
title: '', |
||||
category: null, |
||||
categorySelect: null, |
||||
duration: 1, |
||||
price: 0, |
||||
url: '', |
||||
coverImage: '', |
||||
coverImageId: null, |
||||
is_paid: false, |
||||
is_featured: true, |
||||
is_deferred: false, |
||||
date: '', |
||||
time: null, |
||||
short_description: '', |
||||
content: [], |
||||
gallery: { |
||||
images: [], |
||||
} |
||||
}, |
||||
courseLoading: false, |
||||
courseSaving: false, |
||||
lessons: [], |
||||
lessonsLoading: false, |
||||
lessonSaving: false, |
||||
currentLesson: null, |
||||
is_adding_block: false, |
||||
timeOptions: [ |
||||
{ |
||||
'title': '10:00', |
||||
'value': '10:00', |
||||
}, |
||||
{ |
||||
'title': '11:00', |
||||
'value': '11:00', |
||||
}, |
||||
{ |
||||
'title': '12:00', |
||||
'value': '12:00', |
||||
}, |
||||
{ |
||||
'title': '13:00', |
||||
'value': '13:00', |
||||
}, |
||||
{ |
||||
'title': '14:00', |
||||
'value': '14:00', |
||||
}, |
||||
{ |
||||
'title': '15:00', |
||||
'value': '15:00', |
||||
}, |
||||
{ |
||||
'title': '16:00', |
||||
'value': '16:00', |
||||
}, |
||||
{ |
||||
'title': '17:00', |
||||
'value': '17:00', |
||||
}, |
||||
{ |
||||
'title': '18:00', |
||||
'value': '18:00', |
||||
} |
||||
], |
||||
categoryOptions: [] |
||||
} |
||||
}, |
||||
methods: { |
||||
onCoverImageSelected(event) { |
||||
let file = event.target.files[0]; |
||||
let reader = new FileReader(); |
||||
reader.onload = () => { |
||||
this.$set(this.course, 'coverImage', reader.result); |
||||
api.uploadImage(reader.result, this.accessToken) |
||||
.then((response) => { |
||||
this.course.coverImageId = response.data.id; |
||||
}) |
||||
.catch((error) => { |
||||
console.log('error', error); |
||||
}); |
||||
}; |
||||
if (file) { |
||||
reader.readAsDataURL(file); |
||||
} |
||||
}, |
||||
onCoursePriceChange(event) { |
||||
this.course.price = event.target.value; |
||||
}, |
||||
|
||||
updateCategory() { |
||||
if (this.categoryOptions && Array.isArray(this.categoryOptions) && this.course.category) { |
||||
this.categoryOptions.forEach((category) => { |
||||
if (category.id === this.course.category) { |
||||
this.course.categorySelect = category; |
||||
} |
||||
}); |
||||
} |
||||
}, |
||||
onBlockRemoved(blockIndex) { |
||||
const blockToRemove = this.course.content[blockIndex]; |
||||
// Удаляем блок из Vue |
||||
this.course.content.splice(blockIndex, 1); |
||||
// Если блок уже был записан в БД, отправляем запрос на сервер на удаление блока из БД |
||||
if (blockToRemove.data.id) { |
||||
api.removeContentBlock(blockToRemove, this.accessToken); |
||||
} |
||||
}, |
||||
onBlockAdded(blockData) { |
||||
this.course.content.push(blockData); |
||||
}, |
||||
removeLesson(lessonIndex) { |
||||
if (!confirm('Вы действительно хотите удалить этот урок?')) { |
||||
return |
||||
} |
||||
const lesson = this.lessons[lessonIndex]; |
||||
if (lesson.hasOwnProperty('id') && lesson.id) { |
||||
api.removeCourseLesson(lesson.id, this.accessToken); |
||||
} |
||||
this.lessons.splice(lessonIndex, 1); |
||||
}, |
||||
editLesson(lessonIndex) { |
||||
this.currentLesson = this.lessons[lessonIndex]; |
||||
this.viewSection = 'lessons-edit'; |
||||
}, |
||||
showLessons() { |
||||
this.viewSection = 'lessons'; |
||||
}, |
||||
addLesson() { |
||||
this.currentLesson = { |
||||
title: '', |
||||
short_description: '', |
||||
course_id: this.course.id, |
||||
content: [], |
||||
}; |
||||
this.viewSection = 'lessons-edit'; |
||||
}, |
||||
onSubmit() { |
||||
this.courseSaving = true; |
||||
api.saveCourse(this.course, this.accessToken) |
||||
.then((response) => { |
||||
this.courseSaving = false; |
||||
this.course = api.convertCourseJson(response.data); |
||||
}) |
||||
.catch((err) => { |
||||
this.courseSaving = false; |
||||
}); |
||||
}, |
||||
onLessonSubmit() { |
||||
this.lessonSaving = true; |
||||
const currentLessonId = this.currentLesson.id; |
||||
api.saveLesson(this.currentLesson, this.accessToken) |
||||
.then((response) => { |
||||
this.lessonSaving = false; |
||||
const newLesson = api.convertLessonJson(response.data); |
||||
newLesson.course_id = this.course.id; |
||||
this.currentLesson = newLesson; |
||||
if (!currentLessonId) { |
||||
this.lessons.push(newLesson); |
||||
} |
||||
if (this.lessons && Array.isArray(this.lessons)) { |
||||
this.lessons.forEach((lesson, index) => { |
||||
if (newLesson.id === lesson.id) { |
||||
this.$set('lessons', index, newLesson); |
||||
} |
||||
}); |
||||
} |
||||
}) |
||||
.catch((err) => { |
||||
this.lessonSaving = false; |
||||
}); |
||||
}, |
||||
goToLessons() { |
||||
this.viewSection = 'lessons'; |
||||
}, |
||||
loadCourse(courseId) { |
||||
this.courseLoading = true; |
||||
api.loadCourse(courseId, this.accessToken) |
||||
.then((response) => { |
||||
this.courseLoading = false; |
||||
this.course = api.convertCourseJson(response.data); |
||||
}) |
||||
.catch((err) => { |
||||
this.courseLoading = false; |
||||
console.log('error course loading', err); |
||||
}); |
||||
}, |
||||
loadLessons(courseId) { |
||||
api.getCourseLessons(courseId, this.accessToken) |
||||
.then((response) => { |
||||
this.lessons = response.data.results.map((lessonJson) => { |
||||
return api.convertLessonJson(lessonJson); |
||||
}); |
||||
}) |
||||
.catch((err) => { |
||||
console.log('error course loading', err); |
||||
}); |
||||
} |
||||
}, |
||||
mounted() { |
||||
api.getCategories(this.accessToken) |
||||
.then((response) => { |
||||
if (response.data) { |
||||
this.categoryOptions = response.data.results.map((category) => { |
||||
return { |
||||
title: category.title, |
||||
value: category.id |
||||
} |
||||
}); |
||||
} |
||||
this.updateCategory(); |
||||
}); |
||||
|
||||
if (this.courseId) { |
||||
this.loadCourse(this.courseId); |
||||
this.loadLessons(this.courseId); |
||||
} |
||||
}, |
||||
computed: { |
||||
coverBackgroundStyle() { |
||||
return this.course.coverImage ? `background-image: url(${this.course.coverImage});` : ''; |
||||
}, |
||||
displayPrice: { |
||||
get: function () { |
||||
return this.course.is_paid ? this.course.price : 0; |
||||
}, |
||||
set: function (value) { |
||||
this.course.price = value; |
||||
} |
||||
}, |
||||
categorySelect: { |
||||
get() { |
||||
if (!this.categoryOptions || this.categoryOptions.length === 0 || !this.course || !this.course.category) { |
||||
return null; |
||||
} |
||||
let value; |
||||
this.categoryOptions.forEach((category) => { |
||||
if (category.value === this.course.category) { |
||||
value = category; |
||||
} |
||||
}); |
||||
return value; |
||||
}, |
||||
set(value) { |
||||
this.course.category = value.value; |
||||
} |
||||
}, |
||||
courseFullUrl() { |
||||
let suffix = this.course.url ? this.course.url : 'ваша_ссылка'; |
||||
return `https://lil.city/course/${suffix}`; |
||||
} |
||||
}, |
||||
components: { |
||||
BlockAdd, |
||||
LessonRedactor, |
||||
LilSelect, |
||||
BlockText, |
||||
'link-input': LinkInput, |
||||
'vue-datepicker': DatePicker, |
||||
'block-text': BlockText, |
||||
'block-image': BlockImage, |
||||
'block-image-text': BlockImageText, |
||||
'block-images': BlockImages, |
||||
'block-video': BlockVideo, |
||||
'lesson-redactor': LessonRedactor, |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
.vdp-datepicker__calendar { |
||||
width: 240px; |
||||
margin-top: 10px; |
||||
padding: 5px; |
||||
background: white; |
||||
box-shadow: 0 2px 20px 0 rgba(0, 0, 0, 0.1); |
||||
z-index: 99 !important; |
||||
|
||||
header { |
||||
display: flex; |
||||
margin-bottom: 5px; |
||||
-ms-flex-align: center; |
||||
align-items: center; |
||||
} |
||||
|
||||
.prev, .next { |
||||
font-size: 0; |
||||
cursor: pointer; |
||||
order: 1; |
||||
width: auto !important; |
||||
padding: 10px; |
||||
} |
||||
|
||||
.prev { |
||||
order: 1; |
||||
} |
||||
|
||||
.next { |
||||
order: 3; |
||||
} |
||||
|
||||
.prev:before, .next:before { |
||||
content: ''; |
||||
display: block; |
||||
width: 10px; |
||||
height: 10px; |
||||
border: solid #E6E6E6; |
||||
border-width: 2px 2px 0 0; |
||||
} |
||||
|
||||
.prev:after, .next:after { |
||||
content: none !important; |
||||
} |
||||
|
||||
.prev:before { |
||||
transform: rotate(-135deg); |
||||
} |
||||
|
||||
.next:before { |
||||
transform: rotate(45deg); |
||||
} |
||||
} |
||||
|
||||
.kit__preview { |
||||
img { |
||||
width: 140px; |
||||
height: 140px; |
||||
} |
||||
} |
||||
|
||||
.kit__photo { |
||||
width: 140px; |
||||
height: 140px; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,122 @@ |
||||
<template> |
||||
<div class="section"> |
||||
<div class="section__center center"> |
||||
<div class="kit"> |
||||
<div class="kit__go go"> |
||||
<a href="#" class="go__item" @click.prevent="goBack"> |
||||
<div class="go__arrow"> |
||||
<svg class="icon icon-arrow-left"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-arrow-left"></use> |
||||
</svg> |
||||
</div> |
||||
<div class="go__title">К списку уроков</div> |
||||
</a> |
||||
</div> |
||||
<div class="kit__title title">{{ title }}</div> |
||||
<div class="kit__section"> |
||||
<div class="kit__field field"> |
||||
<div class="field__wrap"> |
||||
<input type="text" class="field__input" placeholder="Название урока" v-model="lesson.title"> |
||||
</div> |
||||
</div> |
||||
<div class="kit__field field"> |
||||
<div class="field__wrap"> |
||||
<textarea class="field__input" placeholder="Описание урока" v-model="lesson.short_description"></textarea> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div v-for="(block, index) in lesson.content"> |
||||
<block-text v-if="block.type === 'text'" |
||||
:index="index" |
||||
:title.sync="block.data.title" |
||||
:text.sync="block.data.text" |
||||
v-on:remove="onBlockRemoved" |
||||
:access-token="accessToken"/> |
||||
<block-image-text v-if="block.type === 'image-text'" |
||||
:index="index" |
||||
:title.sync="block.data.title" |
||||
:text.sync="block.data.text" |
||||
:image-id.sync="block.data.image_id" |
||||
:image-url.sync="block.data.image_url" |
||||
v-on:remove="onBlockRemoved" |
||||
:access-token="accessToken"/> |
||||
<block-image v-if="block.type === 'image'" |
||||
:index="index" |
||||
:title.sync="block.data.title" |
||||
:image-id.sync="block.data.image_id" |
||||
:image-url.sync="block.data.image_url" |
||||
v-on:remove="onBlockRemoved" |
||||
:access-token="accessToken"/> |
||||
<block-images v-if="block.type === 'images'" |
||||
:index="index" |
||||
:title.sync="block.data.title" |
||||
:text.sync="block.data.text" |
||||
:images.sync="block.data.images" |
||||
v-on:remove="onBlockRemoved" |
||||
:access-token="accessToken"/> |
||||
<block-video v-if="block.type === 'video'" |
||||
:index="index" |
||||
:title.sync="block.data.title" |
||||
v-on:remove="onBlockRemoved" |
||||
:video-url.sync="block.data.video_url"/> |
||||
</div> |
||||
<block-add v-on:added="onBlockAdded" /> |
||||
|
||||
<div class="kit__foot"> |
||||
<button class="kit__submit btn btn_md" v-bind:class="{ loading: saving }">Сохранить</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import BlockAdd from "./blocks/BlockAdd"; |
||||
import BlockText from './blocks/BlockText' |
||||
import BlockImage from './blocks/BlockImage' |
||||
import BlockImages from './blocks/BlockImages' |
||||
import BlockImageText from './blocks/BlockImageText' |
||||
import BlockVideo from './blocks/BlockVideo' |
||||
import {api} from "../js/modules/api"; |
||||
|
||||
export default { |
||||
name: "lesson-redactor", |
||||
props: ["lesson", "saving", "accessToken"], |
||||
methods: { |
||||
goBack() { |
||||
this.$emit('back'); |
||||
}, |
||||
onBlockAdded(blockData) { |
||||
this.lesson.content.push(blockData); |
||||
this.$emit('update:lesson', this.lesson); |
||||
}, |
||||
onBlockRemoved(blockIndex) { |
||||
const blockToRemove = this.lesson.content[blockIndex]; |
||||
// Удаляем блок из Vue |
||||
this.lesson.content.splice(blockIndex, 1); |
||||
this.$emit('update:lesson', this.lesson); |
||||
// Если блок уже был записан в БД, отправляем запрос на сервер на удаление блока из БД |
||||
if (blockToRemove.data.id) { |
||||
api.removeContentBlock(blockToRemove, this.accessToken); |
||||
} |
||||
} |
||||
}, |
||||
computed: { |
||||
title() { |
||||
return this.lesson && this.lesson.id ? 'Редактирование урока' : 'Создать урок'; |
||||
} |
||||
}, |
||||
components: { |
||||
BlockAdd, |
||||
'block-text': BlockText, |
||||
'block-image': BlockImage, |
||||
'block-image-text': BlockImageText, |
||||
'block-images': BlockImages, |
||||
'block-video': BlockVideo, |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
|
||||
</style> |
||||
@ -0,0 +1,121 @@ |
||||
<template> |
||||
<div class="kit__section"> |
||||
<div v-if="!isOpen" class="kit__add add"> |
||||
<button type="button" class="add__toggle" @click="isOpen = true"> |
||||
<span class="add__circle"> |
||||
<svg class="icon icon-add-plus"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-add-plus"></use> |
||||
</svg> |
||||
</span> |
||||
<span class="add__title">Добавить блок</span> |
||||
</button> |
||||
</div> |
||||
<div v-if="isOpen" class="kit__add add open"> |
||||
<button type="button" class="add__toggle" @click="isOpen = false"> |
||||
<span class="add__circle"> |
||||
<svg class="icon icon-add-plus"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-add-plus"></use> |
||||
</svg> |
||||
</span> |
||||
<span class="add__title">Добавить блок</span> |
||||
</button> |
||||
<div class="add__list"> |
||||
<button class="add__btn" type="button" @click="addBlockText"> |
||||
<svg class="icon icon-text"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-text"></use> |
||||
</svg> |
||||
</button> |
||||
<button class="add__btn" type="button" @click="addBlockImage"> |
||||
<svg class="icon icon-image"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-image"></use> |
||||
</svg> |
||||
</button> |
||||
<button type="button" class="add__btn" @click="addBlockImageText"> |
||||
<svg class="icon icon-image-text"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-image-text"></use> |
||||
</svg> |
||||
</button> |
||||
<button type="button" class="add__btn" @click="addBlockImages"> |
||||
<svg class="icon icon-images"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-images"></use> |
||||
</svg> |
||||
</button> |
||||
<button type="button" class="add__btn" @click="addBlockVideo"> |
||||
<svg class="icon icon-video-stroke"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-video-stroke"></use> |
||||
</svg> |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
export default { |
||||
name: "block-add", |
||||
data() { |
||||
return { |
||||
isOpen: false |
||||
} |
||||
}, |
||||
methods: { |
||||
add(blockData) { |
||||
this.isOpen = false; |
||||
this.$emit('added', blockData) |
||||
}, |
||||
addBlockText() { |
||||
this.add({ |
||||
type: 'text', |
||||
data: { |
||||
title: '', |
||||
text: '', |
||||
} |
||||
}) |
||||
}, |
||||
addBlockImage() { |
||||
this.add({ |
||||
type: 'image', |
||||
data: { |
||||
title: '', |
||||
image_id: null, |
||||
image_url: null, |
||||
} |
||||
}) |
||||
}, |
||||
addBlockImageText() { |
||||
this.add({ |
||||
type: 'image-text', |
||||
data: { |
||||
title: '', |
||||
text: '', |
||||
image_id: null, |
||||
image_url: null, |
||||
} |
||||
}) |
||||
}, |
||||
addBlockImages() { |
||||
this.add({ |
||||
type: 'images', |
||||
data: { |
||||
title: '', |
||||
text: '', |
||||
images: [], |
||||
} |
||||
}) |
||||
}, |
||||
addBlockVideo() { |
||||
this.add({ |
||||
type: 'video', |
||||
data: { |
||||
title: '', |
||||
video_url: '', |
||||
} |
||||
}) |
||||
}, |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
|
||||
</style> |
||||
@ -0,0 +1,54 @@ |
||||
<template> |
||||
<div class="kit__section kit__section--block"> |
||||
<div class="kit__section-remove"> |
||||
<button type="button" @click="onRemove"> |
||||
<svg class="icon icon-delete"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-delete"></use> |
||||
</svg> |
||||
</button> |
||||
</div> |
||||
<div class="kit__field field"> |
||||
<div class="field__wrap field__wrap--title"> |
||||
<input type="text" |
||||
:value="title" |
||||
class="field__input" |
||||
placeholder="Заголовок раздела" |
||||
@change="onTitleChange"> |
||||
</div> |
||||
</div> |
||||
<div class="kit__row"> |
||||
<lil-image :image-id="imageId" :image-url="imageUrl" v-on:update:imageUrl="onUpdateImageUrl" |
||||
v-on:update:imageId="onUpdateImageId" :access-token="accessToken" /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import LilImage from "./Image"; |
||||
|
||||
export default { |
||||
name: "block-image", |
||||
props: ["index", "title", "imageUrl", "imageId", "accessToken"], |
||||
methods: { |
||||
onTitleChange(event) { |
||||
this.$emit('update:title', event.target.value); |
||||
}, |
||||
onUpdateImageUrl(newValue) { |
||||
this.$emit('update:imageUrl', newValue); |
||||
}, |
||||
onUpdateImageId(newValue) { |
||||
this.$emit('update:imageId', newValue); |
||||
}, |
||||
onRemove() { |
||||
this.$emit('remove', this.index); |
||||
} |
||||
}, |
||||
components: { |
||||
'lil-image': LilImage, |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
|
||||
</style> |
||||
@ -0,0 +1,63 @@ |
||||
<template> |
||||
<div class="kit__section kit__section--block"> |
||||
<div class="kit__section-remove"> |
||||
<button type="button" @click="onRemove"> |
||||
<svg class="icon icon-delete"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-delete"></use> |
||||
</svg> |
||||
</button> |
||||
</div> |
||||
<div class="kit__row"> |
||||
<lil-image :image-id="imageId" :image-url="imageUrl" v-on:update:imageUrl="onUpdateImageUrl" |
||||
v-on:update:imageId="onUpdateImageId" :access-token="accessToken"/> |
||||
<div class="kit__fieldset"> |
||||
<div class="kit__field field"> |
||||
<div class="field__wrap field__wrap--title"> |
||||
<input type="text" |
||||
:value="title" |
||||
class="field__input" |
||||
placeholder="Заголовок раздела" |
||||
@change="onTitleChange"> |
||||
</div> |
||||
</div> |
||||
<div class="kit__field field"> |
||||
<div class="field__wrap"> |
||||
<textarea class="field__textarea field__textarea_sm" |
||||
placeholder="Описание" |
||||
:value="text" |
||||
@change="onTextChange"></textarea> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import LilImage from "./Image"; |
||||
|
||||
export default { |
||||
name: "block-image-text", |
||||
props: ["index", "title", "text", "imageUrl", "imageId", "accessToken"], |
||||
methods: { |
||||
onTitleChange(event) { |
||||
this.$emit('update:title', event.target.value); |
||||
}, |
||||
onTextChange(event) { |
||||
this.$emit('update:text', event.target.value); |
||||
}, |
||||
onUpdateImageUrl(newValue) { |
||||
this.$emit('update:imageUrl', newValue); |
||||
}, |
||||
onUpdateImageId(newValue) { |
||||
this.$emit('update:imageId', newValue); |
||||
}, |
||||
onRemove() { |
||||
this.$emit('remove', this.index); |
||||
} |
||||
}, |
||||
components: { |
||||
'lil-image': LilImage, |
||||
}, |
||||
} |
||||
</script> |
||||
@ -0,0 +1,91 @@ |
||||
<template> |
||||
<div class="kit__section kit__section--block"> |
||||
<div class="kit__section-remove"> |
||||
<button type="button" @click="onRemove"> |
||||
<svg class="icon icon-delete"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-delete"></use> |
||||
</svg> |
||||
</button> |
||||
</div> |
||||
<div class="kit__field field"> |
||||
<div class="field__wrap field__wrap--title"> |
||||
<input type="text" |
||||
:value="title" |
||||
class="field__input" |
||||
placeholder="Заголовок раздела" |
||||
@change="onTitleChange"> |
||||
</div> |
||||
</div> |
||||
<div class="kit__field field"> |
||||
<div class="field__wrap"> |
||||
<textarea class="field__textarea field__textarea_sm" |
||||
:value="text" |
||||
placeholder="Описание" |
||||
@change="onTextChange"></textarea> |
||||
</div> |
||||
</div> |
||||
<div class="kit__gallery"> |
||||
<div class="kit__preview" v-for="image in images" v-bind:class="{ 'kit__preview--loading': image.loading }"> |
||||
<img :src="image.src" class="kit__pic"> |
||||
</div> |
||||
<div class="kit__photo"> |
||||
<svg class="icon icon-add-plus"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-add-plus"></use> |
||||
</svg> |
||||
<input type="file" class="kit__file" multiple @change="onImageAdded"> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import {api} from "../../js/modules/api"; |
||||
|
||||
export default { |
||||
name: "block-images", |
||||
props: ["index", "title", "text", "images", "accessToken"], |
||||
methods: { |
||||
onTitleChange(event) { |
||||
this.$emit('update:title', event.target.value); |
||||
}, |
||||
onTextChange(event) { |
||||
this.$emit('update:text', event.target.value); |
||||
}, |
||||
onImageAdded(event) { |
||||
Array.from(event.target.files).forEach((file) => { |
||||
let reader = new FileReader(); |
||||
reader.onload = () => { |
||||
let images = this.images; |
||||
images.push({ |
||||
src: reader.result, |
||||
loading: true, |
||||
}); |
||||
this.$emit('update:images', images); |
||||
|
||||
api.uploadImage(reader.result, this.accessToken) |
||||
.then((response) => { |
||||
let images = this.images; |
||||
images.forEach((image, index) => { |
||||
if (image.src === reader.result) { |
||||
images[index].img = response.data.id; |
||||
images[index].loading = false; |
||||
images[index].src = response.data.image; |
||||
} |
||||
}); |
||||
this.$emit('update:images', images); |
||||
}) |
||||
.catch((error) => { |
||||
console.log('error', error); |
||||
}); |
||||
}; |
||||
if (file) { |
||||
reader.readAsDataURL(file); |
||||
} |
||||
}); |
||||
}, |
||||
onRemove() { |
||||
this.$emit('remove', this.index); |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
@ -0,0 +1,48 @@ |
||||
<template> |
||||
<div class="kit__section kit__section--block"> |
||||
<div class="kit__section-remove"> |
||||
<button type="button" @click="onRemove"> |
||||
<svg class="icon icon-delete"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-delete"></use> |
||||
</svg> |
||||
</button> |
||||
</div> |
||||
<div class="kit__field field"> |
||||
<div class="field__wrap field__wrap--title"> |
||||
<input type="text" |
||||
:value="title" |
||||
class="field__input" |
||||
placeholder="Заголовок раздела" |
||||
@change="onTitleChange"> |
||||
</div> |
||||
</div> |
||||
<div class="kit__field field"> |
||||
<div class="field__wrap"> |
||||
<vue-redactor :value="text" v-on:update:value="onTextChange" placeholder="Описание"/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import VueRedactor from '../redactor/VueRedactor'; |
||||
|
||||
export default { |
||||
name: "block-text", |
||||
props: ["index", "title", "text"], |
||||
methods: { |
||||
onTitleChange(event) { |
||||
this.$emit('update:title', event.target.value); |
||||
}, |
||||
onTextChange(newValue) { |
||||
this.$emit('update:text', newValue); |
||||
}, |
||||
onRemove() { |
||||
this.$emit('remove', this.index); |
||||
} |
||||
}, |
||||
components: { |
||||
'vue-redactor': VueRedactor, |
||||
} |
||||
} |
||||
</script> |
||||
@ -0,0 +1,53 @@ |
||||
<template> |
||||
<div class="kit__section kit__section--block"> |
||||
<div class="kit__section-remove"> |
||||
<button type="button" @click="onRemove"> |
||||
<svg class="icon icon-delete"> |
||||
<use xlink:href="/static/img/sprite.svg#icon-delete"></use> |
||||
</svg> |
||||
</button> |
||||
</div> |
||||
<div class="kit__field field"> |
||||
<div class="field__wrap field__wrap--title"> |
||||
<input type="text" |
||||
:value="title" |
||||
class="field__input" |
||||
placeholder="Заголовок раздела" |
||||
@change="onTitleChange"> |
||||
</div> |
||||
</div> |
||||
<div class="kit__field field"> |
||||
<div class="field__wrap"> |
||||
<div class="field__flex"> |
||||
<input type="text" |
||||
:value="videoUrl" |
||||
class="field__input field__input_sm" |
||||
placeholder="Вставьте ссылку на Vimeo, YouTube, или другой сервис" |
||||
@change="onVideoUrlChange"> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
export default { |
||||
name: "block-video", |
||||
props: ["index", "title", "videoUrl"], |
||||
methods: { |
||||
onTitleChange(event) { |
||||
this.$emit('update:title', event.target.value); |
||||
}, |
||||
onVideoUrlChange(event) { |
||||
this.$emit('update:videoUrl', event.target.value); |
||||
}, |
||||
onRemove() { |
||||
this.$emit('remove', this.index); |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
|
||||
</style> |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue