Merge remote-tracking branch 'origin/dev' into dev

remotes/origin/hasaccess
nikita 8 years ago
commit bbccc9996d
  1. 0
      api/__init__.py
  2. 74
      api/v1/__init__.py
  3. 35
      api/v1/permissions.py
  4. 337
      api/v1/serializers.py
  5. 51
      api/v1/urls.py
  6. 269
      api/v1/views.py
  7. 26
      apps/auth/views.py
  8. 0
      apps/content/__init__.py
  9. 66
      apps/content/admin.py
  10. 6
      apps/content/apps.py
  11. 93
      apps/content/migrations/0001_initial.py
  12. 29
      apps/content/migrations/0002_auto_20180205_1212.py
  13. 17
      apps/content/migrations/0003_auto_20180205_1246.py
  14. 45
      apps/content/migrations/0004_gallery_galleryimage.py
  15. 31
      apps/content/migrations/0005_auto_20180208_0520.py
  16. 29
      apps/content/migrations/0006_auto_20180208_0551.py
  17. 0
      apps/content/migrations/__init__.py
  18. 97
      apps/content/models.py
  19. 3
      apps/content/tests.py
  20. 2
      apps/content/views.py
  21. 48
      apps/course/migrations/0020_auto_20180202_1716.py
  22. 17
      apps/course/migrations/0021_auto_20180206_0632.py
  23. 11
      apps/course/models.py
  24. 10
      apps/course/templates/course/_items.html
  25. 2
      apps/course/templates/course/course.html
  26. 2
      apps/course/templates/course/inclusion/category_items.html
  27. 26
      apps/course/views.py
  28. 49
      apps/user/forms.py
  29. 18
      apps/user/migrations/0005_user_birthday.py
  30. 18
      apps/user/migrations/0006_auto_20180206_1352.py
  31. 23
      apps/user/migrations/0007_auto_20180207_0808.py
  32. 25
      apps/user/models.py
  33. 209
      apps/user/templates/user/profile-settings.html
  34. 2
      apps/user/templates/user/profile.html
  35. 76
      apps/user/views.py
  36. 10
      docker-compose.yml
  37. 21
      project/celery.py
  38. 3
      project/celery_settings.py
  39. 44
      project/settings.py
  40. 20
      project/templates/lilcity/index.html
  41. 116
      project/templates/lilcity/main.html
  42. 11
      project/urls.py
  43. 8
      requirements.txt
  44. 6
      web/build/css/app.css
  45. 2
      web/build/css/app.css.map
  46. 0
      web/build/img/favicon.ico
  47. BIN
      web/build/img/no_cover.png
  48. 47
      web/build/js/app.js
  49. 2
      web/build/js/app.js.map
  50. 2
      web/gulp/tasks/copy.js
  51. BIN
      web/src/img/favicon.ico
  52. BIN
      web/src/img/no_cover.png
  53. 1
      web/src/js/app.js
  54. 17
      web/src/js/modules/auth.js
  55. 3
      web/src/js/modules/courses.js
  56. 14
      web/src/js/modules/profile.js
  57. 10
      web/src/sass/_common.sass
  58. 4
      web/yarn.lock

@ -0,0 +1,74 @@
import imghdr
import base64
import six
import uuid
from django.core.files.base import ContentFile
from rest_framework import serializers, viewsets
from rest_framework.response import Response
# https://gist.github.com/ivlevdenis/a0c8f5b472b6b8550bbb016c6a30e0be
class ExtendViewSet(object):
"""
This viewset mixin class with extended options list.
"""
permission_map = {}
throttle_scope_map = {}
serializer_class_map = {}
def get_serializer_class(self):
ser = self.serializer_class_map.get(self.action, None)
self.serializer_class = ser or self.serializer_class
return super().get_serializer_class()
def initialize_request(self, request, *args, **kwargs):
request = super().initialize_request(request, *args, **kwargs)
throttle_scope = self.throttle_scope_map.get(self.action, None)
cls_throttle_scope = getattr(self, 'throttle_scope', None)
self.throttle_scope = throttle_scope or cls_throttle_scope or ''
return request
def get_permissions(self):
perms = self.permission_map.get(self.action, None)
if perms and not isinstance(perms, (tuple, list)):
perms = [perms, ]
self.permission_classes = perms or self.permission_classes
return super().get_permissions()
def options(self, request, *args, **kwargs):
if self.metadata_class is None:
return self.http_method_not_allowed(request, *args, **kwargs)
data = self.metadata_class().determine_metadata(request, self)
data['actions']['GET'] = self.query_metadata
return Response(data, status=status.HTTP_200_OK)
class ExtendedModelViewSet(ExtendViewSet, viewsets.ModelViewSet):
pass
class Base64ImageField(serializers.ImageField):
def to_internal_value(self, data):
if isinstance(data, six.string_types):
if 'data:' in data and ';base64,' in data:
header, data = data.split(';base64,')
try:
decoded_file = base64.b64decode(data)
except TypeError:
self.fail('invalid_image')
file_name = str(uuid.uuid4())[:12]
file_extension = self.get_file_extension(
file_name, decoded_file)
complete_file_name = "%s.%s" % (file_name, file_extension,)
data = ContentFile(decoded_file, name=complete_file_name)
return super().to_internal_value(data)
def get_file_extension(self, file_name, decoded_file):
extension = imghdr.what(file_name, decoded_file)
extension = "jpg" if extension == "jpeg" else extension
return extension

@ -0,0 +1,35 @@
from django.contrib.auth import get_user_model
from rest_framework.permissions import BasePermission
User = get_user_model()
class IsAdmin(BasePermission):
def has_permission(self, request, view):
return request.user.is_authenticated and (
request.user.role == User.ADMIN_ROLE or request.user.is_staff or request.user.is_superuser
)
class IsAdminOrIsSelf(BasePermission):
def has_object_permission(self, request, view, user):
return request.user.is_authenticated and (
user == request.user or request.user.is_staff or request.user.is_superuser
)
class IsAuthorOrAdmin(BasePermission):
def has_permission(self, request, view):
return request.user.is_authenticated and (
request.user.role in [
User.AUTHOR_ROLE, User.ADMIN_ROLE
] or request.user.is_staff or request.user.is_superuser
)
class IsAuthorObjectOrAdmin(BasePermission):
def has_object_permission(self, request, view, obj):
return request.user.is_authenticated and (
request.user.role == User.ADMIN_ROLE or request.user.is_staff or request.user.is_superuser
) and request.user == obj.author

