diff --git a/Dockerfile b/Dockerfile index c61bc829..18f28383 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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/ \ No newline at end of file +ADD . /lilcity/ diff --git a/api/v1/serializers/config.py b/api/v1/serializers/config.py index e17ce57e..f6ae89dd 100644 --- a/api/v1/serializers/config.py +++ b/api/v1/serializers/config.py @@ -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() diff --git a/api/v1/serializers/content.py b/api/v1/serializers/content.py index c01a2c65..e576912b 100644 --- a/api/v1/serializers/content.py +++ b/api/v1/serializers/content.py @@ -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', diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index 8139d8fa..7f45c7a2 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -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', + ) diff --git a/api/v1/serializers/payment.py b/api/v1/serializers/payment.py index 9174177d..d005a88d 100644 --- a/api/v1/serializers/payment.py +++ b/api/v1/serializers/payment.py @@ -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', ) diff --git a/api/v1/serializers/user.py b/api/v1/serializers/user.py index 8ce068e2..f334e9a5 100644 --- a/api/v1/serializers/user.py +++ b/api/v1/serializers/user.py @@ -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', + ) diff --git a/api/v1/urls.py b/api/v1/urls.py index ab852c4d..cf52179a 100644 --- a/api/v1/urls.py +++ b/api/v1/urls.py @@ -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') diff --git a/api/v1/views.py b/api/v1/views.py index 68f0f428..909195d4 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -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',) diff --git a/apps/auth/middleware.py b/apps/auth/middleware.py new file mode 100644 index 00000000..1c737a93 --- /dev/null +++ b/apps/auth/middleware.py @@ -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 diff --git a/apps/content/migrations/0015_content_uuid.py b/apps/content/migrations/0015_content_uuid.py new file mode 100644 index 00000000..54a1997c --- /dev/null +++ b/apps/content/migrations/0015_content_uuid.py @@ -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), + ), + ] diff --git a/apps/content/models.py b/apps/content/models.py index 345221f4..5c0b9b66 100644 --- a/apps/content/models.py +++ b/apps/content/models.py @@ -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, diff --git a/apps/course/migrations/0035_comment_deactivated_at.py b/apps/course/migrations/0035_comment_deactivated_at.py new file mode 100644 index 00000000..cd5ba08e --- /dev/null +++ b/apps/course/migrations/0035_comment_deactivated_at.py @@ -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), + ), + ] diff --git a/apps/course/models.py b/apps/course/models.py index ad59b1b1..98a64423 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -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( diff --git a/apps/course/templates/course/_items.html b/apps/course/templates/course/_items.html index 260a482e..33312e53 100644 --- a/apps/course/templates/course/_items.html +++ b/apps/course/templates/course/_items.html @@ -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 %} > - {% if course.cover %} - - {% else %} - - {% endif %} + {% thumbnail course.cover.image "300x170" crop="center" as im %} + + {% empty %} + + {% endthumbnail %}
Подробнее
{% if course.is_featured %}
@@ -86,4 +87,4 @@ - \ No newline at end of file + diff --git a/apps/course/templates/course/blocks/comment.html b/apps/course/templates/course/blocks/comment.html index 1f338358..497c398d 100644 --- a/apps/course/templates/course/blocks/comment.html +++ b/apps/course/templates/course/blocks/comment.html @@ -1,6 +1,7 @@ {% load static %} - -
+{% if not node.deactivated_at %} + +
{% if node.author.photo %}
@@ -24,4 +25,5 @@ {% endif %}
-
\ No newline at end of file + +{% endif %} diff --git a/apps/course/templates/course/blocks/comments.html b/apps/course/templates/course/blocks/comments.html index c762ebc5..b2b74983 100644 --- a/apps/course/templates/course/blocks/comments.html +++ b/apps/course/templates/course/blocks/comments.html @@ -1,5 +1,10 @@ {% load mptt_tags %} {% recursetree object.comments.all %} +{% if not node.deactivated_at %} {% include './comment.html' %} -{{ children }} {% endrecursetree %} \ No newline at end of file +{% if not node.is_leaf_node %} +{{ children }} +{% endif %} +{% endif %} +{% endrecursetree %} diff --git a/apps/course/templates/course/content/gallery.html b/apps/course/templates/course/content/gallery.html index 771df8b6..0d751dc6 100644 --- a/apps/course/templates/course/content/gallery.html +++ b/apps/course/templates/course/content/gallery.html @@ -1,19 +1,28 @@ +{% load thumbnail %} {% if results %}
Галерея итогов обучения
-
+ {% else %}
{{ content.title }}
-
+ -{% endif %} \ No newline at end of file +{% endif %} diff --git a/apps/course/templates/course/content/image.html b/apps/course/templates/course/content/image.html index 5daeba4e..1117d434 100644 --- a/apps/course/templates/course/content/image.html +++ b/apps/course/templates/course/content/image.html @@ -1,6 +1,8 @@
{{ content.title }}
-
- -
\ No newline at end of file + diff --git a/apps/course/templates/course/content/imagetext.html b/apps/course/templates/course/content/imagetext.html index 9849e357..0b3301cc 100644 --- a/apps/course/templates/course/content/imagetext.html +++ b/apps/course/templates/course/content/imagetext.html @@ -4,6 +4,8 @@
{{ content.txt | safe }}
-
- + \ No newline at end of file diff --git a/apps/course/templates/course/course.html b/apps/course/templates/course/course.html index 5f7a00c8..4426225e 100644 --- a/apps/course/templates/course/course.html +++ b/apps/course/templates/course/course.html @@ -409,11 +409,18 @@
Задавайте вопросы:
- {% if user.is_authenticated %} + {% if request.user.is_authenticated %}
- +
В ответ на diff --git a/apps/course/templates/course/lesson.html b/apps/course/templates/course/lesson.html index 9286f106..480d7d33 100644 --- a/apps/course/templates/course/lesson.html +++ b/apps/course/templates/course/lesson.html @@ -33,7 +33,7 @@ {% else %} - {% endif %} + {% endif %} @@ -97,10 +97,17 @@
Задавайте вопросы:
- {% if user.is_authenticated %} + {% if request.user.is_authenticated %}
- +
diff --git a/apps/course/views.py b/apps/course/views.py index 68fd1b1a..42235e27 100644 --- a/apps/course/views.py +++ b/apps/course/views.py @@ -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) diff --git a/apps/notification/templates/notification/email/accept_author.html b/apps/notification/templates/notification/email/accept_author.html new file mode 100644 index 00000000..44221ba5 --- /dev/null +++ b/apps/notification/templates/notification/email/accept_author.html @@ -0,0 +1,13 @@ +{% extends "notification/email/_base.html" %} + +{% block content %} +

