diff --git a/api/v1/__init__.py b/api/v1/__init__.py index 45967a33..a8299561 100644 --- a/api/v1/__init__.py +++ b/api/v1/__init__.py @@ -43,3 +43,20 @@ class ExtendViewSet(object): class ExtendedModelViewSet(ExtendViewSet, viewsets.ModelViewSet): pass + + +class BothListFormatMixin: + + 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) diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index e393aac9..718dad8c 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -133,7 +133,7 @@ class CourseCreateSerializer(DispatchContentMixin, 'is_infinite', 'deferred_start_at', 'category', - 'duration', + 'access_duration', 'is_featured', 'url', 'status', @@ -286,7 +286,7 @@ class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer): 'is_infinite', 'deferred_start_at', 'category', - 'duration', + 'access_duration', 'is_featured', 'url', 'status', diff --git a/api/v1/serializers/mixins.py b/api/v1/serializers/mixins.py index a79ff8e5..451b362a 100644 --- a/api/v1/serializers/mixins.py +++ b/api/v1/serializers/mixins.py @@ -145,6 +145,8 @@ class DispatchGalleryMixin(object): ) if 'images' in gallery: for image in gallery['images']: + if not image.get('img'): + continue if isinstance(image['img'], ImageObject): img = image['img'] else: diff --git a/api/v1/serializers/payment.py b/api/v1/serializers/payment.py index 7c48cf46..4e9e8709 100644 --- a/api/v1/serializers/payment.py +++ b/api/v1/serializers/payment.py @@ -122,7 +122,7 @@ class CoursePaymentCreateSerializer(serializers.ModelSerializer): class Meta: model = CoursePayment - fields = BASE_PAYMENT_FIELDS + ('course',) + fields = BASE_PAYMENT_FIELDS + ('course', 'access_expire') read_only_fields = ( 'id', 'user', @@ -138,7 +138,7 @@ class CoursePaymentSerializer(serializers.ModelSerializer): class Meta: model = CoursePayment - fields = BASE_PAYMENT_FIELDS + ('course',) + fields = BASE_PAYMENT_FIELDS + ('course', 'access_expire') read_only_fields = ( 'id', 'user', diff --git a/api/v1/serializers/user.py b/api/v1/serializers/user.py index 79f17d32..3903a66d 100644 --- a/api/v1/serializers/user.py +++ b/api/v1/serializers/user.py @@ -1,6 +1,6 @@ from phonenumber_field.serializerfields import PhoneNumberField from rest_framework import serializers - +from drf_dynamic_fields import DynamicFieldsMixin from django.contrib.auth import get_user_model from api.v1.serializers.content import GallerySerializer, GalleryImageSerializer, GalleryImageCreateSerializer @@ -11,7 +11,7 @@ from .mixins import DispatchGalleryMixin User = get_user_model() -class UserSerializer(serializers.ModelSerializer): +class UserSerializer(DynamicFieldsMixin, serializers.ModelSerializer): phone = PhoneNumberField(required=False, allow_null=True, allow_blank=True) class Meta: diff --git a/api/v1/views.py b/api/v1/views.py index 6f475b55..f43ca18a 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -10,7 +10,7 @@ from rest_framework.decorators import (detail_route, list_route, action, from rest_framework.response import Response from rest_framework.settings import api_settings -from . import ExtendedModelViewSet +from . import ExtendedModelViewSet, BothListFormatMixin from .serializers.config import ConfigSerializer from .serializers.course import ( @@ -213,7 +213,7 @@ class LikeViewSet(ExtendedModelViewSet): return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) -class CategoryViewSet(ExtendedModelViewSet): +class CategoryViewSet(BothListFormatMixin, ExtendedModelViewSet): queryset = Category.objects.order_by('-id') serializer_class = CategorySerializer search_fields = ('title',) @@ -221,7 +221,7 @@ class CategoryViewSet(ExtendedModelViewSet): # permission_classes = (IsAdmin,) -class CourseViewSet(ExtendedModelViewSet): +class CourseViewSet(BothListFormatMixin, ExtendedModelViewSet): queryset = Course.objects.select_related( 'author', 'category', 'cover', 'gallery', ).prefetch_related( @@ -243,20 +243,6 @@ class CourseViewSet(ExtendedModelViewSet): # '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) @@ -455,8 +441,7 @@ class UserViewSet(ExtendedModelViewSet): serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) - # FIXME - queryset = queryset[:2000] + queryset = queryset[:3000] serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @@ -736,7 +721,7 @@ class FAQViewSet(ExtendedModelViewSet): serializer_class = FAQSerializer -class BonusesViewSet(ExtendedModelViewSet): +class BonusesViewSet(BothListFormatMixin, ExtendedModelViewSet): queryset = UserBonus.objects.all() serializer_class = UserBonusCreateSerializer serializer_class_map = { @@ -753,15 +738,3 @@ class BonusesViewSet(ExtendedModelViewSet): '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) diff --git a/apps/course/management/commands/fix_access_expire.py b/apps/course/management/commands/fix_access_expire.py new file mode 100644 index 00000000..2570e8e0 --- /dev/null +++ b/apps/course/management/commands/fix_access_expire.py @@ -0,0 +1,40 @@ +from datetime import timedelta + +from django.core.management.base import BaseCommand + +from apps.payment.models import CoursePayment +from apps.course.models import Course + + +class Command(BaseCommand): + help = 'Fix access duration in all paid courses' + + def add_arguments(self, parser): + parser.add_argument('access_duration', type=int, help='New access duration',) + + def handle(self, *args, **options): + access_duration = options.get('access_duration') + for course in Course.objects.filter(price__gt=0): + course.access_duration = access_duration + course.save() + + for payment in CoursePayment.objects.filter(status__in=CoursePayment.PW_PAID_STATUSES): + payment.access_expire = payment.created_at.date() + timedelta(days=payment.course.access_duration) + payment.save() + + +''' +TEST + +select c.id, c.title, cp.access_duration +from + course_course c, + (select cp.course_id, count(*), cp.access_expire - date_trunc('day', p.created_at) access_duration + from payment_coursepayment cp, + payment_payment p + where p.id = cp.payment_ptr_id and p.status = 0 + group by cp.course_id, cp.access_expire - date_trunc('day', p.created_at)) cp +where cp.course_id = c.id +order by c.title +''' + diff --git a/apps/course/management/commands/update_courses_slug.py b/apps/course/management/commands/update_courses_slug.py index 24999a2e..f154e8f9 100644 --- a/apps/course/management/commands/update_courses_slug.py +++ b/apps/course/management/commands/update_courses_slug.py @@ -7,7 +7,7 @@ from apps.course.models import Course class Command(BaseCommand): - help = 'Upload users to Roistat' + help = 'Update courses slug' def handle(self, *args, **options): courses = Course.objects.filter(Q(slug__isnull=True) | Q(slug='')) diff --git a/apps/course/migrations/0048_auto_20190206_1710.py b/apps/course/migrations/0048_auto_20190206_1710.py new file mode 100644 index 00000000..9a1bbbe1 --- /dev/null +++ b/apps/course/migrations/0048_auto_20190206_1710.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.7 on 2019-02-06 17:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0047_course_old_price'), + ] + + operations = [ + migrations.AlterField( + model_name='course', + name='duration', + field=models.IntegerField(default=0, verbose_name='Продолжительность доступа к курсу'), + ), + ] diff --git a/apps/course/migrations/0049_auto_20190207_1551.py b/apps/course/migrations/0049_auto_20190207_1551.py new file mode 100644 index 00000000..24f5128e --- /dev/null +++ b/apps/course/migrations/0049_auto_20190207_1551.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.7 on 2019-02-07 15:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0048_auto_20190206_1710'), + ] + + operations = [ + migrations.RenameField( + model_name='course', + old_name='duration', + new_name='access_duration', + ), + ] diff --git a/apps/course/models.py b/apps/course/models.py index e680e643..f7ef5f20 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -98,7 +98,7 @@ class Course(BaseModel, DeactivatedMixin): null=True, blank=True, validators=[deferred_start_at_validator], ) category = models.ForeignKey('Category', null=True, blank=True, on_delete=models.PROTECT, related_name='courses') - duration = models.IntegerField('Продолжительность курса', default=0) + access_duration = models.IntegerField('Продолжительность доступа к курсу', default=0) is_featured = models.BooleanField(default=False) status = models.PositiveSmallIntegerField( 'Статус', default=DRAFT, choices=STATUS_CHOICES diff --git a/apps/course/templates/course/_items.html b/apps/course/templates/course/_items.html index acbccf69..070f5dbc 100644 --- a/apps/course/templates/course/_items.html +++ b/apps/course/templates/course/_items.html @@ -50,11 +50,16 @@ {{ course.category | upper }} {% if not course.is_free %} - {% if course.old_price %} -