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
RUN mkdir /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/
RUN pip install -r requirements.txt
ADD . /lilcity/
ADD . /lilcity/

@ -3,13 +3,13 @@ import base64
import six
import uuid
from django.conf import settings
from django.core.files.base import ContentFile
from rest_framework import serializers
class Base64ImageField(serializers.ImageField):
use_url = False
def to_internal_value(self, data):
if isinstance(data, six.string_types):
if 'data:' in data and ';base64,' in data:
@ -30,3 +30,8 @@ class Base64ImageField(serializers.ImageField):
extension = imghdr.what(file_name, decoded_file)
extension = "jpg" if extension == "jpeg" else 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.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.Serializer):
class ConfigSerializer(serializers.ModelSerializer):
SERVICE_COMMISSION = serializers.IntegerField(required=False)
SERVICE_DISCOUNT_MIN_AMOUNT = serializers.IntegerField(required=False)
SERVICE_DISCOUNT = serializers.IntegerField(required=False)
INSTAGRAM_CLIENT_ACCESS_TOKEN = serializers.CharField(required=False)
INSTAGRAM_CLIENT_SECRET = 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):
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
attribute = instance.get(field.field_name)
ret[field.field_name] = field.to_representation(attribute)
return ret
def to_internal_value(self, data):
ret = OrderedDict(get_values())
for k, v in data.items():
ret[k] = v
return ret
def update(self, instance, validated_data):
for k, v in validated_data.items():
_set_constance_value(k, v)
class Meta:
model = Config
fields = (
'SERVICE_COMMISSION',
'SERVICE_DISCOUNT_MIN_AMOUNT',
'SERVICE_DISCOUNT',
'INSTAGRAM_CLIENT_ACCESS_TOKEN',
'INSTAGRAM_CLIENT_SECRET',
'INSTAGRAM_PROFILE_URL',
'SCHOOL_LOGO_IMAGE',
'MAIN_PAGE_TOP_IMAGE',
)

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