@ -0,0 +1,337 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers
from . import Base64ImageField
from apps.course.models import Category, Course, Material, Lesson, Like
from apps.content.models import (
Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject,
)
User = get_user_model()
class ImageObjectSerializer(serializers.ModelSerializer):
image = Base64ImageField(
required=True, allow_empty_file=False, allow_null=False, read_only=False,
)
class Meta:
model = ImageObject
fields = (
'id',
'image',
'created_at',
'update_at',
)
read_only_fields = (
'id',
'created_at',
'update_at',
)
class MaterialSerializer(serializers.ModelSerializer):
class Meta:
model = Material
fields = (
'id',
'title',
'cover',
'short_description',
'created_at',
'update_at',
)
read_only_fields = (
'id',
'cover',
'created_at',
'update_at',
)
class LikeSerializer(serializers.ModelSerializer):
class Meta:
model = Like
fields = (
'id',
'user',
'created_at',
'update_at',
)
read_only_fields = (
'id',
'created_at',
'update_at',
)
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = (
'id',
'title',
)
read_only_fields = (
'id',
)
class CourseSerializer(serializers.ModelSerializer):
class Meta:
model = Course
fields = (
'id',
'author',
'title',
'short_description',
'from_author',
'cover',
'price',
'is_infinite',
'deferred_start_at',
'category',
'duration',
'is_featured',
'url',
'status',
'likes',
'materials',
'created_at',
'update_at',
'content',
)
read_only_fields = (
'id',
'cover',
'content',
'created_at',
'update_at',
)
class CourseRetrieveSerializer(CourseSerializer):
category = CategorySerializer()
materials = MaterialSerializer(many=True)
class LessonSerializer(serializers.ModelSerializer):
class Meta:
model = Lesson
fields = (
'id',
'title',
'short_description',
'course',
'cover',
'created_at',
'update_at',
)
read_only_fields = (
'id',
'cover',
'created_at',
'update_at',
)
class ImageSerializer(serializers.ModelSerializer):
class Meta:
model = Image
fields = (
'id',
'course',
'lesson',
'title',
'position',
'created_at',
'update_at',
) + ('img',)
read_only_fields = (
'id',
'img',
'created_at',
'update_at',
)
class TextSerializer(serializers.ModelSerializer):
class Meta:
model = Text
fields = (
'id',
'course',
'lesson',
'title',
'position',
'created_at',
'update_at',
) + ('txt',)
read_only_fields = (
'id',
'created_at',
'update_at',
)
class ImageTextSerializer(serializers.ModelSerializer):
class Meta:
model = ImageText
fields = (
'id',
'course',
'lesson',
'title',
'position',
'created_at',
'update_at',
) + ('img', 'txt',)
read_only_fields = (
'id',
'img',
'created_at',
'update_at',
)
class VideoSerializer(serializers.ModelSerializer):
class Meta:
model = Video
fields = (
'id',
'course',
'lesson',
'title',
'position',
'created_at',
'update_at',
) + ('url',)
read_only_fields = (
'id',
'created_at',
'update_at',
)
class GallerySerializer(serializers.ModelSerializer):
class Meta:
model = Gallery
fields = (
'id',
'course',
'title',
'created_at',
'update_at',
)
read_only_fields = (
'id',
'created_at',
'update_at',
)
class GalleryImageSerializer(serializers.ModelSerializer):
class Meta:
model = GalleryImage
fields = (
'id',
'gallery',
'image',
'created_at',
'update_at',
)
read_only_fields = (
'id',
'image',
'created_at',
'update_at',
)
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
'id',
'username',
'email',
'first_name',
'last_name',
'is_staff',
'is_active',
'date_joined',
'role',
'gender',
'country',
'city',
'about',
'instagram',
'facebook',
'twitter',
'pinterest',
'youtube',
'vkontakte',
'fb_id',
'fb_data',
'is_email_proved',
'photo',
)
read_only_fields = (
'id',
'photo',
'date_joined',
'is_staff',
'fb_id',
'fb_data',
)
class CoverImageSerializer(serializers.Serializer):
cover = Base64ImageField(
required=False, allow_empty_file=True, allow_null=True
)
class UserPhotoSerializer(serializers.Serializer):
photo = Base64ImageField(
required=False, allow_empty_file=True, allow_null=True
)
class ContentImageSerializer(serializers.Serializer):
img = Base64ImageField(
required=False, allow_empty_file=True, allow_null=True
)
class GalleryImageSerializer(serializers.Serializer):
image = Base64ImageField(
required=False, allow_empty_file=True, allow_null=True
)

@ -0,0 +1,51 @@
from django.urls import path, include
from rest_framework import permissions
from rest_framework.routers import DefaultRouter
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from .views import (
CategoryViewSet, CourseViewSet,
MaterialViewSet, LikeViewSet,
ImageViewSet, TextViewSet,
ImageTextViewSet, VideoViewSet,
GalleryViewSet, GalleryImageViewSet,
UserViewSet, LessonViewSet, ImageObjectViewSet,
)
router = DefaultRouter()
router.register(r'courses', CourseViewSet, base_name='courses')
router.register(r'categories', CategoryViewSet, base_name='categories')
router.register(r'materials', MaterialViewSet, base_name='materials')
router.register(r'lessons', LessonViewSet, base_name='lessons')
router.register(r'likes', LikeViewSet, base_name='likes')
router.register(r'image-objects', ImageObjectViewSet, base_name='image-objects')
router.register(r'images', ImageViewSet, base_name='images')
router.register(r'texts', TextViewSet, base_name='texts')
router.register(r'image-texts', ImageTextViewSet, base_name='image-texts')
router.register(r'videos', VideoViewSet, base_name='videos')
router.register(r'galleries', GalleryViewSet, base_name='galleries')
router.register(r'gallery-images', GalleryImageViewSet, base_name='gallery-images')
router.register(r'users', UserViewSet, base_name='users')
schema_view = get_schema_view(
openapi.Info(
title="Lil Sity API",
default_version='v1',
description="Routes of Lil City project",
),
validators=['flex', 'ssv'],
public=False,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
path('swagger(<str:format>.json|.yaml)', schema_view.without_ui(cache_timeout=None), name='schema-json'),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'),
path('', include((router.urls, 'api-root')), name='api-root')
]