Поздравляем! Вам одобрено назначение преподавателем!

+
+

Теперь вы можете публиковать курсы.

+ {% if password and email %} +

Параметры входа:

+

email: {{ email }}

+

пароль: {{ password }}

+ {% endif %} +
+{% endblock content %} diff --git a/apps/notification/templates/notification/email/decline_author.html b/apps/notification/templates/notification/email/decline_author.html new file mode 100644 index 00000000..d67dfe3e --- /dev/null +++ b/apps/notification/templates/notification/email/decline_author.html @@ -0,0 +1,8 @@ +{% extends "notification/email/_base.html" %} + +{% block content %} +

К сожалению вам отказано в назначении преподавателем!

+
+

{{ cause }}

+
+{% endblock content %} diff --git a/apps/notification/utils.py b/apps/notification/utils.py index bc206a13..c04b3c0e 100644 --- a/apps/notification/utils.py +++ b/apps/notification/utils.py @@ -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]) diff --git a/apps/payment/models.py b/apps/payment/models.py index 5ec8e1ac..d3b42c8c 100644 --- a/apps/payment/models.py +++ b/apps/payment/models.py @@ -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 diff --git a/apps/payment/tasks.py b/apps/payment/tasks.py new file mode 100644 index 00000000..e5a3f296 --- /dev/null +++ b/apps/payment/tasks.py @@ -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, + } + ) diff --git a/apps/payment/templates/payment/course_payment_success.html b/apps/payment/templates/payment/course_payment_success.html new file mode 100644 index 00000000..3edd1879 --- /dev/null +++ b/apps/payment/templates/payment/course_payment_success.html @@ -0,0 +1,12 @@ +{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %} +
+
+
+
Вы успешно приобрели курс!
+ +
+
+
+{% endblock content %} diff --git a/apps/payment/views.py b/apps/payment/views.py index 52ccd274..e7490ad9 100644 --- a/apps/payment/views.py +++ b/apps/payment/views.py @@ -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) diff --git a/apps/user/admin.py b/apps/user/admin.py index 76e27edc..df3e01d9 100644 --- a/apps/user/admin.py +++ b/apps/user/admin.py @@ -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', + ) diff --git a/apps/user/fixtures/subscription_categories.json b/apps/user/fixtures/subscription_categories.json new file mode 100644 index 00000000..07b3fd10 --- /dev/null +++ b/apps/user/fixtures/subscription_categories.json @@ -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 + } + } +] diff --git a/apps/user/forms.py b/apps/user/forms.py index 2f75ef68..2445d522 100644 --- a/apps/user/forms.py +++ b/apps/user/forms.py @@ -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) diff --git a/apps/user/migrations/0009_authorrequest.py b/apps/user/migrations/0009_authorrequest.py new file mode 100644 index 00000000..9e10d01c --- /dev/null +++ b/apps/user/migrations/0009_authorrequest.py @@ -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)), + ], + ), + ] diff --git a/apps/user/migrations/0010_auto_20180312_1610.py b/apps/user/migrations/0010_auto_20180312_1610.py new file mode 100644 index 00000000..b691deea --- /dev/null +++ b/apps/user/migrations/0010_auto_20180312_1610.py @@ -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'), + ), + ] diff --git a/apps/user/migrations/0011_auto_20180313_0744.py b/apps/user/migrations/0011_auto_20180313_0744.py new file mode 100644 index 00000000..3fe91ef7 --- /dev/null +++ b/apps/user/migrations/0011_auto_20180313_0744.py @@ -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': 'Заявки не преподавателя'}, + ), + ] diff --git a/apps/user/migrations/0012_authorrequest_cause.py b/apps/user/migrations/0012_authorrequest_cause.py new file mode 100644 index 00000000..fef855a2 --- /dev/null +++ b/apps/user/migrations/0012_authorrequest_cause.py @@ -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='Причина отказа'), + ), + ] diff --git a/apps/user/migrations/0013_authorrequest_declined_send_at.py b/apps/user/migrations/0013_authorrequest_declined_send_at.py new file mode 100644 index 00000000..50a6a738 --- /dev/null +++ b/apps/user/migrations/0013_authorrequest_declined_send_at.py @@ -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), + ), + ] diff --git a/apps/user/migrations/0014_authorrequest_accepted_send_at.py b/apps/user/migrations/0014_authorrequest_accepted_send_at.py new file mode 100644 index 00000000..284c5909 --- /dev/null +++ b/apps/user/migrations/0014_authorrequest_accepted_send_at.py @@ -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), + ), + ] diff --git a/apps/user/migrations/0015_auto_20180315_0547.py b/apps/user/migrations/0015_auto_20180315_0547.py new file mode 100644 index 00000000..dddc43d5 --- /dev/null +++ b/apps/user/migrations/0015_auto_20180315_0547.py @@ -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), + ), + ] diff --git a/apps/user/migrations/0016_auto_20180315_0603.py b/apps/user/migrations/0016_auto_20180315_0603.py new file mode 100644 index 00000000..3c5d3f47 --- /dev/null +++ b/apps/user/migrations/0016_auto_20180315_0603.py @@ -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), + ), + ] diff --git a/apps/user/migrations/0017_subscriptioncategory_auto_add.py b/apps/user/migrations/0017_subscriptioncategory_auto_add.py new file mode 100644 index 00000000..9530f62c --- /dev/null +++ b/apps/user/migrations/0017_subscriptioncategory_auto_add.py @@ -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), + ), + ] diff --git a/apps/user/migrations/0018_user_phone.py b/apps/user/migrations/0018_user_phone.py new file mode 100644 index 00000000..d377bcf5 --- /dev/null +++ b/apps/user/migrations/0018_user_phone.py @@ -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), + ), + ] diff --git a/apps/user/migrations/0019_user_show_in_mainpage.py b/apps/user/migrations/0019_user_show_in_mainpage.py new file mode 100644 index 00000000..e624bba1 --- /dev/null +++ b/apps/user/migrations/0019_user_show_in_mainpage.py @@ -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='Показывать на главной странице'), + ), + ] diff --git a/apps/user/models.py b/apps/user/models.py index 43eac1cd..058df6b6 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -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) diff --git a/apps/user/tasks.py b/apps/user/tasks.py new file mode 100644 index 00000000..a95988be --- /dev/null +++ b/apps/user/tasks.py @@ -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, + } + ) diff --git a/apps/user/templates/user/become-author-success.html b/apps/user/templates/user/become-author-success.html new file mode 100644 index 00000000..e6e97f7d --- /dev/null +++ b/apps/user/templates/user/become-author-success.html @@ -0,0 +1,12 @@ +{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %} +
+
+
+
Ваша заявка отправлена!
+ +
+
+
+{% endblock content %} diff --git a/apps/user/templates/user/become-author.html b/apps/user/templates/user/become-author.html new file mode 100644 index 00000000..383b81ab --- /dev/null +++ b/apps/user/templates/user/become-author.html @@ -0,0 +1,69 @@ +{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %} {% if messages %} +
+
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+
+{% endif %} +
+
+ {% csrf_token %} +
+
Стать автором
+
+
+
ИМЯ
+
+ +
+ {% if form.first_name.errors %} +
Укажите корректно свои данные
+ {% endif %} +
+
+
ФАМИЛИЯ
+
+ +
+ {% if form.last_name.errors %} +
Укажите корректно свои данные
+ {% endif %} +
+
+
+
Почта
+
+ +
+ {% if form.email.errors %} +
Укажите корректно свои данные
+ {% endif %} +
+
+
О себе
+
+ +
+ {% if form.about.errors %} +
Укажите корректно свои данные
+ {% endif %} +
+
+
FACEBOOK
+
+ +
+ {% if form.facebook.errors %} +
Укажите корректно свои данные
+ {% endif %} +
+
+
+ +
+ +
+
+{% endblock content %} diff --git a/apps/user/templates/user/notification-settings.html b/apps/user/templates/user/notification-settings.html index e15996e5..3a486926 100644 --- a/apps/user/templates/user/notification-settings.html +++ b/apps/user/templates/user/notification-settings.html @@ -24,42 +24,25 @@ {% endif %}
-
+
{% csrf_token %}
Уведомления и рассылка
+ {% for category in subscription_categories %} - - - - - - + {% endfor %}
-
+
-{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/apps/user/templates/user/profile-settings.html b/apps/user/templates/user/profile-settings.html index 68d41375..f5f2e9fe 100644 --- a/apps/user/templates/user/profile-settings.html +++ b/apps/user/templates/user/profile-settings.html @@ -79,7 +79,16 @@ {% if form.email.errors %}
Укажите корректно свои данные
{% endif %} -
+
+
+
Телефон
+
+ +
+ {% if form.phone.errors %} +
Укажите корректно свои данные
+ {% endif %} +
ГОРОД
@@ -250,4 +259,11 @@ var openFile = function(file) { reader.readAsDataURL(input.files[0]); }; -{% endblock content %} \ No newline at end of file + +{% endblock content %} + +{% block foot %} + +{% endblock foot %} \ No newline at end of file diff --git a/apps/user/views.py b/apps/user/views.py index 69e42bef..d98bcc07 100644 --- a/apps/user/views.py +++ b/apps/user/views.py @@ -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) diff --git a/docker-compose.yml b/docker-compose.yml index ab168db4..b3c84e2f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: - "5432:5432" redis: - image: redis:3-alpine + image: redis:4-alpine ports: - "6379:6379" diff --git a/project/settings.py b/project/settings.py index 6860b3d0..9785e9ca 100644 --- a/project/settings.py +++ b/project/settings.py @@ -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), +} diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index 2f28628d..9a6ee2b2 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -188,8 +188,11 @@
{% if messages %} @@ -270,74 +270,30 @@
+ {% for author in authors %}
+ {% if author.photo %} + + {% else %} + {% endif %}
-
Саша Крю, +
{{ author.get_full_name }}, #lil_персонаж
-
@sashakru
+ {% if author.instagram %} +
{{ author.instagram }}
+ {% endif %} + {% if author.about %}
-

