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

remotes/origin/hasaccess
Vitaly Baev 8 years ago
commit 6255658dd5
  1. 6
      Dockerfile
  2. 1
      api/v1/serializers/config.py
  3. 5
      api/v1/serializers/content.py
  4. 71
      api/v1/serializers/course.py
  5. 2
      api/v1/serializers/payment.py
  6. 36
      api/v1/serializers/user.py
  7. 9
      api/v1/urls.py
  8. 37
      api/v1/views.py
  9. 18
      apps/auth/middleware.py
  10. 18
      apps/content/migrations/0015_content_uuid.py
  11. 1
      apps/content/models.py
  12. 18
      apps/course/migrations/0035_comment_deactivated_at.py
  13. 2
      apps/course/models.py
  14. 13
      apps/course/templates/course/_items.html
  15. 8
      apps/course/templates/course/blocks/comment.html
  16. 7
      apps/course/templates/course/blocks/comments.html
  17. 19
      apps/course/templates/course/content/gallery.html
  18. 8
      apps/course/templates/course/content/image.html
  19. 6
      apps/course/templates/course/content/imagetext.html
  20. 11
      apps/course/templates/course/course.html
  21. 13
      apps/course/templates/course/lesson.html
  22. 7
      apps/course/views.py
  23. 13
      apps/notification/templates/notification/email/accept_author.html
  24. 8
      apps/notification/templates/notification/email/decline_author.html
  25. 2
      apps/notification/utils.py
  26. 2
      apps/payment/models.py
  27. 18
      apps/payment/tasks.py
  28. 12
      apps/payment/templates/payment/course_payment_success.html
  29. 65
      apps/payment/views.py
  30. 32
      apps/user/admin.py
  31. 58
      apps/user/fixtures/subscription_categories.json
  32. 11
      apps/user/forms.py
  33. 27
      apps/user/migrations/0009_authorrequest.py
  34. 18
      apps/user/migrations/0010_auto_20180312_1610.py
  35. 17
      apps/user/migrations/0011_auto_20180313_0744.py
  36. 18
      apps/user/migrations/0012_authorrequest_cause.py
  37. 18
      apps/user/migrations/0013_authorrequest_declined_send_at.py
  38. 18
      apps/user/migrations/0014_authorrequest_accepted_send_at.py
  39. 45
      apps/user/migrations/0015_auto_20180315_0547.py
  40. 20
      apps/user/migrations/0016_auto_20180315_0603.py
  41. 18
      apps/user/migrations/0017_subscriptioncategory_auto_add.py
  42. 19
      apps/user/migrations/0018_user_phone.py
  43. 18
      apps/user/migrations/0019_user_show_in_mainpage.py
  44. 156
      apps/user/models.py
  45. 22
      apps/user/tasks.py
  46. 12
      apps/user/templates/user/become-author-success.html
  47. 69
      apps/user/templates/user/become-author.html
  48. 41
      apps/user/templates/user/notification-settings.html
  49. 20
      apps/user/templates/user/profile-settings.html
  50. 83
      apps/user/views.py
  51. 2
      docker-compose.yml
  52. 22
      project/settings.py
  53. 16
      project/templates/lilcity/index.html
  54. 74
      project/templates/lilcity/main.html
  55. 52
      project/templates/lilcity/school_schedules.html
  56. 27
      project/urls.py
  57. 15
      project/views.py
  58. 36
      requirements.txt
  59. 1
      web/package.json
  60. 2
      web/src/components/CourseRedactor.vue
  61. 6
      web/src/js/modules/common.js
  62. 2
      web/src/js/modules/popup.js
  63. 8
      web/src/sass/_common.sass
  64. 1
      web/src/sass/app.sass
  65. 4
      web/yarn.lock

@ -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/

@ -19,6 +19,7 @@ class ConfigSerializer(serializers.Serializer):
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)
def to_representation(self, instance):
ret = OrderedDict()

@ -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,
@ -368,3 +372,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',
)

@ -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')

@ -15,6 +15,7 @@ from .serializers.course import (
CategorySerializer, LikeSerializer,
CourseSerializer, CourseCreateSerializer,
CourseBulkChangeCategorySerializer,
CommentSerializer,
MaterialSerializer, MaterialCreateSerializer,
LessonSerializer, LessonCreateSerializer,
)
@ -30,24 +31,33 @@ 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.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,
@ -329,3 +339,26 @@ class ConfigViewSet(generics.RetrieveUpdateAPIView):
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':
return queryset
elif is_deactivated == '1':
return queryset.filter(deactivated_at__isnull=True)
elif is_deactivated == '2':
return queryset.filter(deactivated_at__isnull=False)
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

@ -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,

@ -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>
@ -86,4 +87,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 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,19 +1,28 @@
{% load thumbnail %}
{% if results %}
<div class="title">Галерея итогов обучения</div>
<div class="examples">
<div class="examples gallery">
{% for image in course.gallery.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 %}
</div>
{% else %}
<div class="content-block title">{{ content.title }}</div>
<div class="examples">
<div class="examples gallery">
{% 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 %}
</div>
{% endif %}
{% endif %}