@ -0,0 +1,269 @@
from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework import viewsets
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
from . import ExtendedModelViewSet
from .serializers import (
CategorySerializer, CourseSerializer,
MaterialSerializer, LikeSerializer,
ImageSerializer, TextSerializer,
ImageTextSerializer, VideoSerializer,
GallerySerializer, GalleryImageSerializer,
UserSerializer, UserPhotoSerializer,
LessonSerializer, ContentImageSerializer,
GalleryImageSerializer, CoverImageSerializer,
CourseRetrieveSerializer, ImageObjectSerializer,
)
from .permissions import IsAdmin, IsAdminOrIsSelf, IsAuthorOrAdmin, IsAuthorObjectOrAdmin
from apps.course.models import Category, Course, Material, Lesson, Like
from apps.content.models import (
Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject,
)
User = get_user_model()
class ImageObjectViewSet(ExtendedModelViewSet):
queryset = ImageObject.objects.all()
serializer_class = ImageObjectSerializer
# permission_classes = (IsAuthorOrAdmin,)
class MaterialViewSet(ExtendedModelViewSet):
queryset = Material.objects.all()
serializer_class = MaterialSerializer
search_fields = ('title', 'short_description',)
ordering_fields = ('title', 'created_at', 'update_at',)
# permission_classes = (IsAdmin,)
class LikeViewSet(ExtendedModelViewSet):
queryset = Like.objects.select_related('user').all()
serializer_class = LikeSerializer
search_fields = ('user__email', 'user__firstname', 'user__lastname',)
ordering_fields = ('created_at', 'update_at',)
# permission_classes = (IsAdmin,)
class CategoryViewSet(ExtendedModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
search_fields = ('title',)
ordering_fields = ('title',)
# permission_classes = (IsAdmin,)
class CourseViewSet(ExtendedModelViewSet):
queryset = Course.objects.select_related(
'author', 'category'
).prefetch_related(
'likes', 'materials', 'content',
).all()
serializer_class = CourseSerializer
serializer_class_map = {
'list': CourseRetrieveSerializer,
'retrieve': CourseRetrieveSerializer,
'upload_photo': CoverImageSerializer,
}
filter_fields = ('category', 'status', 'is_infinite', 'is_featured',)
search_fields = ('author__email', 'title', 'category__title',)
ordering_fields = ('title', 'created_at', 'update_at',)
# permission_classes = (IsAuthorObjectOrAdmin,)
# permission_map = {
# 'create': IsAuthorOrAdmin,
# 'delete': IsAdmin,
# }
@detail_route(methods=['post'], url_path='upload-photo')
def upload_photo(self, request, pk=None):
course = self.get_object()
serializer = self.get_serializer()
serialized_data = serializer(data=request.data)
if serialized_data.is_valid():
course.cover = serialized_data['cover']
course.save()
return Response({'success': True})
else:
return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST)
class LessonViewSet(ExtendedModelViewSet):
queryset = Lesson.objects.select_related('course').all()
serializer_class = LessonSerializer
serializer_class_map = {
'upload_photo': CoverImageSerializer,
}
filter_fields = ('course',)
search_fields = ('title', 'short_description',)
ordering_fields = ('title', 'created_at', 'update_at',)
# permission_classes = (IsAuthorObjectOrAdmin,)
# permission_map = {
# 'create': IsAuthorOrAdmin,
# 'delete': IsAdmin,
# }
@detail_route(methods=['post'], url_path='upload-photo')
def upload_photo(self, request, pk=None):
lesson = self.get_object()
serializer = self.get_serializer()
serialized_data = serializer(data=request.data)
if serialized_data.is_valid():
lesson.cover = serialized_data['cover']
lesson.save()
return Response({'success': True})
else:
return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST)
class ImageViewSet(ExtendedModelViewSet):
queryset = Image.objects.select_related(
'course', 'lesson'
).all()
serializer_class = ImageSerializer
serializer_class_map = {
'upload_photo': ContentImageSerializer,
}
search_fields = ('title',)
ordering_fields = ('title', 'created_at', 'update_at', 'position',)
# permission_classes = (IsAuthorOrAdmin,)
# permission_map = {
# 'delete': IsAdmin,
# }
@detail_route(methods=['post'], url_path='upload-photo')
def upload_photo(self, request, pk=None):
image = self.get_object()
serializer = self.get_serializer()
serialized_data = serializer(data=request.data)
if serialized_data.is_valid():
image.img = serialized_data['img']
image.save()
return Response({'success': True})
else:
return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST)
class TextViewSet(ExtendedModelViewSet):
queryset = Text.objects.select_related(
'course', 'lesson'
).all()
serializer_class = TextSerializer
search_fields = ('title',)
ordering_fields = ('title', 'created_at', 'update_at', 'position',)
# permission_classes = (IsAuthorOrAdmin,)
# permission_map = {
# 'delete': IsAdmin,
# }
class ImageTextViewSet(ExtendedModelViewSet):
queryset = ImageText.objects.select_related(
'course', 'lesson'
).all()
serializer_class = ImageTextSerializer
serializer_class_map = {
'upload_photo': ContentImageSerializer,
}
search_fields = ('title',)
ordering_fields = ('title', 'created_at', 'update_at', 'position',)
# permission_classes = (IsAuthorOrAdmin,)
# permission_map = {
# 'delete': IsAdmin,
# }
@detail_route(methods=['post'], url_path='upload-photo')
def upload_photo(self, request, pk=None):
image_text = self.get_object()
serializer = self.get_serializer()
serialized_data = serializer(data=request.data)
if serialized_data.is_valid():
image_text.img = serialized_data['img']
image_text.save()
return Response({'success': True})
else:
return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST)
class VideoViewSet(ExtendedModelViewSet):
queryset = Video.objects.select_related(
'course', 'lesson'
).all()
serializer_class = VideoSerializer
search_fields = ('title',)
ordering_fields = ('title', 'created_at', 'update_at', 'position',)
# permission_classes = (IsAuthorOrAdmin,)
# permission_map = {
# 'delete': IsAdmin,
# }
class GalleryViewSet(ExtendedModelViewSet):
queryset = Gallery.objects.select_related('course').all()
serializer_class = GallerySerializer
search_fields = ('title',)
ordering_fields = ('title', 'created_at', 'update_at',)
# permission_classes = (IsAuthorOrAdmin,)
# permission_map = {
# 'delete': IsAdmin,
# }
class GalleryImageViewSet(ExtendedModelViewSet):
queryset = GalleryImage.objects.select_related('gallery').all()
serializer_class = GalleryImageSerializer
serializer_class_map = {
'upload_photo': GalleryImageSerializer,
}
search_fields = ('gallery__title',)
# permission_classes = (IsAuthorOrAdmin,)
# permission_map = {
# 'delete': IsAdmin,
# }
@detail_route(methods=['post'], url_path='upload-photo')
def upload_photo(self, request, pk=None):
gallery_image = self.get_object()
serializer = self.get_serializer()
serialized_data = serializer(data=request.data)
if serialized_data.is_valid():
gallery_image.image = serialized_data['image']
gallery_image.save()
return Response({'success': True})
else:
return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST)
class UserViewSet(ExtendedModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
serializer_class_map = {
'upload_photo': UserPhotoSerializer,
}
filter_fields = ('is_staff', 'is_active', 'role',
'gender', 'is_email_proved', 'fb_id',)
search_fields = ('email', 'first_name', 'last_name',
'country', 'city', 'fb_id',)
ordering_fields = ('email', 'first_name', 'last_name',
'country', 'city', 'date_joined',)
# permission_classes = (IsAdminOrIsSelf,)
# permission_map = {
# 'delete': IsAdmin,
# }
@detail_route(methods=['post'], url_path='upload-photo')
def upload_photo(self, request, pk=None):
user = self.get_object()
serializer = self.get_serializer()
serialized_data = serializer(data=request.data)
if serialized_data.is_valid():
user.photo = serialized_data['photo']
user.save()
return Response({'success': True})
else:
return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST)

@ -5,6 +5,7 @@ from facepy.exceptions import FacepyError
from django.contrib.auth import get_user_model, logout, login, views
from django.contrib.auth.forms import AuthenticationForm
from django.core.files.base import ContentFile
from django.http import JsonResponse
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
@ -135,27 +136,42 @@ class FacebookLoginOrRegistration(View):
graph = GraphAPI(access_token)
try:
data = graph.get('/me?fields=email, first_name, last_name')
photo_data = graph.get('/me/picture?height=120')
except FacepyError:
return JsonResponse({"success": False})
fb_id = data.get('id')
lilcity_user_settings = User.objects.filter(fb_id=fb_id)
if lilcity_user_settings.count():
login(requests, user=lilcity_user_settings[0])
return JsonResponse({"success": True})
try:
user = User.objects.get(fb_id=fb_id)
except User.DoesNotExist:
email = requests.POST.get('email') or data.get('email')
if not email:
return JsonResponse({"success": False,
"errors": {"email": 'is field required'}
})
else:
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
first_name = data.get('first_name', '')
last_name = data.get('last_name', '')
user = User.objects.create_user(username=email, email=email, first_name=first_name, last_name=last_name, password=uuid4().hex)
user.is_email_proved = True
user.fb_id = fb_id
if photo_data:
photo = ContentFile(photo_data)
fname = str(fb_id) + '.jpg'
user.photo.save(fname, photo, save=True)
user.save()
login(requests, user=user)
return JsonResponse({"success": True})
else:
if not user.photo and photo_data:
photo = ContentFile(photo_data)
fname = str(fb_id) + '.jpg'
user.photo.save(fname, photo, save=True)
login(requests, user=user)
return JsonResponse({"success": True})

@ -0,0 +1,66 @@
from django.contrib import admin
from polymorphic.admin import (
PolymorphicParentModelAdmin,
PolymorphicChildModelAdmin,
PolymorphicChildModelFilter,
)
from apps.content.models import (
Content, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject,
)
@admin.register(ImageObject)
class ImageObjectAdmin(admin.ModelAdmin):
list_display = (
'id',
'image',
'created_at',
'update_at',
)
class ContentChildAdmin(PolymorphicChildModelAdmin):
base_model = Content
@admin.register(Image)
class ImageAdmin(ContentChildAdmin):
base_model = Image
@admin.register(Text)
class TextAdmin(ContentChildAdmin):
base_model = Text
@admin.register(ImageText)
class ImageTextAdmin(ContentChildAdmin):
base_model = ImageText
@admin.register(Video)
class VideoAdmin(ContentChildAdmin):
base_model = Video
@admin.register(Content)
class ContentAdmin(PolymorphicParentModelAdmin):
base_model = Content
child_models = (
Image,
Text,
ImageText,
Video
)
@admin.register(Gallery)
class GalleryAdmin(admin.ModelAdmin):
pass
@admin.register(GalleryImage)
class GalleryImageAdmin(admin.ModelAdmin):
pass

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ContentConfig(AppConfig):
name = 'content'
verbose_name = 'Контент'

