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

# Conflicts:
#	apps/course/templates/course/content/gallery.html
#	apps/course/templates/course/content/image.html
#	apps/course/templates/course/content/imagetext.html
#	project/templates/lilcity/main.html
remotes/origin/hasaccess
nikita 8 years ago
commit d10062ba5a
  1. 6
      Dockerfile
  2. 9
      api/v1/serializers/__init__.py
  3. 44
      api/v1/serializers/config.py
  4. 5
      api/v1/serializers/content.py
  5. 81
      api/v1/serializers/course.py
  6. 10
      api/v1/serializers/mixins.py
  7. 2
      api/v1/serializers/payment.py
  8. 36
      api/v1/serializers/user.py
  9. 9
      api/v1/urls.py
  10. 56
      api/v1/views.py
  11. 18
      apps/auth/middleware.py
  12. 4
      apps/auth/templates/auth/password_reset.html
  13. 2
      apps/auth/templates/auth/password_reset.txt
  14. 5
      apps/auth/views.py
  15. 0
      apps/config/__init__.py
  16. 5
      apps/config/admin.py
  17. 5
      apps/config/apps.py
  18. 27
      apps/config/migrations/0001_initial.py
  19. 18
      apps/config/migrations/0002_auto_20180326_1026.py
  20. 23
      apps/config/migrations/0003_auto_20180326_1027.py
  21. 18
      apps/config/migrations/0004_config_main_page_top_image.py
  22. 23
      apps/config/migrations/0005_auto_20180326_1314.py
  23. 0
      apps/config/migrations/__init__.py
  24. 39
      apps/config/models.py
  25. 3
      apps/config/tests.py
  26. 3
      apps/config/views.py
  27. 1
      apps/content/admin.py
  28. 18
      apps/content/migrations/0015_content_uuid.py
  29. 1
      apps/content/models.py
  30. 6
      apps/content/tasks.py
  31. 18
      apps/course/migrations/0035_comment_deactivated_at.py
  32. 2
      apps/course/models.py
  33. 23
      apps/course/templates/course/_items.html
  34. 8
      apps/course/templates/course/blocks/comment.html
  35. 7
      apps/course/templates/course/blocks/comments.html
  36. 51
      apps/course/templates/course/content/gallery.html
  37. 2
      apps/course/templates/course/content/image.html
  38. 2
      apps/course/templates/course/content/imagetext.html
  39. 17
      apps/course/templates/course/course.html
  40. 16
      apps/course/templates/course/course_edit.html
  41. 13
      apps/course/templates/course/lesson.html
  42. 33
      apps/course/views.py
  43. 2
      apps/notification/templates/notification/email/_base.html
  44. 13
      apps/notification/templates/notification/email/accept_author.html
  45. 8
      apps/notification/templates/notification/email/decline_author.html
  46. 2
      apps/notification/utils.py
  47. 6
      apps/payment/models.py
  48. 18
      apps/payment/tasks.py
  49. 12
      apps/payment/templates/payment/course_payment_success.html
  50. 67
      apps/payment/views.py
  51. 32
      apps/user/admin.py
  52. 58
      apps/user/fixtures/subscription_categories.json
  53. 11
      apps/user/forms.py
  54. 27
      apps/user/migrations/0009_authorrequest.py
  55. 18
      apps/user/migrations/0010_auto_20180312_1610.py
  56. 17
      apps/user/migrations/0011_auto_20180313_0744.py
  57. 18
      apps/user/migrations/0012_authorrequest_cause.py
  58. 18
      apps/user/migrations/0013_authorrequest_declined_send_at.py
  59. 18
      apps/user/migrations/0014_authorrequest_accepted_send_at.py
  60. 45
      apps/user/migrations/0015_auto_20180315_0547.py
  61. 20
      apps/user/migrations/0016_auto_20180315_0603.py
  62. 18
      apps/user/migrations/0017_subscriptioncategory_auto_add.py
  63. 19
      apps/user/migrations/0018_user_phone.py
  64. 18
      apps/user/migrations/0019_user_show_in_mainpage.py
  65. 161
      apps/user/models.py
  66. 22
      apps/user/tasks.py
  67. 12
      apps/user/templates/user/become-author-success.html
  68. 69
      apps/user/templates/user/become-author.html
  69. 41
      apps/user/templates/user/notification-settings.html
  70. 20
      apps/user/templates/user/profile-settings.html
  71. 83
      apps/user/views.py
  72. 4
      docker-compose.yml
  73. 5
      project/context_processors.py
  74. 14
      project/fields.py
  75. 88
      project/settings.py
  76. 6
      project/templates/lilcity/edit_index.html
  77. 35
      project/templates/lilcity/index.html
  78. 83
      project/templates/lilcity/main.html
  79. 52
      project/templates/lilcity/school_schedules.html
  80. 39
      project/urls.py
  81. 15
      project/views.py
  82. 35
      requirements.txt
  83. 7
      web/build/img/sprite.svg
  84. 4
      web/build/index.html
  85. 4
      web/build/ui-kit.html
  86. 4
      web/package.json
  87. 194
      web/src/components/CourseRedactor.vue
  88. 5
      web/src/components/blocks/BlockAdd.vue
  89. 8
      web/src/components/blocks/BlockImages.vue
  90. 7
      web/src/components/blocks/BlockText.vue
  91. 18
      web/src/icons/hamburger.svg
  92. 2
      web/src/js/app.js
  93. 13
      web/src/js/modules/api.js
  94. 57
      web/src/js/modules/auth.js
  95. 2
      web/src/js/modules/comments.js
  96. 6
      web/src/js/modules/common.js
  97. 4
      web/src/js/modules/courses.js
  98. 36
      web/src/js/modules/mixpanel.js
  99. 14
      web/src/js/modules/notification.js
  100. 2
      web/src/js/modules/popup.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -2,12 +2,8 @@ FROM python:3.6
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
RUN mkdir /lilcity RUN mkdir /lilcity
WORKDIR /lilcity WORKDIR /lilcity
RUN apt-get update \
&& apt-get install -y postgresql-client-9.4 \
&& rm -rf /var/lib/apt/lists/* \
&& pip install --upgrade pip
ADD requirements.txt /lilcity/ ADD requirements.txt /lilcity/
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
ADD . /lilcity/ ADD . /lilcity/

@ -3,13 +3,13 @@ import base64
import six import six
import uuid import uuid
from django.conf import settings
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from rest_framework import serializers from rest_framework import serializers
class Base64ImageField(serializers.ImageField): class Base64ImageField(serializers.ImageField):
use_url = False
def to_internal_value(self, data): def to_internal_value(self, data):
if isinstance(data, six.string_types): if isinstance(data, six.string_types):
if 'data:' in data and ';base64,' in data: if 'data:' in data and ';base64,' in data:
@ -30,3 +30,8 @@ class Base64ImageField(serializers.ImageField):
extension = imghdr.what(file_name, decoded_file) extension = imghdr.what(file_name, decoded_file)
extension = "jpg" if extension == "jpeg" else extension extension = "jpg" if extension == "jpeg" else extension
return extension return extension
def to_representation(self, value):
file = "%s%s" % (settings.MEDIA_URL, super().to_representation(value),)
return file

@ -1,39 +1,27 @@
from constance import config
from constance.admin import get_values, ConstanceForm
from rest_framework import serializers from rest_framework import serializers
from rest_framework.fields import SkipField
from collections import OrderedDict
from apps.config.models import Config
def _set_constance_value(key, value):
form = ConstanceForm(initial=get_values())
field = form.fields[key]
clean_value = field.clean(field.to_python(value))
setattr(config, key, clean_value)
class ConfigSerializer(serializers.ModelSerializer):
class ConfigSerializer(serializers.Serializer):
SERVICE_COMMISSION = serializers.IntegerField(required=False) SERVICE_COMMISSION = serializers.IntegerField(required=False)
SERVICE_DISCOUNT_MIN_AMOUNT = serializers.IntegerField(required=False) SERVICE_DISCOUNT_MIN_AMOUNT = serializers.IntegerField(required=False)
SERVICE_DISCOUNT = serializers.IntegerField(required=False) SERVICE_DISCOUNT = serializers.IntegerField(required=False)
INSTAGRAM_CLIENT_ACCESS_TOKEN = serializers.CharField(required=False) INSTAGRAM_CLIENT_ACCESS_TOKEN = serializers.CharField(required=False)
INSTAGRAM_CLIENT_SECRET = serializers.CharField(required=False) INSTAGRAM_CLIENT_SECRET = serializers.CharField(required=False)
INSTAGRAM_PROFILE_URL = serializers.CharField(required=False) INSTAGRAM_PROFILE_URL = serializers.CharField(required=False)
SCHOOL_LOGO_IMAGE = serializers.ImageField(required=False, allow_null=True)
MAIN_PAGE_TOP_IMAGE = serializers.ImageField(required=False, allow_null=True)
def to_representation(self, instance): class Meta:
ret = OrderedDict() model = Config
fields = self._readable_fields fields = (
for field in fields: 'SERVICE_COMMISSION',
attribute = instance.get(field.field_name) 'SERVICE_DISCOUNT_MIN_AMOUNT',
ret[field.field_name] = field.to_representation(attribute) 'SERVICE_DISCOUNT',
return ret 'INSTAGRAM_CLIENT_ACCESS_TOKEN',
'INSTAGRAM_CLIENT_SECRET',
def to_internal_value(self, data): 'INSTAGRAM_PROFILE_URL',
ret = OrderedDict(get_values()) 'SCHOOL_LOGO_IMAGE',
for k, v in data.items(): 'MAIN_PAGE_TOP_IMAGE',
ret[k] = v )
return ret
def update(self, instance, validated_data):
for k, v in validated_data.items():
_set_constance_value(k, v)

@ -61,6 +61,7 @@ class ImageCreateSerializer(serializers.ModelSerializer):
model = Image model = Image
fields = ( fields = (
'id', 'id',
'uuid',
'course', 'course',
'lesson', 'lesson',
'title', 'title',
@ -93,6 +94,7 @@ class TextCreateSerializer(serializers.ModelSerializer):
model = Text model = Text
fields = ( fields = (
'id', 'id',
'uuid',
'course', 'course',
'lesson', 'lesson',
'title', 'title',
@ -124,6 +126,7 @@ class ImageTextCreateSerializer(serializers.ModelSerializer):
model = ImageText model = ImageText
fields = ( fields = (
'id', 'id',
'uuid',
'course', 'course',
'lesson', 'lesson',
'title', 'title',
@ -157,6 +160,7 @@ class VideoCreateSerializer(serializers.ModelSerializer):
model = Video model = Video
fields = ( fields = (
'id', 'id',
'uuid',
'course', 'course',
'lesson', 'lesson',
'title', 'title',
@ -212,6 +216,7 @@ class GallerySerializer(serializers.ModelSerializer):
model = Gallery model = Gallery
fields = ( fields = (
'id', 'id',
'uuid',
'course', 'course',
'lesson', 'lesson',
'title', 'title',

@ -1,7 +1,11 @@
from rest_framework import serializers from rest_framework import serializers
from apps.course.models import Category, Course, Material, Lesson, Like from apps.course.models import (
Category, Course,
Comment, CourseComment, LessonComment,
Material, Lesson,
Like,
)
from .content import ( from .content import (
ImageObjectSerializer, ContentSerializer, ContentCreateSerializer, ImageObjectSerializer, ContentSerializer, ContentCreateSerializer,
GallerySerializer, GalleryImageSerializer, GallerySerializer, GalleryImageSerializer,
@ -84,7 +88,7 @@ class CourseCreateSerializer(DispatchContentMixin,
): ):
title = serializers.CharField(allow_blank=True) title = serializers.CharField(allow_blank=True)
short_description = serializers.CharField(allow_blank=True) short_description = serializers.CharField(allow_blank=True)
slug = serializers.SlugField(allow_unicode=True, allow_blank=True, required=False) slug = serializers.SlugField(allow_unicode=True, allow_blank=True, allow_null=True, required=False)
content = serializers.ListSerializer( content = serializers.ListSerializer(
child=ContentCreateSerializer(), child=ContentCreateSerializer(),
required=False, required=False,
@ -199,6 +203,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
t.title = cdata['title'] t.title = cdata['title']
t.lesson = lesson t.lesson = lesson
t.txt = cdata['txt'] t.txt = cdata['txt']
t.uuid = cdata['uuid']
t.save() t.save()
else: else:
t = Text.objects.create( t = Text.objects.create(
@ -206,6 +211,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
title=cdata['title'], title=cdata['title'],
lesson=lesson, lesson=lesson,
txt=cdata['txt'], txt=cdata['txt'],
uuid=cdata['uuid'],
) )
elif ctype == 'image': elif ctype == 'image':
if 'id' in cdata and cdata['id']: if 'id' in cdata and cdata['id']:
@ -214,6 +220,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
image.title = cdata['title'] image.title = cdata['title']
image.lesson = lesson image.lesson = lesson
image.img = ImageObject.objects.get(id=cdata['img']) image.img = ImageObject.objects.get(id=cdata['img'])
image.uuid = cdata['uuid']
image.save() image.save()
else: else:
image = Image.objects.create( image = Image.objects.create(
@ -221,6 +228,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
title=cdata['title'], title=cdata['title'],
lesson=lesson, lesson=lesson,
img=ImageObject.objects.get(id=cdata['img']), img=ImageObject.objects.get(id=cdata['img']),
uuid=cdata['uuid'],
) )
elif ctype == 'image-text': elif ctype == 'image-text':
if 'id' in cdata and cdata['id']: if 'id' in cdata and cdata['id']:
@ -230,6 +238,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
it.lesson = lesson it.lesson = lesson
it.img = ImageObject.objects.get(id=cdata['img']) it.img = ImageObject.objects.get(id=cdata['img'])
it.txt = cdata['txt'] it.txt = cdata['txt']
it.uuid = cdata['uuid']
it.save() it.save()
else: else:
it = ImageText.objects.create( it = ImageText.objects.create(
@ -238,6 +247,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
lesson=lesson, lesson=lesson,
img=ImageObject.objects.get(id=cdata['img']), img=ImageObject.objects.get(id=cdata['img']),
txt=cdata['txt'], txt=cdata['txt'],
uuid=cdata['uuid'],
) )
elif ctype == 'video': elif ctype == 'video':
if 'id' in cdata and cdata['id']: if 'id' in cdata and cdata['id']:
@ -246,6 +256,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
v.title = cdata['title'] v.title = cdata['title']
v.lesson = lesson v.lesson = lesson
v.url = cdata['url'] v.url = cdata['url']
v.uuid = cdata['uuid']
v.save() v.save()
else: else:
v = Video.objects.create( v = Video.objects.create(
@ -253,6 +264,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
title=cdata['title'], title=cdata['title'],
lesson=lesson, lesson=lesson,
url=cdata['url'], url=cdata['url'],
uuid=cdata['uuid'],
) )
elif ctype == 'images': elif ctype == 'images':
if 'id' in cdata and cdata['id']: if 'id' in cdata and cdata['id']:
@ -260,6 +272,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
g.position = cdata['position'] g.position = cdata['position']
g.title = cdata['title'] g.title = cdata['title']
g.lesson = lesson g.lesson = lesson
g.uuid = cdata['uuid']
g.save() g.save()
if 'images' in cdata: if 'images' in cdata:
for image in cdata['images']: for image in cdata['images']:
@ -272,6 +285,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
lesson=lesson, lesson=lesson,
position=cdata['position'], position=cdata['position'],
title=cdata['title'], title=cdata['title'],
uuid=cdata['uuid'],
) )
if 'images' in cdata: if 'images' in cdata:
for image in cdata['images']: for image in cdata['images']:
@ -368,3 +382,64 @@ class CourseSerializer(serializers.ModelSerializer):
'update_at', 'update_at',
'deactivated_at', 'deactivated_at',
) )
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer()
class Meta:
model = Comment
fields = (
'id',
'content',
'author',
'parent',
'deactivated_at',
'created_at',
'update_at',
)
read_only_fields = (
'id',
'deactivated_at',
'created_at',
'update_at',
)
def to_representation(self, instance):
if isinstance(instance, CourseComment):
return CourseCommentSerializer(instance, context=self.context).to_representation(instance)
elif isinstance(instance, LessonComment):
return LessonCommentSerializer(instance, context=self.context).to_representation(instance)
class CourseCommentSerializer(serializers.ModelSerializer):
author = UserSerializer()
children = CommentSerializer(many=True)
class Meta:
model = CourseComment
fields = CommentSerializer.Meta.fields + (
'course',
'children',
)
read_only_fields = CommentSerializer.Meta.read_only_fields + (
'children',
)
class LessonCommentSerializer(serializers.ModelSerializer):
author = UserSerializer()
children = CommentSerializer(many=True)
class Meta:
model = LessonComment
fields = CommentSerializer.Meta.fields + (
'lesson',
'children',
)
read_only_fields = CommentSerializer.Meta.read_only_fields + (
'children',
)

@ -21,6 +21,7 @@ class DispatchContentMixin(object):
t.title = cdata['title'] t.title = cdata['title']
t.course = course t.course = course
t.txt = cdata['txt'] t.txt = cdata['txt']
t.uuid = cdata['uuid']
t.save() t.save()
else: else:
t = Text.objects.create( t = Text.objects.create(
@ -28,6 +29,7 @@ class DispatchContentMixin(object):
title=cdata['title'], title=cdata['title'],
course=course, course=course,
txt=cdata['txt'], txt=cdata['txt'],
uuid=cdata['uuid'],
) )
elif ctype == 'image': elif ctype == 'image':
if 'id' in cdata and cdata['id']: if 'id' in cdata and cdata['id']:
@ -36,6 +38,7 @@ class DispatchContentMixin(object):
image.title = cdata['title'] image.title = cdata['title']
image.course = course image.course = course
image.img = ImageObject.objects.get(id=cdata['img']) image.img = ImageObject.objects.get(id=cdata['img'])
image.uuid = cdata['uuid']
image.save() image.save()
else: else:
image = Image.objects.create( image = Image.objects.create(
@ -43,6 +46,7 @@ class DispatchContentMixin(object):
title=cdata['title'], title=cdata['title'],
course=course, course=course,
img=ImageObject.objects.get(id=cdata['img']), img=ImageObject.objects.get(id=cdata['img']),
uuid=cdata['uuid'],
) )
elif ctype == 'image-text': elif ctype == 'image-text':
if 'id' in cdata and cdata['id']: if 'id' in cdata and cdata['id']:
@ -52,6 +56,7 @@ class DispatchContentMixin(object):
it.course = course it.course = course
it.img = ImageObject.objects.get(id=cdata['img']) it.img = ImageObject.objects.get(id=cdata['img'])
it.txt = cdata['txt'] it.txt = cdata['txt']
it.uuid = cdata['uuid']
it.save() it.save()
else: else:
it = ImageText.objects.create( it = ImageText.objects.create(
@ -60,6 +65,7 @@ class DispatchContentMixin(object):
course=course, course=course,
img=ImageObject.objects.get(id=cdata['img']), img=ImageObject.objects.get(id=cdata['img']),
txt=cdata['txt'], txt=cdata['txt'],
uuid=cdata['uuid'],
) )
elif ctype == 'video': elif ctype == 'video':
if 'id' in cdata and cdata['id']: if 'id' in cdata and cdata['id']:
@ -68,6 +74,7 @@ class DispatchContentMixin(object):
v.title = cdata['title'] v.title = cdata['title']
v.course = course v.course = course
v.url = cdata['url'] v.url = cdata['url']
v.uuid = cdata['uuid']
v.save() v.save()
else: else:
v = Video.objects.create( v = Video.objects.create(
@ -75,6 +82,7 @@ class DispatchContentMixin(object):
title=cdata['title'], title=cdata['title'],
course=course, course=course,
url=cdata['url'], url=cdata['url'],
uuid=cdata['uuid'],
) )
elif ctype == 'images': elif ctype == 'images':
if 'id' in cdata and cdata['id']: if 'id' in cdata and cdata['id']:
@ -82,6 +90,7 @@ class DispatchContentMixin(object):
g.course = course g.course = course
g.position = cdata['position'] g.position = cdata['position']
g.title = cdata['title'] g.title = cdata['title']
g.uuid = cdata['uuid']
g.save() g.save()
if 'images' in cdata: if 'images' in cdata:
for image in cdata['images']: for image in cdata['images']:
@ -99,6 +108,7 @@ class DispatchContentMixin(object):
course=course, course=course,
position=cdata['position'], position=cdata['position'],
title=cdata['title'], title=cdata['title'],
uuid=cdata['uuid'],
) )
if 'images' in cdata: if 'images' in cdata:
for image in cdata['images']: for image in cdata['images']:

@ -16,6 +16,7 @@ class AuthorBalanceCreateSerializer(serializers.ModelSerializer):
'commission', 'commission',
'status', 'status',
'payment', 'payment',
'card',
'cause', 'cause',
) )
@ -43,6 +44,7 @@ class AuthorBalanceSerializer(serializers.ModelSerializer):
'commission', 'commission',
'status', 'status',
'payment', 'payment',
'card',
'cause', 'cause',
) )

@ -1,13 +1,16 @@
from django.contrib.auth import get_user_model from phonenumber_field.serializerfields import PhoneNumberField
from rest_framework import serializers from rest_framework import serializers
from . import Base64ImageField from django.contrib.auth import get_user_model
from . import Base64ImageField
from apps.user.models import AuthorRequest
User = get_user_model() User = get_user_model()
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
phone = PhoneNumberField()
class Meta: class Meta:
model = User model = User
@ -15,6 +18,7 @@ class UserSerializer(serializers.ModelSerializer):
'id', 'id',
'username', 'username',
'email', 'email',
'phone',
'first_name', 'first_name',
'last_name', 'last_name',
'is_staff', 'is_staff',
@ -36,6 +40,7 @@ class UserSerializer(serializers.ModelSerializer):
'is_email_proved', 'is_email_proved',
'photo', 'photo',
'balance', 'balance',
'show_in_mainpage',
) )
read_only_fields = ( read_only_fields = (
@ -53,3 +58,30 @@ class UserPhotoSerializer(serializers.Serializer):
photo = Base64ImageField( photo = Base64ImageField(
required=False, allow_empty_file=True, allow_null=True required=False, allow_empty_file=True, allow_null=True
) )
class AuthorRequestSerializer(serializers.ModelSerializer):
class Meta:
model = AuthorRequest
fields = (
'id',
'first_name',
'last_name',
'email',
'about',
'facebook',
'status',
'cause',
'accepted_send_at',
'declined_send_at',
'created_at',
'update_at',
)
read_only_fields = (
'id',
'accepted_send_at',
'declined_send_at',
'created_at',
'update_at',
)

@ -8,8 +8,9 @@ from drf_yasg import openapi
from .auth import ObtainToken from .auth import ObtainToken
from .views import ( from .views import (
AuthorBalanceViewSet, ConfigViewSet, AuthorBalanceViewSet, AuthorRequestViewSet,
CategoryViewSet, CourseViewSet, ConfigViewSet, CategoryViewSet,
CourseViewSet, CommentViewSet,
MaterialViewSet, LikeViewSet, MaterialViewSet, LikeViewSet,
ImageViewSet, TextViewSet, ImageViewSet, TextViewSet,
ImageTextViewSet, VideoViewSet, ImageTextViewSet, VideoViewSet,
@ -19,9 +20,11 @@ from .views import (
) )
router = DefaultRouter() router = DefaultRouter()
router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests')
router.register(r'author-balance', AuthorBalanceViewSet, base_name='author-balance') router.register(r'author-balance', AuthorBalanceViewSet, base_name='author-balance')
router.register(r'courses', CourseViewSet, base_name='courses')
router.register(r'categories', CategoryViewSet, base_name='categories') router.register(r'categories', CategoryViewSet, base_name='categories')
router.register(r'courses', CourseViewSet, base_name='courses')
router.register(r'comments', CommentViewSet, base_name='comments')
router.register(r'materials', MaterialViewSet, base_name='materials') router.register(r'materials', MaterialViewSet, base_name='materials')
router.register(r'lessons', LessonViewSet, base_name='lessons') router.register(r'lessons', LessonViewSet, base_name='lessons')
router.register(r'likes', LikeViewSet, base_name='likes') router.register(r'likes', LikeViewSet, base_name='likes')

@ -1,10 +1,6 @@
from constance.admin import get_values
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework import status from rest_framework import status, views, viewsets, generics
from rest_framework import views, viewsets
from rest_framework import generics
from rest_framework.decorators import detail_route, list_route from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response from rest_framework.response import Response
@ -15,6 +11,7 @@ from .serializers.course import (
CategorySerializer, LikeSerializer, CategorySerializer, LikeSerializer,
CourseSerializer, CourseCreateSerializer, CourseSerializer, CourseCreateSerializer,
CourseBulkChangeCategorySerializer, CourseBulkChangeCategorySerializer,
CommentSerializer,
MaterialSerializer, MaterialCreateSerializer, MaterialSerializer, MaterialCreateSerializer,
LessonSerializer, LessonCreateSerializer, LessonSerializer, LessonCreateSerializer,
) )
@ -30,24 +27,34 @@ from .serializers.content import (
from .serializers.school import SchoolScheduleSerializer from .serializers.school import SchoolScheduleSerializer
from .serializers.payment import AuthorBalanceSerializer, AuthorBalanceCreateSerializer from .serializers.payment import AuthorBalanceSerializer, AuthorBalanceCreateSerializer
from .serializers.user import ( from .serializers.user import (
AuthorRequestSerializer,
UserSerializer, UserPhotoSerializer, UserSerializer, UserPhotoSerializer,
) )
from .permissions import IsAdmin, IsAdminOrIsSelf, IsAuthorOrAdmin, IsAuthorObjectOrAdmin from .permissions import IsAdmin, IsAdminOrIsSelf, IsAuthorOrAdmin, IsAuthorObjectOrAdmin
from apps.course.models import Category, Course, Material, Lesson, Like from apps.course.models import (
Category, Course,
Comment, CourseComment, LessonComment,
Material, Lesson,
Like,
)
from apps.config.models import Config
from apps.content.models import ( from apps.content.models import (
Image, Text, ImageText, Video, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject, Gallery, GalleryImage, ImageObject,
) )
from apps.payment.models import AuthorBalance from apps.payment.models import AuthorBalance
from apps.school.models import SchoolSchedule from apps.school.models import SchoolSchedule
from apps.user.models import AuthorRequest
User = get_user_model() User = get_user_model()
class AuthorBalanceViewSet(ExtendedModelViewSet): class AuthorBalanceViewSet(ExtendedModelViewSet):
queryset = AuthorBalance.objects.all() queryset = AuthorBalance.objects.filter(
author__role__in=[User.AUTHOR_ROLE, User.ADMIN_ROLE],
)
serializer_class = AuthorBalanceCreateSerializer serializer_class = AuthorBalanceCreateSerializer
serializer_class_map = { serializer_class_map = {
'list': AuthorBalanceSerializer, 'list': AuthorBalanceSerializer,
@ -317,15 +324,34 @@ class SchoolScheduleViewSet(ExtendedModelViewSet):
class ConfigViewSet(generics.RetrieveUpdateAPIView): class ConfigViewSet(generics.RetrieveUpdateAPIView):
queryset = Config.objects.all()
serializer_class = ConfigSerializer serializer_class = ConfigSerializer
permission_classes = (IsAdmin,) permission_classes = (IsAdmin,)
def retrieve(self, request, *args, **kwargs): def get_object(self):
serializer = ConfigSerializer(get_values()) return Config.load()
return Response(serializer.data)
def patch(self, request, *args, **kwargs):
serializer = ConfigSerializer(data=request.data) class CommentViewSet(ExtendedModelViewSet):
if serializer.is_valid(): queryset = Comment.objects.filter(level=0)
serializer.update(get_values(), serializer.validated_data) serializer_class = CommentSerializer
return Response(serializer.data) permission_classes = (IsAdmin,)
def get_queryset(self):
queryset = self.queryset
is_deactivated = self.request.query_params.get('is_deactivated', '0')
if is_deactivated == '0':
queryset = queryset
elif is_deactivated == '1':
queryset = queryset.filter(deactivated_at__isnull=True)
elif is_deactivated == '2':
queryset = queryset.filter(deactivated_at__isnull=False)
return queryset
class AuthorRequestViewSet(ExtendedModelViewSet):
queryset = AuthorRequest.objects.all()
serializer_class = AuthorRequestSerializer
permission_classes = (IsAdmin,)
filter_fields = ('status',)

@ -0,0 +1,18 @@
from django.contrib.auth import login
from django.utils.deprecation import MiddlewareMixin
from rest_framework.authtoken.models import Token
class TokenAuthLoginMiddleware(MiddlewareMixin):
def process_request(self, request):
if 'token' in request.GET:
token = request.GET.get('token')
if token:
try:
token = Token.objects.get(key=token)
user = token.user
login(request, user)
except Token.DoesNotExist:
pass

@ -3,8 +3,8 @@
{% block content %} {% block content %}
<p style="margin: 0 0 20px">Для восстановления пароля нажмите кнопку ниже.</p> <p style="margin: 0 0 20px">Для восстановления пароля нажмите кнопку ниже.</p>
<div style="margin-bottom: 10px;"> <div style="margin-bottom: 10px;">
<a href="{{ protocol}}://{{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %}" style="text-decoration: none; position: relative; padding: 13px 24px 12px; background-image: linear-gradient(-225deg, #D1FF7F 0%, #56FFFD 100%); border-radius: 3px; font-size: 12px; color: #191919; text-transform: uppercase; letter-spacing: 2px; text-align: center; transition: all .2s; z-index: 2;">Нажмите для восстановления</a> <a href="{{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %}" style="text-decoration: none; position: relative; padding: 13px 24px 12px; background-image: linear-gradient(-225deg, #D1FF7F 0%, #56FFFD 100%); border-radius: 3px; font-size: 12px; color: #191919; text-transform: uppercase; letter-spacing: 2px; text-align: center; transition: all .2s; z-index: 2;">Нажмите для восстановления</a>
<p>Или скопируйте ссылку ниже, и вставьте её в адресную строку браузера.</p> <p>Или скопируйте ссылку ниже, и вставьте её в адресную строку браузера.</p>
<p>{{ protocol}}://{{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %}</p> <p>{{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %}</p>
</div> </div>
{% endblock content %} {% endblock content %}

@ -1,2 +1,2 @@
Восстановление пароля для {{ email }}. Перейдите по ссылке ниже: Восстановление пароля для {{ email }}. Перейдите по ссылке ниже:
{{ protocol}}://{{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %} {{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %}

@ -48,8 +48,9 @@ class LearnerRegistrationView(FormView):
# fixme: change email text # fixme: change email text
# fixme: async send email # fixme: async send email
refferer = self.request.META.get('HTTP_REFERER')
token = verification_email_token.make_token(user) token = verification_email_token.make_token(user)
url = self.request.scheme + '://' + self.request.get_host() + str(reverse_lazy('lilcity:verification-email', args=[token])) url = refferer + str(reverse_lazy('lilcity:verification-email', args=[token]))
send_email('Verification Email', email, "notification/email/verification_email.html", url=url) send_email('Verification Email', email, "notification/email/verification_email.html", url=url)
return JsonResponse({"success": True}, status=201) return JsonResponse({"success": True}, status=201)
@ -106,7 +107,9 @@ class PasswordResetView(views.PasswordContextMixin, BaseFormView):
token_generator = views.default_token_generator token_generator = views.default_token_generator
def form_valid(self, form): def form_valid(self, form):
refferer = self.request.META.get('HTTP_REFERER')
opts = { opts = {
'domain_override': refferer,
'use_https': self.request.is_secure(), 'use_https': self.request.is_secure(),
'token_generator': self.token_generator, 'token_generator': self.token_generator,
'from_email': self.from_email, 'from_email': self.from_email,

@ -0,0 +1,5 @@
from django.contrib import admin
from .models import Config
admin.site.register(Config)

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ConfigConfig(AppConfig):
name = 'config'

@ -0,0 +1,27 @@
# Generated by Django 2.0.3 on 2018-03-26 10:25
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Config',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('INSTAGRAM_CLIENT_ACCESS_TOKEN', models.CharField(default='7145314808.f6fa114.6b737a5355534e0eb5cf7c40cb4998f6', max_length=51)),
('INSTAGRAM_CLIENT_SECRET', models.CharField(default='2334a921425140ccb180d145dcd35b25', max_length=32)),
('INSTAGRAM_PROFILE_URL', models.CharField(default='#', max_length=126)),
('SERVICE_COMMISSION', models.IntegerField(default=10)),
('SERVICE_DISCOUNT_MIN_AMOUNT', models.IntegerField(default=3500)),
('SERVICE_DISCOUNT', models.ImageField(default=1000, upload_to='')),
('SCHOOL_LOGO_IMAGE', models.ImageField(null=True, upload_to='')),
],
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.3 on 2018-03-26 10:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='config',
name='SCHOOL_LOGO_IMAGE',
field=models.FileField(null=True, upload_to=''),
),
]

@ -0,0 +1,23 @@
# Generated by Django 2.0.3 on 2018-03-26 10:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0002_auto_20180326_1026'),
]
operations = [
migrations.AlterField(
model_name='config',
name='SCHOOL_LOGO_IMAGE',
field=models.ImageField(null=True, upload_to=''),
),
migrations.AlterField(
model_name='config',
name='SERVICE_DISCOUNT',
field=models.IntegerField(default=1000),
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.3 on 2018-03-26 11:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0003_auto_20180326_1027'),
]
operations = [
migrations.AddField(
model_name='config',
name='MAIN_PAGE_TOP_IMAGE',
field=models.ImageField(null=True, upload_to=''),
),
]

@ -0,0 +1,23 @@
# Generated by Django 2.0.3 on 2018-03-26 13:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0004_config_main_page_top_image'),
]
operations = [
migrations.AlterField(
model_name='config',
name='MAIN_PAGE_TOP_IMAGE',
field=models.ImageField(blank=True, null=True, upload_to=''),
),
migrations.AlterField(
model_name='config',
name='SCHOOL_LOGO_IMAGE',
field=models.ImageField(blank=True, null=True, upload_to=''),
),
]

@ -0,0 +1,39 @@
from django.db import models
class Config(models.Model):
INSTAGRAM_CLIENT_ACCESS_TOKEN = models.CharField(
max_length=51, default='7145314808.f6fa114.6b737a5355534e0eb5cf7c40cb4998f6'
)
INSTAGRAM_CLIENT_SECRET = models.CharField(max_length=32, default='2334a921425140ccb180d145dcd35b25')
INSTAGRAM_PROFILE_URL = models.CharField(max_length=126, default='#')
SERVICE_COMMISSION = models.IntegerField(default=10)
SERVICE_DISCOUNT_MIN_AMOUNT = models.IntegerField(default=3500)
SERVICE_DISCOUNT = models.IntegerField(default=1000)
SCHOOL_LOGO_IMAGE = models.ImageField(null=True, blank=True)
MAIN_PAGE_TOP_IMAGE = models.ImageField(null=True, blank=True)
def save(self, *args, **kwargs):
self.pk = 1
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
pass
@classmethod
def load(cls):
try:
obj, created = cls.objects.get_or_create(pk=1)
except:
# This magic for migrate
obj = {
'INSTAGRAM_CLIENT_ACCESS_TOKEN': '',
'INSTAGRAM_CLIENT_SECRET': '',
'INSTAGRAM_PROFILE_URL': '',
'SERVICE_COMMISSION': '',
'SERVICE_DISCOUNT_MIN_AMOUNT': '',
'SERVICE_DISCOUNT': '',
'SCHOOL_LOGO_IMAGE': '',
'MAIN_PAGE_TOP_IMAGE': '',
}
return obj

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

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

@ -63,7 +63,6 @@ class ContentAdmin(PolymorphicParentModelAdmin):
Text, Text,
ImageText, ImageText,
Video, Video,
# GalleryAdmin,
) )

@ -0,0 +1,18 @@
# Generated by Django 2.0.3 on 2018-03-16 11:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content', '0014_auto_20180215_1503'),
]
operations = [
migrations.AddField(
model_name='content',
name='uuid',
field=models.UUIDField(blank=True, null=True),
),
]

@ -17,6 +17,7 @@ class ImageObject(models.Model):
class Content(PolymorphicModel): class Content(PolymorphicModel):
uuid = models.UUIDField(null=True, blank=True)
course = models.ForeignKey( course = models.ForeignKey(
'course.Course', on_delete=models.CASCADE, 'course.Course', on_delete=models.CASCADE,
null=True, blank=True, null=True, blank=True,

@ -3,22 +3,24 @@ import json
import requests import requests
import shutil import shutil
from constance import config
from instagram.client import InstagramAPI from instagram.client import InstagramAPI
from project.celery import app from project.celery import app
from time import sleep from time import sleep
from django.conf import settings from django.conf import settings
from apps.config.models import Config
@app.task @app.task
def retrieve_photos(): def retrieve_photos():
config = Config.load()
api = InstagramAPI( api = InstagramAPI(
access_token=config.INSTAGRAM_CLIENT_ACCESS_TOKEN, access_token=config.INSTAGRAM_CLIENT_ACCESS_TOKEN,
client_secret=config.INSTAGRAM_CLIENT_SECRET, client_secret=config.INSTAGRAM_CLIENT_SECRET,
) )
recent_media, next_ = api.user_recent_media(user_id='self', count=20) recent_media, next_ = api.user_recent_media(user_id='self', count=20)
path = os.path.join(settings.BASE_DIR, config.INSTAGRAM_RESULTS_PATH) path = os.path.join(settings.BASE_DIR, settings.INSTAGRAM_RESULTS_PATH)
for idx, media in enumerate(recent_media): for idx, media in enumerate(recent_media):
try: try:
fname = os.path.join(path, f'{idx}.jpg') fname = os.path.join(path, f'{idx}.jpg')

@ -0,0 +1,18 @@
# Generated by Django 2.0.2 on 2018-03-12 10:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0034_auto_20180215_1503'),
]
operations = [
migrations.AddField(
model_name='comment',
name='deactivated_at',
field=models.DateTimeField(blank=True, default=None, null=True),
),
]

@ -196,7 +196,7 @@ class Material(models.Model):
ordering = ('title',) ordering = ('title',)
class Comment(PolymorphicMPTTModel): class Comment(PolymorphicMPTTModel, DeactivatedMixin):
content = models.TextField('Текст комментария', default='') content = models.TextField('Текст комментария', default='')
author = models.ForeignKey(User, on_delete=models.CASCADE) author = models.ForeignKey(User, on_delete=models.CASCADE)
parent = PolymorphicTreeForeignKey( parent = PolymorphicTreeForeignKey(

@ -1,3 +1,4 @@
{% load thumbnail %}
{% load static %} {% load static %}
{% load data_liked from data_liked %} {% load data_liked from data_liked %}
@ -7,11 +8,11 @@
{% if course.is_deferred_start %}data-future-course data-future-course-time={{ course.deferred_start_at.timestamp }}{% endif %} {% if course.is_deferred_start %}data-future-course data-future-course-time={{ course.deferred_start_at.timestamp }}{% endif %}
> >
<a class="courses__preview" href="{% if course.status == 0 %}{% url 'course_edit' course.id %}{% else %}{% url 'course' course.id %}?next={{ request.get_full_path }}{% endif %}"> <a class="courses__preview" href="{% if course.status == 0 %}{% url 'course_edit' course.id %}{% else %}{% url 'course' course.id %}?next={{ request.get_full_path }}{% endif %}">
{% if course.cover %} {% thumbnail course.cover.image "300x170" crop="center" as im %}
<img width="300px" height="170px" class="courses__pic" src="{{ course.cover.image.url }}"/> <img class="courses__pic" src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}"/>
{% else %} {% empty %}
<img width="300px" height="170px" class="courses__pic" src="{% static 'img/no_cover.png' %}"/> <img class="courses__pic" src="{% static 'img/no_cover.png' %}" width="300px" height="170px"/>
{% endif %} {% endthumbnail %}
<div class="courses__view">Подробнее</div> <div class="courses__view">Подробнее</div>
{% if course.is_featured %} {% if course.is_featured %}
<div class="courses__label courses__label_fav"></div> <div class="courses__label courses__label_fav"></div>
@ -32,6 +33,16 @@
<div class="courses__time">ЧЕРНОВИК</div> <div class="courses__time">ЧЕРНОВИК</div>
</div> </div>
<div class="courses__label courses__label_draft"></div> <div class="courses__label courses__label_draft"></div>
{% elif course.status == 3 %}
<div class="courses__soon">
<div class="courses__time">В АРХИВЕ</div>
</div>
<div class="courses__label courses__label_draft"></div>
{% elif course.status == 4 %}
<div class="courses__soon">
<div class="courses__time">ОТКЛОНЕН</div>
</div>
<div class="courses__label courses__label_draft"></div>
{% endif %} {% endif %}
</a> </a>
<div class="courses__details"> <div class="courses__details">
@ -86,4 +97,4 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>

@ -1,6 +1,7 @@
{% load static %} {% load static %}
{% if not node.deactivated_at %}
<div id="question__{{ node.id }}" class="questions__item {% if node.is_child_node %}questions__item_reply{% endif %}"> <a class="questions__anchor" id="question__{{ node.id }}"></a>
<div id="question__replyto__{{ node.id }}" class="questions__item {% if node.is_child_node %}questions__item_reply{% endif %}">
{% if node.author.photo %} {% if node.author.photo %}
<div class="questions__ava ava"> <div class="questions__ava ava">
<img class="ava__pic" src="{{ node.author.photo.url }}"> <img class="ava__pic" src="{{ node.author.photo.url }}">
@ -24,4 +25,5 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endif %}

@ -1,5 +1,10 @@
{% load mptt_tags %} {% load mptt_tags %}
{% recursetree object.comments.all %} {% recursetree object.comments.all %}
{% if not node.deactivated_at %}
{% include './comment.html' %} {% include './comment.html' %}
{{ children }} {% endrecursetree %} {% if not node.is_leaf_node %}
{{ children }}
{% endif %}
{% endif %}
{% endrecursetree %}

@ -1,35 +1,32 @@
{% load thumbnail %}
{% if results %} {% if results %}
<div class="title">Галерея итогов обучения</div> <div class="title">Галерея итогов обучения</div>
<div class="examples"> <div class="examples gallery">
{% for image in course.gallery.gallery_images.all %} {% for image in course.gallery.gallery_images.all %}
<div class="examples__item"> <div class="examples__item">
<img class="examples__pic" src="{{ image.img.image.url }}"> <a href="{{ image.img.image.url }}">
{% thumbnail image.img.image "140x140" crop="center" as im %}
<img class="examples__pic" src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}">
{% endthumbnail %}
</a>
</div>
{% endfor %}
</div> </div>
{% endfor %}
</div>
{% else %} {% else %}
<div class="section section_gradient"> <div class="section section_gradient">
<div class="section__center center center_sm"> <div class="section__center center center_sm">
<div class="title">{{ content.title }}</div> <div class="title">{{ content.title }}</div>
<div class="examples"> <div class="examples">
{% for image in content.gallery_images.all %} {% for image in content.gallery_images.all %}
<div class="examples__item"> <div class="examples__item">
<img class="examples__pic" src="{{ image.img.image.url }}"> <a href="{{ image.img.image.url }}">
{% thumbnail image.img.image "140x140" crop="center" as im %}
<img class="examples__pic" src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}">
{% endthumbnail %}
</a>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div>
</div> </div>
{% endif %}
<!--<div class="section section_gradient">
<div class="section__center center center_sm">
<div class="title">Галерея итогов обучения</div>
<div class="examples">
</div>
</div> </div>
</div>--> {% endif %}

@ -7,4 +7,4 @@
<img class="content-block pic" src="{{ content.img.image.url }}" alt=""> <img class="content-block pic" src="{{ content.img.image.url }}" alt="">
</div> </div>
</div> </div>
</div> </div>

@ -10,4 +10,4 @@
</div> </div>
</div> </div>
</div> </div>

@ -14,7 +14,7 @@
{% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %} {% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %}
{% block content %} {% block content %}
<div class="section section_border"> <div class="section section_border course">
<div class="section__center center center_sm"> <div class="section__center center center_sm">
<div class="go"> <div class="go">
<a class="go__item" href="{% if next %}{{next}}{% else %}{% url 'courses' %}{% endif %}"> <a class="go__item" href="{% if next %}{{next}}{% else %}{% url 'courses' %}{% endif %}">
@ -30,6 +30,7 @@
class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md" class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md"
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if not pending %} {% if not pending %}
data-course-buy
href="{% url 'course-checkout' course.id %}" href="{% url 'course-checkout' course.id %}"
{% endif %} {% endif %}
{% else %} {% else %}
@ -173,7 +174,7 @@
{% if course.is_deferred_start %} {% if course.is_deferred_start %}
<div class="video__soon"> <div class="video__soon">
<div class="video__title">Курс начнется:</div> <div class="video__title">Курс начнется:</div>
<div class="video__time">{{ course.deferred_start_at_humanize }}</div> <div class="video__time" data-future>{{ course.deferred_start_at_humanize }}</div>
</div> </div>
{% else %} {% else %}
{% comment %} <svg class="icon icon-play"> {% comment %} <svg class="icon icon-play">
@ -392,6 +393,7 @@
class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md" class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md"
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if not pending %} {% if not pending %}
data-course-buy
href="{% url 'course-checkout' course.id %}" href="{% url 'course-checkout' course.id %}"
{% endif %} {% endif %}
{% else %} {% else %}
@ -407,11 +409,18 @@
<div class="title">Задавайте вопросы:</div> <div class="title">Задавайте вопросы:</div>
<div class="questions"> <div class="questions">
{% if user.is_authenticated %} {% if request.user.is_authenticated %}
<form class="questions__form" method="post" action="{% url 'coursecomment' course_id=course.id %}"> <form class="questions__form" method="post" action="{% url 'coursecomment' course_id=course.id %}">
<input type="hidden" name="reply_id"> <input type="hidden" name="reply_id">
<div class="questions__ava ava"> <div class="questions__ava ava">
<img class="ava__pic" src="{% static 'img/user.jpg' %}"> <img
class="ava__pic"
{% if request.user.photo %}
src="{{ request.user.photo.url }}"
{% else %}
src="{% static 'img/user.jpg' %}"
{% endif %}
>
</div> </div>
<div class="questions__wrap"> <div class="questions__wrap">
<div class="questions__reply-info">В ответ на <div class="questions__reply-info">В ответ на

@ -1,13 +1,19 @@
{% extends "templates/lilcity/edit_index.html" %} {% extends "templates/lilcity/edit_index.html" %}
{% load static %} {% load static %}
{% block title %}{% if course %}Редактирование курса {{ course.title }}{% else %}Создание курса{% endif %}{% endblock title%} {% block title %}
{% if course %}
Редактирование {% if not live %}курса{% else %}стрима{% endif %} {{ course.title }}
{% else %}
Создание {% if not live %}курса{% else %}стрима{% endif %}
{% endif %}
{% endblock title%}
{% block content %} {% block content %}
<course-redactor author-picture="{% if request.user.photo %}{{ request.user.photo.url }}{% else %}{% static 'img/user.jpg' %}{% endif %}" <course-redactor :live="{{ live }}" 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 }}" author-name="{{ request.user.first_name }} {{ request.user.last_name }}"
access-token="{{ request.user.auth_token }}" access-token="{{ request.user.auth_token }}"
{% if course and course.id %}:course-id="{{ course.id }}"{% endif %}></course-redactor> {% if course and course.id %}:course-id="{{ course.id }}"{% endif %}></course-redactor>
{% endblock content %} {% endblock content %}
{% block foot %} {% block foot %}
<script type="text/javascript" src={% static "courseRedactor.js" %}></script> <script type="text/javascript" src="{% static "courseRedactor.js" %}"></script>
<link rel="stylesheet" href={% static "courseRedactor.css" %}/> <link rel="stylesheet" href="{% static "courseRedactor.css" %}" />
{% endblock foot %} {% endblock foot %}

@ -33,7 +33,7 @@
<img class="video__pic" src="{{ lesson.cover.image.url }}"/> <img class="video__pic" src="{{ lesson.cover.image.url }}"/>
{% else %} {% else %}
<img class="video__pic" src="{% static 'img/no_cover.png' %}"/> <img class="video__pic" src="{% static 'img/no_cover.png' %}"/>
{% endif %} {% endif %}
<svg class="icon icon-play"> <svg class="icon icon-play">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-play"></use> <use xlink:href="{% static 'img/sprite.svg' %}#icon-play"></use>
</svg> </svg>
@ -95,10 +95,17 @@
<div class="section__center center center_sm"> <div class="section__center center center_sm">
<div class="title">Задавайте вопросы:</div> <div class="title">Задавайте вопросы:</div>
<div class="questions"> <div class="questions">
{% if user.is_authenticated %} {% if request.user.is_authenticated %}
<form class="questions__form" method="post" action="{% url 'lessoncomment' lesson_id=lesson.id %}"> <form class="questions__form" method="post" action="{% url 'lessoncomment' lesson_id=lesson.id %}">
<div class="questions__ava ava"> <div class="questions__ava ava">
<img class="ava__pic" src="{% static 'img/user.jpg' %}"> <img
class="ava__pic"
{% if request.user.photo %}
src="{{ request.user.photo.url }}"
{% else %}
src="{% static 'img/user.jpg' %}"
{% endif %}
>
</div> </div>
<div class="questions__wrap"> <div class="questions__wrap">
<div class="questions__field"> <div class="questions__field">

@ -7,6 +7,7 @@ from django.http import JsonResponse, Http404
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.template import loader, Context, Template from django.template import loader, Context, Template
from django.views.generic import View, CreateView, DetailView, ListView, TemplateView from django.views.generic import View, CreateView, DetailView, ListView, TemplateView
from django.utils.cache import add_never_cache_headers
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
@ -170,11 +171,37 @@ class CourseEditView(TemplateView):
def get_context_data(self): def get_context_data(self):
context = super().get_context_data() context = super().get_context_data()
context['live'] = 'false'
if self.object: if self.object:
context['course'] = self.object context['course'] = self.object
return context return context
@method_decorator(login_required, name='dispatch')
class CourseLiveEditView(TemplateView):
template_name = 'course/course_edit.html'
def get(self, request, pk=None):
drafts = Course.objects.filter(
author=request.user, status=Course.DRAFT
)
if pk:
self.object = get_object_or_404(Course, pk=pk)
elif drafts.exists():
self.object = drafts.last()
else:
self.object = Course.objects.create()
if request.user != self.object.author and request.user.role not in [User.ADMIN_ROLE, User.AUTHOR_ROLE]:
raise Http404
return super().get(request)
def get_context_data(self):
context = super().get_context_data()
context['live'] = 'true'
if self.object:
context['course'] = self.object
return context
# @method_decorator(login_required, name='dispatch') # @method_decorator(login_required, name='dispatch')
class CourseView(DetailView): class CourseView(DetailView):
model = Course model = Course
@ -185,7 +212,7 @@ class CourseView(DetailView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs) response = super().get(request, *args, **kwargs)
context = self.get_context_data() context = self.get_context_data()
if not request.user.is_authenticated or (not request.user.is_authenticated and self.object.status != Course.PUBLISHED) or\ if (not request.user.is_authenticated and self.object.status != Course.PUBLISHED) or\
(request.user.is_authenticated and request.user.role not in [User.AUTHOR_ROLE, User.ADMIN_ROLE] and self.object.author != request.user and self.only_lessons and not context['paid']): (request.user.is_authenticated and request.user.role not in [User.AUTHOR_ROLE, User.ADMIN_ROLE] and self.object.author != request.user and self.only_lessons and not context['paid']):
raise Http404 raise Http404
return response return response
@ -245,12 +272,14 @@ class CoursesView(ListView):
else: else:
prev_url = None prev_url = None
next_url = None next_url = None
return JsonResponse({ response = JsonResponse({
'success': True, 'success': True,
'content': html, 'content': html,
'prev_url': prev_url, 'prev_url': prev_url,
'next_url': next_url, 'next_url': next_url,
}) })
add_never_cache_headers(response)
return response
else: else:
return super().get(request, args, kwargs) return super().get(request, args, kwargs)

@ -45,7 +45,7 @@
<table style="width:100%;padding:0;border-collapse:collapse;border-top:1px solid #979797"> <table style="width:100%;padding:0;border-collapse:collapse;border-top:1px solid #979797">
<tbody> <tbody>
<tr> <tr>
<td style="width:33.33%;padding:35px 0;vertical-align:middle;font-size:12px;color:#888888">2017 © Lil City, UAB. </td> <td style="width:33.33%;padding:35px 0;vertical-align:middle;font-size:12px;color:#888888">{% now "Y" %} © Lil City, UAB. </td>
<td style="width:33.33%;padding:35px 0;vertical-align:middle;text-align:center"> <td style="width:33.33%;padding:35px 0;vertical-align:middle;text-align:center">
<a href="#" style="display:inline-block;margin:0 5px;vertical-align:middle;font-size:0"> <a href="#" style="display:inline-block;margin:0 5px;vertical-align:middle;font-size:0">
<img width="16" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAAEzo7pQAAABL1BMVEUAAAD///9VVVVAQEAzM2YrK1UkSUkuLkYnJzsiM0QgMEAeLTwmJkAkMT0hLDcdJzskLjciKzwgKDgeLTweKzceKjUcKDkgKzUdJjogKjggKTcfKDYeJjUeKTgdKDYcKTYfKDgeJzcdJjgdKTcdKDYeJzceKDcdJzUdJjccKDYcKDYeJzUeJzcdKDceJzYdJzYdJzUdJzYdJjcdKDYdJzUcJzcdJjYdKDUcJjUcJjceJzYdJjcdJzYcJzUdJzYdJzYdJjUdJjUcJzYdJzUdJjYdJzYcJjYdJzUcJjUdJzUdJjYdJjYcJzUcJzYdJjUcJzYdJzUdJzYdJjYcJjYcJzUcJzYdJzYdJjUcJzYcJzYcJzUdJjYdJjYdJjUdJzUdJzUcJzYcJjYcJjUcJjUcJjWqhzSBAAAAZHRSTlMAAQMEBQYHCw0PEBEUFRcaHB4gIiorLTA1Nzg5Q0RHUVJUV1hZXGZpamxtbm90d3t8hIyNj5GTlJmam5+qrLC4uru8wMHDyM3O0tTV19nd4Obn6Onr7O3w8vP09vf4+fr7/P3+8ZYTQAAAAZRJREFUOMt1k1dj00AQhD8TK4gOCQIEoofeQdgodAOGWJQkkGDR0c3//w08nM9eKXhfpJsZbZk9gTRG/AEJqAASCTiPjcQ/MsQVuAmIznH+ehBgA0pJysF5cQ2w/QAEfIYvyBNCgNh1FhFbDOjBwKWhbryqLmXa6GUi3fx6aOGpTL5owgbgzVWAaAawV5IueVVetJIS1ZpFDyAfh+MoAboq4qBP3QDqzKZwSZghaEoPdJ7pw5JpfVnXOalHpvX9AIoDsO8bANceB+BoCcCZ58Y9gPLcFHg1Am4bP3giaatpbnB13AJWc/rr5pwJ6Bs76i5A1LcWWbrXBehrveGISbihHuQqmBuFcirFFlrsNLrW2N9SAI4MJf100q87syVqKlhY0+vD/vXid13eKXirlel3naG/FVZwTGum9mm9bAvi+sfumeCW32ejxIqqpcDf06fFHQIOvNfv+6cOLl8YSnf/MwXAiYfvNj++uLEHO2apdL6TqUYktcvm8ZmrE4gGckUat8k4LZwG0eS3z8uqveuqzBOAf9XIdaH2KUvyAAAAAElFTkSuQmCC"> <img width="16" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAAEzo7pQAAABL1BMVEUAAAD///9VVVVAQEAzM2YrK1UkSUkuLkYnJzsiM0QgMEAeLTwmJkAkMT0hLDcdJzskLjciKzwgKDgeLTweKzceKjUcKDkgKzUdJjogKjggKTcfKDYeJjUeKTgdKDYcKTYfKDgeJzcdJjgdKTcdKDYeJzceKDcdJzUdJjccKDYcKDYeJzUeJzcdKDceJzYdJzYdJzUdJzYdJjcdKDYdJzUcJzcdJjYdKDUcJjUcJjceJzYdJjcdJzYcJzUdJzYdJzYdJjUdJjUcJzYdJzUdJjYdJzYcJjYdJzUcJjUdJzUdJjYdJjYcJzUcJzYdJjUcJzYdJzUdJzYdJjYcJjYcJzUcJzYdJzYdJjUcJzYcJzYcJzUdJjYdJjYdJjUdJzUdJzUcJzYcJjYcJjUcJjUcJjWqhzSBAAAAZHRSTlMAAQMEBQYHCw0PEBEUFRcaHB4gIiorLTA1Nzg5Q0RHUVJUV1hZXGZpamxtbm90d3t8hIyNj5GTlJmam5+qrLC4uru8wMHDyM3O0tTV19nd4Obn6Onr7O3w8vP09vf4+fr7/P3+8ZYTQAAAAZRJREFUOMt1k1dj00AQhD8TK4gOCQIEoofeQdgodAOGWJQkkGDR0c3//w08nM9eKXhfpJsZbZk9gTRG/AEJqAASCTiPjcQ/MsQVuAmIznH+ehBgA0pJysF5cQ2w/QAEfIYvyBNCgNh1FhFbDOjBwKWhbryqLmXa6GUi3fx6aOGpTL5owgbgzVWAaAawV5IueVVetJIS1ZpFDyAfh+MoAboq4qBP3QDqzKZwSZghaEoPdJ7pw5JpfVnXOalHpvX9AIoDsO8bANceB+BoCcCZ58Y9gPLcFHg1Am4bP3giaatpbnB13AJWc/rr5pwJ6Bs76i5A1LcWWbrXBehrveGISbihHuQqmBuFcirFFlrsNLrW2N9SAI4MJf100q87syVqKlhY0+vD/vXid13eKXirlel3naG/FVZwTGum9mm9bAvi+sfumeCW32ejxIqqpcDf06fFHQIOvNfv+6cOLl8YSnf/MwXAiYfvNj++uLEHO2apdL6TqUYktcvm8ZmrE4gGckUat8k4LZwG0eS3z8uqveuqzBOAf9XIdaH2KUvyAAAAAElFTkSuQmCC">

@ -0,0 +1,13 @@
{% extends "notification/email/_base.html" %}
{% block content %}
<p style="margin: 0 0 20px">Поздравляем! Вам одобрено назначение преподавателем!</p>
<div style="margin-bottom: 10px;">
<p>Теперь вы можете публиковать курсы.</p>
{% if password and email %}
<p><strong>Параметры входа:</strong></p>
<p><strong>email:</strong> {{ email }}</p>
<p><strong>пароль:</strong> {{ password }}</p>
{% endif %}
</div>
{% endblock content %}

@ -0,0 +1,8 @@
{% extends "notification/email/_base.html" %}
{% block content %}
<p style="margin: 0 0 20px">К сожалению вам отказано в назначении преподавателем!</p>
<div style="margin-bottom: 10px;">
<p>{{ cause }}</p>
</div>
{% endblock content %}

@ -3,8 +3,10 @@ from twilio.rest import Client
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.conf import settings from django.conf import settings
from django.template.loader import get_template from django.template.loader import get_template
from project.celery import app
@app.task
def send_email(subject, to_email, template_name, **kwargs): def send_email(subject, to_email, template_name, **kwargs):
html = get_template(template_name).render(kwargs) html = get_template(template_name).render(kwargs)
email = EmailMessage(subject, html, to=[to_email]) email = EmailMessage(subject, html, to=[to_email])

@ -1,4 +1,3 @@
from constance import config
from paymentwall import Pingback from paymentwall import Pingback
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
@ -9,9 +8,12 @@ from django.core.validators import RegexValidator
from django.utils.timezone import now from django.utils.timezone import now
from apps.course.models import Course from apps.course.models import Course
from apps.config.models import Config
from apps.school.models import SchoolSchedule from apps.school.models import SchoolSchedule
from apps.notification.utils import send_email from apps.notification.utils import send_email
config = Config.load()
User = get_user_model() User = get_user_model()
CREDIT_CARD_RE = r'^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\d{11})$' CREDIT_CARD_RE = r'^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\d{11})$'
@ -157,7 +159,7 @@ class SchoolPayment(Payment):
models.Sum('month_price'), models.Sum('month_price'),
) )
month_price_sum = aggregate.get('month_price__sum', 0) month_price_sum = aggregate.get('month_price__sum', 0)
if len(self.weekdays) > 7: if month_price_sum > config.SERVICE_DISCOUNT_MIN_AMOUNT:
discount = config.SERVICE_DISCOUNT discount = config.SERVICE_DISCOUNT
else: else:
discount = 0 discount = 0

@ -0,0 +1,18 @@
from mixpanel import Mixpanel
from django.conf import settings
from project.celery import app
@app.task
def transaction_to_mixpanel(user_id, amount, time, product_type):
mix = Mixpanel(settings.MIX_TOKEN)
mix.people_track_charge(
user_id,
amount,
{
'$time': time,
'product_type': product_type,
}
)

@ -0,0 +1,12 @@
{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %}
<div class="section">
<div class="section__center center center_xs">
<div class="done">
<div class="done__title title">Вы успешно приобрели курс!</div>
<div class="done__foot">
<a class="done__btn btn btn_md btn_stroke" href="{% url 'course' course.id %}">ПЕРЕЙТИ К КУРСУ</a>
</div>
</div>
</div>
</div>
{% endblock content %}

@ -6,7 +6,7 @@ from datetime import timedelta
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect, get_object_or_404
from django.views.generic import View, TemplateView from django.views.generic import View, TemplateView
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -17,12 +17,27 @@ from paymentwall import Pingback, Product, Widget
from apps.course.models import Course from apps.course.models import Course
from apps.school.models import SchoolSchedule from apps.school.models import SchoolSchedule
from apps.payment.tasks import transaction_to_mixpanel
from .models import AuthorBalance, CoursePayment, SchoolPayment from .models import AuthorBalance, CoursePayment, SchoolPayment
logger = logging.getLogger('django') logger = logging.getLogger('django')
@method_decorator(login_required, name='dispatch')
class CourseBuySuccessView(TemplateView):
template_name = 'payment/course_payment_success.html'
def get(self, request, pk=None, *args, **kwargs):
course = get_object_or_404(Course, pk=pk)
return self.render_to_response(context={'course': course})
@method_decorator(login_required, name='dispatch')
class SchoolBuySuccessView(TemplateView):
template_name = 'payment/payment_success.html'
@method_decorator(login_required, name='dispatch') @method_decorator(login_required, name='dispatch')
class CourseBuyView(TemplateView): class CourseBuyView(TemplateView):
template_name = 'payment/paymentwall_widget.html' template_name = 'payment/paymentwall_widget.html'
@ -31,7 +46,7 @@ class CourseBuyView(TemplateView):
host = request.scheme + '://' + request.get_host() host = request.scheme + '://' + request.get_host()
course = Course.objects.get(id=pk) course = Course.objects.get(id=pk)
if request.user == course.author: if request.user == course.author:
messages.error('Вы не можете приобрести свой курс.') messages.error(request, 'Вы не можете приобрести свой курс.')
return redirect(reverse_lazy('course', args=[course.id])) return redirect(reverse_lazy('course', args=[course.id]))
course_payment = CoursePayment.objects.create( course_payment = CoursePayment.objects.create(
user=request.user, user=request.user,
@ -52,7 +67,7 @@ class CourseBuyView(TemplateView):
'evaluation': 1, 'evaluation': 1,
'demo': 1, 'demo': 1,
'test_mode': 1, 'test_mode': 1,
'success_url': host + str(reverse_lazy('payment-success')), 'success_url': host + str(reverse_lazy('course_payment_success', args=[course.id])),
'failure_url': host + str(reverse_lazy('payment-error')), 'failure_url': host + str(reverse_lazy('payment-error')),
} }
) )
@ -132,27 +147,35 @@ class PaymentwallCallbackView(View):
logger.info( logger.info(
json.dumps(payment_raw_data), json.dumps(payment_raw_data),
) )
payment.status = pingback.get_type() payment.status = pingback.get_type()
payment.data = payment_raw_data payment.data = payment_raw_data
if pingback.is_deliverable() and product_type_name == 'school': if pingback.is_deliverable():
school_payment = SchoolPayment.objects.filter( transaction_to_mixpanel.delay(
user=payment.user, payment.user.id,
date_start__lte=now(), payment.amount,
date_end__gt=now(), now().strftime('%Y-%m-%dT%H:%M:%S'),
status__in=[ product_type_name,
Pingback.PINGBACK_TYPE_REGULAR, )
Pingback.PINGBACK_TYPE_GOODWILL, if product_type_name == 'school':
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, school_payment = SchoolPayment.objects.filter(
], user=payment.user,
).last() date_start__lte=now(),
if school_payment: date_end__gt=now(),
date_start = school_payment.date_end + timedelta(days=1) status__in=[
date_end = date_start + timedelta(days=30) Pingback.PINGBACK_TYPE_REGULAR,
else: Pingback.PINGBACK_TYPE_GOODWILL,
date_start = now() Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
date_end = now() + timedelta(days=30) ],
payment.date_start = date_start ).last()
payment.date_end = date_end if school_payment:
date_start = school_payment.date_end + timedelta(days=1)
date_end = date_start + timedelta(days=30)
else:
date_start = now()
date_end = now() + timedelta(days=30)
payment.date_start = date_start
payment.date_end = date_end
payment.save() payment.save()
author_balance = getattr(payment, 'author_balance', None) author_balance = getattr(payment, 'author_balance', None)

@ -3,6 +3,8 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .models import AuthorRequest, EmailSubscription, SubscriptionCategory
User = get_user_model() User = get_user_model()
@ -13,7 +15,35 @@ class UserAdmin(BaseUserAdmin):
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'gender', 'about', 'photo')}), (_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'gender', 'about', 'photo')}),
('Facebook Auth data', {'fields': ('fb_id', 'fb_data', 'is_email_proved')}), ('Facebook Auth data', {'fields': ('fb_id', 'fb_data', 'is_email_proved')}),
(_('Permissions'), {'fields': ('role', 'is_active', 'is_staff', 'is_superuser', (_('Permissions'), {'fields': ('role', 'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}), 'groups', 'user_permissions', 'show_in_mainpage')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}), (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
('Social urls', {'fields': ('instagram', 'facebook', 'twitter', 'pinterest', 'youtube', 'vkontakte', )}), ('Social urls', {'fields': ('instagram', 'facebook', 'twitter', 'pinterest', 'youtube', 'vkontakte', )}),
) )
@admin.register(AuthorRequest)
class AuthorRequestAdmin(admin.ModelAdmin):
list_display = (
'email',
'first_name',
'last_name',
'status',
'accepted_send_at',
'declined_send_at',
'created_at',
'update_at',
)
@admin.register(SubscriptionCategory)
class SubscriptionCategoryAdmin(admin.ModelAdmin):
list_display = ('title',)
@admin.register(EmailSubscription)
class EmailSubscriptionAdmin(admin.ModelAdmin):
list_display = (
'id',
'user',
'email',
)

@ -0,0 +1,58 @@
[
{
"model": "user.subscriptioncategory",
"pk": 1,
"fields": {
"title": "Новости школы",
"auto_add": false
}
},
{
"model": "user.subscriptioncategory",
"pk": 2,
"fields": {
"title": "Новые курсы",
"auto_add": true
}
},
{
"model": "user.subscriptioncategory",
"pk": 3,
"fields": {
"title": "Бонусы от партнёров",
"auto_add": false
}
},
{
"model": "user.subscriptioncategory",
"pk": 4,
"fields": {
"title": "Акции",
"auto_add": true
}
},
{
"model": "user.subscriptioncategory",
"pk": 5,
"fields": {
"title": "Партнёрские акции",
"auto_add": false
}
},
{
"model": "user.subscriptioncategory",
"pk": 6,
"fields": {
"title": "Новости компании",
"auto_add": true
}
},
{
"model": "user.subscriptioncategory",
"pk": 7,
"fields": {
"title": "Комментарии в которых участвуете",
"auto_add": false
}
}
]

@ -1,5 +1,6 @@
from django import forms from django import forms
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from phonenumber_field.formfields import PhoneNumberField
from .fields import CreditCardField from .fields import CreditCardField
@ -10,6 +11,7 @@ class UserEditForm(forms.ModelForm):
# first_name = forms.CharField() # first_name = forms.CharField()
# last_name = forms.CharField() # last_name = forms.CharField()
# email = forms.CharField() # email = forms.CharField()
phone = PhoneNumberField()
# city = forms.CharField() # city = forms.CharField()
# country = forms.CharField() # country = forms.CharField()
birthday = forms.DateField(input_formats=['%d.%m.%Y'], required=False) birthday = forms.DateField(input_formats=['%d.%m.%Y'], required=False)
@ -33,6 +35,7 @@ class UserEditForm(forms.ModelForm):
'first_name', 'first_name',
'last_name', 'last_name',
'email', 'email',
'phone',
'city', 'city',
'country', 'country',
'birthday', 'birthday',
@ -54,3 +57,11 @@ class UserEditForm(forms.ModelForm):
class WithdrawalForm(forms.Form): class WithdrawalForm(forms.Form):
amount = forms.DecimalField(required=True, min_value=2000) amount = forms.DecimalField(required=True, min_value=2000)
card = CreditCardField(required=True) card = CreditCardField(required=True)
class AuthorRequesForm(forms.Form):
first_name = forms.CharField()
last_name = forms.CharField()
email = forms.CharField()
about = forms.CharField()
facebook = forms.URLField(required=False)

@ -0,0 +1,27 @@
# Generated by Django 2.0.2 on 2018-03-12 14:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0008_auto_20180212_0750'),
]
operations = [
migrations.CreateModel(
name='AuthorRequest',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=30, verbose_name='first name')),
('last_name', models.CharField(max_length=150, verbose_name='last name')),
('email', models.EmailField(max_length=254, verbose_name='email address')),
('about', models.CharField(blank=True, max_length=1000, null=True, verbose_name='О себе')),
('facebook', models.URLField(blank=True, default='', null=True)),
('status', models.PositiveSmallIntegerField(choices=[(0, 'pending'), (1, 'accepted'), (2, 'declined')], default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('update_at', models.DateTimeField(auto_now=True)),
],
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.2 on 2018-03-12 16:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0009_authorrequest'),
]
operations = [
migrations.AlterField(
model_name='authorrequest',
name='email',
field=models.EmailField(max_length=254, unique=True, verbose_name='email address'),
),
]

@ -0,0 +1,17 @@
# Generated by Django 2.0.3 on 2018-03-13 07:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('user', '0010_auto_20180312_1610'),
]
operations = [
migrations.AlterModelOptions(
name='authorrequest',
options={'ordering': ('-created_at',), 'verbose_name': 'Заявка не преподавателя', 'verbose_name_plural': 'Заявки не преподавателя'},
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.3 on 2018-03-13 07:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0011_auto_20180313_0744'),
]
operations = [
migrations.AddField(
model_name='authorrequest',
name='cause',
field=models.TextField(blank=True, null=True, verbose_name='Причина отказа'),
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.3 on 2018-03-13 10:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0012_authorrequest_cause'),
]
operations = [
migrations.AddField(
model_name='authorrequest',
name='declined_send_at',
field=models.DateTimeField(blank=True, null=True),
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.3 on 2018-03-13 10:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0013_authorrequest_declined_send_at'),
]
operations = [
migrations.AddField(
model_name='authorrequest',
name='accepted_send_at',
field=models.DateTimeField(blank=True, null=True),
),
]

@ -0,0 +1,45 @@
# Generated by Django 2.0.3 on 2018-03-15 05:47
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('user', '0014_authorrequest_accepted_send_at'),
]
operations = [
migrations.CreateModel(
name='EmailSubscription',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')),
('mailchimp_status', models.PositiveSmallIntegerField(choices=[(0, 'error'), (1, 'sent')], default=0)),
],
),
migrations.CreateModel(
name='SubscriptionCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100)),
],
options={
'verbose_name': 'Категория подписки',
'verbose_name_plural': 'Категории подписки',
'ordering': ('title',),
},
),
migrations.AddField(
model_name='emailsubscription',
name='categories',
field=models.ManyToManyField(to='user.SubscriptionCategory'),
),
migrations.AddField(
model_name='emailsubscription',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

@ -0,0 +1,20 @@
# Generated by Django 2.0.3 on 2018-03-15 06:03
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('user', '0015_auto_20180315_0547'),
]
operations = [
migrations.AlterField(
model_name='emailsubscription',
name='user',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='email_subscription', to=settings.AUTH_USER_MODEL),
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.3 on 2018-03-15 06:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0016_auto_20180315_0603'),
]
operations = [
migrations.AddField(
model_name='subscriptioncategory',
name='auto_add',
field=models.BooleanField(default=False),
),
]

@ -0,0 +1,19 @@
# Generated by Django 2.0.3 on 2018-03-15 17:19
from django.db import migrations
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
('user', '0017_subscriptioncategory_auto_add'),
]
operations = [
migrations.AddField(
model_name='user',
name='phone',
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, unique=True),
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.3 on 2018-03-20 08:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0018_user_phone'),
]
operations = [
migrations.AddField(
model_name='user',
name='show_in_mainpage',
field=models.BooleanField(default=False, verbose_name='Показывать на главной странице'),
),
]

@ -1,14 +1,19 @@
from json import dumps
from rest_framework.authtoken.models import Token
from phonenumber_field.modelfields import PhoneNumberField
from django.db import models from django.db import models
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.auth.models import AbstractUser, UserManager from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.postgres import fields as pgfields from django.contrib.postgres import fields as pgfields
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.authtoken.models import Token
from json import dumps
from api.v1 import serializers from api.v1 import serializers
from apps.notification.utils import send_email
from apps.user.tasks import user_to_mixpanel
class User(AbstractUser): class User(AbstractUser):
@ -29,6 +34,7 @@ class User(AbstractUser):
(FEMALE, 'Женщина'), (FEMALE, 'Женщина'),
) )
email = models.EmailField(_('email address'), unique=True) email = models.EmailField(_('email address'), unique=True)
phone = PhoneNumberField(null=True, blank=True, unique=True)
role = models.PositiveSmallIntegerField( role = models.PositiveSmallIntegerField(
'Роль', default=0, choices=ROLE_CHOICES) 'Роль', default=0, choices=ROLE_CHOICES)
gender = models.CharField( gender = models.CharField(
@ -49,6 +55,7 @@ class User(AbstractUser):
'Верифицирован по email', default=False 'Верифицирован по email', default=False
) )
photo = models.ImageField('Фото', null=True, blank=True, upload_to='users') photo = models.ImageField('Фото', null=True, blank=True, upload_to='users')
show_in_mainpage = models.BooleanField('Показывать на главной странице', default=False)
USERNAME_FIELD = 'email' USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username'] REQUIRED_FIELDS = ['username']
@ -63,7 +70,7 @@ class User(AbstractUser):
@property @property
def balance(self): def balance(self):
aggregate = self.balances.aggregate( aggregate = self.balances.filter(type=0).aggregate(
models.Sum('amount'), models.Sum('amount'),
models.Sum('commission'), models.Sum('commission'),
) )
@ -85,3 +92,149 @@ def create_auth_token(sender, instance=None, created=False, **kwargs):
instance.role not in [User.AUTHOR_ROLE, User.ADMIN_ROLE] instance.role not in [User.AUTHOR_ROLE, User.ADMIN_ROLE]
) and hasattr(instance, 'auth_token'): ) and hasattr(instance, 'auth_token'):
instance.auth_token.delete() instance.auth_token.delete()
@receiver(post_save, sender=User)
def send_user_info_to_mixpanel(sender, instance=None, created=False, **kwargs):
user_to_mixpanel.delay(
instance.id,
instance.email,
str(instance.phone),
instance.first_name,
instance.last_name,
instance.date_joined,
dict(User.ROLE_CHOICES).get(instance.role),
[subscription.title.lower() for subscription in instance.email_subscription.categories.all()] if hasattr(instance, 'email_subscription') else [],
)
@receiver(post_save, sender=User)
def auto_create_subscription(sender, instance=None, created=False, **kwargs):
try:
es = EmailSubscription.objects.get(email=instance.email)
if not es.user:
es.user = instance
es.save()
except EmailSubscription.DoesNotExist:
instance.email_subscription = EmailSubscription.objects.create(
user=instance,
email=instance.email,
)
instance.email_subscription.categories.set(SubscriptionCategory.objects.filter(auto_add=True))
instance.save()
class AuthorRequestManager(models.Manager):
def create_by_user(self, user):
obj = self.model(
first_name=user.first_name,
last_name=user.last_name,
email=user.email,
about=user.about,
facebook=user.facebook,
)
self._for_write = True
obj.save(force_insert=True, using=self.db)
return obj
class AuthorRequest(models.Model):
PENDING = 0
ACCEPTED = 1
DECLINED = 2
STATUS_CHOICES = (
(PENDING, 'pending'),
(ACCEPTED, 'accepted'),
(DECLINED, 'declined'),
)
first_name = models.CharField(_('first name'), max_length=30)
last_name = models.CharField(_('last name'), max_length=150)
email = models.EmailField(_('email address'), unique=True)
about = models.CharField('О себе', max_length=1000, null=True, blank=True)
facebook = models.URLField(default='', null=True, blank=True)
status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, default=PENDING)
cause = models.TextField('Причина отказа', null=True, blank=True)
accepted_send_at = models.DateTimeField(null=True, blank=True)
declined_send_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
objects = AuthorRequestManager()
class Meta:
verbose_name = 'Заявка не преподавателя'
verbose_name_plural = 'Заявки не преподавателя'
ordering = ('-created_at',)
@receiver(post_save, sender=AuthorRequest)
def handle_authorrequest_update(sender, instance=None, created=False, update_fields=[], **kwargs):
if not created:
if instance.status == AuthorRequest.DECLINED and not instance.declined_send_at:
send_email.delay(
'Отказ заявки на преподавателя',
instance.email,
'notification/email/decline_author.html',
cause=instance.cause,
)
instance.declined_send_at = now()
instance.save()
elif instance.status == AuthorRequest.ACCEPTED and not instance.accepted_send_at:
email = None
password = None
try:
user = User.objects.get(email=instance.email)
except User.DoesNotExist:
email = instance.email
password = User.objects.make_random_password()
user = User.objects.create(
first_name=instance.first_name,
last_name=instance.last_name,
username=instance.email,
email=instance.email,
about=instance.about,
facebook=instance.facebook,
is_active=True,
is_email_proved=True,
)
user.set_password(password)
user.role = User.AUTHOR_ROLE
user.save()
send_email.delay(
'Заявка на преподавателя одобрена',
instance.email,
'notification/email/accept_author.html',
email=email,
password=password,
)
instance.accepted_send_at = now()
instance.save()
class SubscriptionCategory(models.Model):
title = models.CharField(max_length=100)
auto_add = models.BooleanField(default=False)
class Meta:
verbose_name = 'Категория подписки'
verbose_name_plural = 'Категории подписки'
ordering = ('title',)
def __str__(self):
return self.title
class EmailSubscription(models.Model):
ERROR = 0
SENT = 1
MAILCHIMP_STATUS_CHOICES = (
(ERROR, 'error'),
(SENT, 'sent'),
)
user = models.OneToOneField(User, null=True, blank=True, on_delete=models.CASCADE, related_name='email_subscription')
email = models.EmailField(_('email address'), unique=True)
categories = models.ManyToManyField(SubscriptionCategory)
mailchimp_status = models.PositiveSmallIntegerField(choices=MAILCHIMP_STATUS_CHOICES, default=ERROR)

@ -0,0 +1,22 @@
from mixpanel import Mixpanel
from django.conf import settings
from project.celery import app
@app.task
def user_to_mixpanel(user_id, email, phone, first_name, last_name, date_joined, role, subscriptions):
mix = Mixpanel(settings.MIX_TOKEN)
mix.people_set(
user_id,
{
'$email': email,
'$phone': phone,
'$first_name': first_name,
'$last_name': last_name,
'$created': date_joined,
'role': role,
'subscriptions': subscriptions,
}
)

@ -0,0 +1,12 @@
{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %}
<div class="section">
<div class="section__center center center_xs">
<div class="done">
<div class="done__title title">Ваша заявка отправлена!</div>
<div class="done__foot">
<a class="done__btn btn btn_md btn_stroke" href="/">ПЕРЕЙТИ К ГЛАВНОЙ</a>
</div>
</div>
</div>
</div>
{% endblock content %}

@ -0,0 +1,69 @@
{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %} {% if messages %}
<div class="section 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 %}
<div class="section">
<div class="section__center center center_xs">
<form class="form" method="POST">{% csrf_token %}
<div class="form__group">
<div class="form__title">Стать автором</div>
<div class="form__fieldset">
<div class="form__field field{% if form.first_name.errors %} error{% endif %}">
<div class="field__label">ИМЯ</div>
<div class="field__wrap">
<input name='first_name' class="field__input" type="text" placeholder="Имя" value="{{ form.first_name.value }}">
</div>
{% if form.first_name.errors %}
<div class="field__error">Укажите корректно свои данные</div>
{% endif %}
</div>
<div class="form__field field{% if form.last_name.errors %} error{% endif %}">
<div class="field__label">ФАМИЛИЯ</div>
<div class="field__wrap">
<input name='last_name' class="field__input" type="text" placeholder="Фамилия" value="{{ form.last_name.value }}">
</div>
{% if form.last_name.errors %}
<div class="field__error">Укажите корректно свои данные</div>
{% endif %}
</div>
</div>
<div class="form__field field{% if form.email.errors %} error{% endif %}">
<div class="field__label">Почта</div>
<div class="field__wrap">
<input name='email' class="field__input" type="email" placeholder="Почта" value="{{ form.email.value }}">
</div>
{% if form.email.errors %}
<div class="field__error">Укажите корректно свои данные</div>
{% endif %}
</div>
<div class="form__field field{% if form.about.errors %} error{% endif %}">
<div class="field__label">О себе</div>
<div class="field__wrap">
<textarea name='about' class="field__textarea" placeholder="Расскажите о себе и своем опыте">{% if form.about.value %}{{ form.about.value }}{% endif %}</textarea>
</div>
{% if form.about.errors %}
<div class="field__error">Укажите корректно свои данные</div>
{% endif %}
</div>
<div class="form__field field{% if form.facebook.errors %} error{% endif %}">
<div class="field__label">FACEBOOK</div>
<div class="field__wrap">
<input name='facebook' class="field__input" type="text" placeholder="https://facebook.com/lilcitycompany" value="{% if form.facebook.value %}{{ form.facebook.value }}{% endif %}">
</div>
{% if form.facebook.errors %}
<div class="field__error">Укажите корректно свои данные</div>
{% endif %}
</div>
</div>
<div class="form__foot">
<button class="form__btn btn btn_md">СОХРАНИТЬ</button>
</div>
</form>
</div>
</div>
{% endblock content %}

@ -24,42 +24,25 @@
{% endif %} {% endif %}
<div class="section section_gray"> <div class="section section_gray">
<div class="section__center center center_xs"> <div class="section__center center center_xs">
<div class="form"> <form class="form" method="POST">{% csrf_token %}
<div class="form__group"> <div class="form__group">
<div class="form__title">Уведомления и рассылка</div> <div class="form__title">Уведомления и рассылка</div>
{% for category in subscription_categories %}
<label class="form__switch switch switch_blue"> <label class="form__switch switch switch_blue">
<input class="switch__input" type="checkbox" checked> <input
<span class="switch__content">Новости школы</span> name='category'
</label> value="{{ category.id }}"
<label class="form__switch switch switch_blue"> class="switch__input"
<input class="switch__input" type="checkbox" checked> type="checkbox"
<span class="switch__content">Новые курсы</span> {% if request.user.email_subscription and category in request.user.email_subscription.categories.all %}checked{% endif %}>
</label> <span class="switch__content">{{ category.title }}</span>
<label class="form__switch switch switch_blue">
<input class="switch__input" type="checkbox">
<span class="switch__content">Бонусы от партнеров</span>
</label>
<label class="form__switch switch switch_blue">
<input class="switch__input" type="checkbox">
<span class="switch__content">Акции</span>
</label>
<label class="form__switch switch switch_blue">
<input class="switch__input" type="checkbox" checked>
<span class="switch__content">Партнерские акции</span>
</label>
<label class="form__switch switch switch_blue">
<input class="switch__input" type="checkbox">
<span class="switch__content">Новости компании</span>
</label>
<label class="form__switch switch switch_blue">
<input class="switch__input" type="checkbox">
<span class="switch__content">Комментарии в которых участвуете</span>
</label> </label>
{% endfor %}
</div> </div>
<div class="form__foot"> <div class="form__foot">
<button class="form__btn btn btn_md">СОХРАНИТЬ</button> <button class="form__btn btn btn_md">СОХРАНИТЬ</button>
</div> </div>
</div> </form>
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

@ -79,7 +79,16 @@
{% if form.email.errors %} {% if form.email.errors %}
<div class="field__error">Укажите корректно свои данные</div> <div class="field__error">Укажите корректно свои данные</div>
{% endif %} {% endif %}
</div> </div>
<div class="form__field field{% if form.phone.errors %} error{% endif %}">
<div class="field__label">Телефон</div>
<div class="field__wrap">
<input name='phone' class="field__input" type="phone" placeholder="+7 (999) 999-99-99" value="{{ user.phone }}">
</div>
{% if form.phone.errors %}
<div class="field__error">Укажите корректно свои данные</div>
{% endif %}
</div>
<div class="form__fieldset"> <div class="form__fieldset">
<div class="form__field field{% if form.city.errors %} error{% endif %}"> <div class="form__field field{% if form.city.errors %} error{% endif %}">
<div class="field__label">ГОРОД</div> <div class="field__label">ГОРОД</div>
@ -250,4 +259,11 @@ var openFile = function(file) {
reader.readAsDataURL(input.files[0]); reader.readAsDataURL(input.files[0]);
}; };
</script> </script>
{% endblock content %}
{% endblock content %}
{% block foot %}
<script>
(new Inputmask('+7 (999) 999-99-99')).mask(document.querySelector('[name=phone]'));
</script>
{% endblock foot %}

@ -10,6 +10,7 @@ from django.conf import settings
from django.contrib.auth import login from django.contrib.auth import login
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.shortcuts import render, reverse, redirect from django.shortcuts import render, reverse, redirect
from django.views import View
from django.views.generic import DetailView, UpdateView, TemplateView, FormView from django.views.generic import DetailView, UpdateView, TemplateView, FormView
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -25,8 +26,9 @@ from apps.course.models import Course
from apps.notification.utils import send_email from apps.notification.utils import send_email
from apps.school.models import SchoolSchedule from apps.school.models import SchoolSchedule
from apps.payment.models import AuthorBalance, CoursePayment, SchoolPayment from apps.payment.models import AuthorBalance, CoursePayment, SchoolPayment
from apps.user.models import AuthorRequest, EmailSubscription, SubscriptionCategory
from .forms import UserEditForm, WithdrawalForm from .forms import AuthorRequesForm, UserEditForm, WithdrawalForm
User = get_user_model() User = get_user_model()
@ -85,6 +87,28 @@ class UserView(DetailView):
return context return context
class SubscribeView(View):
def post(self, request, pk=None, **kwargs):
refferer = request.META.get('HTTP_REFERER')
if request.user.is_authenticated:
messages.info(request, 'Вы уже подписаны на рассылки.')
return redirect(refferer)
email = request.POST.get('email', None)
if email:
email_subscription = EmailSubscription.objects.create(
email=email,
)
email_subscription.categories.set(
SubscriptionCategory.objects.filter(auto_add=True)
)
messages.info(request, 'Вы подписаны на новости.')
return redirect(refferer)
else:
messages.error(request, 'Введите адрес электронной почты.')
return redirect(refferer)
@method_decorator(login_required, name='dispatch') @method_decorator(login_required, name='dispatch')
class NotificationEditView(TemplateView): class NotificationEditView(TemplateView):
template_name = 'user/notification-settings.html' template_name = 'user/notification-settings.html'
@ -92,6 +116,18 @@ class NotificationEditView(TemplateView):
def get(self, request, pk=None): def get(self, request, pk=None):
return super().get(request) return super().get(request)
def post(self, request, pk=None):
categories = request.POST.getlist('category', [])
request.user.email_subscription.categories.set(
SubscriptionCategory.objects.filter(id__in=categories)
)
return redirect('user-edit-notifications', request.user.id)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['subscription_categories'] = SubscriptionCategory.objects.all()
return context
@method_decorator(login_required, name='dispatch') @method_decorator(login_required, name='dispatch')
class PaymentHistoryView(FormView): class PaymentHistoryView(FormView):
@ -115,7 +151,7 @@ class PaymentHistoryView(FormView):
type=AuthorBalance.OUT, type=AuthorBalance.OUT,
amount=form.cleaned_data['amount'], amount=form.cleaned_data['amount'],
status=AuthorBalance.PENDING, status=AuthorBalance.PENDING,
card=form.cleaned_data['amount'], card=form.cleaned_data['card'],
) )
return self.form_valid(form) return self.form_valid(form)
else: else:
@ -182,3 +218,46 @@ class UserEditView(UpdateView):
def get_success_url(self): def get_success_url(self):
return reverse('user-edit-profile', args=[self.object.id]) return reverse('user-edit-profile', args=[self.object.id])
class AuthorRequestView(FormView):
template_name = 'user/become-author.html'
form_class = AuthorRequesForm
success_url = reverse_lazy('author-request-success')
def post(self, request, pk=None):
form = self.get_form()
if form.is_valid():
if request.user.is_authenticated:
email = request.user.email
if request.user.role in [User.AUTHOR_ROLE, User.ADMIN_ROLE]:
messages.info(request, 'Вы уже являетесь автором')
return self.form_invalid(form)
else:
email = form.cleaned_data['email']
if AuthorRequest.objects.filter(email=email).exists():
messages.error(request, 'Вы уже отправили заявку на преподавателя')
return self.form_invalid(form)
AuthorRequest.objects.create(
first_name=form.cleaned_data['first_name'],
last_name=form.cleaned_data['last_name'],
email=email,
about=form.cleaned_data['about'],
facebook=form.cleaned_data['facebook'],
)
return self.form_valid(form)
else:
return self.form_invalid(form)
def get_context_data(self, **kwargs):
if self.request.user.is_authenticated:
self.initial = {
'first_name': self.request.user.first_name,
'last_name': self.request.user.last_name,
'email': self.request.user.email,
'about': self.request.user.about,
'facebook': self.request.user.facebook,
}
return super().get_context_data(**kwargs)

@ -13,7 +13,7 @@ services:
- "5432:5432" - "5432:5432"
redis: redis:
image: redis:3-alpine image: redis:4-alpine
ports: ports:
- "6379:6379" - "6379:6379"
@ -22,7 +22,7 @@ services:
restart: always restart: always
volumes: volumes:
- .:/lilcity - .:/lilcity
command: bash -c "python manage.py migrate && python manage.py loaddata /lilcity/apps/*/fixtures/*.json && gunicorn --workers=4 project.wsgi --bind=0.0.0.0:8000 --worker-class=gthread --reload" command: bash -c "python manage.py collectstatic --no-input && python manage.py migrate && python manage.py loaddata /lilcity/apps/*/fixtures/*.json && gunicorn --workers=4 project.wsgi --bind=0.0.0.0:8000 --worker-class=gthread --reload"
environment: environment:
- DJANGO_SETTINGS_MODULE=project.settings - DJANGO_SETTINGS_MODULE=project.settings
- DATABASE_SERVICE_HOST=db - DATABASE_SERVICE_HOST=db

@ -0,0 +1,5 @@
from apps.config.models import Config
def config(request):
return {"config": Config.load()}

@ -0,0 +1,14 @@
from django.forms import ImageField as BaseImageField
class ImageField(BaseImageField):
def to_internal_value(self, data):
# if data is None image field was not uploaded
if data:
file_object = super(ImageField, self).to_internal_value(data)
django_field = self._DjangoImageField()
django_field.error_messages = self.error_messages
django_field.to_python(file_object)
return file_object
return data

@ -11,6 +11,8 @@ https://docs.djangoproject.com/en/2.0/ref/settings/
""" """
import os import os
import raven
from celery.schedules import crontab from celery.schedules import crontab
from collections import OrderedDict from collections import OrderedDict
from datetime import timedelta from datetime import timedelta
@ -50,8 +52,8 @@ INSTALLED_APPS = [
'rest_framework.authtoken', 'rest_framework.authtoken',
'drf_yasg', 'drf_yasg',
'corsheaders', 'corsheaders',
'constance', 'sorl.thumbnail',
'constance.backends.database', 'raven.contrib.django.raven_compat',
] + [ ] + [
'apps.auth.apps', 'apps.auth.apps',
'apps.user', 'apps.user',
@ -59,10 +61,9 @@ INSTALLED_APPS = [
'apps.payment', 'apps.payment',
'apps.course', 'apps.course',
'apps.content', 'apps.content',
'apps.config',
'apps.school', 'apps.school',
] ]
if DEBUG:
INSTALLED_APPS += ['silk']
MIDDLEWARE = [ MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', 'corsheaders.middleware.CorsMiddleware',
@ -73,9 +74,8 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'apps.auth.middleware.TokenAuthLoginMiddleware',
] ]
if DEBUG:
MIDDLEWARE += ['silk.middleware.SilkyMiddleware']
ROOT_URLCONF = 'project.urls' ROOT_URLCONF = 'project.urls'
@ -85,32 +85,26 @@ TEMPLATES = [
'DIRS': [ 'DIRS': [
'project', 'project',
], ],
'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
'constance.context_processors.config', 'project.context_processors.config',
'django.template.context_processors.debug', 'django.template.context_processors.debug',
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
], ],
'loaders': [
('django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
]),
],
}, },
}, },
] ]
WSGI_APPLICATION = 'project.wsgi.application' WSGI_APPLICATION = 'project.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
# }
# }
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.postgresql',
@ -202,6 +196,7 @@ REST_FRAMEWORK = {
), ),
'DEFAULT_RENDERER_CLASSES': ( 'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.JSONRenderer',
# 'rest_framework.renderers.BrowsableAPIRenderer',
), ),
'DEFAULT_FILTER_BACKENDS': ( 'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend', 'django_filters.rest_framework.DjangoFilterBackend',
@ -221,46 +216,11 @@ CELERY_TASK_SERIALIZER = 'json'
CELERY_BEAT_SCHEDULE = { CELERY_BEAT_SCHEDULE = {
'retrieve_photos_from_instagram': { 'retrieve_photos_from_instagram': {
'task': 'apps.content.tasks.retrieve_photos', 'task': 'apps.content.tasks.retrieve_photos',
'schedule': timedelta(minutes=2) if DEBUG else crontab(minute=0, hour=0), 'schedule': timedelta(minutes=5) if DEBUG else crontab(minute=0, hour=0),
'args': (), 'args': (),
}, },
} }
# Dynamic settings
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
CONSTANCE_CONFIG = OrderedDict((
('INSTAGRAM_CLIENT_ACCESS_TOKEN', ('7145314808.f6fa114.ce354a5d876041fc9d3db04b0045587d', '')),
('INSTAGRAM_CLIENT_SECRET', ('2334a921425140ccb180d145dcd35b25', '')),
('INSTAGRAM_PROFILE_URL', ('#', 'URL профиля Instagram.')),
('INSTAGRAM_RESULTS_TAG', ('#lil_акварель', 'Тэг результатов работ.')),
('INSTAGRAM_RESULTS_PATH', ('media/instagram/results/', 'Путь до результатов работ.')),
('SERVICE_COMMISSION', (10, 'Комиссия сервиса в процентах.')),
('SERVICE_DISCOUNT_MIN_AMOUNT', (3500, 'Минимальная сумма платежа для школы, после которой вычитывается скидка SERVICE_DISCOUNT.')),
('SERVICE_DISCOUNT', (1000, 'Комиссия сервиса при покупке всех дней.')),
))
CONSTANCE_CONFIG_FIELDSETS = OrderedDict({
'Service': (
'SERVICE_COMMISSION',
'SERVICE_DISCOUNT_MIN_AMOUNT',
'SERVICE_DISCOUNT',
),
'Instagram': (
'INSTAGRAM_CLIENT_ACCESS_TOKEN',
'INSTAGRAM_CLIENT_SECRET',
'INSTAGRAM_PROFILE_URL',
'INSTAGRAM_RESULTS_TAG',
'INSTAGRAM_RESULTS_PATH',
),
})
try:
from .local_settings import *
except ImportError:
pass
try: try:
from paymentwall import * from paymentwall import *
except ImportError: except ImportError:
@ -270,6 +230,9 @@ else:
Paymentwall.set_app_key('d6f02b90cf6b16220932f4037578aff7') Paymentwall.set_app_key('d6f02b90cf6b16220932f4037578aff7')
Paymentwall.set_secret_key('4ea515bf94e34cf28646c2e12a7b8707') Paymentwall.set_secret_key('4ea515bf94e34cf28646c2e12a7b8707')
# Mixpanel settings
MIX_TOKEN = '79bd6bfd98667ed977737e6810b8abcd'
# CORS settings # CORS settings
if DEBUG: if DEBUG:
@ -280,3 +243,18 @@ if DEBUG:
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'DOC_EXPANSION': 'none', 'DOC_EXPANSION': 'none',
} }
# Raven settings
RAVEN_CONFIG = {
'dsn': 'https://bff536c4d71c4166afb91f83b9f73d55:ca47ad791a53480b9d40a85a26abf141@sentry.io/306843',
# If you are using git, you can also automatically configure the
# release based on the git info.
'release': raven.fetch_git_sha(BASE_DIR),
}
INSTAGRAM_RESULTS_PATH = 'media/instagram/results/'
try:
from .local_settings import *
except ImportError:
pass

@ -278,7 +278,7 @@
<div class="buy__row"> <div class="buy__row">
<div class="buy__col"> <div class="buy__col">
<div class="buy__head buy__head_main"> <div class="buy__head buy__head_main">
<div class="buy__title">Выбор урока/дня</div> <div class="buy__title">Выбор курса/дня</div>
<div class="buy__content">При записи на 5 уроков скидка 10%.</div> <div class="buy__content">При записи на 5 уроков скидка 10%.</div>
</div> </div>
</div> </div>
@ -345,6 +345,10 @@
</div> </div>
</div> </div>
<script type="text/javascript" src={% static "app.js" %}></script> <script type="text/javascript" src={% static "app.js" %}></script>
<script>
var schoolDiscount = parseFloat({{ config.SERVICE_DISCOUNT }});
var schoolAmountForDiscount = parseFloat({{ config.SERVICE_DISCOUNT_MIN_AMOUNT }});
</script>
{% block foot %}{% endblock foot %} {% block foot %}{% endblock foot %}
</body> </body>

@ -47,12 +47,23 @@
viewportmeta.content = 'width=device-width, maximum-scale=1.6, initial-scale=1.0'; viewportmeta.content = 'width=device-width, maximum-scale=1.6, initial-scale=1.0';
} }
} }
</script> </script>
<script> <script>
LIL_SERVER_TIME = "{% now 'U' %}"; LIL_SERVER_TIME = "{% now 'U' %}";
LIL_SERVER_TIME_DIFF = Math.floor((new Date().getTime()) / 1000) - parseInt(LIL_SERVER_TIME); LIL_SERVER_TIME_DIFF = Math.floor((new Date().getTime()) / 1000) - parseInt(LIL_SERVER_TIME);
USER_ID = "{{ request.user.id }}";
COURSE_ID = "{{ course.id }}";
MIXPANEL_CUSTOM_LIB_URL = "/static/mixpanel-2-latest.js";
</script>
{% block mixpanel %}
<!-- start Mixpanel -->
<script type="text/javascript">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+"=([^&]*)")))?l[1]:null};g&&c(g,"state")&&(i=JSON.parse(decodeURIComponent(c(g,"state"))),"mpeditor"===i.action&&(b.sessionStorage.setItem("_mpcehash",g),history.replaceState(i.desiredHash||"",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(".");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,
0)))}}var d=a;"undefined"!==typeof f?d=a[f]=[]:f="mixpanel";d.people=d.people||[];d.toString=function(b){var a="mixpanel";"mixpanel"!==f&&(a+="."+f);b||(a+=" (stub)");return a};d.people.toString=function(){return d.toString(1)+".people (stub)"};k="disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" ");
for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement("script");b.type="text/javascript";b.async=!0;b.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:"file:"===e.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";c=e.getElementsByTagName("script")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);
mixpanel.init("79bd6bfd98667ed977737e6810b8abcd");
</script> </script>
<!-- end Mixpanel -->
{% endblock mixpanel %}
</head> </head>
<body> <body>
@ -120,7 +131,7 @@
</a> {% endcomment %} </a> {% endcomment %}
</div> </div>
</div> </div>
<div class="header__group"><a class="header__section header__section_sub js-header-section {% active_link 'courses' %}" href="{% url 'courses' %}">ВИДЕО-КУРСЫ</a> <div class="header__group"><a class="header__section header__section_sub js-header-section {% active_link 'courses' %}" href="{% url 'courses' %}">ВИДЕОКУРСЫ</a>
<div class="header__list js-header-list"> <div class="header__list js-header-list">
{% category_menu_items category %} {% category_menu_items category %}
</div> </div>
@ -188,8 +199,11 @@
</div> </div>
<div class="footer__col"> <div class="footer__col">
<div class="footer__title">Программы</div> <div class="footer__title">Программы</div>
<nav class="footer__nav"><a class="footer__link" href="#">Онлайн-школа</a><a class="footer__link" href="#">Онлайн-курсы</a><a <nav class="footer__nav">
class="footer__link" href="#">Стать автором</a></nav> <a class="footer__link" href="#">Онлайн-школа</a>
<a class="footer__link" href="#">Онлайн-курсы</a>
<a class="footer__link" href="{% url 'author_request' %}">Стать автором</a>
</nav>
</div> </div>
<div class="footer__col"> <div class="footer__col">
<div class="footer__title">Контакты</div> <div class="footer__title">Контакты</div>
@ -199,13 +213,15 @@
</div> </div>
<div class="footer__col footer__col_md"> <div class="footer__col footer__col_md">
<div class="footer__title">ПОДПИСАТЬСЯ НА НОВОСТИ</div> <div class="footer__title">ПОДПИСАТЬСЯ НА НОВОСТИ</div>
<div class="subscribe"> <form class="subscribe" method="POST" action="{% url 'subscribe' %}">{% csrf_token %}
<div class="subscribe__field"><input class="subscribe__input" type="text" placeholder="Email"></div> <div class="subscribe__field">
<input class="subscribe__input" type="text" name="email" placeholder="Email">
</div>
<button class="subscribe__btn btn btn_light">ПОДПИСАТЬСЯ</button> <button class="subscribe__btn btn btn_light">ПОДПИСАТЬСЯ</button>
<div class="subscribe__content">Мы сами не любим спам, поэтому вы будете подучать от только важные новости о <div class="subscribe__content">Мы сами не любим спам, поэтому вы будете подучать от только важные новости о
школе, новых курсах и бонусах от Lil City. школе, новых курсах и бонусах от Lil City.
</div> </div>
</div> </form>
</div> </div>
</div> </div>
<div class="footer__row footer__row_second"> <div class="footer__row footer__row_second">
@ -232,7 +248,7 @@
</div> </div>
<div class="footer__col footer__col_lg"> <div class="footer__col footer__col_lg">
<div class="footer__group"> <div class="footer__group">
<div class="footer__copyright">2017 © Lil City, UAB.</div> <div class="footer__copyright">{% now "Y" %} © Lil City, UAB.</div>
<div class="footer__links"> <div class="footer__links">
<a class="footer__link" href="{% url 'terms' %}">Договор-оферта</a> <a class="footer__link" href="{% url 'terms' %}">Договор-оферта</a>
<div class="footer__divider">|</div> <div class="footer__divider">|</div>
@ -423,7 +439,7 @@
<div class="buy__row"> <div class="buy__row">
<div class="buy__col"> <div class="buy__col">
<div class="buy__head buy__head_main"> <div class="buy__head buy__head_main">
<div class="buy__title">Выбор урока/дня</div> <div class="buy__title">Выбор курса/дня</div>
<!-- <div class="buy__content">При записи на 5 уроков скидка 10%.</div> --> <!-- <div class="buy__content">При записи на 5 уроков скидка 10%.</div> -->
</div> </div>
</div> </div>
@ -495,6 +511,7 @@
<script type="text/javascript" src={% static "app.js" %}></script> <script type="text/javascript" src={% static "app.js" %}></script>
<script> <script>
var schoolDiscount = parseFloat({{ config.SERVICE_DISCOUNT }}); var schoolDiscount = parseFloat({{ config.SERVICE_DISCOUNT }});
var schoolAmountForDiscount = parseFloat({{ config.SERVICE_DISCOUNT_MIN_AMOUNT }});
</script> </script>
{% block foot %}{% endblock foot %} {% block foot %}{% endblock foot %}
</body> </body>

@ -2,7 +2,14 @@
{% block title %}School LIL.CITY{% endblock title %} {% block title %}School LIL.CITY{% endblock title %}
{% block content %} {% block content %}
<div class="main" style="background-image: url({% static 'img/bg-1.jpg' %});"> <div
class="main"
{% if config.MAIN_PAGE_TOP_IMAGE %}
style="background-image: url({{ config.MAIN_PAGE_TOP_IMAGE.url }});"
{% else %}
style="background-image: url({% static 'img/bg-1.jpg' %});"
{% endif %}
>
<div class="main__center center"> <div class="main__center center">
<div class="main__title">Первая онлайн-школа креативного мышления для детей! 5+</div> <div class="main__title">Первая онлайн-школа креативного мышления для детей! 5+</div>
<a <a
@ -13,7 +20,7 @@
{% endif %} {% endif %}
class="main__btn btn" class="main__btn btn"
href="#" href="#"
>КУПИТЬ ДОСТУП ОТ 500р. в мес.</a> >КУПИТЬ ДОСТУП ОТ {{ min_school_price }}р. в мес.</a>
</div> </div>
</div> </div>
{% if messages %} {% if messages %}
@ -270,74 +277,30 @@
<img class="text__curve text__curve_three" src="{% static 'img/curve-3.svg' %}"> <img class="text__curve text__curve_three" src="{% static 'img/curve-3.svg' %}">
</div> </div>
<div class="teachers"> <div class="teachers">
{% for author in authors %}
<div class="teachers__item"> <div class="teachers__item">
<div class="teachers__ava ava"> <div class="teachers__ava ava">
{% if author.photo %}
<img class="ava__pic" src="{{ author.photo.url }}">
{% else %}
<img class="ava__pic" src="{% static 'img/user.jpg' %}"> <img class="ava__pic" src="{% static 'img/user.jpg' %}">
{% endif %}
</div> </div>
<div class="teachers__wrap"> <div class="teachers__wrap">
<div class="teachers__title">Саша Крю, <div class="teachers__title">{{ author.get_full_name }},
<a href='#'>#lil_персонаж</a> <a href='#'>#lil_персонаж</a>
</div> </div>
<div class="teachers__name">@sashakru</div> {% if author.instagram %}
<div class="teachers__name">{{ author.instagram }}</div>
{% endif %}
{% if author.about %}
<div class="teachers__content"> <div class="teachers__content">
<p>Закончила ПХУ им К.А.Савицкого художник театра и кино. Работала с&nbsp;крупнейшими российскими и зарубежными {{ author.about }}
издательствами. </p>
<p>Участник и победитель международных выставок. </p>
<p>Основатель компании "Lil City".</p>
</div>
</div>
</div>
<div class="teachers__item">
<div class="teachers__ava ava">
<img class="ava__pic" src="{% static 'img/user.jpg' %}">
</div>
<div class="teachers__wrap">
<div class="teachers__title">Саша Крю,
<a href='#'>#lil_персонаж</a>
</div>
<div class="teachers__name">@sashakru</div>
<div class="teachers__content">
<p>Закончила ПХУ им К.А.Савицкого художник театра и кино. Работала с&nbsp;крупнейшими российскими и зарубежными
издательствами. </p>
<p>Участник и победитель международных выставок. </p>
<p>Основатель компании "Lil City".</p>
</div>
</div>
</div>
<div class="teachers__item">
<div class="teachers__ava ava">
<img class="ava__pic" src="{% static 'img/user.jpg' %}">
</div>
<div class="teachers__wrap">
<div class="teachers__title">Саша Крю,
<a href='#'>#lil_персонаж</a>
</div>
<div class="teachers__name">@sashakru</div>
<div class="teachers__content">
<p>Закончила ПХУ им К.А.Савицкого художник театра и кино. Работала с&nbsp;крупнейшими российскими и зарубежными
издательствами. </p>
<p>Участник и победитель международных выставок. </p>
<p>Основатель компании "Lil City".</p>
</div>
</div>
</div>
<div class="teachers__item">
<div class="teachers__ava ava">
<img class="ava__pic" src="{% static 'img/user.jpg' %}">
</div>
<div class="teachers__wrap">
<div class="teachers__title">Саша Крю,
<a href='#'>#lil_персонаж</a>
</div>
<div class="teachers__name">@sashakru</div>
<div class="teachers__content">
<p>Закончила ПХУ им К.А.Савицкого художник театра и кино. Работала с&nbsp;крупнейшими российскими и зарубежными
издательствами. </p>
<p>Участник и победитель международных выставок. </p>
<p>Основатель компании "Lil City".</p>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
{% endfor %}
</div> </div>
<div class="text text_mb0">Если хотите к нам в команду, то отправьте нам заявку</div> <div class="text text_mb0">Если хотите к нам в команду, то отправьте нам заявку</div>
</div> </div>
@ -367,7 +330,7 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="text text_mb0"> <div class="text text_mb0">
<a href='#'>Распечатать расписание</a> чтобы не забыть</div> <a target="_blank" href="{% url 'school_schedules' %}">Распечатать расписание</a> чтобы не забыть</div>
</div> </div>
</div> </div>
{% if course_items %} {% if course_items %}

@ -0,0 +1,52 @@
{% load static %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" media="all" href={% static "app.css" %}>
</head>
<body>
<div class="section section_gray">
<div class="section__center center center_md">
<a id="schedule" name="schedule">
<div class="title title_center">Расписание</div>
</a>
<div class="schedule">
{% for school_schedule in school_schedules %}
<div class="schedule__item">
<div class="schedule__day">{{ school_schedule }}</div>
<div class="schedule__wrap">
<div class="schedule__title">{{ school_schedule.title }}</div>
<div class="schedule__content">{{ school_schedule.description }}</div>
<div class="schedule__toggle toggle">
<button class="toggle__head js-toggle-head active">Материалы
<svg class="icon icon-arrow-down">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-down"></use>
</svg>
</button>
<div class="toggle__body">{{ school_schedule.materials }}</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% comment %} <div class="text text_mb0">
<a href='#'>Распечатать расписание</a> чтобы не забыть
</div> {% endcomment %}
</div>
</div>
<script type="text/javascript" src={% static "app.js" %}></script>
<script type="text/javascript">
var toggle__body = Array.from(document.getElementsByClassName("toggle__body"));
toggle__body.forEach(function (item, i, toggle__body) {
item.style.display = "block"
});
window.print();
window.close();
</script>
</body>
</html>

@ -13,30 +13,40 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import path, include
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.conf import settings from django.urls import path, include
from apps.course.views import ( from apps.course.views import (
CoursesView, likes, coursecomment, CoursesView, likes, coursecomment,
CourseView, LessonView, SearchView, CourseView, LessonView, SearchView,
lessoncomment, CourseEditView, lessoncomment, CourseEditView,
CourseOnModerationView, CourseOnModerationView, CourseLiveEditView,
) )
from apps.user.views import ( from apps.user.views import (
UserView, UserEditView, NotificationEditView, AuthorRequestView, UserView,
UserEditView, NotificationEditView,
PaymentHistoryView, resend_email_verify, PaymentHistoryView, resend_email_verify,
SubscribeView,
)
from apps.payment.views import (
CourseBuySuccessView, CourseBuyView,
PaymentwallCallbackView, SchoolBuySuccessView,
SchoolBuyView,
) )
from apps.payment.views import CourseBuyView, PaymentwallCallbackView, SchoolBuyView
from .views import IndexView from .views import IndexView, SchoolSchedulesView
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('auth/', include(('apps.auth.urls', 'lilcity'))), path('auth/', include(('apps.auth.urls', 'lilcity'))),
path('author-request/', AuthorRequestView.as_view(), name='author_request'),
path('author-request/success/', TemplateView.as_view(template_name='user/become-author-success.html'), name='author-request-success'),
path('courses/', CoursesView.as_view(), name='courses'), path('courses/', CoursesView.as_view(), name='courses'),
path('course/create', CourseEditView.as_view(), name='course_create'), path('course/create', CourseEditView.as_view(), name='course_create'),
path('course/create/live', CourseLiveEditView.as_view(), name='course_create_live'),
path('course/on-moderation', CourseOnModerationView.as_view(), name='course-on-moderation'), path('course/on-moderation', CourseOnModerationView.as_view(), name='course-on-moderation'),
path('course/<int:pk>/', CourseView.as_view(), name='course'), path('course/<int:pk>/', CourseView.as_view(), name='course'),
path('course/<str:slug>/', CourseView.as_view(), name='course'), path('course/<str:slug>/', CourseView.as_view(), name='course'),
@ -49,7 +59,8 @@ urlpatterns = [
path('lesson/<int:lesson_id>/comment', lessoncomment, name='lessoncomment'), path('lesson/<int:lesson_id>/comment', lessoncomment, name='lessoncomment'),
path('payments/ping', PaymentwallCallbackView.as_view(), name='payment-ping'), path('payments/ping', PaymentwallCallbackView.as_view(), name='payment-ping'),
path('paymentwall/pingback', PaymentwallCallbackView.as_view(), name='payment-ping-second'), path('paymentwall/pingback', PaymentwallCallbackView.as_view(), name='payment-ping-second'),
path('payments/success', TemplateView.as_view(template_name='payment/payment_success.html'), name='payment-success'), path('payments/course/<int:pk>/success', CourseBuySuccessView.as_view(), name='course_payment_success'),
path('payments/school/success', SchoolBuySuccessView.as_view(), name='payment-success'),
path('payments/error', TemplateView.as_view(template_name='payment/payment_error.html'), name='payment-error'), path('payments/error', TemplateView.as_view(template_name='payment/payment_error.html'), name='payment-error'),
path('school/checkout', SchoolBuyView.as_view(), name='school-checkout'), path('school/checkout', SchoolBuyView.as_view(), name='school-checkout'),
path('search/', SearchView.as_view(), name='search'), path('search/', SearchView.as_view(), name='search'),
@ -58,12 +69,14 @@ urlpatterns = [
path('user/<int:pk>/notifications', NotificationEditView.as_view(), name='user-edit-notifications'), path('user/<int:pk>/notifications', NotificationEditView.as_view(), name='user-edit-notifications'),
path('user/<int:pk>/payments', PaymentHistoryView.as_view(), name='user-edit-payments'), path('user/<int:pk>/payments', PaymentHistoryView.as_view(), name='user-edit-payments'),
path('user/resend-email-verify', resend_email_verify, name='resend-email-verify'), path('user/resend-email-verify', resend_email_verify, name='resend-email-verify'),
path('privacy', TemplateView.as_view(template_name="templates/lilcity/privacy_policy.html"), name='privacy'), path('subscribe', SubscribeView.as_view(), name='subscribe'),
path('terms', TemplateView.as_view(template_name="templates/lilcity/terms.html"), name='terms'), path('privacy', TemplateView.as_view(template_name='templates/lilcity/privacy_policy.html'), name='privacy'),
path('refund-policy', TemplateView.as_view(template_name="templates/lilcity/refund_policy.html"), name='refund_policy'), 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('school-schedules', SchoolSchedulesView.as_view(), name='school_schedules'),
path('', IndexView.as_view(), name='index'), path('', IndexView.as_view(), name='index'),
path('api/v1/', include(('api.v1.urls', 'api_v1'))), path('api/v1/', include(('api.v1.urls', 'api_v1'))),
path('test', TemplateView.as_view(template_name="templates/lilcity/test.html"), name='test'), path('test', TemplateView.as_view(template_name='templates/lilcity/test.html'), name='test'),
] ]
@ -71,6 +84,8 @@ if settings.DEBUG:
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns 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 += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += staticfiles_urlpatterns() urlpatterns += staticfiles_urlpatterns()
if 'silk' in settings.INSTALLED_APPS:
urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))]

@ -1,8 +1,12 @@
from django.db.models import Min
from django.contrib.auth import get_user_model
from django.views.generic import TemplateView from django.views.generic import TemplateView
from apps.course.models import Course from apps.course.models import Course
from apps.school.models import SchoolSchedule from apps.school.models import SchoolSchedule
User = get_user_model()
class IndexView(TemplateView): class IndexView(TemplateView):
template_name = 'templates/lilcity/main.html' template_name = 'templates/lilcity/main.html'
@ -12,5 +16,16 @@ class IndexView(TemplateView):
context.update({ context.update({
'course_items': Course.objects.filter(status=Course.PUBLISHED)[:3], 'course_items': Course.objects.filter(status=Course.PUBLISHED)[:3],
'school_schedules': SchoolSchedule.objects.all(), 'school_schedules': SchoolSchedule.objects.all(),
'min_school_price': SchoolSchedule.objects.all().aggregate(Min('month_price'))['month_price__min'],
'authors': User.objects.filter(role=User.AUTHOR_ROLE, show_in_mainpage=True),
}) })
return context return context
class SchoolSchedulesView(TemplateView):
template_name = 'templates/lilcity/school_schedules.html'
def get_context_data(self):
context = super().get_context_data()
context['school_schedules'] = SchoolSchedule.objects.all()
return context

@ -1,24 +1,27 @@
# Python-3.6 # Python-3.6
gunicorn==19.7.1
requests==2.18.4
Django==2.0.2
django-anymail[mailgun]==1.2
# paymentwall-python==1.0.7
git+https://github.com/ivlevdenis/paymentwall-python.git
twilio==6.10.0
psycopg2==2.7.3.2
facepy==1.0.9
Pillow==5.0.0
django-active-link==0.1.2
arrow==0.12.1 arrow==0.12.1
celery[redis]==4.1.0
Django==2.0.3
django-active-link==0.1.2
django-anymail[mailgun]==2.0
django-cors-headers==2.2.0
django-filter==2.0.0.dev1 django-filter==2.0.0.dev1
django-mptt==0.9.0 django-mptt==0.9.0
django-silk==2.0.0
django-phonenumber-field==2.0.0
django-polymorphic-tree==1.5 django-polymorphic-tree==1.5
celery[redis]==4.1.0
djangorestframework==3.7.7 djangorestframework==3.7.7
drf-yasg[validation]==1.4.0 drf-yasg[validation]==1.5.0
django-silk==2.0.0 facepy==1.0.9
django-cors-headers==2.1.0 gunicorn==19.7.1
django-constance[database]==2.1.0 mixpanel==4.3.2
psycopg2-binary==2.7.4
Pillow==5.0.0
raven==6.6.0
requests==2.18.4
sorl-thumbnail==12.4.1
twilio==6.10.5
# paymentwall-python==1.0.7
git+https://github.com/ivlevdenis/paymentwall-python.git
# python-instagram==1.3.2 # python-instagram==1.3.2
git+https://github.com/ivlevdenis/python-instagram.git git+https://github.com/ivlevdenis/python-instagram.git

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs> <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs>
<path id="a" d="M0 0h15v-2H0v2z"/>
<path id="a" fill-rule="evenodd" d="M12.5 0C3.846 0 0 7 0 7s3.846 7 12.5 7S25 7 25 7s-3.846-7-12.5-7zm0 1C4.808 1 1.154 7 1.154 7s3.654 6 11.346 6 11.346-6 11.346-6S20.192 1 12.5 1zm0 10c2.124 0 3.846-1.79 3.846-4S14.624 3 12.5 3 8.654 4.79 8.654 7s1.722 4 3.846 4zm0-1c1.593 0 2.885-1.343 2.885-3S14.093 4 12.5 4c-1.593 0-2.885 1.343-2.885 3s1.292 3 2.885 3zm0-2c.531 0 .961-.448.961-1s-.43-1-.961-1c-.531 0-.961.448-.961 1s.43 1 .961 1z"/> <path id="a" fill-rule="evenodd" d="M12.5 0C3.846 0 0 7 0 7s3.846 7 12.5 7S25 7 25 7s-3.846-7-12.5-7zm0 1C4.808 1 1.154 7 1.154 7s3.654 6 11.346 6 11.346-6 11.346-6S20.192 1 12.5 1zm0 10c2.124 0 3.846-1.79 3.846-4S14.624 3 12.5 3 8.654 4.79 8.654 7s1.722 4 3.846 4zm0-1c1.593 0 2.885-1.343 2.885-3S14.093 4 12.5 4c-1.593 0-2.885 1.343-2.885 3s1.292 3 2.885 3zm0-2c.531 0 .961-.448.961-1s-.43-1-.961-1c-.531 0-.961.448-.961 1s.43 1 .961 1z"/>
<path id="a" fill-rule="evenodd" d="M4.912 13.12C1.555 11.173 0 8.5 0 8.5s3.846-6.611 12.5-6.611c1.254 0 2.408.139 3.463.376l-.817.803a14.781 14.781 0 0 0-2.646-.235C4.808 2.833 1.154 8.5 1.154 8.5s1.456 2.259 4.466 3.924l-.708.696zm4.125 1.615c1.055.237 2.209.376 3.463.376C21.154 15.111 25 8.5 25 8.5s-1.555-2.673-4.912-4.62l-.708.696C22.39 6.24 23.846 8.5 23.846 8.5s-3.654 5.667-11.346 5.667c-.942 0-1.824-.085-2.646-.235l-.817.803zM16.25 7.65c.064.273.097.557.097.849 0 2.086-1.722 3.778-3.846 3.778-.297 0-.586-.033-.864-.096l.864-.849c.738 0 1.476-.276 2.04-.83a2.799 2.799 0 0 0 .845-2.003l.864-.849zm-2.885-2.833a3.925 3.925 0 0 0-.864-.096c-2.124 0-3.846 1.692-3.846 3.778 0 .292.034.576.097.849l.864-.849c0-.725.282-1.45.845-2.003a2.902 2.902 0 0 1 2.04-.83l.864-.85zM20.192 0L3.846 16.056l.962.944L21.154.944 20.192 0z"/> <path id="a" fill-rule="evenodd" d="M4.912 13.12C1.555 11.173 0 8.5 0 8.5s3.846-6.611 12.5-6.611c1.254 0 2.408.139 3.463.376l-.817.803a14.781 14.781 0 0 0-2.646-.235C4.808 2.833 1.154 8.5 1.154 8.5s1.456 2.259 4.466 3.924l-.708.696zm4.125 1.615c1.055.237 2.209.376 3.463.376C21.154 15.111 25 8.5 25 8.5s-1.555-2.673-4.912-4.62l-.708.696C22.39 6.24 23.846 8.5 23.846 8.5s-3.654 5.667-11.346 5.667c-.942 0-1.824-.085-2.646-.235l-.817.803zM16.25 7.65c.064.273.097.557.097.849 0 2.086-1.722 3.778-3.846 3.778-.297 0-.586-.033-.864-.096l.864-.849c.738 0 1.476-.276 2.04-.83a2.799 2.799 0 0 0 .845-2.003l.864-.849zm-2.885-2.833a3.925 3.925 0 0 0-.864-.096c-2.124 0-3.846 1.692-3.846 3.778 0 .292.034.576.097.849l.864-.849c0-.725.282-1.45.845-2.003a2.902 2.902 0 0 1 2.04-.83l.864-.85zM20.192 0L3.846 16.056l.962.944L21.154.944 20.192 0z"/>
@ -24,6 +26,11 @@
<path fill-rule="evenodd" d="M6.821 20v-9h2.733L10 7H6.821V5.052C6.821 4.022 6.848 3 8.287 3h1.458V.14c0-.043-1.253-.14-2.52-.14C4.58 0 2.924 1.657 2.924 4.7V7H0v4h2.923v9h3.898z"/> <path fill-rule="evenodd" d="M6.821 20v-9h2.733L10 7H6.821V5.052C6.821 4.022 6.848 3 8.287 3h1.458V.14c0-.043-1.253-.14-2.52-.14C4.58 0 2.924 1.657 2.924 4.7V7H0v4h2.923v9h3.898z"/>
</symbol><symbol id="icon-fb" viewBox="0 0 8 18"> </symbol><symbol id="icon-fb" viewBox="0 0 8 18">
<path d="M2 7.5H0v-1h2v-.505A5.493 5.493 0 0 1 7.495.5v1A4.493 4.493 0 0 0 3 5.995V6.5h3v1H3v10H2v-10z"/> <path d="M2 7.5H0v-1h2v-.505A5.493 5.493 0 0 1 7.495.5v1A4.493 4.493 0 0 0 3 5.995V6.5h3v1H3v10H2v-10z"/>
</symbol><symbol id="icon-hamburger" viewBox="0 0 15 12">
<use xlink:href="#a" transform="translate(0 12)"/>
<use xlink:href="#a" transform="translate(0 7)"/>
<use xlink:href="#a" transform="translate(0 2)"/>
</symbol><symbol id="icon-image-text" viewBox="0 0 31 15"> </symbol><symbol id="icon-image-text" viewBox="0 0 31 15">
<path fill-rule="evenodd" d="M14 13.295V1H1v8.294l4.505-4.501L14 13.295zm-.709.705L5.505 6.207 1 10.707V14h12.291zm13.229 0h-1.66V2.14h-4.22V.66h10.12v1.48h-4.24V14zM0 0h15v15H0V0zm10.5 6.5a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0-1a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/> <path fill-rule="evenodd" d="M14 13.295V1H1v8.294l4.505-4.501L14 13.295zm-.709.705L5.505 6.207 1 10.707V14h12.291zm13.229 0h-1.66V2.14h-4.22V.66h10.12v1.48h-4.24V14zM0 0h15v15H0V0zm10.5 6.5a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0-1a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/>
</symbol><symbol id="icon-image" viewBox="0 0 22 22"> </symbol><symbol id="icon-image" viewBox="0 0 22 22">

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

@ -87,7 +87,7 @@
</a> </a>
</div> </div>
</div> </div>
<div class="header__group"><a class="header__section header__section_sub js-header-section" href="#">ВИДЕО-КУРСЫ</a> <div class="header__group"><a class="header__section header__section_sub js-header-section" href="#">ВИДЕОКУРСЫ</a>
<div class="header__list js-header-list"> <div class="header__list js-header-list">
<a class="header__link" href="#"> <a class="header__link" href="#">
<div class="header__title">ПЕРСОНАЖ</div> <div class="header__title">ПЕРСОНАЖ</div>
@ -264,7 +264,7 @@
<div class="buy__row"> <div class="buy__row">
<div class="buy__col"> <div class="buy__col">
<div class="buy__head buy__head_main"> <div class="buy__head buy__head_main">
<div class="buy__title">Выбор урока/дня</div> <div class="buy__title">Выбор курса/дня</div>
<div class="buy__content">При записи на 5 уроков скидка 10%.</div> <div class="buy__content">При записи на 5 уроков скидка 10%.</div>
</div> </div>
</div> </div>

@ -87,7 +87,7 @@
</a> </a>
</div> </div>
</div> </div>
<div class="header__group"><a class="header__section header__section_sub js-header-section" href="#">ВИДЕО-КУРСЫ</a> <div class="header__group"><a class="header__section header__section_sub js-header-section" href="#">ВИДЕОКУРСЫ</a>
<div class="header__list js-header-list"> <div class="header__list js-header-list">
<a class="header__link" href="#"> <a class="header__link" href="#">
<div class="header__title">ПЕРСОНАЖ</div> <div class="header__title">ПЕРСОНАЖ</div>
@ -352,7 +352,7 @@
<div class="buy__row"> <div class="buy__row">
<div class="buy__col"> <div class="buy__col">
<div class="buy__head buy__head_main"> <div class="buy__head buy__head_main">
<div class="buy__title">Выбор урока/дня</div> <div class="buy__title">Выбор курса/дня</div>
<div class="buy__content">При записи на 5 уроков скидка 10%.</div> <div class="buy__content">При записи на 5 уроков скидка 10%.</div>
</div> </div>
</div> </div>

@ -55,6 +55,7 @@
"dependencies": { "dependencies": {
"axios": "^0.17.1", "axios": "^0.17.1",
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"baguettebox.js": "^1.10.0",
"history": "^4.7.2", "history": "^4.7.2",
"ilyabirman-likely": "^2.3.0", "ilyabirman-likely": "^2.3.0",
"inputmask": "^3.3.11", "inputmask": "^3.3.11",
@ -64,9 +65,12 @@
"owl.carousel": "^2.2.0", "owl.carousel": "^2.2.0",
"slugify": "^1.2.9", "slugify": "^1.2.9",
"smooth-scroll": "^12.1.5", "smooth-scroll": "^12.1.5",
"sortablejs": "^1.7.0",
"uuid": "^3.2.1",
"validator": "^9.2.0", "validator": "^9.2.0",
"vue": "^2.5.13", "vue": "^2.5.13",
"vue-autosize": "^1.0.2", "vue-autosize": "^1.0.2",
"vuedraggable": "^2.16.0",
"vuejs-datepicker": "^0.9.25", "vuejs-datepicker": "^0.9.25",
"vuelidate": "^0.6.1" "vuelidate": "^0.6.1"
} }

@ -36,7 +36,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="info__foot"> <div class="info__foot" v-if="!live">
<div class="info__field field field_info info__field--light" <div class="info__field field field_info info__field--light"
v-bind:class="{ error: ($v.course.category.$dirty || showErrors) && $v.course.category.$invalid }"> v-bind:class="{ error: ($v.course.category.$dirty || showErrors) && $v.course.category.$invalid }">
<div class="field__label field__label_gray">КАТЕГОРИЯ</div> <div class="field__label field__label_gray">КАТЕГОРИЯ</div>
@ -72,7 +72,22 @@
</div> </div>
<div class="field__wrap field__wrap--additional">{{ courseFullUrl }}</div> <div class="field__wrap field__wrap--additional">{{ courseFullUrl }}</div>
</div> </div>
<div class="info__field field">
<div v-if="live" class="info__field field">
<div class="field__label field__label_gray">ССЫЛКА НА VIMEO</div>
<div class="field__wrap">
<input type="text" class="field__input" v-model="course.stream_url">
</div>
</div>
<div v-if="live" class="info__field field">
<div class="field__label">ДАТА</div>
<div class="field__wrap">
<lil-select :value.sync="course.time" :options="dateOptions" placeholder="Выберите дату"/>
</div>
</div>
<div v-if="!live" class="info__field field">
<div class="field__label field__label_gray">ДОСТУП</div> <div class="field__label field__label_gray">ДОСТУП</div>
<div class="field__wrap"> <div class="field__wrap">
<label class="field__switch switch switch_lg switch_circle"> <label class="field__switch switch switch_lg switch_circle">
@ -85,12 +100,12 @@
</label> </label>
</div> </div>
</div> </div>
<label class="info__switch switch switch_lg"> <label v-if="!live" class="info__switch switch switch_lg">
<input type="checkbox" class="switch__input" v-model="course.is_featured"> <input type="checkbox" class="switch__input" v-model="course.is_featured">
<span class="switch__content">Выделить</span> <span class="switch__content">Выделить</span>
</label> </label>
</div> </div>
<div class="info__fieldset"> <div v-if="!live" class="info__fieldset">
<div class="info__field field"> <div class="info__field field">
<div class="field__label field__label_gray">ЗАПУСК</div> <div class="field__label field__label_gray">ЗАПУСК</div>
<div class="field__wrap"> <div class="field__wrap">
@ -135,11 +150,17 @@
v-model="course.short_description"></textarea> v-model="course.short_description"></textarea>
</div> </div>
</div> </div>
<block-images
:index="0"
:readOnly="true"
title="Результаты урока"
:images.sync="course.gallery.images"
:access-token="accessToken"/>
</div> </div>
<div id="course-redactor__nav" class="kit__nav"> <div v-if="!live" id="course-redactor__nav" class="kit__nav">
<button class="kit__btn btn btn_lg" <button class="kit__btn btn btn_lg"
v-bind:class="{ 'btn_stroke': viewSection === 'course', 'btn_gray': viewSection !== 'course' }" v-bind:class="{ 'btn_stroke': viewSection === 'course', 'btn_gray': viewSection !== 'course' }"
type="button" @click="viewSection = 'course'">Описание type="button" @click="showCourse">Описание
курса курса
</button> </button>
<button class="kit__btn btn btn_lg" <button class="kit__btn btn btn_lg"
@ -151,41 +172,43 @@
</button> </button>
</div> </div>
<div v-if="viewSection === 'course'" class="kit__body"> <div v-if="viewSection === 'course'" class="kit__body">
<div v-for="(block, index) in course.content"> <vue-draggable v-model="course.content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }">
<block-text v-if="block.type === 'text'" <div v-for="(block, index) in course.content" :key="block.data.id ? block.data.id : block.data.guid">
:index="index" <block-text v-if="block.type === 'text'"
: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" :index="index"
:title.sync="block.data.title" :title.sync="block.data.title"
:text.sync="block.data.text" :text.sync="block.data.text"
:images.sync="block.data.images"
v-on:remove="onBlockRemoved" v-on:remove="onBlockRemoved"
:access-token="accessToken"/> :access-token="accessToken"/>
<block-video v-if="block.type === 'video'" <block-image-text v-if="block.type === 'image-text'"
:index="index" :index="index"
:title.sync="block.data.title" :title.sync="block.data.title"
v-on:remove="onBlockRemoved" :text.sync="block.data.text"
:video-url.sync="block.data.video_url"/> :image-id.sync="block.data.image_id"
</div> :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>
</vue-draggable>
<block-add v-on:added="onBlockAdded"/> <block-add v-on:added="onBlockAdded"/>
@ -257,10 +280,15 @@
import $ from 'jquery'; import $ from 'jquery';
import {required, minValue, numeric } from 'vuelidate/lib/validators' import {required, minValue, numeric } from 'vuelidate/lib/validators'
import slugify from 'slugify'; import slugify from 'slugify';
import Draggable from 'vuedraggable';
import {showNotification} from "../js/modules/notification";
import createHistory from "history/createBrowserHistory";
const history = createHistory();
export default { export default {
name: "course-redactor", name: "course-redactor",
props: ["authorName", "authorPicture", "accessToken", "courseId"], props: ["authorName", "authorPicture", "accessToken", "courseId", "live"],
data() { data() {
return { return {
viewSection: 'course', viewSection: 'course',
@ -335,6 +363,25 @@
'value': '18:00', 'value': '18:00',
} }
], ],
dateOptions: [
{
'title': 'Акварельс (Понедельник, 2 апр)',
'value': 'Акварельс (Понедельник, 2 апр)',
},
{
'title': 'Рельсотрон (Вторник, 3 апр)',
'value': 'Рельсотрон (Вторник, 3 апр)',
},
{
'title': 'Коломёт (Среда, 4 апр)',
'value': 'Коломёт (Среда, 4 апр)',
},
{
'title': 'Зиккурат (Четверг, 5 апр)',
'value': 'Зиккурат (Четверг, 5 апр)',
},
],
showErrors: false, showErrors: false,
savingTimeout: null, savingTimeout: null,
savingDebounceTimeout: null, savingDebounceTimeout: null,
@ -422,9 +469,21 @@
}, },
editLesson(lessonIndex) { editLesson(lessonIndex) {
this.currentLesson = this.lessons[lessonIndex]; this.currentLesson = this.lessons[lessonIndex];
if (this.viewSection !== 'lessons-edit') {
history.push("/course/create/lessons/new");
}
this.viewSection = 'lessons-edit'; this.viewSection = 'lessons-edit';
}, },
showCourse() {
if (this.viewSection !== 'course') {
history.push("/course/create");
}
this.viewSection = 'course'
},
showLessons() { showLessons() {
if (this.viewSection !== 'lessons') {
history.push("/course/create/lessons");
}
this.viewSection = 'lessons'; this.viewSection = 'lessons';
}, },
addLesson() { addLesson() {
@ -434,6 +493,9 @@
course_id: this.course.id, course_id: this.course.id,
content: [], content: [],
}; };
if (this.viewSection !== 'lessons-edit') {
history.push("/course/create/lessons/new");
}
this.viewSection = 'lessons-edit'; this.viewSection = 'lessons-edit';
window.scrollTo(0, 0); window.scrollTo(0, 0);
}, },
@ -515,6 +577,7 @@
onCoursePublish() { onCoursePublish() {
this.showErrors = true; this.showErrors = true;
if (this.$v.$invalid) { if (this.$v.$invalid) {
showNotification("error", "Заполните все необходимые поля");
return; return;
} }
const publishButton = $('#course-redactor__publish-button'); const publishButton = $('#course-redactor__publish-button');
@ -551,7 +614,7 @@
clearTimeout(this.savingTimeout); clearTimeout(this.savingTimeout);
document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНИЕ...'; document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНИЕ...';
const courseObject = this.course; const courseObject = this.course;
courseObject.url = slugify(courseObject.url); courseObject.url = (courseObject.url) ? slugify(courseObject.url):courseObject.url;
api.saveCourse(courseObject, this.accessToken) api.saveCourse(courseObject, this.accessToken)
.then((response) => { .then((response) => {
this.courseSaving = false; this.courseSaving = false;
@ -567,7 +630,17 @@
if (this.course.is_deferred) { if (this.course.is_deferred) {
courseData.is_deferred = true; courseData.is_deferred = true;
} }
this.course = courseData; let remoteUUIDMapper = {}
if (courseData.content) {
courseData.content.forEach((contentElement) => {
remoteUUIDMapper[contentElement.uuid] = contentElement.data.id
})
}
this.course.content.forEach((contentElement, index) => {
if (!contentElement.data.id) {
this.$set(this.course.content[index].data, 'id', remoteUUIDMapper[contentElement.uuid])
}
})
if (courseData.url) { if (courseData.url) {
this.slugChanged = true; this.slugChanged = true;
} }
@ -579,10 +652,22 @@
this.courseSyncHook = false; this.courseSyncHook = false;
this.courseSaving = false; this.courseSaving = false;
}); });
}, 2000); }, 3000);
} }
}, },
mounted() { mounted() {
console.log('live', this.live);
// Listen for changes to the current location.
this.unlisten = history.listen((location, action) => {
if (location.pathname === '/course/create/lessons') {
this.viewSection = 'lessons';
} else if (location.pathname === '/course/create') {
this.viewSection = 'course';
} else if (location.pathname === '/course/create/lessons/new') {
this.viewSection = 'lessons-edit';
}
});
api.getCategories(this.accessToken) api.getCategories(this.accessToken)
.then((response) => { .then((response) => {
if (response.data) { if (response.data) {
@ -629,10 +714,10 @@
}, },
displayPrice: { displayPrice: {
get: function () { get: function () {
return this.course.is_paid ? this.course.price : 0; return this.course.is_paid ? (this.course.price || 0) : 0;
}, },
set: function (value) { set: function (value) {
this.course.price = value; this.course.price = value || 0;
} }
}, },
categorySelect: { categorySelect: {
@ -677,6 +762,9 @@
return `https://lil.city/course/${suffix}`; return `https://lil.city/course/${suffix}`;
}, },
}, },
beforeDestroy() {
this.unlisten();
},
watch: { watch: {
'course': { 'course': {
handler: function (newValue, oldValue) { handler: function (newValue, oldValue) {
@ -707,6 +795,7 @@
'block-images': BlockImages, 'block-images': BlockImages,
'block-video': BlockVideo, 'block-video': BlockVideo,
'lesson-redactor': LessonRedactor, 'lesson-redactor': LessonRedactor,
'vue-draggable': Draggable,
} }
} }
</script> </script>
@ -776,4 +865,23 @@
width: 140px; width: 140px;
height: 140px; height: 140px;
} }
</style>
.kit__section-remove {
button.sortable__handle {
margin-right: 10px;
cursor: -webkit-grab;
cursor: grab;
svg.icon-hamburger {
width: 1em;
height: 1em;
}
}
}
.sortable-ghost, .sortable-chosen {
background: white;
border-radius: 10px;
}
</style>

@ -51,6 +51,8 @@
</template> </template>
<script> <script>
import uuidv4 from 'uuid/v4';
export default { export default {
name: "block-add", name: "block-add",
data() { data() {
@ -60,6 +62,7 @@
}, },
methods: { methods: {
add(blockData) { add(blockData) {
blockData.uuid = uuidv4();
this.isOpen = false; this.isOpen = false;
this.$emit('added', blockData) this.$emit('added', blockData)
}, },
@ -118,4 +121,4 @@
<style scoped> <style scoped>
</style> </style>

@ -1,6 +1,6 @@
<template> <template>
<div class="kit__section kit__section--block"> <div class="kit__section kit__section--block">
<div class="kit__section-remove"> <div v-if="!readOnly" class="kit__section-remove">
<button type="button" @click="onRemove"> <button type="button" @click="onRemove">
<svg class="icon icon-delete"> <svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use> <use xlink:href="/static/img/sprite.svg#icon-delete"></use>
@ -9,14 +9,14 @@
</div> </div>
<div class="kit__field field"> <div class="kit__field field">
<div class="field__wrap field__wrap--title"> <div class="field__wrap field__wrap--title">
<input type="text" <input :readonly="readOnly" type="text"
:value="title" :value="title"
class="field__input" class="field__input"
placeholder="Заголовок раздела" placeholder="Заголовок раздела"
@change="onTitleChange"> @change="onTitleChange">
</div> </div>
</div> </div>
<div class="kit__field field"> <div v-if="!readOnly" class="kit__field field">
<div class="field__wrap"> <div class="field__wrap">
<textarea class="field__textarea field__textarea_sm" <textarea class="field__textarea field__textarea_sm"
:value="text" :value="text"
@ -48,7 +48,7 @@
export default { export default {
name: "block-images", name: "block-images",
props: ["index", "title", "text", "images", "accessToken"], props: ["index", "title", "text", "images", "accessToken", "readOnly"],
methods: { methods: {
onTitleChange(event) { onTitleChange(event) {
this.$emit('update:title', event.target.value); this.$emit('update:title', event.target.value);

@ -1,6 +1,11 @@
<template> <template>
<div class="kit__section kit__section--block"> <div class="kit__section kit__section--block">
<div class="kit__section-remove"> <div class="kit__section-remove">
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<button type="button" @click="onRemove"> <button type="button" @click="onRemove">
<svg class="icon icon-delete"> <svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use> <use xlink:href="/static/img/sprite.svg#icon-delete"></use>
@ -45,4 +50,4 @@
'vue-redactor': VueRedactor, 'vue-redactor': VueRedactor,
} }
} }
</script> </script>

@ -0,0 +1,18 @@
<svg width="15" height="12" viewBox="0 0 15 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Canvas" transform="translate(-249 99)">
<g id="hamburger">
<g id="Line">
<use xlink:href="#path0_stroke" transform="translate(249 -87)" fill="#C8C8C8"/>
</g>
<g id="Line">
<use xlink:href="#path0_stroke" transform="translate(249 -92)" fill="#C8C8C8"/>
</g>
<g id="Line">
<use xlink:href="#path0_stroke" transform="translate(249 -97)" fill="#C8C8C8"/>
</g>
</g>
</g>
<defs>
<path id="path0_stroke" d="M 0 0L 15 0L 15 -2L 0 -2L 0 0Z"/>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 597 B

@ -13,5 +13,7 @@ import "./modules/courses";
import "./modules/comments"; import "./modules/comments";
import "./modules/password-show"; import "./modules/password-show";
import "./modules/profile"; import "./modules/profile";
import "./modules/notification";
import "./modules/mixpanel";
import "../sass/app.sass"; import "../sass/app.sass";

@ -83,6 +83,7 @@ export const api = {
'type': 'text', 'type': 'text',
'data': { 'data': {
'id': block.data.id ? block.data.id : null, 'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index, 'position': ++index,
'title': block.data.title, 'title': block.data.title,
'txt': block.data.text, 'txt': block.data.text,
@ -93,6 +94,7 @@ export const api = {
'type': 'image', 'type': 'image',
'data': { 'data': {
'id': block.data.id ? block.data.id : null, 'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index, 'position': ++index,
'title': block.data.title, 'title': block.data.title,
'img': block.data.image_id, 'img': block.data.image_id,
@ -103,6 +105,7 @@ export const api = {
'type': 'image-text', 'type': 'image-text',
'data': { 'data': {
'id': block.data.id ? block.data.id : null, 'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index, 'position': ++index,
'title': block.data.title, 'title': block.data.title,
'img': block.data.image_id, 'img': block.data.image_id,
@ -114,6 +117,7 @@ export const api = {
'type': 'images', 'type': 'images',
'data': { 'data': {
'id': block.data.id ? block.data.id : null, 'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index, 'position': ++index,
'title': block.data.title, 'title': block.data.title,
'images': block.data.images.map((galleryImage) => { 'images': block.data.images.map((galleryImage) => {
@ -129,6 +133,7 @@ export const api = {
'type': 'video', 'type': 'video',
'data': { 'data': {
'id': block.data.id ? block.data.id : null, 'id': block.data.id ? block.data.id : null,
'uuid': block.uuid,
'position': ++index, 'position': ++index,
'title': block.data.title, 'title': block.data.title,
'url': block.data.video_url, 'url': block.data.video_url,
@ -254,6 +259,7 @@ export const api = {
coverImageId: courseJSON.cover && courseJSON.cover.id ? courseJSON.cover.id : null, coverImageId: courseJSON.cover && courseJSON.cover.id ? courseJSON.cover.id : null,
coverImage: courseJSON.cover && courseJSON.cover.image ? courseJSON.cover.image : null, coverImage: courseJSON.cover && courseJSON.cover.image ? courseJSON.cover.image : null,
content: api.convertContentResponse(courseJSON.content), content: api.convertContentResponse(courseJSON.content),
gallery: {images: courseJSON.gallery.gallery_images},
} }
}, },
convertContentResponse: (contentJson) => { convertContentResponse: (contentJson) => {
@ -269,6 +275,7 @@ export const api = {
if (contentItem.type === 'text') { if (contentItem.type === 'text') {
return { return {
'type': 'text', 'type': 'text',
'uuid': contentItem.uuid,
'data': { 'data': {
'id': contentItem.id ? contentItem.id : null, 'id': contentItem.id ? contentItem.id : null,
'title': contentItem.title, 'title': contentItem.title,
@ -278,6 +285,7 @@ export const api = {
} else if (contentItem.type === 'image') { } else if (contentItem.type === 'image') {
return { return {
'type': 'image', 'type': 'image',
'uuid': contentItem.uuid,
'data': { 'data': {
'id': contentItem.id ? contentItem.id : null, 'id': contentItem.id ? contentItem.id : null,
'title': contentItem.title, 'title': contentItem.title,
@ -288,6 +296,7 @@ export const api = {
} else if (contentItem.type === 'image-text') { } else if (contentItem.type === 'image-text') {
return { return {
'type': 'image-text', 'type': 'image-text',
'uuid': contentItem.uuid,
'data': { 'data': {
'id': contentItem.id ? contentItem.id : null, 'id': contentItem.id ? contentItem.id : null,
'title': contentItem.title, 'title': contentItem.title,
@ -299,6 +308,7 @@ export const api = {
} else if (contentItem.type === 'images') { } else if (contentItem.type === 'images') {
return { return {
'type': 'images', 'type': 'images',
'uuid': contentItem.uuid,
'data': { 'data': {
'id': contentItem.id ? contentItem.id : null, 'id': contentItem.id ? contentItem.id : null,
'title': contentItem.title, 'title': contentItem.title,
@ -314,6 +324,7 @@ export const api = {
} else if (contentItem.type === 'video') { } else if (contentItem.type === 'video') {
return { return {
'type': 'video', 'type': 'video',
'uuid': contentItem.uuid,
'data': { 'data': {
'id': contentItem.id ? contentItem.id : null, 'id': contentItem.id ? contentItem.id : null,
'title': contentItem.title, 'title': contentItem.title,
@ -407,4 +418,4 @@ export const api = {
} }
}); });
} }
}; };

@ -41,7 +41,7 @@ $(document).ready(function () {
pass.hide(); pass.hide();
login.fadeIn(); login.fadeIn();
}); });
$('#password-reset__success-hide').on('click', function (e) { $('#password-reset__success-hide').on('click', function (e) {
e.preventDefault(); e.preventDefault();
$('#password-reset__form-wrapper').show(); $('#password-reset__form-wrapper').show();
@ -265,36 +265,45 @@ $(document).ready(function () {
}); });
$.ajaxSetup({cache: true}); $.ajaxSetup({cache: true});
$.getScript('https://connect.facebook.net/en_US/sdk.js'); load_facebook();
const facebookButton = $('button.btn_fb'); const facebookButton = $('button.btn_fb');
facebookButton.on('click', function () { facebookButton.on('click', function () {
$('.auth-register__common-error').hide(); $('.auth-register__common-error').hide();
facebookButton.addClass('loading'); facebookButton.addClass('loading');
$.getScript('https://connect.facebook.net/en_US/sdk.js', function () {
FB.init({ if (facebookResponse) {
appId: '161924711105785', if (facebookResponse.status === 'connected') {
version: 'v2.7' login_with_facebook(facebookResponse.authResponse.accessToken);
}); return;
FB.getLoginStatus(function (response) { }
if (response.status === 'connected') { }
login_with_facebook(response.authResponse.accessToken);
} FB.login(function (response) {
else { if (response.status === 'connected') {
FB.login(function (response) { login_with_facebook(response.authResponse.accessToken);
if (response.status === 'connected') { } else {
login_with_facebook(response.authResponse.accessToken); facebookButton.removeClass('loading');
} else { $('.auth-register__common-error').text('Не удалось авторизоваться через Facebook');
facebookButton.removeClass('loading'); }
$('.auth-register__common-error').text('Не удалось авторизоваться через Facebook'); }, {scope: 'public_profile,email'});
}
}, {scope: 'public_profile,email'});
}
});
});
}); });
}); });
let facebookResponse;
function load_facebook() {
$.getScript('https://connect.facebook.net/en_US/sdk.js', function () {
FB.init({
appId: '161924711105785',
version: 'v2.7'
});
FB.getLoginStatus(function (response) {
facebookResponse = response;
});
});
}
function login_with_facebook(accessToken) { function login_with_facebook(accessToken) {
$.ajax('/auth/facebook_login/', { $.ajax('/auth/facebook_login/', {
method: 'POST', method: 'POST',
@ -316,4 +325,4 @@ function login_with_facebook(accessToken) {
.always(function () { .always(function () {
$('button.btn_fb').removeClass('loading'); $('button.btn_fb').removeClass('loading');
}); });
} }

@ -22,7 +22,7 @@ $(document).ready(function () {
.done(function (data) { .done(function (data) {
if (data.success === true) { if (data.success === true) {
if (replyToValue > 0) { if (replyToValue > 0) {
$(`#question__${replyToValue}`).after(data.comment); $(`#question__replyto__${replyToValue}`).after(data.comment);
} else { } else {
$('.questions__list').append(data.comment); $('.questions__list').append(data.comment);
} }

@ -1,8 +1,14 @@
import $ from 'jquery'; import $ from 'jquery';
import Inputmask from "inputmask"; import Inputmask from "inputmask";
import SmoothScroll from 'smooth-scroll/dist/js/smooth-scroll'; import SmoothScroll from 'smooth-scroll/dist/js/smooth-scroll';
import baguetteBox from 'baguettebox.js'
window.Inputmask = Inputmask; window.Inputmask = Inputmask;
window.baguetteBox = baguetteBox;
$(document).ready(function () { $(document).ready(function () {
baguetteBox.run('.gallery');
// Добавляем заголовок X-CSRFToken для всех AJAX запросов JQuery. // Добавляем заголовок X-CSRFToken для всех AJAX запросов JQuery.
$.ajaxSetup({ $.ajaxSetup({
headers: { headers: {

@ -17,7 +17,9 @@ $(document).ready(function () {
$('div[data-future-course]').each((_, element) => { $('div[data-future-course]').each((_, element) => {
const courseTime = parseInt($(element).attr('data-future-course-time')) + LIL_SERVER_TIME_DIFF; const courseTime = parseInt($(element).attr('data-future-course-time')) + LIL_SERVER_TIME_DIFF;
const relativeTimeString = moment(courseTime, 'X').fromNow(); const relativeTimeString = moment(courseTime, 'X').fromNow();
$(element).find('div.courses__time').text(relativeTimeString); $(element).find('div.courses__time').text(relativeTimeString);
$(element).find('div.video__time').text(relativeTimeString);
}); });
}, 1000); }, 1000);
@ -31,7 +33,7 @@ $(document).ready(function () {
e.preventDefault(); e.preventDefault();
const currentCategory = $(this).attr('data-category-name'); const currentCategory = $(this).attr('data-category-name');
$('[data-category-name]').removeClass('active'); $('[data-category-name]').removeClass('active');
$(`[data-category-name=${currentCategory}]`).addClass('active'); $(`[data-category-name='${currentCategory}']`).addClass('active');
history.replace($(this).attr('data-category-url')); history.replace($(this).attr('data-category-url'));
load_courses($(this).attr('data-category-url'), true); load_courses($(this).attr('data-category-url'), true);
}); });

@ -0,0 +1,36 @@
import $ from 'jquery';
$(document).ready(function (e) {
if (typeof mixpanel != 'undefined') {
mixpanel.identify(USER_ID);
let body = $('body'),
cource = $('.course');
if (cource.length) {
mixpanel.track(
'Open course',
{ 'course_id': COURSE_ID }
);
};
body.on('click', '[data-popup]', function (e) {
let data = $(this).data('popup');
if (data === '.js-popup-buy') {
mixpanel.track(
'Open school buy popup'
);
}
});
body.on('click', '[data-course-buy]', function (e) {
e.preventDefault();
let href = $(this).attr('href');
let t = mixpanel.track(
'Click course buy button',
{ 'course_id': COURSE_ID },
function () {
window.location = href;
}
);
});
}
});

@ -0,0 +1,14 @@
import $ from 'jquery';
import '../../sass/components/notification.scss';
export function showNotification(style, text) {
let htmlNode = document.createElement('div');
let htmlElement = $(htmlNode).addClass('notification').addClass(`notification--${style}`).text(text).appendTo($('body'));
setTimeout(() => {
htmlElement.fadeOut(400, () => {
htmlElement.remove();
})
}, 3500);
}

@ -73,7 +73,7 @@ $(document).ready(function () {
} }
var text = ''; var text = '';
if(weekdays.length >= 7) { if(schoolAmountForDiscount <= price) {
text = '<del>'+price+'</del> '+(price-schoolDiscount)+'р.'; text = '<del>'+price+'</del> '+(price-schoolDiscount)+'р.';
} else { } else {
text = price+'p.'; text = price+'p.';

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save