@ -1,7 +1,11 @@
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 (
ImageObjectSerializer, ContentSerializer, ContentCreateSerializer,
GallerySerializer, GalleryImageSerializer,
@ -84,7 +88,7 @@ class CourseCreateSerializer(DispatchContentMixin,
):
title = 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(
child=ContentCreateSerializer(),
required=False,
@ -199,6 +203,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
t.title = cdata['title']
t.lesson = lesson
t.txt = cdata['txt']
t.uuid = cdata['uuid']
t.save()
else:
t = Text.objects.create(
@ -206,6 +211,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
title=cdata['title'],
lesson=lesson,
txt=cdata['txt'],
uuid=cdata['uuid'],
)
elif ctype == 'image':
if 'id' in cdata and cdata['id']:
@ -214,6 +220,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
image.title = cdata['title']
image.lesson = lesson
image.img = ImageObject.objects.get(id=cdata['img'])
image.uuid = cdata['uuid']
image.save()
else:
image = Image.objects.create(
@ -221,6 +228,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
title=cdata['title'],
lesson=lesson,
img=ImageObject.objects.get(id=cdata['img']),
uuid=cdata['uuid'],
)
elif ctype == 'image-text':
if 'id' in cdata and cdata['id']:
@ -230,6 +238,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
it.lesson = lesson
it.img = ImageObject.objects.get(id=cdata['img'])
it.txt = cdata['txt']
it.uuid = cdata['uuid']
it.save()
else:
it = ImageText.objects.create(
@ -238,6 +247,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
lesson=lesson,
img=ImageObject.objects.get(id=cdata['img']),
txt=cdata['txt'],
uuid=cdata['uuid'],
)
elif ctype == 'video':
if 'id' in cdata and cdata['id']:
@ -246,6 +256,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
v.title = cdata['title']
v.lesson = lesson
v.url = cdata['url']
v.uuid = cdata['uuid']
v.save()
else:
v = Video.objects.create(
@ -253,6 +264,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
title=cdata['title'],
lesson=lesson,
url=cdata['url'],
uuid=cdata['uuid'],
)
elif ctype == 'images':
if 'id' in cdata and cdata['id']:
@ -260,6 +272,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
g.position = cdata['position']
g.title = cdata['title']
g.lesson = lesson
g.uuid = cdata['uuid']
g.save()
if 'images' in cdata:
for image in cdata['images']:
@ -272,6 +285,7 @@ class LessonCreateSerializer(serializers.ModelSerializer):
lesson=lesson,
position=cdata['position'],
title=cdata['title'],
uuid=cdata['uuid'],
)
if 'images' in cdata:
for image in cdata['images']:
@ -368,3 +382,64 @@ class CourseSerializer(serializers.ModelSerializer):
'update_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.course = course
t.txt = cdata['txt']
t.uuid = cdata['uuid']
t.save()
else:
t = Text.objects.create(
@ -28,6 +29,7 @@ class DispatchContentMixin(object):
title=cdata['title'],
course=course,
txt=cdata['txt'],
uuid=cdata['uuid'],
)
elif ctype == 'image':
if 'id' in cdata and cdata['id']:
@ -36,6 +38,7 @@ class DispatchContentMixin(object):
image.title = cdata['title']
image.course = course
image.img = ImageObject.objects.get(id=cdata['img'])
image.uuid = cdata['uuid']
image.save()
else:
image = Image.objects.create(
@ -43,6 +46,7 @@ class DispatchContentMixin(object):
title=cdata['title'],
course=course,
img=ImageObject.objects.get(id=cdata['img']),
uuid=cdata['uuid'],
)
elif ctype == 'image-text':
if 'id' in cdata and cdata['id']:
@ -52,6 +56,7 @@ class DispatchContentMixin(object):
it.course = course
it.img = ImageObject.objects.get(id=cdata['img'])
it.txt = cdata['txt']
it.uuid = cdata['uuid']
it.save()
else:
it = ImageText.objects.create(
@ -60,6 +65,7 @@ class DispatchContentMixin(object):
course=course,
img=ImageObject.objects.get(id=cdata['img']),
txt=cdata['txt'],
uuid=cdata['uuid'],
)
elif ctype == 'video':
if 'id' in cdata and cdata['id']:
@ -68,6 +74,7 @@ class DispatchContentMixin(object):
v.title = cdata['title']
v.course = course
v.url = cdata['url']
v.uuid = cdata['uuid']
v.save()
else:
v = Video.objects.create(
@ -75,6 +82,7 @@ class DispatchContentMixin(object):
title=cdata['title'],
course=course,
url=cdata['url'],
uuid=cdata['uuid'],
)
elif ctype == 'images':
if 'id' in cdata and cdata['id']:
@ -82,6 +90,7 @@ class DispatchContentMixin(object):
g.course = course
g.position = cdata['position']
g.title = cdata['title']
g.uuid = cdata['uuid']
g.save()
if 'images' in cdata:
for image in cdata['images']:
@ -99,6 +108,7 @@ class DispatchContentMixin(object):
course=course,
position=cdata['position'],
title=cdata['title'],
uuid=cdata['uuid'],
)
if 'images' in cdata:
for image in cdata['images']:

@ -16,6 +16,7 @@ class AuthorBalanceCreateSerializer(serializers.ModelSerializer):
'commission',
'status',
'payment',
'card',
'cause',
)
@ -43,6 +44,7 @@ class AuthorBalanceSerializer(serializers.ModelSerializer):
'commission',
'status',
'payment',
'card',
'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 . import Base64ImageField
from django.contrib.auth import get_user_model
from . import Base64ImageField
from apps.user.models import AuthorRequest
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
phone = PhoneNumberField()
class Meta:
model = User
@ -15,6 +18,7 @@ class UserSerializer(serializers.ModelSerializer):
'id',
'username',
'email',
'phone',
'first_name',
'last_name',
'is_staff',
@ -36,6 +40,7 @@ class UserSerializer(serializers.ModelSerializer):
'is_email_proved',
'photo',
'balance',
'show_in_mainpage',
)
read_only_fields = (
@ -53,3 +58,30 @@ class UserPhotoSerializer(serializers.Serializer):
photo = Base64ImageField(
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 .views import (
AuthorBalanceViewSet, ConfigViewSet,
CategoryViewSet, CourseViewSet,
AuthorBalanceViewSet, AuthorRequestViewSet,
ConfigViewSet, CategoryViewSet,
CourseViewSet, CommentViewSet,
MaterialViewSet, LikeViewSet,
ImageViewSet, TextViewSet,
ImageTextViewSet, VideoViewSet,
@ -19,9 +20,11 @@ from .views import (
)
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'courses', CourseViewSet, base_name='courses')
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'lessons', LessonViewSet, base_name='lessons')
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 rest_framework import status
from rest_framework import views, viewsets
from rest_framework import generics
from rest_framework import status, views, viewsets, generics
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
@ -15,6 +11,7 @@ from .serializers.course import (
CategorySerializer, LikeSerializer,
CourseSerializer, CourseCreateSerializer,
CourseBulkChangeCategorySerializer,
CommentSerializer,
MaterialSerializer, MaterialCreateSerializer,
LessonSerializer, LessonCreateSerializer,
)
@ -30,24 +27,34 @@ from .serializers.content import (
from .serializers.school import SchoolScheduleSerializer
from .serializers.payment import AuthorBalanceSerializer, AuthorBalanceCreateSerializer
from .serializers.user import (
AuthorRequestSerializer,
UserSerializer, UserPhotoSerializer,
)
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 (
Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject,
)
from apps.payment.models import AuthorBalance
from apps.school.models import SchoolSchedule
from apps.user.models import AuthorRequest
User = get_user_model()
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_map = {
'list': AuthorBalanceSerializer,
@ -317,15 +324,34 @@ class SchoolScheduleViewSet(ExtendedModelViewSet):
class ConfigViewSet(generics.RetrieveUpdateAPIView):
queryset = Config.objects.all()
serializer_class = ConfigSerializer
permission_classes = (IsAdmin,)
def retrieve(self, request, *args, **kwargs):
serializer = ConfigSerializer(get_values())
return Response(serializer.data)
def get_object(self):
return Config.load()
def patch(self, request, *args, **kwargs):
serializer = ConfigSerializer(data=request.data)
if serializer.is_valid():
serializer.update(get_values(), serializer.validated_data)
return Response(serializer.data)
class CommentViewSet(ExtendedModelViewSet):
queryset = Comment.objects.filter(level=0)
serializer_class = CommentSerializer
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 %}
<p style="margin: 0 0 20px">Для восстановления пароля нажмите кнопку ниже.</p>
<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>{{ 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>
{% endblock content %}

@ -1,2 +1,2 @@
Восстановление пароля для {{ 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: async send email
refferer = self.request.META.get('HTTP_REFERER')
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)
return JsonResponse({"success": True}, status=201)
@ -106,7 +107,9 @@ class PasswordResetView(views.PasswordContextMixin, BaseFormView):
token_generator = views.default_token_generator
def form_valid(self, form):
refferer = self.request.META.get('HTTP_REFERER')
opts = {
'domain_override': refferer,
'use_https': self.request.is_secure(),
'token_generator': self.token_generator,
'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,
ImageText,
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):
uuid = models.UUIDField(null=True, blank=True)
course = models.ForeignKey(
'course.Course', on_delete=models.CASCADE,
null=True, blank=True,

@ -3,22 +3,24 @@ import json
import requests
import shutil
from constance import config
from instagram.client import InstagramAPI
from project.celery import app
from time import sleep
from django.conf import settings
from apps.config.models import Config
@app.task
def retrieve_photos():
config = Config.load()
api = InstagramAPI(
access_token=config.INSTAGRAM_CLIENT_ACCESS_TOKEN,
client_secret=config.INSTAGRAM_CLIENT_SECRET,
)
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):
try:
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',)
class Comment(PolymorphicMPTTModel):
class Comment(PolymorphicMPTTModel, DeactivatedMixin):
content = models.TextField('Текст комментария', default='')
author = models.ForeignKey(User, on_delete=models.CASCADE)
parent = PolymorphicTreeForeignKey(

@ -1,3 +1,4 @@
{% load thumbnail %}
{% load static %}
{% 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 %}
>
<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 %}
<img width="300px" height="170px" class="courses__pic" src="{{ course.cover.image.url }}"/>
{% else %}
<img width="300px" height="170px" class="courses__pic" src="{% static 'img/no_cover.png' %}"/>
{% endif %}
{% thumbnail course.cover.image "300x170" crop="center" as im %}
<img class="courses__pic" src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}"/>
{% empty %}
<img class="courses__pic" src="{% static 'img/no_cover.png' %}" width="300px" height="170px"/>
{% endthumbnail %}
<div class="courses__view">Подробнее</div>
{% if course.is_featured %}
<div class="courses__label courses__label_fav"></div>
@ -32,6 +33,16 @@
<div class="courses__time">ЧЕРНОВИК</div>
</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 %}
</a>
<div class="courses__details">
@ -86,4 +97,4 @@
</div>
</div>
</div>
</div>
</div>

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

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

@ -1,35 +1,32 @@
{% load thumbnail %}
{% if results %}
<div class="title">Галерея итогов обучения</div>
<div class="examples">
{% for image in course.gallery.gallery_images.all %}
<div class="examples__item">
<img class="examples__pic" src="{{ image.img.image.url }}">
<div class="title">Галерея итогов обучения</div>
<div class="examples gallery">
{% for image in course.gallery.gallery_images.all %}
<div class="examples__item">
<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>
{% endfor %}
</div>
{% else %}
<div class="section section_gradient">
<div class="section__center center center_sm">
<div class="title">{{ content.title }}</div>
<div class="examples">
{% for image in content.gallery_images.all %}
<div class="section section_gradient">
<div class="section__center center center_sm">
<div class="title">{{ content.title }}</div>
<div class="examples">
{% for image in content.gallery_images.all %}
<div class="examples__item">
<img class="examples__pic" src="{{ image.img.image.url }}">
<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 %}
{% endfor %}
</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>-->
{% endif %}

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

@ -14,7 +14,7 @@
{% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %}
{% block content %}
<div class="section section_border">
<div class="section section_border course">
<div class="section__center center center_sm">
<div class="go">
<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"
{% if user.is_authenticated %}
{% if not pending %}
data-course-buy
href="{% url 'course-checkout' course.id %}"
{% endif %}
{% else %}
@ -173,7 +174,7 @@
{% if course.is_deferred_start %}
<div class="video__soon">
<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>
{% else %}
{% comment %} <svg class="icon icon-play">
@ -392,6 +393,7 @@
class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md"
{% if user.is_authenticated %}
{% if not pending %}
data-course-buy
href="{% url 'course-checkout' course.id %}"
{% endif %}
{% else %}
@ -407,11 +409,18 @@
<div class="title">Задавайте вопросы:</div>
<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 %}">
<input type="hidden" name="reply_id">
<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 class="questions__wrap">
<div class="questions__reply-info">В ответ на

@ -1,13 +1,19 @@
{% extends "templates/lilcity/edit_index.html" %}
{% 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 %}
<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 }}"
access-token="{{ request.user.auth_token }}"
{% if course and course.id %}:course-id="{{ course.id }}"{% endif %}></course-redactor>
{% endblock content %}
{% block foot %}
<script type="text/javascript" src={% static "courseRedactor.js" %}></script>
<link rel="stylesheet" href={% static "courseRedactor.css" %}/>
{% endblock foot %}
<script type="text/javascript" src="{% static "courseRedactor.js" %}"></script>
<link rel="stylesheet" href="{% static "courseRedactor.css" %}" />
{% endblock foot %}

