from datetime import timedelta from itertools import groupby from paymentwall import Pingback from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required from django.db.models import Q 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 from django.utils.translation import gettext as _ from django.utils.timezone import now from apps.content.models import Banner from apps.payment.models import AuthorBalance, CoursePayment from .models import Course, Like, Lesson, CourseComment, LessonComment, Category, CourseTags, Tag from .filters import CourseFilter from project.utils.db import ModelFieldsNames, format_sql, execute_sql User = get_user_model() @login_required @csrf_exempt @require_http_methods(['POST']) def likes(request, course_id): try: course = Course.objects.prefetch_related('likes').get(id=course_id) except Course.DoesNotExist: return JsonResponse({ 'success': False, 'errors': ['Course with id f{course_id} not found'] }, status=400) else: course_user_likes = course.likes.filter(user=request.user) if course_user_likes.exists(): is_liked = False for _course in course_user_likes.all(): course.likes.remove(_course) course_user_likes.delete() else: is_liked = True course.likes.add(Like.objects.create(user=request.user)) count = course.likes.count() return JsonResponse({ "success": True, "likes_count": count, "is_liked": is_liked, }) @login_required @csrf_exempt @require_http_methods(['POST']) def coursecomment(request, course_id): try: course = Course.objects.get(id=course_id) except Course.DoesNotExist: return JsonResponse({ 'success': False, 'errors': ['Course with id f{course_id} not found'] }, status=400) else: reply_to = request.POST.get('reply_id', 0) comment = request.POST.get('comment', '') if not comment: return JsonResponse({ 'success': False, 'errors': ['Comment can not be empty'] }, status=400) if not int(reply_to): coursecomment = CourseComment.objects.create( author=request.user, content=comment, course=course, ) else: try: _coursecomment = CourseComment.objects.get(id=reply_to) except CourseComment.DoesNotExist: return JsonResponse({ 'success': False, 'errors': ['CourseComment with id f{reply_to} not found'] }, status=400) else: coursecomment = CourseComment.objects.create( author=request.user, content=comment, course=course, parent=_coursecomment, ) ctx = {'node': coursecomment, 'user': request.user} html = loader.render_to_string('templates/blocks/comment.html', ctx) return JsonResponse({ 'success': True, 'comment': html, }) @login_required @csrf_exempt @require_http_methods(['POST']) def lessoncomment(request, lesson_id): try: lesson = Lesson.objects.get(id=lesson_id) except Lesson.DoesNotExist: return JsonResponse({ 'success': False, 'errors': ['Lesson with id f{lesson_id} not found'] }, status=400) else: reply_to = request.POST.get('reply_id', 0) comment = request.POST.get('comment', '') if not comment: return JsonResponse({ 'success': False, 'errors': ['Comment can not be empty'] }, status=400) if not int(reply_to): lessoncomment = LessonComment.objects.create( author=request.user, content=comment, lesson=lesson, ) else: try: _lessoncomment = LessonComment.objects.get(id=reply_to) except LessonComment.DoesNotExist: return JsonResponse({ 'success': False, 'errors': ['LessonComment with id f{reply_to} not found'] }, status=400) else: lessoncomment = LessonComment.objects.create( author=request.user, content=comment, lesson=lesson, parent=_lessoncomment, ) ctx = {'node': lessoncomment, 'user': request.user} html = loader.render_to_string('templates/blocks/comment.html', ctx) return JsonResponse({ 'success': True, 'comment': html, }) @method_decorator(login_required, name='dispatch') class CourseOnModerationView(TemplateView): template_name = 'course/course_on_moderation.html' @method_decorator(login_required, name='dispatch') class CourseEditView(TemplateView): template_name = 'course/course_edit.html' def get(self, request, pk=None, lesson=None): drafts = Course.objects.filter( author=request.user, status=Course.DRAFT ) if pk: self.object = get_object_or_404(Course, pk=pk) elif drafts.exists(): self.object = drafts.last() else: self.object = Course.objects.create( author=request.user, ) if request.user != self.object.author and request.user.role != User.ADMIN_ROLE: raise Http404 return super().get(request) def get_context_data(self): context = super().get_context_data() context['live'] = 'false' context['object'] = self.object return context # @method_decorator(login_required, name='dispatch') class CourseView(DetailView): model = Course context_object_name = 'course' template_name = 'course/course.html' only_lessons = False def get(self, request, *args, **kwargs): response = super().get(request, *args, **kwargs) context = self.get_context_data() # если это не админ или автор if not context.get('has_full_access'): # если это не опубл курс или это страница уроков, курс платный, а юзер не оплатил курс - 404 if (self.object.status != Course.PUBLISHED) or \ (self.only_lessons and self.object.price and not context.get('paid')): raise Http404 return response # ((self.object.status != Course.PUBLISHED and request.user.role != User.ADMIN_ROLE) or # (self.object.status != Course.PUBLISHED and request.user.role != User.AUTHOR_ROLE and self.object.author != request.user)): def get_object(self, queryset=None): if queryset is None: queryset = self.get_queryset() pk = self.kwargs.get(self.pk_url_kwarg) slug = self.kwargs.get(self.slug_url_kwarg) if pk is not None: queryset = queryset.filter(pk=pk) if slug is not None and (pk is None or self.query_pk_and_slug): slug_field = self.get_slug_field() queryset = queryset.filter(**{'%s__iexact' % slug_field: slug}) if pk is None and slug is None: raise AttributeError("Generic detail view %s must be called with " "either an object pk or a slug." % self.__class__.__name__) try: # Get the single item from the filtered queryset obj = queryset.get() except queryset.model.DoesNotExist: raise Http404(_("No %(verbose_name)s found matching the query") % {'verbose_name': queryset.model._meta.verbose_name}) return obj def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) if self.request.user.is_authenticated: context['next'] = self.request.GET.get('next', None) # берем последнюю оплату курса payments = self.object.payments.filter( user=self.request.user, status__in=[ Pingback.PINGBACK_TYPE_REGULAR, Pingback.PINGBACK_TYPE_GOODWILL, Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, ]) payment = payments.filter(access_expire__gte=now().date()).order_by('-access_expire').first() context['payment'] = payment context['access_duration'] = ((payment.access_expire - now().date()).days + 1) if payment else self.object.access_duration context['paid'] = bool(payment) context['can_buy_again'] = bool(self.object.price) and (context['access_duration'] <= 7 if payment else payments.filter(access_expire__lt=now().date()).exists()) context['pending'] = self.object.payments.filter( user=self.request.user, status=Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW, ).exists() context['only_lessons'] = self.only_lessons if self.only_lessons: context['lessons'] = self.object.lessons.order_by('position') context['is_owner'] = self.object.author == self.request.user context['is_admin'] = self.request.user.role == User.ADMIN_ROLE context['has_full_access'] = context['is_owner'] or context['is_admin'] context['course_price'] = self.object.price / 2 if context.get('can_buy_again') else self.object.price return context def get_queryset(self): queryset = super().get_queryset().select_related( 'author', 'category', ).prefetch_related( 'likes', 'materials', 'content', ) return queryset class CoursesView(ListView): model = Course context_object_name = 'course_items' paginate_by = 12 def get(self, request, *args, **kwargs): self.object_list = self.get_queryset() if request.is_ajax(): context = self.get_context_data() template_name = self.get_template_names() html = loader.render_to_string( template_name, context, request=request) is_paginated = context.get('is_paginated') if is_paginated: page_obj = context.get('page_obj') prev_url = request.path + '?page=' + \ str(page_obj.previous_page_number() ) if page_obj.has_previous() else None next_url = request.path + '?page=' + \ str(page_obj.next_page_number() ) if page_obj.has_next() else None else: prev_url = None next_url = None 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) def get_queryset(self): queryset = super().get_queryset().select_related( 'author', 'category', 'cover', ).prefetch_related( 'likes', 'materials', 'content', ).filter(status=Course.PUBLISHED) q = self.request.GET.get('q') if q: if q.startswith('#'): queryset = queryset.filter(tags__tag__istartswith=q[1:]).distinct() else: queryset = queryset.filter(Q(tags__tag__icontains=q) | Q(title__icontains=q) | Q(short_description__icontains=q) | Q(author__first_name__icontains=q) | Q(author__last_name__icontains=q) | Q(author__email__icontains=q)).distinct() filtered = CourseFilter(self.request.GET, queryset=queryset) return filtered.qs def get_context_data(self): context = super().get_context_data() filtered = CourseFilter(self.request.GET) context.update(filtered.data) sql = format_sql(''' select {ct.tag_id} from {ct} group by {ct.tag_id} order by count(*) desc limit 15''', ct=CourseTags) tags = [t[0] for t in execute_sql(sql)] context['tags'] = Tag.objects.filter(id__in=tags).order_by('tag') context['search_query'] = self.request.GET.get('q', '') context['banners'] = Banner.get_for_page(Banner.PAGE_COURSES) context['course_items'] = Course.shuffle(context.get('course_items')) context['new_courses'] = Course.objects.filter(status=Course.PUBLISHED).order_by('-created_at')[:4] context['ages'] = Course.AGE_CHOICES[1:] age = context.get('age') if age and age[0]: age = int(age[0]) context['age'] = [age] context['age_name'] = dict(Course.AGE_CHOICES).get(age, '') else: context['age_name'] = '' if not context.get('category') and context.get('cat'): context['category'] = Category.objects.filter(title__iexact=context.get('cat')[0]).values_list( 'id', flat=True)[:1] if self.request.user.is_authenticated: can_buy_again_courses = list(CoursePayment.objects.filter(user=self.request.user, status__in=CoursePayment.PW_PAID_STATUSES, access_expire__lte=now().date() + timedelta(7)).values_list('course_id', flat=True)) for course in context['course_items']: if course.id in can_buy_again_courses: course.buy_again_price = course.price / 2 for course in context['course_items']: course.cover = None return context def get_template_names(self): if self.request.is_ajax(): return 'course/course_items.html' return 'course/courses.html' @method_decorator(login_required, name='dispatch') class LessonView(DetailView): model = Lesson context_object_name = 'lesson' template_name = 'course/lesson.html' def get(self, request, *args, **kwargs): response = super().get(request, *args, **kwargs) paid = request.user.is_authenticated and self.object.course.payments.filter( user=self.request.user, status__in=[ Pingback.PINGBACK_TYPE_REGULAR, Pingback.PINGBACK_TYPE_GOODWILL, Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, ], access_expire__gte=now().date(), ).exists() # если это не автор или админ if not (request.user.is_authenticated and (request.user.role == User.ADMIN_ROLE or self.object.course.author == request.user)): # если курс не опубликован или он платный и не оплачен - 404 if self.object.course.status != Course.PUBLISHED or (self.object.course.price and not paid): raise Http404 return response def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['next'] = self.request.GET.get('next', None) lessons = list(self.object.course.lessons.values_list('id', flat=True)) index = lessons.index(self.object.id) context['next_lesson'] = lessons[index + 1] if index < len(lessons) - 1 else None return context class SearchView(CoursesView): template_name = 'course/result.html' def get_queryset(self): search_query = self.request.GET.get('q', None) queryset = super().get_queryset() if search_query: query = Q(title__icontains=search_query) | Q( short_description__icontains=search_query) queryset = queryset.filter(query) else: queryset = queryset.none() return queryset def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['q'] = self.request.GET.get('q', None) or '' return context if 'is_paginated' in context and context['is_paginated']: page_obj = context.get('page_obj') context['page'] = page_obj.number context['next_page'] = str( page_obj.next_page_number()) if page_obj.has_next() else None else: context['page'] = 1 context['next_page'] = None return context def get_template_names(self): if self.request.is_ajax(): return 'course/course_items.html' return 'course/result.html'