Закончила ПХУ им К.А.Савицкого художник театра и кино. Работала с крупнейшими российскими и зарубежными - издательствами.

-

Участник и победитель международных выставок.

-

Основатель компании "Lil City".

-
-
-
-
-
- -
-
-
Саша Крю, - #lil_персонаж -
-
@sashakru
-
-

Закончила ПХУ им К.А.Савицкого художник театра и кино. Работала с крупнейшими российскими и зарубежными - издательствами.

-

Участник и победитель международных выставок.

-

Основатель компании "Lil City".

-
-
-
-
-
- -
-
-
Саша Крю, - #lil_персонаж -
-
@sashakru
-
-

Закончила ПХУ им К.А.Савицкого художник театра и кино. Работала с крупнейшими российскими и зарубежными - издательствами.

-

Участник и победитель международных выставок.

-

Основатель компании "Lil City".

-
-
-
-
-
- -
-
-
Саша Крю, - #lil_персонаж -
-
@sashakru
-
-

Закончила ПХУ им К.А.Савицкого художник театра и кино. Работала с крупнейшими российскими и зарубежными - издательствами.

-

Участник и победитель международных выставок.

-

Основатель компании "Lil City".

+ {{ author.about }}
+ {% endif %}
+ {% endfor %}
Если хотите к нам в команду, то отправьте нам заявку
@@ -367,7 +323,7 @@ {% endfor %}
- Распечатать расписание чтобы не забыть
+ Распечатать расписание чтобы не забыть
{% if course_items %} diff --git a/project/templates/lilcity/school_schedules.html b/project/templates/lilcity/school_schedules.html new file mode 100644 index 00000000..88601188 --- /dev/null +++ b/project/templates/lilcity/school_schedules.html @@ -0,0 +1,52 @@ +{% load static %} + + + + + + + + + + + +
+
+ +
Расписание
+
+
+ {% for school_schedule in school_schedules %} +
+
{{ school_schedule }}
+
+
{{ school_schedule.title }}
+
{{ school_schedule.description }}
+
+ +
{{ school_schedule.materials }}
+
+
+
+ {% endfor %} +
+ {% comment %}
+ Распечатать расписание чтобы не забыть +
{% endcomment %} +
+
+ + + + diff --git a/project/urls.py b/project/urls.py index 30a14cbd..b0c6ddb8 100644 --- a/project/urls.py +++ b/project/urls.py @@ -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//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//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//notifications', NotificationEditView.as_view(), name='user-edit-notifications'), path('user//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'), ] diff --git a/project/views.py b/project/views.py index 9dafb0a4..6f73fff7 100644 --- a/project/views.py +++ b/project/views.py @@ -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 diff --git a/requirements.txt b/requirements.txt index 2b469cf2..710cf88d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/web/package.json b/web/package.json index 96794b84..ba9de0b4 100755 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/components/CourseRedactor.vue b/web/src/components/CourseRedactor.vue index 0f94eb3e..d173f8cf 100644 --- a/web/src/components/CourseRedactor.vue +++ b/web/src/components/CourseRedactor.vue @@ -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; diff --git a/web/src/js/modules/common.js b/web/src/js/modules/common.js index ecbd4dee..dea36989 100644 --- a/web/src/js/modules/common.js +++ b/web/src/js/modules/common.js @@ -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: { diff --git a/web/src/js/modules/popup.js b/web/src/js/modules/popup.js index db9824b1..feaa8a8a 100644 --- a/web/src/js/modules/popup.js +++ b/web/src/js/modules/popup.js @@ -73,7 +73,7 @@ $(document).ready(function () { } var text = ''; - if(weekdays.length >= 7) { + if(schoolAmountForDiscount <= price) { text = ''+price+' '+(price-schoolDiscount)+'р.'; } else { text = price+'p.'; diff --git a/web/src/sass/_common.sass b/web/src/sass/_common.sass index de962138..e1279091 100755 --- a/web/src/sass/_common.sass +++ b/web/src/sass/_common.sass @@ -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 diff --git a/web/src/sass/app.sass b/web/src/sass/app.sass index 9272f307..82cdc94a 100755 --- a/web/src/sass/app.sass +++ b/web/src/sass/app.sass @@ -2,3 +2,4 @@ @import helpers/all @import generated/sprite-svg @import common +@import '~baguettebox.js/dist/baguetteBox.min.css'; \ No newline at end of file diff --git a/web/yarn.lock b/web/yarn.lock index 06389b2f..0a35d84c 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -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"