@ -0,0 +1,93 @@
# Generated by Django 2.0.2 on 2018-02-05 12:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('course', '0020_auto_20180202_1716'),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Content',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(default='', max_length=100, verbose_name='Заголовок')),
('position', models.PositiveSmallIntegerField(default=1, unique=True, verbose_name='Положение на странице')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='Image',
fields=[
('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='content.Content')),
('img', models.ImageField(upload_to='content/images', verbose_name='Изображение')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('content.content',),
),
migrations.CreateModel(
name='ImageText',
fields=[
('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='content.Content')),
('img', models.ImageField(upload_to='content/images', verbose_name='Изображение')),
('txt', models.TextField(default='', verbose_name='Текст')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('content.content',),
),
migrations.CreateModel(
name='Text',
fields=[
('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='content.Content')),
('txt', models.TextField(default='', verbose_name='Текст')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('content.content',),
),
migrations.CreateModel(
name='Video',
fields=[
('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='content.Content')),
('url', models.URLField(verbose_name='Ссылка')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('content.content',),
),
migrations.AddField(
model_name='content',
name='course',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='course.Course', verbose_name='Курс'),
),
migrations.AddField(
model_name='content',
name='lesson',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='course.Lesson', verbose_name='Урок'),
),
migrations.AddField(
model_name='content',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_content.content_set+', to='contenttypes.ContentType'),
),
]

@ -0,0 +1,29 @@
# Generated by Django 2.0.2 on 2018-02-05 12:12
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('content', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='content',
options={'verbose_name': 'Контент', 'verbose_name_plural': 'Контент'},
),
migrations.AddField(
model_name='content',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='content',
name='update_at',
field=models.DateTimeField(auto_now=True),
),
]

@ -0,0 +1,17 @@
# Generated by Django 2.0.2 on 2018-02-05 12:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('content', '0002_auto_20180205_1212'),
]
operations = [
migrations.AlterModelOptions(
name='content',
options={'ordering': ('-created_at',), 'verbose_name': 'Контент', 'verbose_name_plural': 'Контент'},
),
]

@ -0,0 +1,45 @@
# Generated by Django 2.0.2 on 2018-02-05 13:09
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('course', '0020_auto_20180202_1716'),
('content', '0003_auto_20180205_1246'),
]
operations = [
migrations.CreateModel(
name='Gallery',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(default='', max_length=100, verbose_name='Заголовок')),
('created_at', models.DateTimeField(auto_now_add=True)),
('update_at', models.DateTimeField(auto_now=True)),
('course', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='course.Course', verbose_name='Курс')),
],
options={
'verbose_name': 'Галерея',
'verbose_name_plural': 'Галереи',
'ordering': ('-created_at',),
},
),
migrations.CreateModel(
name='GalleryImage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='content/gallery_images', verbose_name='Изображение')),
('created_at', models.DateTimeField(auto_now_add=True)),
('update_at', models.DateTimeField(auto_now=True)),
('gallery', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.Gallery', verbose_name='Галерея')),
],
options={
'verbose_name': 'Изображение в галерее',
'verbose_name_plural': 'Изображения в галерее',
'ordering': ('-created_at',),
},
),
]

@ -0,0 +1,31 @@
# Generated by Django 2.0.2 on 2018-02-08 05:20
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('content', '0004_gallery_galleryimage'),
]
operations = [
migrations.CreateModel(
name='ImageObject',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='content/imageobject', verbose_name='Изображение')),
],
),
migrations.AlterField(
model_name='content',
name='course',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='content', to='course.Course', verbose_name='Курс'),
),
migrations.AlterField(
model_name='content',
name='lesson',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='content', to='course.Lesson', verbose_name='Урок'),
),
]

@ -0,0 +1,29 @@
# Generated by Django 2.0.2 on 2018-02-08 05:51
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('content', '0005_auto_20180208_0520'),
]
operations = [
migrations.AlterModelOptions(
name='imageobject',
options={'ordering': ('-created_at',), 'verbose_name': 'Объект изображения', 'verbose_name_plural': 'Объекты изображения'},
),
migrations.AddField(
model_name='imageobject',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='imageobject',
name='update_at',
field=models.DateTimeField(auto_now=True),
),
]

@ -0,0 +1,97 @@
from django.db import models
from polymorphic.models import PolymorphicModel
from apps.course.models import Course, Lesson
class ImageObject(models.Model):
image = models.ImageField('Изображение', upload_to='content/imageobject')
created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'Объект изображения'
verbose_name_plural = 'Объекты изображения'
ordering = ('-created_at',)
class Content(PolymorphicModel):
course = models.ForeignKey(
Course, on_delete=models.CASCADE,
null=True, blank=True,
verbose_name='Курс',
related_name='content',
)
lesson = models.ForeignKey(
Lesson, on_delete=models.CASCADE,
null=True, blank=True,
verbose_name='Урок',
related_name='content',
)
title = models.CharField('Заголовок', max_length=100, default='')
position = models.PositiveSmallIntegerField(
'Положение на странице',
default=1, unique=True
)
created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'Контент'
verbose_name_plural = 'Контент'
ordering = ('-created_at',)
class Image(Content):
img = models.ImageField('Изображение', upload_to='content/images')
class Text(Content):
txt = models.TextField('Текст', default='')
class ImageText(Content):
img = models.ImageField('Изображение', upload_to='content/images')
txt = models.TextField('Текст', default='')
class Video(Content):
url = models.URLField('Ссылка')
class Gallery(models.Model):
course = models.ForeignKey(
Course, on_delete=models.CASCADE,
null=True, blank=True,
verbose_name='Курс'
)
title = models.CharField('Заголовок', max_length=100, default='')
created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'Галерея'
verbose_name_plural = 'Галереи'
ordering = ('-created_at',)
class GalleryImage(models.Model):
gallery = models.ForeignKey(
Gallery, on_delete=models.CASCADE,
verbose_name='Галерея'
)
image = models.ImageField(
'Изображение', upload_to='content/gallery_images'
)
created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'Изображение в галерее'
verbose_name_plural = 'Изображения в галерее'
ordering = ('-created_at',)

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,2 @@
from django.shortcuts import render

@ -0,0 +1,48 @@
# Generated by Django 2.0.2 on 2018-02-02 17:16
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import polymorphic_tree.models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('course', '0019_auto_20180130_1630'),
]
operations = [
migrations.AlterModelOptions(
name='coursecomment',
options={'base_manager_name': 'objects', 'ordering': ('tree_id', 'lft'), 'verbose_name': 'Комментарий курса', 'verbose_name_plural': 'Комментарии курсов'},
),
migrations.AlterModelOptions(
name='lessoncomment',
options={'base_manager_name': 'objects', 'ordering': ('tree_id', 'lft'), 'verbose_name': 'Комментарий урока', 'verbose_name_plural': 'Комментарии уроков'},
),
migrations.RemoveField(
model_name='coursecomment',
name='parent',
),
migrations.RemoveField(
model_name='lessoncomment',
name='parent',
),
migrations.AddField(
model_name='comment',
name='parent',
field=polymorphic_tree.models.PolymorphicTreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='children', to='course.Comment'),
),
migrations.AddField(
model_name='comment',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_course.comment_set+', to='contenttypes.ContentType'),
),
migrations.AlterField(
model_name='comment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

@ -0,0 +1,17 @@
# Generated by Django 2.0.2 on 2018-02-06 06:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('course', '0020_auto_20180202_1716'),
]
operations = [
migrations.AlterModelOptions(
name='category',
options={'ordering': ['title'], 'verbose_name': 'Категория', 'verbose_name_plural': 'Категории'},
),
]