@ -33,7 +33,7 @@
<img class="video__pic" src="{{ lesson.cover.image.url }}"/>
{% else %}
<img class="video__pic" src="{% static 'img/no_cover.png' %}"/>
{% endif %}
{% endif %}
<svg class="icon icon-play">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-play"></use>
</svg>
@ -95,10 +95,17 @@
<div class="section__center center center_sm">
<div class="title">Задавайте вопросы:</div>
<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 %}">
<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 class="questions__wrap">
<div class="questions__field">

@ -7,6 +7,7 @@ from django.http import JsonResponse, Http404
from django.shortcuts import get_object_or_404
from django.template import loader, Context, Template
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.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
@ -170,11 +171,37 @@ class CourseEditView(TemplateView):
def get_context_data(self):
context = super().get_context_data()
context['live'] = 'false'
if self.object:
context['course'] = self.object
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')
class CourseView(DetailView):
model = Course
@ -185,7 +212,7 @@ class CourseView(DetailView):
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
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']):
raise Http404
return response
@ -245,12 +272,14 @@ class CoursesView(ListView):
else:
prev_url = None
next_url = None
return JsonResponse({
response = JsonResponse({
'success': True,
'content': html,
'prev_url': prev_url,
'next_url': next_url,
})
add_never_cache_headers(response)
return response
else:
return super().get(request, args, kwargs)

@ -45,7 +45,7 @@
<table style="width:100%;padding:0;border-collapse:collapse;border-top:1px solid #979797">
<tbody>
<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">
<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">

@ -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.conf import settings
from django.template.loader import get_template
from project.celery import app
@app.task
def send_email(subject, to_email, template_name, **kwargs):
html = get_template(template_name).render(kwargs)
email = EmailMessage(subject, html, to=[to_email])

@ -1,4 +1,3 @@
from constance import config
from paymentwall import Pingback
from polymorphic.models import PolymorphicModel
@ -9,9 +8,12 @@ from django.core.validators import RegexValidator
from django.utils.timezone import now
from apps.course.models import Course
from apps.config.models import Config
from apps.school.models import SchoolSchedule
from apps.notification.utils import send_email
config = Config.load()
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})$'
@ -157,7 +159,7 @@ class SchoolPayment(Payment):
models.Sum('month_price'),
)
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
else:
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.auth.decorators import login_required
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.decorators.csrf import csrf_exempt
from django.urls import reverse_lazy
@ -17,12 +17,27 @@ from paymentwall import Pingback, Product, Widget
from apps.course.models import Course
from apps.school.models import SchoolSchedule
from apps.payment.tasks import transaction_to_mixpanel
from .models import AuthorBalance, CoursePayment, SchoolPayment
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')
class CourseBuyView(TemplateView):
template_name = 'payment/paymentwall_widget.html'
@ -31,7 +46,7 @@ class CourseBuyView(TemplateView):
host = request.scheme + '://' + request.get_host()
course = Course.objects.get(id=pk)
if request.user == course.author:
messages.error('Вы не можете приобрести свой курс.')
messages.error(request, 'Вы не можете приобрести свой курс.')
return redirect(reverse_lazy('course', args=[course.id]))
course_payment = CoursePayment.objects.create(
user=request.user,
@ -52,7 +67,7 @@ class CourseBuyView(TemplateView):
'evaluation': 1,
'demo': 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')),
}
)
@ -132,27 +147,35 @@ class PaymentwallCallbackView(View):
logger.info(
json.dumps(payment_raw_data),
)
payment.status = pingback.get_type()
payment.data = payment_raw_data
if pingback.is_deliverable() and product_type_name == 'school':
school_payment = SchoolPayment.objects.filter(
user=payment.user,
date_start__lte=now(),
date_end__gt=now(),
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
).last()
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
if pingback.is_deliverable():
transaction_to_mixpanel.delay(
payment.user.id,
payment.amount,
now().strftime('%Y-%m-%dT%H:%M:%S'),
product_type_name,
)
if product_type_name == 'school':
school_payment = SchoolPayment.objects.filter(
user=payment.user,
date_start__lte=now(),
date_end__gt=now(),
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
).last()
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()
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.utils.translation import gettext_lazy as _
from .models import AuthorRequest, EmailSubscription, SubscriptionCategory
User = get_user_model()
@ -13,7 +15,35 @@ class UserAdmin(BaseUserAdmin):
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'gender', 'about', 'photo')}),
('Facebook Auth data', {'fields': ('fb_id', 'fb_data', 'is_email_proved')}),
(_('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')}),
('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.contrib.auth import get_user_model
from phonenumber_field.formfields import PhoneNumberField
from .fields import CreditCardField
@ -10,6 +11,7 @@ class UserEditForm(forms.ModelForm):
# first_name = forms.CharField()
# last_name = forms.CharField()
# email = forms.CharField()
phone = PhoneNumberField()
# city = forms.CharField()
# country = forms.CharField()
birthday = forms.DateField(input_formats=['%d.%m.%Y'], required=False)
@ -33,6 +35,7 @@ class UserEditForm(forms.ModelForm):
'first_name',
'last_name',
'email',
'phone',
'city',
'country',
'birthday',
@ -54,3 +57,11 @@ class UserEditForm(forms.ModelForm):
class WithdrawalForm(forms.Form):
amount = forms.DecimalField(required=True, min_value=2000)
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.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.postgres import fields as pgfields
from django.utils.timezone import now
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 apps.notification.utils import send_email
from apps.user.tasks import user_to_mixpanel
class User(AbstractUser):
@ -29,6 +34,7 @@ class User(AbstractUser):
(FEMALE, 'Женщина'),
)
email = models.EmailField(_('email address'), unique=True)
phone = PhoneNumberField(null=True, blank=True, unique=True)
role = models.PositiveSmallIntegerField(
'Роль', default=0, choices=ROLE_CHOICES)
gender = models.CharField(
@ -49,6 +55,7 @@ class User(AbstractUser):
'Верифицирован по email', default=False
)
photo = models.ImageField('Фото', null=True, blank=True, upload_to='users')
show_in_mainpage = models.BooleanField('Показывать на главной странице', default=False)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
@ -63,7 +70,7 @@ class User(AbstractUser):
@property
def balance(self):
aggregate = self.balances.aggregate(
aggregate = self.balances.filter(type=0).aggregate(
models.Sum('amount'),
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]
) and hasattr(instance, 'auth_token'):
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 %}
<div class="section section_gray">
<div class="section__center center center_xs">
<div class="form">
<form class="form" method="POST">{% csrf_token %}
<div class="form__group">
<div class="form__title">Уведомления и рассылка</div>
{% for category in subscription_categories %}
<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" 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 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>
<input
name='category'
value="{{ category.id }}"
class="switch__input"
type="checkbox"
{% if request.user.email_subscription and category in request.user.email_subscription.categories.all %}checked{% endif %}>
<span class="switch__content">{{ category.title }}</span>
</label>
{% endfor %}
</div>
<div class="form__foot">
<button class="form__btn btn btn_md">СОХРАНИТЬ</button>
</div>
</div>
</form>
</div>
</div>
{% endblock content %}
{% endblock content %}

