import json from datetime import datetime from decimal import Decimal from django.contrib.auth import get_user_model from django.db.models import Q from rest_framework import status, views, viewsets, generics, mixins from rest_framework.decorators import (detail_route, list_route, action, permission_classes, authentication_classes as auth_classes) from rest_framework.response import Response from rest_framework.settings import api_settings from . import ExtendedModelViewSet from .serializers.config import ConfigSerializer from .serializers.course import ( CategorySerializer, LikeSerializer, CourseSerializer, CourseCreateSerializer, CourseBulkChangeCategorySerializer, CommentSerializer, CommentCreateSerializer, MaterialSerializer, MaterialCreateSerializer, LessonSerializer, LessonCreateSerializer, LikeCreateSerializer, CourseCommentSerializer, LessonCommentSerializer, LiveLessonCommentSerializer,) from .serializers.content import ( BanerSerializer, ImageSerializer, ImageCreateSerializer, TextSerializer, TextCreateSerializer, ImageTextSerializer, ImageTextCreateSerializer, VideoSerializer, VideoCreateSerializer, GallerySerializer, GalleryImageSerializer, GalleryImageCreateSerializer, ImageObjectSerializer, FAQSerializer, ) from .serializers.school import ( SchoolScheduleSerializer, LiveLessonCreateSerializer, LiveLessonSerializer, ) from .serializers.payment import ( AuthorBalanceSerializer, AuthorBalanceCreateSerializer, PaymentSerializer, UserBonusSerializer, UserBonusCreateSerializer, CoursePaymentCreateSerializer, SchoolPaymentCreateSerializer) from .serializers.user import ( AuthorRequestSerializer, UserSerializer, UserPhotoSerializer, UserGallerySerializer, UserGalleryUpdateSerializer) from .serializers.contest import ( ContestCreateSerializer, ContestSerializer, ContestWorkSerializer, ContestWorkCreateSerializer ) from .permissions import ( IsAdmin, IsAuthorOrAdmin, IsAuthorObjectOrAdmin, IsTeacherOrAdmin, ) from apps.course.models import ( Category, Course, Comment, CourseComment, LessonComment, Material, Lesson, Like, LiveLessonComment) from apps.config.models import Config from apps.content.models import ( Baner, Image, Text, ImageText, Video, Gallery, GalleryImage, ImageObject, Contest, ContestWork, FAQ) from apps.payment.models import ( AuthorBalance, Payment, CoursePayment, SchoolPayment, UserBonus, ) from apps.school.models import SchoolSchedule, LiveLesson from apps.user.models import AuthorRequest, EmailLog from project.pusher import pusher from project.sengrid import get_sendgrid_client User = get_user_model() class AuthorBalanceViewSet(ExtendedModelViewSet): queryset = AuthorBalance.objects.filter( Q(type=1) | Q(payment__status__in=Payment.PW_PAID_STATUSES), # author__role__in=[User.AUTHOR_ROLE, User.ADMIN_ROLE, User.TEACHER_ROLE], ) serializer_class = AuthorBalanceCreateSerializer serializer_class_map = { 'list': AuthorBalanceSerializer, } permission_classes = (IsAdmin,) filter_fields = ('author', 'status', 'type') search_fields = ( 'author__email', 'author__first_name', 'author__last_name', ) def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) if request.query_params.get('page'): page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @action(methods=['post'], detail=False, url_path='add-withdrawal') def add_withdrawal(self, request): author_id = request.data.get('author_id') amount = request.data.get('amount') card = request.data.get('card') if not all([author_id, amount,]): return Response(status=status.HTTP_400_BAD_REQUEST) AuthorBalance.objects.create( author_id=author_id, type=AuthorBalance.OUT, amount=Decimal(amount), status=AuthorBalance.ACCEPTED, card=card, ) return Response(status=status.HTTP_200_OK) class AuthorBalanceUsersViewSet(views.APIView): def get(self, request): users = AuthorBalance.objects.filter(amount__gt=0).values_list('author', flat=True) users = User.objects.filter(id__in=users, is_active=True).order_by('first_name', 'last_name') return Response(UserSerializer(users, many=True).data) class BanerViewSet(ExtendedModelViewSet): queryset = Baner.objects.all() serializer_class = BanerSerializer permission_classes = (IsAdmin,) filter_fields = ('use',) ordering_fields = ('created_at', 'update_at',) search_fields = ('text',) def perform_create(self, serializer): files = dict() for k, v in self.request.FILES.items(): if v: files[k] = v serializer.save(**files) def perform_update(self, serializer): files = dict() for k, v in self.request.FILES.items(): if v: files[k] = v serializer.save(**files) class ImageObjectViewSet(ExtendedModelViewSet): queryset = ImageObject.objects.all() serializer_class = ImageObjectSerializer # FIXME authentication_classes = [] # permission_classes = (IsAuthorOrAdmin,) class MaterialViewSet(ExtendedModelViewSet): queryset = Material.objects.all() serializer_class = MaterialCreateSerializer serializer_class_map = { 'list': MaterialSerializer, 'retrieve': MaterialSerializer, } search_fields = ('title', 'short_description',) ordering_fields = ('title', 'created_at', 'update_at',) # permission_classes = (IsAdmin,) class LikeViewSet(ExtendedModelViewSet): OBJ_TYPE_CONTEST_WORK = 'contest_work' queryset = Like.objects.select_related('user').all() serializer_class = LikeCreateSerializer serializer_class_map = { 'list': LikeSerializer, 'retrieve': LikeSerializer, } search_fields = ('user__email', 'user__firstname', 'user__lastname',) ordering_fields = ('created_at', 'update_at',) # permission_classes = (IsAdmin,) # FIXME authentication_classes = [] def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) obj_type = request.data.get('obj_type') obj_id = request.data.get('obj_id') user = serializer.validated_data.get('user') if not user.is_active: # FIXME and user.is_authenticated): return Response(status=status.HTTP_403_FORBIDDEN) if obj_type == self.OBJ_TYPE_CONTEST_WORK: contest_work = ContestWork.objects.get(pk=obj_id) if contest_work.user == user: return Response({'error': u'Нельзя голосовать за свою работу'}, status=status.HTTP_400_BAD_REQUEST) if contest_work.likes.filter(user=user).exists(): return Response({'error': u'Вы уже голосовали за эту работу'}, status=status.HTTP_400_BAD_REQUEST) if contest_work.contest.finished: return Response({'error': u'Голосование закончено'}, status=status.HTTP_400_BAD_REQUEST) instance = serializer.save() if obj_type == self.OBJ_TYPE_CONTEST_WORK: contest_work.likes.add(instance) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) class CategoryViewSet(ExtendedModelViewSet): queryset = Category.objects.order_by('-id') serializer_class = CategorySerializer search_fields = ('title',) ordering_fields = ('title',) # permission_classes = (IsAdmin,) class CourseViewSet(ExtendedModelViewSet): queryset = Course.objects.select_related( 'author', 'category', 'cover', 'gallery', ).prefetch_related( 'likes', 'materials', 'content', ).all() serializer_class = CourseCreateSerializer serializer_class_map = { 'list': CourseSerializer, 'retrieve': CourseSerializer, 'draft': CourseSerializer, 'change_category_bulk': CourseBulkChangeCategorySerializer, } filter_fields = ('category', 'status', 'is_infinite', 'is_featured',) search_fields = ('author__email', 'title', 'category__title',) ordering_fields = ('id', 'title', 'created_at', 'update_at',) # permission_classes = (IsAuthorObjectOrAdmin,) # permission_map = { # 'create': IsAuthorOrAdmin, # 'delete': IsAdmin, # } def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) if request.query_params.get('page'): page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) else: return Response({'results': []}) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @list_route(methods=['get']) def draft(self, request): drafts = Course.objects.filter(author=request.user, status=Course.DRAFT) serializer = self.get_serializer_class() serialized_data = serializer(instance=drafts.last()) return Response(serialized_data.data) @list_route(methods=['patch'], url_path='change-category-bulk') def change_category_bulk(self, request): serializer_class = self.get_serializer_class() serializer = serializer_class(data=request.data) if serializer.is_valid(): old_category_id = serializer.validated_data['old_category'] new_category_id = serializer.validated_data['new_category'] try: old_category = Category.objects.get(id=old_category_id) except Category.DoesNotExist: return Response( {'success': False, 'detail': f'Category with id {old_category_id} not found'}, status=status.HTTP_400_BAD_REQUEST, ) try: new_category = Category.objects.get(id=new_category_id) except Category.DoesNotExist: return Response( {'success': False, 'detail': f'Category with id {new_category_id} not found'}, status=status.HTTP_400_BAD_REQUEST, ) c = Course.objects.filter( category=old_category, ).update( category=new_category ) return Response({'success': True}) else: return Response( {'success': False}, status=status.HTTP_400_BAD_REQUEST, ) class LessonViewSet(ExtendedModelViewSet): queryset = Lesson.objects.select_related( 'course', 'cover' ).prefetch_related('content').all() serializer_class = LessonCreateSerializer serializer_class_map = { 'list': LessonSerializer, 'retrieve': LessonSerializer, } filter_fields = ('course',) search_fields = ('title', 'short_description',) ordering_fields = ('title', 'created_at', 'update_at',) permission_classes = (IsAuthorObjectOrAdmin,) permission_map = { 'create': IsAuthorOrAdmin, 'delete': IsAdmin, } class LiveLessonViewSet(ExtendedModelViewSet): queryset = LiveLesson.objects.prefetch_related('content').all() serializer_class = LiveLessonCreateSerializer serializer_class_map = { 'list': LiveLessonSerializer, 'retrieve': LiveLessonSerializer, } search_fields = ('title', 'short_description',) ordering_fields = ('title', 'created_at', 'update_at',) permission_classes = (IsTeacherOrAdmin,) permission_map = { 'delete': IsAdmin, } class ImageViewSet(ExtendedModelViewSet): queryset = Image.objects.select_related( 'course', 'lesson', 'img', ).all() serializer_class = ImageCreateSerializer serializer_class_map = { 'list': ImageSerializer, 'retrieve': ImageSerializer, } search_fields = ('title',) ordering_fields = ('title', 'created_at', 'update_at', 'position',) # permission_classes = (IsAuthorOrAdmin,) # permission_map = { # 'delete': IsAdmin, # } class TextViewSet(ExtendedModelViewSet): queryset = Text.objects.select_related( 'course', 'lesson' ).all() serializer_class = TextCreateSerializer serializer_class_map = { 'list': TextSerializer, 'retrieve': TextSerializer, } search_fields = ('title',) ordering_fields = ('title', 'created_at', 'update_at', 'position',) # permission_classes = (IsAuthorOrAdmin,) # permission_map = { # 'delete': IsAdmin, # } class ImageTextViewSet(ExtendedModelViewSet): queryset = ImageText.objects.select_related( 'course', 'lesson', 'img' ).all() serializer_class = ImageTextCreateSerializer serializer_class_map = { 'list': ImageTextSerializer, 'retrieve': ImageTextSerializer, } search_fields = ('title',) ordering_fields = ('title', 'created_at', 'update_at', 'position',) # permission_classes = (IsAuthorOrAdmin,) # permission_map = { # 'delete': IsAdmin, # } class VideoViewSet(ExtendedModelViewSet): queryset = Video.objects.select_related( 'course', 'lesson' ).all() serializer_class = VideoCreateSerializer serializer_class_map = { 'list': VideoSerializer, 'retrieve': VideoSerializer, } search_fields = ('title',) ordering_fields = ('title', 'created_at', 'update_at', 'position',) # permission_classes = (IsAuthorOrAdmin,) # permission_map = { # 'delete': IsAdmin, # } class GalleryViewSet(ExtendedModelViewSet): queryset = Gallery.objects.all() serializer_class = GallerySerializer search_fields = ('title',) ordering_fields = ('title', 'created_at', 'update_at',) # permission_classes = (IsAuthorOrAdmin,) # permission_map = { # 'delete': IsAdmin, # } class GalleryImageViewSet(ExtendedModelViewSet): queryset = GalleryImage.objects.select_related( 'gallery', 'img', ).all() serializer_class = GalleryImageCreateSerializer search_fields = ('gallery__title',) serializer_class_map = { 'list': GalleryImageSerializer, 'retrieve': GalleryImageSerializer, } # permission_classes = (IsAuthorOrAdmin,) # permission_map = { # 'delete': IsAdmin, # } class UserViewSet(ExtendedModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer serializer_class_map = { 'upload_photo': UserPhotoSerializer, } filter_fields = ('is_staff', 'is_active', 'role', 'gender', 'is_email_proved', 'fb_id',) search_fields = ('email', 'first_name', 'last_name', 'country', 'city', 'fb_id',) ordering_fields = ('email', 'first_name', 'last_name', 'country', 'city', 'date_joined',) # permission_classes = (IsAdminOrIsSelf,) # permission_map = { # 'delete': IsAdmin, # } def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) if request.query_params.get('page'): page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) queryset = queryset[:3000] serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @list_route(methods=['get']) def me(self, request): serializer = self.get_serializer_class() serialized_data = serializer(instance=request.user) return Response(serialized_data.data) @detail_route(methods=['post'], url_path='upload-photo') def upload_photo(self, request, pk=None): user = self.get_object() serializer = self.get_serializer() serialized_data = serializer(data=request.data) if serialized_data.is_valid(): user.photo = serialized_data['photo'] user.save() return Response({'success': True}) else: return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST) class UserGalleryViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): queryset = User.objects.all() serializer_class = UserGalleryUpdateSerializer # FIXME authentication_classes = [] class SchoolScheduleViewSet(ExtendedModelViewSet): queryset = SchoolSchedule.objects.all() serializer_class = SchoolScheduleSerializer permission_classes = (IsTeacherOrAdmin,) class ConfigViewSet(generics.RetrieveUpdateAPIView): queryset = Config.objects.all() serializer_class = ConfigSerializer permission_classes = (IsAdmin,) def perform_update(self, serializer): files = dict() for k, v in self.request.FILES.items(): if v: files[k] = v serializer.save(**files) def get_object(self): return Config.load() class CommentViewSet(ExtendedModelViewSet): queryset = Comment.objects.all() serializer_class = CommentSerializer permission_classes = (IsAuthorObjectOrAdmin,) def get_queryset(self): queryset = self.queryset is_deactivated = self.request.query_params.get('is_deactivated', '0') if is_deactivated == '0': queryset = queryset.filter(level=0) elif is_deactivated == '1': queryset = queryset.filter(deactivated_at__isnull=True) elif is_deactivated == '2': queryset = queryset.filter(deactivated_at__isnull=False) return queryset class ObjectCommentsViewSet(ExtendedModelViewSet): queryset = Comment.objects.all() serializer_class = CommentCreateSerializer ordering_fields = ('update_at', ) authentication_classes = [] def get_queryset(self): queryset = self.queryset obj_type = self.request.query_params.get('obj_type') obj_id = self.request.query_params.get('obj_id') is_deactivated = self.request.query_params.get('is_deactivated') if obj_type == Comment.OBJ_TYPE_COURSE: queryset = CourseComment.objects.filter(course=obj_id) elif obj_type == Comment.OBJ_TYPE_LESSON: queryset = LessonComment.objects.filter(lesson=obj_id) elif obj_type == Comment.OBJ_TYPE_LIVE_LESSON: queryset = LiveLessonComment.objects.filter(live_lesson=obj_id) if is_deactivated == '0': queryset = queryset.filter(level=0) elif is_deactivated == '1': queryset = queryset.filter(deactivated_at__isnull=True) elif is_deactivated == '2': queryset = queryset.filter(deactivated_at__isnull=False) return queryset def get_serializer_class(self): if self.request.method == 'POST': return CommentCreateSerializer obj_type = self.request.query_params.get('obj_type') serializer_class = CommentSerializer if obj_type == Comment.OBJ_TYPE_COURSE: serializer_class = CourseCommentSerializer elif obj_type == Comment.OBJ_TYPE_LESSON: serializer_class = LessonCommentSerializer elif obj_type == Comment.OBJ_TYPE_LIVE_LESSON: serializer_class = LiveLessonCommentSerializer return serializer_class def perform_create(self, serializer): obj_type = self.request.data.get('obj_type') obj_id = self.request.data.get('obj_id') serializer.save() try: pusher().trigger(f'comments_{obj_type}_{obj_id}', 'add', serializer.data) except Exception as e: print(e) @permission_classes((IsAuthorObjectOrAdmin,)) def destroy(self, request, *args, **kwargs): return super().destroy(request, *args, **kwargs) def perform_destroy(self, instance): obj_type = None obj_id = None if isinstance(instance, LessonComment): obj_type = Comment.OBJ_TYPE_LESSON obj_id = instance.lesson_id elif isinstance(instance, CourseComment): obj_type = Comment.OBJ_TYPE_COURSE obj_id = instance.course_id elif isinstance(instance, LiveLessonComment): obj_type = Comment.OBJ_TYPE_LIVE_LESSON obj_id = instance.live_lesson_id serializer = self.get_serializer(instance) try: pusher().trigger(f'comments_{obj_type}_{obj_id}', 'delete', serializer.data) except Exception as e: print(e) instance.delete() def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()).filter(parent__isnull=True) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) class AuthorRequestViewSet(ExtendedModelViewSet): queryset = AuthorRequest.objects.all() serializer_class = AuthorRequestSerializer permission_classes = (IsAdmin,) filter_fields = ('status',) class PaymentViewSet(viewsets.ModelViewSet): queryset = Payment.objects.all() serializer_class = PaymentSerializer permission_classes = (IsAdmin,) filter_fields = ('status', 'user',) ordering_fields = ( 'id', 'user__email', 'user__first_name', 'user__last_name', 'amount', 'created_at', ) search_fields = ('user__email', 'user__first_name', 'user__last_name',) def get_serializer(self, instance, *args, **kwargs): serializer_class = self.get_serializer_class() if 'update' in self.action: if isinstance(instance, CoursePayment): serializer_class = CoursePaymentCreateSerializer elif isinstance(instance, SchoolPayment): serializer_class = SchoolPaymentCreateSerializer kwargs['context'] = self.get_serializer_context() return serializer_class(instance, *args, **kwargs) def get_queryset(self): queryset = self.queryset course = self.request.query_params.get('course') weekdays = self.request.query_params.getlist('weekdays[]') if course: queryset = CoursePayment.objects.filter(course=course) if weekdays: queryset = SchoolPayment.objects.filter(weekdays__overlap=weekdays) return queryset.filter(status__isnull=False).order_by('-created_at') @action(methods=['get'], detail=False, url_path='calc-amount', authentication_classes=[], permission_classes=[]) def calc_amount(self, request, pk=None): user = request.query_params.get('user') course = request.query_params.get('course') weekdays = request.query_params.getlist('weekdays[]') date_start = request.query_params.get('date_start') user = user and User.objects.get(pk=user) course = course and Course.objects.get(pk=course) date_start = date_start and datetime.strptime(date_start, '%Y-%m-%d') return Response(Payment.calc_amount(user=user, course=course, date_start=date_start, weekdays=weekdays)) class ContestViewSet(ExtendedModelViewSet): queryset = Contest.objects.all() serializer_class = ContestCreateSerializer serializer_class_map = { 'list': ContestSerializer, 'retrieve': ContestSerializer, } filter_fields = ('active',) search_fields = ('description', 'title', 'slug',) ordering_fields = ('id', 'title', 'active', 'date_start', 'date_end',) permission_classes = (IsAdmin,) class ContestWorkViewSet(ExtendedModelViewSet): queryset = ContestWork.objects.all() serializer_class = ContestWorkCreateSerializer serializer_class_map = { 'list': ContestWorkSerializer, 'retrieve': ContestWorkSerializer, } filter_fields = ('contest',) # FIXME authentication_classes = [] def create(self, request, *args, **kwargs): # FIXME user = User.objects.get(pk=request.data.get('user')) if ContestWork.objects.filter(user=user).exists(): return Response(status=status.HTTP_400_BAD_REQUEST) return super().create(request, *args, **kwargs) class CaptureEmail(views.APIView): authentication_classes = () def post(self, request): list_id = None list_name = 'captured-emails' email = request.data.get('email') sg = get_sendgrid_client() if not email: return Response({'error': 'No email'}, status=status.HTTP_400_BAD_REQUEST) EmailLog.objects.create(email=email, source=EmailLog.SOURCE_TRIAL_LESSON) # берем все списки response = sg.client.contactdb.lists.get() if response.status_code != 200: return Response({'error': 'Cannot get list of lists'}, status=status.HTTP_400_BAD_REQUEST) # ищем нужный список for sg_list in response.to_dict.get('lists'): if sg_list.get('name') == list_name: list_id = sg_list.get('id') break # не нашли - создаем if not list_id: response = sg.client.contactdb.lists.post(request_body={'name': list_name}) if response.status_code != 201: return Response({'error': 'List was not created'}, status=status.HTTP_400_BAD_REQUEST) list_id = response.to_dict.get('id') # добавляем получателя response = sg.client.contactdb.recipients.patch(request_body=[{ 'email': email, }]) if response.status_code != 201 or not response.to_dict.get('persisted_recipients'): return Response({'error': 'Cannot update recipients'}, status=status.HTTP_400_BAD_REQUEST) recipient_id = response.to_dict.get('persisted_recipients')[0] # добавляем получателя в отдельный список response = sg.client.contactdb.lists._(list_id).recipients._(recipient_id).post() if response.status_code != 201: return Response({'error': 'Cannot add recipient to list'}, status=status.HTTP_400_BAD_REQUEST) return Response({'status': 'ok'}) class FAQViewSet(ExtendedModelViewSet): queryset = FAQ.objects.all() serializer_class = FAQSerializer class BonusesViewSet(ExtendedModelViewSet): queryset = UserBonus.objects.all() serializer_class = UserBonusCreateSerializer serializer_class_map = { 'list': UserBonusSerializer, } permission_classes = (IsAdmin,) filter_fields = ('user', 'referral', 'payment', 'is_service', 'action_name') search_fields = ( 'action_name', 'user__email', 'user__first_name', 'user__last_name', 'referral__referral__email', 'referral__referral__first_name', 'referral__referral__last_name', ) def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) if request.query_params.get('page'): page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data)