@ -3,7 +3,7 @@ from django.db import models
from django.utils import timezone
from django.contrib.auth import get_user_model
from mptt.models import MPTTModel, TreeForeignKey
from polymorphic_tree.models import PolymorphicMPTTModel, PolymorphicTreeForeignKey
from .manager import CategoryQuerySet
@ -86,6 +86,7 @@ class Category(models.Model):
class Meta:
verbose_name = 'Категория'
verbose_name_plural = 'Категории'
ordering = ['title']
class Lesson(models.Model):
@ -123,9 +124,10 @@ class Material(models.Model):
ordering = ('title',)
class Comment(MPTTModel):
author = models.ForeignKey(User, on_delete=models.PROTECT)
class Comment(PolymorphicMPTTModel):
content = models.TextField('Текст комментария', default='')
author = models.ForeignKey(User, on_delete=models.CASCADE)
parent = PolymorphicTreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.PROTECT)
created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
@ -142,11 +144,9 @@ class Comment(MPTTModel):
class MPTTMeta:
order_insertion_by = ['-created_at']
abstract = True
class CourseComment(Comment):
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.PROTECT)
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='comments')
class Meta(Comment.Meta):
@ -155,7 +155,6 @@ class CourseComment(Comment):
class LessonComment(Comment):
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True, on_delete=models.PROTECT)
lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE, related_name='comments')
class Meta(Comment.Meta):

@ -6,8 +6,12 @@
data-course data-course-id={{ course.id }}
{% if course.is_deferred_start %}data-future-course data-future-course-time={{ course.deferred_start_at.timestamp }}{% endif %}
>
<a class="courses__preview" href="{% url 'course' course.id %}">
<a class="courses__preview" href="{% url 'course' course.id %}?next={{ request.get_full_path }}">
{% if course.cover %}
<img class="courses__pic" src="{{ course.cover.url }}"/>
{% else %}
<img class="courses__pic" src="{% static 'img/no_cover.png' %}"/>
{% endif %}
<div class="courses__view">Подробнее</div>
{% if course.is_featured %}
<div class="courses__label courses__label_fav"></div>
@ -26,7 +30,7 @@
<div class="courses__price">{{ course.price|floatformat:"-2" }}₽</div>
{% endif %}
</div>
<a class="courses__title" href="{% url 'course' course.id %}">{{ course.title }}</a>
<a class="courses__title" href="{% url 'course' course.id %}?next={{ request.get_full_path }}">{{ course.title }}</a>
<div class="courses__content">{{ course.short_description }}
</div>
<div class="courses__user user">
@ -42,7 +46,7 @@
{% endif %}
</a>
<div class="user__info">
<a href="{% url 'user' course.author.id %}">
<a href="{% url 'user' course.author.id %}" class="link--black">
<div class="user__name">{{ course.author.get_full_name }}</div>
</a>
<div class="user__meta">

@ -7,7 +7,7 @@
<div class="section section_border">
<div class="section__center center center_sm">
<div class="go">
<a class="go__item" href="{% url 'courses' %}">
<a class="go__item" href="{% if next %}{{next}}{% else %}{% url 'courses' %}{% endif %}">
<div class="go__arrow">
<svg class="icon icon-arrow-left">
<use xlink:href="{% static '/img/sprite.svg' %}#icon-arrow-left"></use>

@ -1,5 +1,5 @@
{% for cat in category_items %}
<div class="select__option js-select-option{% if category and category.0 == cat.title %} active{% endif %}" data-category-option data-category-url="{% url 'courses' %}?category={{ cat.title }}">
<div class="select__option js-select-option{% if category and category.0 == cat.title %} active{% endif %}" data-category-option data-category-name="{{ cat.title }}" data-category-url="{% url 'courses' %}?category={{ cat.title }}">
<div class="select__title">{{ cat.title }}</div>
</div>
{% endfor %}

@ -90,7 +90,7 @@ def coursecomment(request, course_id):
@login_required
@csrf_exempt
@require_http_methods(['POST'])
def lessoncomment(request, Lesson):
def lessoncomment(request, lesson_id):
try:
lesson = Lesson.objects.get(id=lesson_id)
except Lesson.DoesNotExist:
@ -111,7 +111,7 @@ def lessoncomment(request, Lesson):
lessoncomment = LessonComment.objects.create(
author=request.user,
content=comment,
course=course,
lesson=lesson,
)
else:
try:
@ -119,13 +119,13 @@ def lessoncomment(request, Lesson):
except LessonComment.DoesNotExist:
return JsonResponse({
'success': False,
'errors': ['CourseComment with id f{reply_to} not found']
'errors': ['LessonComment with id f{reply_to} not found']
}, status=400)
else:
lessoncomment = LessonComment.objects.create(
author=request.user,
content=comment,
course=course,
lesson=lesson,
parent=_lessoncomment,
)
ctx = {'node': lessoncomment, 'user': request.user}
@ -141,6 +141,11 @@ class CourseView(DetailView):
context_object_name = 'course'
template_name = 'course/course.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['next'] = self.request.GET.get('next', None)
return context
class CoursesView(ListView):
model = Course
@ -153,7 +158,7 @@ class CoursesView(ListView):
if request.is_ajax():
context = self.get_context_data()
template_name = self.get_template_names()
html = loader.render_to_string(template_name, context)
html = loader.render_to_string(template_name, context, request=request)
is_paginated = context.get('is_paginated')
if is_paginated:
page_obj = context.get('page_obj')
@ -172,7 +177,11 @@ class CoursesView(ListView):
return super().get(request, args, kwargs)
def get_queryset(self):
queryset = super().get_queryset()
queryset = super().get_queryset().select_related(
'author', 'category'
).prefetch_related(
'likes', 'materials', 'content',
)
filtered = CourseFilter(self.request.GET, queryset=queryset)
return filtered.qs
@ -207,9 +216,10 @@ class SearchView(CoursesView):
queryset = queryset.none()
return queryset
def get_context_data(self):
context = super().get_context_data()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['q'] = self.request.GET.get('q', None) or ''
return context
if 'is_paginated' in context and context['is_paginated']:
page_obj = context.get('page_obj')
context['page'] = page_obj.number

@ -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='Страна'),
),
]

@ -5,22 +5,29 @@ from django.utils.translation import gettext_lazy as _
class User(AbstractUser):
USER_ROLE = 0
AUTHOR_ROLE = 1
ADMIN_ROLE = 2
ROLE_CHOICES = (
(0, 'пользователь'),
(1, 'автор'),
(2, 'администратор'),
(USER_ROLE, 'пользователь'),
(AUTHOR_ROLE, 'автор'),
(ADMIN_ROLE, 'администратор'),
)
NOT_DEFINED = 'n'
MALE = 'm'
FEMALE = 'f'
GENDER_CHOICES = (
('n', 'не указан'),
('m', 'Мужчина'),
('f', 'Женщина'),
(NOT_DEFINED, 'не указан'),
(MALE, 'Мужчина'),
(FEMALE, 'Женщина'),
)
email = models.EmailField(_('email address'), unique=True)
role = models.PositiveSmallIntegerField('Роль', default=0, choices=ROLE_CHOICES)
gender = models.CharField('Пол', max_length=1, default='n', choices=GENDER_CHOICES)
country = models.CharField('Страна', max_length=50, default='')
city = models.CharField('Город', max_length=85, default='')
about = models.CharField('О себе', max_length=1000, default='', blank=True)
birthday = models.DateField('День рождения', null=True, blank=True)
country = models.CharField('Страна', max_length=50, null=True, blank=True)
city = models.CharField('Город', max_length=85, null=True, blank=True)
about = models.CharField('О себе', max_length=1000, null=True, blank=True)
instagram = models.URLField(default='', null=True, blank=True)
facebook = models.URLField(default='', null=True, blank=True)
twitter = models.URLField(default='', null=True, blank=True)

@ -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 %}

@ -2,7 +2,7 @@
<div class="section">
<div class="section__center center">
<div class="profile">
<a class="profile__btn profile__btn_edit btn" href="#">Редактировать</a>
<a class="profile__btn profile__btn_edit btn" href="{% url 'user-edit' user.id %}">Редактировать</a>
{% if user.photo %}
<div class="profile__ava ava">
<img class="ava__pic" src="{{ user.photo.url }}">