@ -79,7 +79,16 @@
{% if form.email.errors %}
<div class="field__error">Укажите корректно свои данные</div>
{% 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__field field{% if form.city.errors %} error{% endif %}">
<div class="field__label">ГОРОД</div>
@ -250,4 +259,11 @@ var openFile = function(file) {
reader.readAsDataURL(input.files[0]);
};
</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.core.exceptions import ValidationError
from django.shortcuts import render, reverse, redirect
from django.views import View
from django.views.generic import DetailView, UpdateView, TemplateView, FormView
from django.contrib import messages
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.school.models import SchoolSchedule
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()
@ -85,6 +87,28 @@ class UserView(DetailView):
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')
class NotificationEditView(TemplateView):
template_name = 'user/notification-settings.html'
@ -92,6 +116,18 @@ class NotificationEditView(TemplateView):
def get(self, request, pk=None):
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')
class PaymentHistoryView(FormView):
@ -115,7 +151,7 @@ class PaymentHistoryView(FormView):
type=AuthorBalance.OUT,
amount=form.cleaned_data['amount'],
status=AuthorBalance.PENDING,
card=form.cleaned_data['amount'],
card=form.cleaned_data['card'],
)
return self.form_valid(form)
else:
@ -182,3 +218,46 @@ class UserEditView(UpdateView):
def get_success_url(self):
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"
redis:
image: redis:3-alpine
image: redis:4-alpine
ports:
- "6379:6379"
@ -22,7 +22,7 @@ services:
restart: always
volumes:
- .:/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:
- DJANGO_SETTINGS_MODULE=project.settings
- 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 raven
from celery.schedules import crontab
from collections import OrderedDict
from datetime import timedelta
@ -50,8 +52,8 @@ INSTALLED_APPS = [
'rest_framework.authtoken',
'drf_yasg',
'corsheaders',
'constance',
'constance.backends.database',
'sorl.thumbnail',
'raven.contrib.django.raven_compat',
] + [
'apps.auth.apps',
'apps.user',
@ -59,10 +61,9 @@ INSTALLED_APPS = [
'apps.payment',
'apps.course',
'apps.content',
'apps.config',
'apps.school',
]
if DEBUG:
INSTALLED_APPS += ['silk']
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
@ -73,9 +74,8 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'apps.auth.middleware.TokenAuthLoginMiddleware',
]
if DEBUG:
MIDDLEWARE += ['silk.middleware.SilkyMiddleware']
ROOT_URLCONF = 'project.urls'
@ -85,32 +85,26 @@ TEMPLATES = [
'DIRS': [
'project',
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'constance.context_processors.config',
'project.context_processors.config',
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'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'
# 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 = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
@ -202,6 +196,7 @@ REST_FRAMEWORK = {
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
# 'rest_framework.renderers.BrowsableAPIRenderer',
),
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
@ -221,46 +216,11 @@ CELERY_TASK_SERIALIZER = 'json'
CELERY_BEAT_SCHEDULE = {
'retrieve_photos_from_instagram': {
'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': (),
},
}
# 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:
from paymentwall import *
except ImportError:
@ -270,6 +230,9 @@ else:
Paymentwall.set_app_key('d6f02b90cf6b16220932f4037578aff7')
Paymentwall.set_secret_key('4ea515bf94e34cf28646c2e12a7b8707')
# Mixpanel settings
MIX_TOKEN = '79bd6bfd98667ed977737e6810b8abcd'
# CORS settings
if DEBUG:
@ -280,3 +243,18 @@ if DEBUG:
SWAGGER_SETTINGS = {
'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__col">
<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>
</div>
@ -345,6 +345,10 @@
</div>
</div>
<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 %}
</body>

@ -47,12 +47,23 @@
viewportmeta.content = 'width=device-width, maximum-scale=1.6, initial-scale=1.0';
}
}
</script>
<script>
LIL_SERVER_TIME = "{% now 'U' %}";
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>
<!-- end Mixpanel -->
{% endblock mixpanel %}
</head>
<body>
@ -120,7 +131,7 @@
</a> {% endcomment %}
</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">
{% category_menu_items category %}
</div>
@ -188,8 +199,11 @@
</div>
<div class="footer__col">
<div class="footer__title">Программы</div>
<nav class="footer__nav"><a class="footer__link" href="#">Онлайн-школа</a><a class="footer__link" href="#">Онлайн-курсы</a><a
class="footer__link" href="#">Стать автором</a></nav>
<nav class="footer__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 class="footer__col">
<div class="footer__title">Контакты</div>
@ -199,13 +213,15 @@
</div>
<div class="footer__col footer__col_md">
<div class="footer__title">ПОДПИСАТЬСЯ НА НОВОСТИ</div>
<div class="subscribe">
<div class="subscribe__field"><input class="subscribe__input" type="text" placeholder="Email"></div>
<form class="subscribe" method="POST" action="{% url 'subscribe' %}">{% csrf_token %}
<div class="subscribe__field">
<input class="subscribe__input" type="text" name="email" placeholder="Email">
</div>
<button class="subscribe__btn btn btn_light">ПОДПИСАТЬСЯ</button>
<div class="subscribe__content">Мы сами не любим спам, поэтому вы будете подучать от только важные новости о
школе, новых курсах и бонусах от Lil City.
</div>
</div>
</form>
</div>
</div>
<div class="footer__row footer__row_second">
@ -232,7 +248,7 @@
</div>
<div class="footer__col footer__col_lg">
<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">
<a class="footer__link" href="{% url 'terms' %}">Договор-оферта</a>
<div class="footer__divider">|</div>
@ -423,7 +439,7 @@
<div class="buy__row">
<div class="buy__col">
<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>
</div>
@ -495,6 +511,7 @@
<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 %}
</body>

