You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
440 lines
18 KiB
440 lines
18 KiB
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, Discount
|
|
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())
|
|
all_courses_discount, courses_discounts = Discount.objects.get_courses_discounts(user=self.request.user,
|
|
course=self.object)
|
|
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']
|
|
else:
|
|
all_courses_discount, courses_discounts = Discount.objects.get_courses_discounts(course=self.object)
|
|
context['all_courses_discount'] = all_courses_discount
|
|
context['discounts_per_course'] = courses_discounts
|
|
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:
|
|
all_courses_discount, courses_discounts = Discount.objects.get_courses_discounts(user=self.request.user)
|
|
else:
|
|
all_courses_discount, courses_discounts = Discount.objects.get_courses_discounts()
|
|
context['all_courses_discount'] = all_courses_discount
|
|
context['discounts_per_course'] = courses_discounts
|
|
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'
|
|
|