@ -1,9 +1,20 @@
from django.shortcuts import render
from django.views.generic import DetailView
from io import BytesIO
from PIL import Image
from os.path import splitext
from django.contrib.auth import login
from django.shortcuts import render, reverse
from django.views.generic import DetailView, UpdateView
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.hashers import check_password, make_password
from django.http import Http404
from django.utils.decorators import method_decorator
from apps.course.models import Course
from .forms import UserEditForm
User = get_user_model()
@ -13,6 +24,65 @@ class UserView(DetailView):
def get_context_data(self, object):
context = super().get_context_data()
context['published'] = Course.objects.filter(author=self.object, status=Course.PUBLISHED)
context['published'] = Course.objects.filter(
author=self.object, status=Course.PUBLISHED
)
context['paid'] = Course.objects.none()
return context
class UserEditView(UpdateView):
model = User
template_name = 'user/profile-settings.html'
form_class = UserEditForm
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
if request.user != self.object:
raise Http404()
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
# it's magic *-*-*-*-*
if 'photo' in request.FILES:
photo_fp = request.FILES.pop('photo')[0]
fname = photo_fp.name
photo = Image.open(photo_fp)
lowest_side = min(photo.size)
horizontal_padding = (lowest_side - photo.size[0]) / 2
vertical_padding = (lowest_side - photo.size[1]) / 2
photo = photo.crop(
(
-horizontal_padding,
-vertical_padding,
photo.size[0] + horizontal_padding,
photo.size[1] + vertical_padding
)
)
if photo.size[0] > 512:
photo = photo.resize((512, 512,))
buffer = BytesIO()
ext = splitext(fname)[1][1:].upper()
if ext == 'JPG':
ext = 'JPEG'
photo.save(buffer, ext)
self.object.photo.save(fname, buffer)
buffer.close()
if not request.POST._mutable:
request.POST._mutable = True
old_password = request.POST.pop('old_password')[0]
new_password1 = request.POST.pop('new_password1')[0]
new_password2 = request.POST.pop('new_password2')[0]
if old_password:
if request.user.check_password(old_password) and new_password1 == new_password2:
request.user.set_password(new_password1)
request.user.save()
login(request, request.user)
else:
messages.error(request, 'Неверный пароль.')
messages.info(request, 'Данные сохранены.')
return super().post(request, *args, **kwargs)
def get_success_url(self):
return reverse('user-edit', args=[self.object.id])

@ -12,18 +12,26 @@ services:
ports:
- "5432:5432"
redis:
image: redis:3-alpine
ports:
- "6379:6379"
web:
build: .
restart: always
volumes:
- .:/lilcity
command: bash -c "python manage.py migrate && python manage.py loaddata /lilcity/apps/*/fixtures/*.json && python manage.py runserver 0.0.0.0:8000"
command: bash -c "python manage.py migrate && python manage.py loaddata /lilcity/apps/*/fixtures/*.json && python manage.py runserver 0.0.0.0:8000 && celery worker -A project"
environment:
- DJANGO_SETTINGS_MODULE=project.settings
- DATABASE_SERVICE_HOST=db
- REDIS_SERVICE_HOST=redis
ports:
- "${PORT}:8000"
depends_on:
- db
- redis
links:
- db
- redis

@ -0,0 +1,21 @@
import os
from celery import Celery
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
app = Celery('project')
# Using a string here means the worker don't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('project.celery_settings')
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
return f'Request: {self.request}'

@ -0,0 +1,3 @@
broker_url = 'redis://redis:6379/0'
result_backend = 'redis://redis:6379/1'
task_serializer = 'json'

@ -40,16 +40,26 @@ INSTALLED_APPS = [
'anymail',
'active_link',
'django_filters',
'polymorphic_tree',
'polymorphic',
'mptt',
'rest_framework',
'rest_framework.authtoken',
'drf_yasg',
'corsheaders',
] + [
'apps.auth.apps',
'apps.user',
'apps.notification',
'apps.payment',
'apps.course',
'apps.content',
]
if DEBUG:
INSTALLED_APPS += ['silk']
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@ -58,6 +68,8 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
if DEBUG:
MIDDLEWARE += ['silk.middleware.SilkyMiddleware']
ROOT_URLCONF = 'project.urls'
@ -150,7 +162,7 @@ STATICFILES_DIRS = [
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
LOGIN_URL = '/'
# Email
# https://github.com/anymail/django-anymail
@ -170,3 +182,33 @@ TWILIO_TOKEN = '559a6b1fce121759c9af2dcbb3f755ea'
TWILIO_FROM_PHONE = '+37128914409'
ACTIVE_LINK_STRICT = True
# DRF settings
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
}
# CORS settings
if DEBUG:
CORS_ORIGIN_ALLOW_ALL = True
# Swagger doc settings
SWAGGER_SETTINGS = {
'DOC_EXPANSION': 'none',
}