@ -2,7 +2,14 @@
{% block title %}School LIL.CITY{% endblock title %}
{% 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__title">Первая онлайн-школа креативного мышления для детей! 5+</div>
<a
@ -13,7 +20,7 @@
{% endif %}
class="main__btn btn"
href="#"
>КУПИТЬ ДОСТУП ОТ 500р. в мес.</a>
>КУПИТЬ ДОСТУП ОТ {{ min_school_price }}р. в мес.</a>
</div>
</div>
{% if messages %}
@ -270,74 +277,30 @@
<img class="text__curve text__curve_three" src="{% static 'img/curve-3.svg' %}">
</div>
<div class="teachers">
{% for author in authors %}
<div class="teachers__item">
<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' %}">
{% endif %}
</div>
<div class="teachers__wrap">
<div class="teachers__title">Саша Крю,
<div class="teachers__title">{{ author.get_full_name }},
<a href='#'>#lil_персонаж</a>
</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">
<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>
</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>
{{ author.about }}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<div class="text text_mb0">Если хотите к нам в команду, то отправьте нам заявку</div>
</div>
@ -367,7 +330,7 @@
{% endfor %}
</div>
<div class="text text_mb0">
<a href='#'>Распечатать расписание</a> чтобы не забыть</div>
<a target="_blank" href="{% url 'school_schedules' %}">Распечатать расписание</a> чтобы не забыть</div>
</div>
</div>
{% 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
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.contrib import admin
from django.urls import path, include
from django.views.generic import TemplateView
from django.conf import settings
from django.urls import path, include
from apps.course.views import (
CoursesView, likes, coursecomment,
CourseView, LessonView, SearchView,
lessoncomment, CourseEditView,
CourseOnModerationView,
CourseOnModerationView, CourseLiveEditView,
)
from apps.user.views import (
UserView, UserEditView, NotificationEditView,
AuthorRequestView, UserView,
UserEditView, NotificationEditView,
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 = [
path('admin/', admin.site.urls),
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('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/<int:pk>/', 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('payments/ping', PaymentwallCallbackView.as_view(), name='payment-ping'),
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('school/checkout', SchoolBuyView.as_view(), name='school-checkout'),
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>/payments', PaymentHistoryView.as_view(), name='user-edit-payments'),
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('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('subscribe', SubscribeView.as_view(), name='subscribe'),
path('privacy', TemplateView.as_view(template_name='templates/lilcity/privacy_policy.html'), name='privacy'),
path('terms', TemplateView.as_view(template_name='templates/lilcity/terms.html'), name='terms'),
path('refund-policy', TemplateView.as_view(template_name='templates/lilcity/refund_policy.html'), name='refund_policy'),
path('school-schedules', SchoolSchedulesView.as_view(), name='school_schedules'),
path('', IndexView.as_view(), name='index'),
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.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += staticfiles_urlpatterns()
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 apps.course.models import Course
from apps.school.models import SchoolSchedule
User = get_user_model()
class IndexView(TemplateView):
template_name = 'templates/lilcity/main.html'
@ -12,5 +16,16 @@ class IndexView(TemplateView):
context.update({
'course_items': Course.objects.filter(status=Course.PUBLISHED)[:3],
'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
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
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
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-mptt==0.9.0
django-silk==2.0.0
django-phonenumber-field==2.0.0
django-polymorphic-tree==1.5
celery[redis]==4.1.0
djangorestframework==3.7.7
drf-yasg[validation]==1.4.0
django-silk==2.0.0
django-cors-headers==2.1.0
django-constance[database]==2.1.0
drf-yasg[validation]==1.5.0
facepy==1.0.9
gunicorn==19.7.1
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
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>
<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="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"/>
</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"/>
</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">
<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">

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

@ -87,7 +87,7 @@
</a>
</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">
<a class="header__link" href="#">
<div class="header__title">ПЕРСОНАЖ</div>
@ -264,7 +264,7 @@
<div class="buy__row">
<div class="buy__col">
<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>
</div>

@ -87,7 +87,7 @@
</a>
</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">
<a class="header__link" href="#">
<div class="header__title">ПЕРСОНАЖ</div>
@ -352,7 +352,7 @@
<div class="buy__row">
<div class="buy__col">
<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>
</div>

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

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

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

@ -1,6 +1,11 @@
<template>
<div class="kit__section kit__section--block">
<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">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
@ -45,4 +50,4 @@
'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/password-show";
import "./modules/profile";
import "./modules/notification";
import "./modules/mixpanel";
import "../sass/app.sass";

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

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

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

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

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

@ -0,0 +1,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 = '';
if(weekdays.length >= 7) {
if(schoolAmountForDiscount <= price) {
text = '<del>'+price+'</del> '+(price-schoolDiscount)+'р.';
} else {
text = price+'p.';

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

Loading…
Cancel
Save