@ -1,6 +1,8 @@
<div class="content-block title">
{{ content.title }}
</div>
<div>
<img class="content-block pic" src="{{ content.img.image.url }}" alt="">
</div>
<div class="gallery">
<a href="{{ content.img.image.url }}">
<img class="content-block pic" src="{{ content.img.image.url }}" alt="">
</a>
</div>

@ -4,6 +4,8 @@
<div class="content-block text">
{{ content.txt | safe }}
</div>
<div>
<img class="content-block pic" src="{{ content.img.image.url }}" alt="">
<div class="gallery">
<a href="{{ content.img.image.url }}">
<img class="content-block pic" src="{{ content.img.image.url }}" alt="">
</a>
</div>

@ -409,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">В ответ на

@ -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>
@ -97,10 +97,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
@ -185,7 +186,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 +246,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)

@ -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])

@ -157,7 +157,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'
@ -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,144 @@ 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):
if not hasattr(instance, 'email_subscription'):
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"

@ -11,10 +11,13 @@ 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
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -52,6 +55,8 @@ INSTALLED_APPS = [
'corsheaders',
'constance',
'constance.backends.database',
'sorl.thumbnail',
'raven.contrib.django.raven_compat',
] + [
'apps.auth.apps',
'apps.user',
@ -73,6 +78,7 @@ 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']
@ -229,6 +235,9 @@ CELERY_BEAT_SCHEDULE = {
# Dynamic settings
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
CONSTANCE_ADDITIONAL_FIELDS = {
'image_field': ['django.forms.ImageField', {}]
}
CONSTANCE_CONFIG = OrderedDict((
('INSTAGRAM_CLIENT_ACCESS_TOKEN', ('7145314808.f6fa114.ce354a5d876041fc9d3db04b0045587d', '')),
('INSTAGRAM_CLIENT_SECRET', ('2334a921425140ccb180d145dcd35b25', '')),
@ -238,6 +247,7 @@ CONSTANCE_CONFIG = OrderedDict((
('SERVICE_COMMISSION', (10, 'Комиссия сервиса в процентах.')),
('SERVICE_DISCOUNT_MIN_AMOUNT', (3500, 'Минимальная сумма платежа для школы, после которой вычитывается скидка SERVICE_DISCOUNT.')),
('SERVICE_DISCOUNT', (1000, 'Комиссия сервиса при покупке всех дней.')),
# ('SCHOOL_LOGO_IMAGE', ('default.png', 'Изображение в диалоге покупки школы', 'image_field')),
))
CONSTANCE_CONFIG_FIELDSETS = OrderedDict({
@ -245,6 +255,7 @@ CONSTANCE_CONFIG_FIELDSETS = OrderedDict({
'SERVICE_COMMISSION',
'SERVICE_DISCOUNT_MIN_AMOUNT',
'SERVICE_DISCOUNT',
# 'SCHOOL_LOGO_IMAGE',
),
'Instagram': (
'INSTAGRAM_CLIENT_ACCESS_TOKEN',
@ -270,6 +281,9 @@ else:
Paymentwall.set_app_key('d6f02b90cf6b16220932f4037578aff7')
Paymentwall.set_secret_key('4ea515bf94e34cf28646c2e12a7b8707')
# Mixpanel settings
MIX_TOKEN = '79bd6bfd98667ed977737e6810b8abcd'
# CORS settings
if DEBUG:
@ -280,3 +294,11 @@ 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),
}

@ -188,8 +188,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 +202,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">
@ -495,6 +500,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>

@ -13,7 +13,7 @@
{% endif %}
class="main__btn btn"
href="#"
>КУПИТЬ ДОСТУП ОТ 2000р. в мес.</a>
>КУПИТЬ ДОСТУП ОТ {{ min_school_price }}р. в мес.</a>
</div>
</div>
{% if messages %}
@ -270,74 +270,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 +323,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>

@ -25,16 +25,24 @@ from apps.course.views import (
CourseOnModerationView,
)
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/on-moderation', CourseOnModerationView.as_view(), name='course-on-moderation'),
@ -49,7 +57,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 +67,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'),
]

@ -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,28 @@
# 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-constance[database]==2.1.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

@ -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",

@ -551,7 +551,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;

@ -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: {

@ -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.';

@ -778,6 +778,7 @@ a[name]
&__pic
display: block
width: 100%
height: 100%
border-radius: 50%
&__input
position: absolute
@ -1857,7 +1858,7 @@ a.grey-link
left: -10px
right: -10px
height: 2px
margin-top: -2px
margin-top: 5px
background-image: linear-gradient(-225deg, #FFE2EB 0%, #D8F5F5 100%)
&__title
display: table
@ -2487,6 +2488,11 @@ a.grey-link
width: 100%
.questions
&__anchor
display: block;
position: relative;
top: -110px;
visibility: hidden;
&__form,
&__item
display: flex

@ -2,3 +2,4 @@
@import helpers/all
@import generated/sprite-svg
@import common
@import '~baguettebox.js/dist/baguetteBox.min.css';

@ -887,6 +887,10 @@ backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
baguettebox.js@^1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/baguettebox.js/-/baguettebox.js-1.10.0.tgz#f24500b2f02433f52338cf77016ecf00cfe7f974"
balanced-match@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"

Loading…
Cancel
Save