@ -33,7 +33,7 @@
<meta property="fb:admins" content="Facebook numeric ID">
<meta name="csrf-token" content="{{ csrf_token }}">
<link rel="stylesheet" media="all" href={% static "css/app.css" %}>
<link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}"/>
<link rel="shortcut icon" type="image/png" href="{% static 'img/favicon.ico' %}"/>
<script>
var viewportmeta = document.querySelector('meta[name="viewport"]');
if (viewportmeta) {
@ -126,10 +126,10 @@
<div class="header__group"><a class="header__section" target="_blank" href="http://blog.lil.school">БЛОГ</a></div>
</nav>
</div>
{% if user.is_authenticated %}
{% if request.user.is_authenticated %}
<div class="header__login">
{% if user.photo %}
<div class="header__ava ava"><img class="ava__pic" src="{{ user.photo.url }}"></div>
{% if request.user.photo %}
<div class="header__ava ava"><img class="ava__pic" src="{{ request.user.photo.url }}"></div>
{% else %}
<div class="header__ava ava"><img class="ava__pic" src="{% static 'img/user.jpg' %}"></div>
{% endif %}
@ -138,7 +138,7 @@
<a class="header__link header__link_green" href="#">
<div class="header__title">ДОБАВИТЬ КУРС</div>
</a>
<a class="header__link" href="{% url 'user' user.id %}">
<a class="header__link" href="{% url 'user' request.user.id %}">
<div class="header__title">ПРОФИЛЬ</div>
</a>
<a class="header__link" href="#">
@ -158,7 +158,7 @@
{% endif %}
</div>
</div>
{% if user.is_authenticated and not user.fb_id or user.is_authenticated and not user.is_email_proved %}
{% if request.user.is_authenticated and not request.user.fb_id or request.user.is_authenticated and not request.user.is_email_proved %}
<div class="message message_error">Необходимо подтвердить электронную почту</div>
{% endif %}
</header>
@ -284,7 +284,7 @@
<div id="learner-auth-field-error__all" class="auth-register__common-error form__common-error learner-auth-form__field-error"></div>
</div>
<div class="auth__foot">
<button class="auth__btn btn btn_light" tabindex="3">ВОЙТИ</button>
<button id="learner-auth__button" class="auth__btn btn btn_light" tabindex="3">ВОЙТИ</button>
<div class="auth__or">или</div>
<button type="button" class="auth__btn btn btn_fb">
<svg class="icon icon-facebook">
@ -303,7 +303,7 @@
class="auth__field field learner-registration-form__field">
<div class="field__label">ИМЯ</div>
<div class="field__wrap"><input id="learner-registration-form__first-name" class="field__input"
type="text" name="first_name" placeholder="Sasha"></div>
type="text" name="first_name" placeholder=""></div>
<div id="learner-registration-field-error__first-name"
class="field__error learner-registration-form__field-error"></div>
</div>
@ -311,7 +311,7 @@
class="auth__field field learner-registration-form__field">
<div class="field__label">ФАМИЛИЯ</div>
<div class="field__wrap"><input id="learner-registration-form__last-name" class="field__input"
type="text" name="last_name" placeholder="Kru"></div>
type="text" name="last_name" placeholder=""></div>
<div id="learner-registration-field-error__last-name"
class="field__error learner-registration-form__field-error"></div>
</div>
@ -385,7 +385,7 @@
class="form__common-error password-reset-form__field-error"></div>
</div>
<div class="auth__foot">
<button class="auth__btn btn btn_light">ОТПРАВИТЬ</button>
<button id="password-reset__button" class="auth__btn btn btn_light">ОТПРАВИТЬ</button>
</div>
</div>
</form>

@ -455,128 +455,24 @@
<a href='#'>Распечатать расписание</a> чтобы не забыть</div>
</div>
</div>
{% if course_items %}
<div class="section">
<div class="section__center center">
<div class="title title_center">Онлайн-курсы</div>
<div class="text">Помимо онлайн-школы Lil City у нас есть отдельные
<a href='#'>курсы в записи</a>. Учитесь и развивайте креативное мышление когда вам удобно.</div>
<a href='{% url 'courses' %}'>курсы в записи</a>. Учитесь и развивайте креативное мышление когда вам удобно.
</div>
<div class="courses">
<div class="courses__list">
<div class="courses__item">
<a class="courses__preview" href="#">
<img class="courses__pic" src="{% static 'img/pic-1.jpg' %}" />
<div class="courses__view">Подробнее</div>
<div class="courses__label courses__label_fav"></div>
</a>
<div class="courses__details">
<a class="courses__theme theme" href="#">АНИМАЦИЯ</a>
<div class="courses__price">30$</div>
</div>
<a class="courses__title" href="#">Базовый курс для детей по основам иллюстрации</a>
<div class="courses__content">Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.</div>
<div class="courses__user user">
<div class="user__ava ava">
<img class="ava__pic" src="{% static 'img/user.jpg' %}" />
</div>
<div class="user__info">
<div class="user__name">Александра Неимоверноумная</div>
<div class="user__meta">
<div class="user__date">SEPT 12, 2017</div>
<a class="user__likes likes" href="#">
<div class="likes__counter">253</div>
<div class="likes__icon">
<svg class="icon icon-like">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-like"></use>
</svg>
<svg class="icon icon-like-fill">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-like-fill"></use>
</svg>
</div>
</a>
</div>
</div>
</div>
</div>
<div class="courses__item">
<a class="courses__preview" href="#">
<img class="courses__pic" src="{% static 'img/pic-1.jpg' %}" />
<div class="courses__view">Подробнее</div>
<div class="courses__soon">
<div class="courses__left">Курс начнется:</div>
<div class="courses__time">через 16 часов 13 минут</div>
</div>
<div class="courses__label courses__label_clock"></div>
</a>
<div class="courses__details">
<a class="courses__theme theme theme_green" href="#">АНИМАЦИЯ</a>
<div class="courses__price">30$</div>
</div>
<a class="courses__title" href="#">Базовый курс для детей по основам иллюстрации</a>
<div class="courses__content">Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.</div>
<div class="courses__user user">
<div class="user__ava ava">
<img class="ava__pic" src="{% static 'img/user.jpg' %}" />
</div>
<div class="user__info">
<div class="user__name">Александра Неимоверноумная</div>
<div class="user__meta">
<div class="user__date">SEPT 12, 2017</div>
<a class="user__likes likes" href="#">
<div class="likes__counter">253</div>
<div class="likes__icon">
<svg class="icon icon-like">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-like"></use>
</svg>
<svg class="icon icon-like-fill">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-like-fill"></use>
</svg>
</div>
</a>
</div>
</div>
</div>
</div>
<div class="courses__item">
<a class="courses__preview" href="#">
<img class="courses__pic" src="{% static 'img/pic-1.jpg' %}" />
<div class="courses__view">Подробнее</div>
</a>
<div class="courses__details">
<a class="courses__theme theme theme_violet" href="#">АНИМАЦИЯ</a>
<div class="courses__price">30$</div>
</div>
<a class="courses__title" href="#">Базовый курс для детей по основам иллюстрации</a>
<div class="courses__content">Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.</div>
<div class="courses__user user">
<div class="user__ava ava">
<img class="ava__pic" src="{% static 'img/user.jpg' %}" />
</div>
<div class="user__info">
<div class="user__name">Александра Неимоверноумная</div>
<div class="user__meta">
<div class="user__date">SEPT 12, 2017</div>
<a class="user__likes likes" href="#">
<div class="likes__counter">253</div>
<div class="likes__icon">
<svg class="icon icon-like">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-like"></use>
</svg>
<svg class="icon icon-like-fill">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-like-fill"></use>
</svg>
</div>
</a>
</div>
</div>
</div>
</div>
{% include "course/course_items.html" %}
</div>
<div class="courses__more more">
<a class="more__btn btn btn_light" href="#">УЗНАТЬ ПОДРОБНЕЕ</a>
<a class="more__btn btn btn_light" href="{% url 'courses' %}">УЗНАТЬ ПОДРОБНЕЕ</a>
</div>
</div>
</div>
</div>
{% endif %}
<div class="game">
<div class="game__center center">
<div class="game__wrap">

@ -23,7 +23,8 @@ from apps.course.views import (
CourseView, LessonView, SearchView,
lessoncomment,
)
from apps.user.views import UserView
from apps.course.models import Course
from apps.user.views import UserView, UserEditView
urlpatterns = [
path('admin/', admin.site.urls),
@ -36,13 +37,19 @@ urlpatterns = [
path('lesson/<int:lesson_id>/comment', lessoncomment, name='lessoncomment'),
path('search/', SearchView.as_view(), name='search'),
path('user/<int:pk>/', UserView.as_view(), name='user'),
path('user/<int:pk>/edit/', UserEditView.as_view(), name='user-edit'),
path('privacy', TemplateView.as_view(template_name="templates/lilcity/privacy_policy.html"), name='privacy'),
path('terms', TemplateView.as_view(template_name="templates/lilcity/terms.html"), name='terms'),
path('refund-policy', TemplateView.as_view(template_name="templates/lilcity/refund_policy.html"), name='refund_policy'),
path('', TemplateView.as_view(template_name="templates/lilcity/main.html"), name='index'),
path('', TemplateView.as_view(template_name="templates/lilcity/main.html", extra_context={'course_items': Course.objects.all()[:3]}), name='index'),
path('api/v1/', include(('api.v1.urls', 'api_v1'))),
]
if settings.DEBUG:
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += staticfiles_urlpatterns()

@ -1,5 +1,5 @@
# Python-3.6
Django==2.0.1
Django==2.0.2
django-anymail[mailgun]==1.2
paymentwall-python==1.0.7
twilio==6.10.0
@ -10,3 +10,9 @@ django-active-link==0.1.2
arrow==0.12.1
django-filter==2.0.0.dev1
django-mptt==0.9.0
django-polymorphic-tree==1.5
celery[redis]==4.1.0
djangorestframework==3.7.7
drf-yasg[validation]==1.4.0
django-silk==2.0.0
django-cors-headers==2.1.0

@ -739,7 +739,7 @@ a.grey-link:hover { color: #000; border-bottom: 1px #545454 solid; }
.select__option.active { color: #191919; }
.select__option.active .select__title:after { content: ''; position: absolute; top: 100%; left: -10px; right: -10px; height: 2px; margin-top: 2px; background-image: linear-gradient(-225deg, #FFE2EB 0%, #D8F5F5 100%); }
.select__option.active .select__title:after { content: ''; position: absolute; top: 50%; left: -10px; right: -10px; height: 2px; margin-top: -2px; background-image: linear-gradient(-225deg, #FFE2EB 0%, #D8F5F5 100%); }
.select__title { display: table; position: relative; }
@ -1399,6 +1399,8 @@ a.grey-link:hover { color: #000; border-bottom: 1px #545454 solid; }
.message_error { background: #FF9393; }
.message_info { background: #8ECFC0; }
.searching { display: -ms-flexbox; display: flex; margin-bottom: 40px; }
.searching__input { height: 40px; padding: 0 10px; border: 1px solid #E6E6E6; border-radius: 3px 0 0 3px; font-size: 18px; transition: border-color .2s; -ms-flex: 0 0 calc(100% - 100px); flex: 0 0 calc(100% - 100px); }
@ -1407,6 +1409,8 @@ a.grey-link:hover { color: #000; border-bottom: 1px #545454 solid; }
.searching__btn { border-radius: 0 3px 3px 0; -ms-flex: 0 0 100px; flex: 0 0 100px; }
a.link--black { color: #000; }
@media only screen and (max-width: 1023px) {
body { font-size: 14px; line-height: 1.57; }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

@ -26857,6 +26857,8 @@ __webpack_require__(152);
__webpack_require__(153);
__webpack_require__(154);
/***/ }),
/* 127 */
/***/ (function(module, exports, __webpack_require__) {
@ -27724,6 +27726,9 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
(0, _jquery2.default)('.form__common-error').hide();
var passwordResetButton = (0, _jquery2.default)('#password-reset__button');
passwordResetButton.addClass('loading');
_jquery2.default.ajax(passwordResetForm.attr('action'), {
method: 'POST',
data: passwordResetForm.serialize()
@ -27753,6 +27758,8 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
}
}
(0, _jquery2.default)('#learner-auth-field-error__all').text('Произошла незвестная ошибка');
}).always(function () {
passwordResetButton.removeClass('loading');
});
});
@ -27786,15 +27793,21 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
return;
}
var authButton = (0, _jquery2.default)('#learner-auth__button');
authButton.addClass('loading');
_jquery2.default.ajax(authForm.attr('action'), {
method: 'POST',
data: authForm.serialize()
}).done(function (data) {
if (data.success === true) {
location.reload();
} else {
authButton.removeClass('loading');
}
}).fail(function (xhr) {
console.log('error', xhr);
authButton.removeClass('loading');
if (xhr.status === 400) {
if (xhr.responseJSON.errors) {
for (var errorField in xhr.responseJSON.errors) {
@ -27859,6 +27872,8 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
}
(0, _jquery2.default)('.form__common-error').hide();
var registrationButton = (0, _jquery2.default)('#learner-registration-form__submit-button');
registrationButton.addClass('loading');
_jquery2.default.ajax(registrationForm.attr('action'), {
method: 'POST',
@ -27866,9 +27881,12 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
}).done(function (data) {
if (data.success === true) {
location.reload();
} else {
registrationButton.removeClass('loading');
}
}).fail(function (xhr) {
console.log('error', xhr);
registrationButton.removeClass('loading');
if (xhr.status === 400) {
if (xhr.responseJSON.errors) {
for (var errorField in xhr.responseJSON.errors) {
@ -28405,6 +28423,9 @@ var history = (0, _createBrowserHistory2.default)();
// Обработчик выбора категории
(0, _jquery2.default)('div.js-select-option[data-category-option]').on('click', function (e) {
e.preventDefault();
var currentCategory = (0, _jquery2.default)(this).attr('data-category-name');
(0, _jquery2.default)('[data-category-name]').removeClass('active');
(0, _jquery2.default)('[data-category-name=' + currentCategory + ']').addClass('active');
history.replace((0, _jquery2.default)(this).attr('data-category-url'));
load_courses((0, _jquery2.default)(this).attr('data-category-url'), true);
});
@ -29613,6 +29634,32 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
});
});
/***/ }),
/* 154 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
var _jquery = __webpack_require__(1);
var _jquery2 = _interopRequireDefault(_jquery);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
(0, _jquery2.default)(document).ready(function () {
// Обработчик выбора пола
var genderInput = (0, _jquery2.default)('#gender');
(0, _jquery2.default)('div.js-select-option[data-gender-option]').on('click', function (e) {
e.preventDefault();
var currentGender = (0, _jquery2.default)(this).attr('data-gender');
(0, _jquery2.default)('[data-gender]').removeClass('active');
(0, _jquery2.default)('[data-gender=' + currentGender + ']').addClass('active');
genderInput.val(currentGender);
});
});
/***/ })
/******/ ]);
//# sourceMappingURL=app.js.map

File diff suppressed because one or more lines are too long

@ -22,7 +22,7 @@ gulp.task('copy:rootfiles', function() {
gulp.task('copy:img', function() {
return gulp
.src([
config.src.img + '/**/*.{jpg,png,jpeg,svg,gif}',
config.src.img + '/**/*.{jpg,png,jpeg,svg,gif,ico}',
'!' + config.src.img + '/svgo/**/*.*'
])
.pipe(gulp.dest(config.dest.img));

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

@ -12,3 +12,4 @@ import "./modules/popup";
import "./modules/courses";
import "./modules/comments";
import "./modules/password-show";
import "./modules/profile";

@ -73,6 +73,9 @@ $(document).ready(function () {
$('.form__common-error').hide();
let passwordResetButton = $('#password-reset__button');
passwordResetButton.addClass('loading');
$.ajax(passwordResetForm.attr('action'), {
method: 'POST',
data: passwordResetForm.serialize(),
@ -104,6 +107,9 @@ $(document).ready(function () {
}
}
$('#learner-auth-field-error__all').text('Произошла незвестная ошибка');
})
.always(() => {
passwordResetButton.removeClass('loading');
});
});
@ -137,6 +143,9 @@ $(document).ready(function () {
return;
}
let authButton = $('#learner-auth__button');
authButton.addClass('loading');
$.ajax(authForm.attr('action'), {
method: 'POST',
data: authForm.serialize(),
@ -144,10 +153,13 @@ $(document).ready(function () {
.done(function (data) {
if (data.success === true) {
location.reload();
} else {
authButton.removeClass('loading');
}
})
.fail(function (xhr) {
console.log('error', xhr);
authButton.removeClass('loading');
if (xhr.status === 400) {
if (xhr.responseJSON.errors) {
for (let errorField in xhr.responseJSON.errors) {
@ -212,6 +224,8 @@ $(document).ready(function () {
}
$('.form__common-error').hide();
let registrationButton = $('#learner-registration-form__submit-button');
registrationButton.addClass('loading');
$.ajax(registrationForm.attr('action'), {
method: 'POST',
@ -220,10 +234,13 @@ $(document).ready(function () {
.done(function (data) {
if (data.success === true) {
location.reload();
} else {
registrationButton.removeClass('loading');
}
})
.fail(function (xhr) {
console.log('error', xhr);
registrationButton.removeClass('loading');
if (xhr.status === 400) {
if (xhr.responseJSON.errors) {
for (let errorField in xhr.responseJSON.errors) {

@ -25,6 +25,9 @@ $(document).ready(function () {
// Обработчик выбора категории
$('div.js-select-option[data-category-option]').on('click', function (e) {
e.preventDefault();
const currentCategory = $(this).attr('data-category-name');
$('[data-category-name]').removeClass('active');
$(`[data-category-name=${currentCategory}]`).addClass('active');
history.replace($(this).attr('data-category-url'));
load_courses($(this).attr('data-category-url'), true);
});

@ -0,0 +1,14 @@
import $ from 'jquery';
$(document).ready(function () {
// Обработчик выбора пола
let genderInput = $('#gender')
$('div.js-select-option[data-gender-option]').on('click', function (e) {
e.preventDefault();
const currentGender = $(this).attr('data-gender');
$('[data-gender]').removeClass('active');
$(`[data-gender=${currentGender}]`).addClass('active');
genderInput.val(currentGender)
});
})

@ -1837,11 +1837,11 @@ a.grey-link
&:after
content: ''
position: absolute
top: 100%
top: 50%
left: -10px
right: -10px
height: 2px
margin-top: 2px
margin-top: -2px
background-image: linear-gradient(-225deg, #FFE2EB 0%, #D8F5F5 100%)
&__title
display: table
@ -3218,6 +3218,8 @@ a.grey-link
z-index: 999
&_error
background: $pink
&_info
background: $green
.mobile-hide
@ -3241,7 +3243,9 @@ a.grey-link
border-radius: 0 3px 3px 0
flex: 0 0 100px
a
&.link--black
color: #000

@ -5386,6 +5386,10 @@ slash@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
smooth-scroll@^12.1.5:
version "12.1.5"
resolved "https://registry.yarnpkg.com/smooth-scroll/-/smooth-scroll-12.1.5.tgz#b6d8deca371a9edd5ea3ad291ef0fb7e741ed41e"
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"

Loading…
Cancel
Save