diff --git a/api/v1/serializers/content.py b/api/v1/serializers/content.py
index b2d47d15..d84e357b 100644
--- a/api/v1/serializers/content.py
+++ b/api/v1/serializers/content.py
@@ -4,7 +4,7 @@ from django.conf import settings
from apps.content.models import (
Banner, Content, Image, Text, ImageText, Video,
- Gallery, GalleryImage, ImageObject, FAQ)
+ Gallery, GalleryImage, ImageObject, FAQ, Package)
from . import Base64ImageField
@@ -268,3 +268,10 @@ class FAQSerializer(serializers.ModelSerializer):
class Meta:
model = FAQ
fields = '__all__'
+
+
+class PackageSerializer(serializers.ModelSerializer):
+
+ class Meta:
+ model = Package
+ fields = '__all__'
diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py
index 718dad8c..54ccc44c 100644
--- a/api/v1/serializers/course.py
+++ b/api/v1/serializers/course.py
@@ -9,16 +9,22 @@ from apps.course.models import (
Comment, CourseComment, LessonComment,
Material, Lesson,
Like,
- LiveLessonComment)
+ LiveLessonComment, Tag)
from .content import (
ImageObjectSerializer, ContentSerializer, ContentCreateSerializer,
GallerySerializer, )
-from .mixins import DispatchContentMixin, DispatchGalleryMixin, DispatchMaterialMixin
+from .mixins import DispatchContentMixin, DispatchGalleryMixin, DispatchMaterialMixin, DispatchTagsMixin
from .user import UserSerializer
User = get_user_model()
+class TagSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Tag
+ fields = ('__all__')
+
+
class MaterialCreateSerializer(serializers.ModelSerializer):
class Meta:
@@ -103,6 +109,7 @@ class CourseBulkChangeCategorySerializer(serializers.Serializer):
class CourseCreateSerializer(DispatchContentMixin,
DispatchGalleryMixin,
DispatchMaterialMixin,
+ DispatchTagsMixin,
serializers.ModelSerializer
):
title = serializers.CharField(allow_blank=True)
@@ -116,6 +123,7 @@ class CourseCreateSerializer(DispatchContentMixin,
)
materials = MaterialSerializer(many=True, required=False)
gallery = GallerySerializer()
+ tags = TagSerializer(many=True, required=False)
class Meta:
model = Course
@@ -145,6 +153,7 @@ class CourseCreateSerializer(DispatchContentMixin,
'content',
'gallery',
'lessons',
+ 'tags',
)
read_only_fields = (
@@ -161,12 +170,14 @@ class CourseCreateSerializer(DispatchContentMixin,
materials = validated_data.pop('materials', [])
gallery = validated_data.pop('gallery', {})
author = validated_data.get('author', None)
+ tags = validated_data.pop('tags', [])
if not author:
validated_data['author'] = self.context['request'].user
course = super().create(validated_data)
self.dispatch_content(course, content)
self.dispatch_materials(course, materials)
self.dispatch_gallery(course, gallery)
+ self.dispatch_tags(course, tags)
return course
def update(self, instance, validated_data):
@@ -174,12 +185,14 @@ class CourseCreateSerializer(DispatchContentMixin,
materials = validated_data.pop('materials', [])
gallery = validated_data.pop('gallery', {})
author = validated_data.get('author', None)
+ tags = validated_data.pop('tags', [])
if not instance.author or author and instance.author != author:
validated_data['author'] = self.context['request'].user
course = super().update(instance, validated_data)
self.dispatch_materials(course, materials)
self.dispatch_content(course, content)
self.dispatch_gallery(course, gallery)
+ self.dispatch_tags(course, tags)
return course
def to_representation(self, instance):
@@ -269,6 +282,7 @@ class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
gallery = GallerySerializer()
content = ContentSerializer(many=True)
lessons = LessonSerializer(many=True)
+ tags = TagSerializer(many=True, required=False)
class Meta:
model = Course
@@ -298,6 +312,7 @@ class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
'content',
'gallery',
'lessons',
+ 'tags',
)
read_only_fields = (
diff --git a/api/v1/serializers/mixins.py b/api/v1/serializers/mixins.py
index 451b362a..d488a1c8 100644
--- a/api/v1/serializers/mixins.py
+++ b/api/v1/serializers/mixins.py
@@ -1,4 +1,4 @@
-from apps.course.models import Category, Course, Material, Lesson, Like
+from apps.course.models import Category, Course, Material, Lesson, Like, Tag, CourseTags
from apps.school.models import LiveLesson
from apps.content.models import (
@@ -163,3 +163,30 @@ class DispatchGalleryMixin(object):
)
obj.gallery = g
obj.save()
+
+
+class DispatchTagsMixin(object):
+
+ def dispatch_tags(self, obj, tags):
+ current_tags = list(obj.tags.all())
+ new_tags = []
+ if tags:
+ for tag in tags:
+ id = tag.get('id')
+ tag_text = tag.get('tag')
+ if not id and tag_text:
+ t, created = Tag.objects.get_or_create(tag=tag_text)
+ id = t.id
+ new_tags.append(id)
+ if isinstance(obj, Course):
+ CourseTags.objects.filter(course=obj, tag__in=current_tags).delete()
+ for tag in Tag.objects.filter(id__in=new_tags):
+ CourseTags.objects.get_or_create(course=obj, tag=tag)
+ else:
+ obj.tags.clear(current_tags)
+ obj.tags.add(Tag.objects.filter(id__in=set(new_tags)))
+ for tag in current_tags:
+ if not tag.course_set.all().count():
+ tag.delete()
+
+
diff --git a/api/v1/urls.py b/api/v1/urls.py
index 08434dfc..ab6e3525 100644
--- a/api/v1/urls.py
+++ b/api/v1/urls.py
@@ -19,7 +19,7 @@ from .views import (
SchoolScheduleViewSet, LiveLessonViewSet,
PaymentViewSet, ObjectCommentsViewSet,
ContestViewSet, ContestWorkViewSet, NotifiedAboutBonuses,
- AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet, UserGalleryViewSet, BonusesViewSet)
+ AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet, UserGalleryViewSet, BonusesViewSet, PackageViewSet, TagViewSet)
router = DefaultRouter()
router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests')
@@ -48,6 +48,8 @@ router.register(r'users', UserViewSet, base_name='users')
router.register(r'user-gallery', UserGalleryViewSet, base_name='user-gallery')
router.register(r'contests', ContestViewSet, base_name='contests')
router.register(r'contest-works', ContestWorkViewSet, base_name='contest_works')
+router.register(r'packages', PackageViewSet, base_name='packages')
+router.register(r'tags', TagViewSet, base_name='tags')
# router.register(r'configs', ConfigViewSet, base_name='configs')
diff --git a/api/v1/views.py b/api/v1/views.py
index 4bde8660..6972826e 100644
--- a/api/v1/views.py
+++ b/api/v1/views.py
@@ -11,6 +11,7 @@ from rest_framework.response import Response
from rest_framework.settings import api_settings
from django.utils.timezone import now
+from project.utils.db import format_sql, execute_sql
from . import ExtendedModelViewSet, BothListFormatMixin
from .serializers.config import ConfigSerializer
from .serializers.course import (
@@ -21,7 +22,7 @@ from .serializers.course import (
MaterialSerializer, MaterialCreateSerializer,
LessonSerializer, LessonCreateSerializer,
LikeCreateSerializer, CourseCommentSerializer, LessonCommentSerializer,
- LiveLessonCommentSerializer,)
+ LiveLessonCommentSerializer, TagSerializer)
from .serializers.content import (
BannerSerializer,
ImageSerializer, ImageCreateSerializer,
@@ -30,7 +31,7 @@ from .serializers.content import (
VideoSerializer, VideoCreateSerializer,
GallerySerializer,
GalleryImageSerializer, GalleryImageCreateSerializer,
- ImageObjectSerializer, FAQSerializer,
+ ImageObjectSerializer, FAQSerializer, PackageSerializer,
)
from .serializers.school import (
SchoolScheduleSerializer,
@@ -60,12 +61,12 @@ from apps.course.models import (
Comment, CourseComment, LessonComment,
Material, Lesson,
Like,
- LiveLessonComment)
+ LiveLessonComment, Tag, CourseTags)
from apps.config.models import Config
from apps.content.models import (
Banner, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject,
- Contest, ContestWork, FAQ)
+ Contest, ContestWork, FAQ, Package)
from apps.payment.models import (
AuthorBalance, Payment,
CoursePayment, SchoolPayment, UserBonus,
@@ -652,14 +653,13 @@ class PaymentViewSet(viewsets.ModelViewSet):
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')
is_camp = bool(request.query_params.get('is_camp'))
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,
+ return Response(Payment.calc_amount(user=user, course=course, date_start=date_start,
is_camp=is_camp))
@@ -774,3 +774,30 @@ class NotifiedAboutBonuses(views.APIView):
b.save()
return Response({'status': 'ok'})
+class PackageViewSet(ExtendedModelViewSet):
+ queryset = Package.objects.all()
+ serializer_class = PackageSerializer
+ permission_classes = (IsAdmin,)
+
+class TagViewSet(ExtendedModelViewSet):
+ queryset = Tag.objects.all()
+ serializer_class = TagSerializer
+ search_fields = ('tag',)
+
+ def list(self, request, *args, **kwargs):
+ queryset = self.filter_queryset(self.get_queryset())
+ serializer = self.get_serializer(queryset, many=True)
+ return Response(serializer.data)
+
+ @action(methods=['get'], detail=False, url_path='popular', authentication_classes=[], permission_classes=[])
+ def popular(self, *args, **kwargs):
+ 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)]
+ tags = Tag.objects.filter(id__in=tags).order_by('tag')
+ serializer = self.get_serializer(tags, many=True)
+ return Response(serializer.data)
diff --git a/apps/content/admin.py b/apps/content/admin.py
index a407d671..07d86380 100644
--- a/apps/content/admin.py
+++ b/apps/content/admin.py
@@ -8,7 +8,7 @@ from polymorphic.admin import (
from apps.content.models import (
Banner, Content, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject,
- Contest, ContestWork, FAQ,
+ Contest, ContestWork, FAQ, Package,
)
@@ -99,3 +99,8 @@ class ContestWorkAdmin(admin.ModelAdmin):
@admin.register(FAQ)
class FAQAdmin(admin.ModelAdmin):
base_model = FAQ
+
+
+@admin.register(Package)
+class PackageAdmin(admin.ModelAdmin):
+ base_model = Package
diff --git a/apps/content/migrations/0029_auto_20190730_2032.py b/apps/content/migrations/0029_auto_20190730_2032.py
new file mode 100644
index 00000000..645e97e2
--- /dev/null
+++ b/apps/content/migrations/0029_auto_20190730_2032.py
@@ -0,0 +1,38 @@
+# Generated by Django 2.0.7 on 2019-07-30 20:32
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('content', '0028_auto_20190726_0106'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Package',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('price', models.DecimalField(decimal_places=2, max_digits=10)),
+ ('high_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
+ ('description', models.TextField(db_index=True, default='', verbose_name='Описание')),
+ ('duration', models.PositiveSmallIntegerField()),
+ ('options', models.TextField(db_index=True, default='', verbose_name='Описание')),
+ ],
+ options={
+ 'ordering': ('duration',),
+ },
+ ),
+ migrations.AlterField(
+ model_name='banner',
+ name='main_banner',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(choices=[(1, 'Главная'), (2, 'Курсы'), (3, 'Школа'), (4, 'Пакеты')]), blank=True, default=[], size=None),
+ ),
+ migrations.AlterField(
+ model_name='banner',
+ name='pages',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(choices=[(1, 'Главная'), (2, 'Курсы'), (3, 'Школа'), (4, 'Пакеты')]), blank=True, default=[], size=None),
+ ),
+ ]
diff --git a/apps/content/migrations/0030_auto_20190809_0133.py b/apps/content/migrations/0030_auto_20190809_0133.py
new file mode 100644
index 00000000..da8fed67
--- /dev/null
+++ b/apps/content/migrations/0030_auto_20190809_0133.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.0.7 on 2019-08-09 01:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('content', '0029_auto_20190730_2032'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='package',
+ name='options',
+ field=models.TextField(db_index=True, default='', verbose_name='Опции'),
+ ),
+ ]
diff --git a/apps/content/models.py b/apps/content/models.py
index 101ffe7e..7178efd4 100644
--- a/apps/content/models.py
+++ b/apps/content/models.py
@@ -146,11 +146,13 @@ class Banner(models.Model):
PAGE_INDEX = 1
PAGE_COURSES = 2
PAGE_SCHOOL = 3
+ PAGE_PACKAGES = 4
PAGE_CHOICES = (
(PAGE_INDEX, 'Главная'),
(PAGE_COURSES, 'Курсы'),
(PAGE_SCHOOL, 'Школа'),
+ (PAGE_PACKAGES, 'Пакеты')
)
text = models.TextField(blank=True, default='')
@@ -178,6 +180,12 @@ class Banner(models.Model):
is_main=RawSQL('main_banner @> %s', ([page],))
).order_by('-is_main')
+ @property
+ def is_video_url(self):
+ return self.url and ('vimeo.com' in self.url
+ or 'youtube.com' in self.url and 'watch' in self.url
+ or 'youto.be' in self.url)
+
class Contest(models.Model):
title = models.CharField(max_length=255)
@@ -241,3 +249,22 @@ class ContestWork(models.Model):
class FAQ(models.Model):
question = models.TextField(max_length=1000,)
answer = models.TextField(max_length=1000,)
+
+
+class Package(models.Model):
+ price = models.DecimalField(max_digits=10, decimal_places=2,)
+ high_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
+ description = models.TextField(
+ 'Описание', default='', db_index=True
+ )
+ duration = models.PositiveSmallIntegerField()
+ options = models.TextField(
+ 'Опции', default='', db_index=True
+ )
+
+ class Meta:
+ ordering = ('duration',)
+
+ @property
+ def options_html(self):
+ return ''.join(map(lambda x: '
%s
' % x, self.options.split('\n')))
diff --git a/apps/course/admin.py b/apps/course/admin.py
index 39d0f54d..70ea387e 100644
--- a/apps/course/admin.py
+++ b/apps/course/admin.py
@@ -1,7 +1,7 @@
from django.contrib import admin
from mptt.admin import MPTTModelAdmin, DraggableMPTTAdmin
-from .models import Course, Category, Lesson, Material, CourseComment, LessonComment
+from .models import Course, Category, Lesson, Material, CourseComment, LessonComment, Tag, CourseTags
@admin.register(Course)
@@ -47,3 +47,13 @@ class CourseCommentAdmin(DraggableMPTTAdmin):
@admin.register(LessonComment)
class LessonCommentAdmin(DraggableMPTTAdmin):
pass
+
+
+@admin.register(Tag)
+class TagAdmin(admin.ModelAdmin):
+ pass
+
+
+@admin.register(CourseTags)
+class CourseTagAdmin(admin.ModelAdmin):
+ pass
diff --git a/apps/course/migrations/0050_auto_20190818_1043.py b/apps/course/migrations/0050_auto_20190818_1043.py
new file mode 100644
index 00000000..d6635716
--- /dev/null
+++ b/apps/course/migrations/0050_auto_20190818_1043.py
@@ -0,0 +1,38 @@
+# Generated by Django 2.0.7 on 2019-08-18 10:43
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('course', '0049_auto_20190207_1551'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='CourseTags',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.Course')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Tag',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('tag', models.CharField(max_length=20)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='coursetags',
+ name='tag',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.Tag'),
+ ),
+ migrations.AddField(
+ model_name='course',
+ name='tags',
+ field=models.ManyToManyField(blank=True, through='course.CourseTags', to='course.Tag'),
+ ),
+ ]
diff --git a/apps/course/models.py b/apps/course/models.py
index f7ef5f20..ad520377 100644
--- a/apps/course/models.py
+++ b/apps/course/models.py
@@ -21,6 +21,17 @@ from apps.content.models import ImageObject, Gallery, Video, ContestWork
User = get_user_model()
+def default_slug():
+ return str(uuid4())
+
+
+def deferred_start_at_validator(value):
+ if value < now():
+ raise ValidationError(
+ 'Дата и время начала курса не может быть меньше текущих.',
+ )
+
+
class Like(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
ip = models.GenericIPAddressField(blank=True, null=True)
@@ -29,15 +40,13 @@ class Like(models.Model):
update_at = models.DateTimeField(auto_now=True)
-def default_slug():
- return str(uuid4())
+class Tag(models.Model):
+ tag = models.CharField(max_length=20,)
-def deferred_start_at_validator(value):
- if value < now():
- raise ValidationError(
- 'Дата и время начала курса не может быть меньше текущих.',
- )
+class CourseTags(models.Model):
+ tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
+ course = models.ForeignKey('Course', on_delete=models.CASCADE)
class Course(BaseModel, DeactivatedMixin):
@@ -110,6 +119,7 @@ class Course(BaseModel, DeactivatedMixin):
on_delete=models.CASCADE, null=True, blank=True,
related_name='results_gallery',
)
+ tags = models.ManyToManyField('Tag', through=CourseTags, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
diff --git a/apps/course/templates/course/_items.html b/apps/course/templates/course/_items.html
index 070f5dbc..d0380e96 100644
--- a/apps/course/templates/course/_items.html
+++ b/apps/course/templates/course/_items.html
@@ -8,12 +8,11 @@
{% if course.is_deferred_start and course.status == 2 %}data-future-course data-future-course-time={{ course.deferred_start_at.timestamp }}{% endif %}
>
- {% thumbnail course.cover.image "300x200" crop="center" as im %}
-
- {% empty %}
+ {% if course.cover %}
+
+ {% else %}
- {% endthumbnail %}
- Подробнее
+ {% endif %}
{% if course.is_featured %}
{% endif %}
@@ -51,11 +50,11 @@
href="{% url 'courses' %}?category={{ course.category.id }}">{{ course.category | upper }}
{% if not course.is_free %}
{% if course.buy_again_price %}
- {{ course.price|floatformat:"-2" }}₽
+ {{ course.price|floatformat:"-2" }}₽
{{ course.buy_again_price|floatformat:"-2" }}₽
{% else %}
{% if course.old_price %}
- {{ course.old_price|floatformat:"-2" }}₽
+ {{ course.old_price|floatformat:"-2" }}₽
{% endif %}
{{ course.price|floatformat:"-2" }}₽
diff --git a/apps/course/templates/course/course.html b/apps/course/templates/course/course.html
index 79bab74d..99642e86 100644
--- a/apps/course/templates/course/course.html
+++ b/apps/course/templates/course/course.html
@@ -23,7 +23,7 @@
{% block ogdescription %}{{ course.short_description | striptags }}{% endblock ogdescription %}
{% block content %}
-
+
+ {% if not request.user_agent.is_mobile %}
+ {% endif %}
@@ -356,9 +358,9 @@
{% if not is_owner and course.price %}
{% if not paid or can_buy_again %}
-
+
{% endif %}
+
+
+ {% if tags|length %}
+
+ ИСКАТЬ ПО ТЕГАМ
+
+
+ {% endif %}
+
+
+
-
-
Учите и развивайте креативное мышление когда и где угодно. Если вам не совсем удобно заниматься с нами в прямом эфире каждый день, как в
- нашей онлайн-школе, специально для вас мы делаем отдельные уроки в записи, которые вы можете проходить,
- когда вам будет удобно.

-
Курсы
-
+
Курсы
+
-
+
Категории
@@ -35,7 +59,7 @@
-
+
{% if age_name %}{{ age_name }}{% else %}Возраст{% endif %}
diff --git a/apps/course/views.py b/apps/course/views.py
index edd6ffa6..34f329eb 100644
--- a/apps/course/views.py
+++ b/apps/course/views.py
@@ -1,4 +1,5 @@
from datetime import timedelta
+from itertools import groupby
from paymentwall import Pingback
from django.contrib.auth import get_user_model
@@ -17,8 +18,9 @@ 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
+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()
@@ -228,6 +230,7 @@ class CourseView(DetailView):
except queryset.model.DoesNotExist:
raise Http404(_("No %(verbose_name)s found matching the query") %
{'verbose_name': queryset.model._meta.verbose_name})
+ obj.cover = None
return obj
def get_context_data(self, **kwargs):
@@ -312,6 +315,14 @@ class CoursesView(ListView):
).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
@@ -319,8 +330,18 @@ class CoursesView(ListView):
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]:
@@ -339,6 +360,10 @@ class CoursesView(ListView):
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
+ for course in context['new_courses']:
+ course.cover = None
return context
def get_template_names(self):
diff --git a/apps/notification/templates/notification/email/buy_email.html b/apps/notification/templates/notification/email/buy_email.html
index 4e7c71dd..5733ed7a 100644
--- a/apps/notification/templates/notification/email/buy_email.html
+++ b/apps/notification/templates/notification/email/buy_email.html
@@ -3,12 +3,20 @@
{% block content %}
{% if product_type == 'course' %}
-
Курс ждет вас по ссылке
+ Добрый день! Спасибо за покупку знаний в «Lil School»!
+ Где искать уроки?
+ После оплаты курс появится в вашем личном кабинете на платформе.
+
https://{% setting 'MAIN_HOST' %}{{ url }}
+ Все ваши покупки будут храниться там в рамках срока доступа к курсу.
{% endif %}
{% if product_type == 'school' %}
- Школа ждет вас по ссылке
+ Добрый день! Спасибо за покупку знаний в «Lil School»!
+ Где искать уроки?
+ После оплаты уроки появятся в вашем личном кабинете на платформе.
+
https://{% setting 'MAIN_HOST' %}{% url 'school:school' %}
+ В онлайн-школе урок хранится неделю. Ровно до следующего урока.
{% endif %}
{% if product_type == 'drawing_camp' %}
{% if date_start.month == 7 and date_start.day == 1 and date_end.day == 31 %}
@@ -72,7 +80,6 @@
https://{% setting 'MAIN_HOST' %}{% url 'school:drawing-camp' %}
{% endif %}
{% endif %}
-Так же вы можете найти ссылку в личном кабинете в разделе «Мои покупки».
Занимайтесь с удовольствием!
diff --git a/apps/payment/migrations/0038_auto_20190814_1506.py b/apps/payment/migrations/0038_auto_20190814_1506.py
new file mode 100644
index 00000000..90dbfce2
--- /dev/null
+++ b/apps/payment/migrations/0038_auto_20190814_1506.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.0.7 on 2019-08-14 15:06
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('content', '0030_auto_20190809_0133'),
+ ('payment', '0037_add_paid_one_more_bonuses'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='schoolpayment',
+ name='package',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='content.Package'),
+ ),
+ migrations.AlterField(
+ model_name='payment',
+ name='bonus',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_payments', to='payment.UserBonus'),
+ ),
+ ]
diff --git a/apps/payment/models.py b/apps/payment/models.py
index b8e2d8c1..0d4d7c25 100644
--- a/apps/payment/models.py
+++ b/apps/payment/models.py
@@ -16,12 +16,14 @@ from django.core.validators import RegexValidator
from django.utils.timezone import now
from django.conf import settings
+from apps.content.models import Package
from project.utils import weekdays_in_date_range
from apps.course.models import Course
from apps.config.models import Config
from apps.school.models import SchoolSchedule
from apps.notification.utils import send_email
+from project.utils import dates_overlap
config = Config.load()
@@ -126,11 +128,11 @@ class Payment(PolymorphicModel):
ordering = ('created_at',)
@classmethod
- def ajust_date_bounds(cls, date_start=None, date_end=None, is_camp=False):
- school_start = date(now().year, 9, 1)
- school_end = date(now().year, 5, 31)
- camp_start = date(now().year, 6, 1)
- camp_end = date(now().year, 8, 31)
+ def adjust_date_bounds(cls, date_start=None, date_end=None, is_camp=False):
+ school_start = date((date_start or date_end).year, 9, 1)
+ school_end = date((date_end or date_start).year, 5, 31)
+ camp_start = date((date_start or date_end).year, 6, 1)
+ camp_end = date((date_end or date_start).year, 8, 31)
if date_start:
if is_camp:
@@ -151,24 +153,32 @@ class Payment(PolymorphicModel):
else:
return date_start or date_end
+ @classmethod
+ def date_add(cls, date_start, days=0, months=0, is_camp=False):
+ date_end = arrow.get(date_start + timedelta(days=days), settings.TIME_ZONE).shift(
+ months=months).date() - timedelta(1)
+ if months == 1:
+ if is_camp or (date_start.month == 2 and date_start.day >= 28) or (
+ date_start.day == 31 and date_end.day <= 30) \
+ or (date_start.month == 1 and date_start.day >= 29 and date_end.day == 28):
+ date_end = date_start.replace(day=1, month=date_start.month + 1) - timedelta(1)
+ return date_end
+
@classmethod
def get_date_range(cls, date_start=None, days=0, months=0, is_camp=False):
date_start = date_start or now().date()
if isinstance(date_start, datetime):
date_start = date_start.date()
- date_start = cls.ajust_date_bounds(date_start=date_start, is_camp=is_camp)
+ date_start = cls.adjust_date_bounds(date_start=date_start, is_camp=is_camp)
if is_camp and date_start.month == 6 and date_start.day > 16:
date_start = date_start.replace(month=7, day=1)
- date_end = arrow.get(date_start + timedelta(days=days), settings.TIME_ZONE).shift(months=months).date() - timedelta(1)
- if months == 1:
- if is_camp or (date_start.month == 2 and date_start.day >= 28) or (date_start.day == 31 and date_end.day <= 30) \
- or (date_start.month == 1 and date_start.day >= 29 and date_end.day == 28):
- date_end = date_start.replace(day=1, month=date_start.month + 1) - timedelta(1)
- date_end = cls.ajust_date_bounds(date_end=date_end, is_camp=is_camp)
+ date_end = cls.date_add(date_start, days, months, is_camp)
+ if is_camp:
+ date_end = cls.adjust_date_bounds(date_end, is_camp=is_camp)
return [date_start, date_end]
@classmethod
- def calc_amount(cls, payment=None, user=None, course=None, date_start=None, weekdays=None, is_camp=False):
+ def calc_amount(cls, package=None, payment=None, user=None, course=None, date_start=None, weekdays=None, is_camp=False):
price = 0
discount = 0
referral_bonus = 0
@@ -178,14 +188,14 @@ class Payment(PolymorphicModel):
user = payment.user
if isinstance(payment, SchoolPayment):
user = payment.user
- weekdays = payment.weekdays
date_start = payment.date_start
+ package=payment.package
if isinstance(payment, DrawingCampPayment):
user = payment.user
date_start = payment.date_start
if issubclass(cls, DrawingCampPayment):
is_camp = True
- date_start, date_end = Payment.get_date_range(date_start, months=1, is_camp=is_camp)
+ date_start, date_end = Payment.get_date_range(date_start, months=package.duration if package else 1, is_camp=is_camp)
if hasattr(user, 'referral') and not user.referral.payment:
referral_bonus = user.referral.bonus
referrer_bonus = user.referral.referrer_bonus
@@ -213,40 +223,22 @@ class Payment(PolymorphicModel):
date_end__gte=date_start,
status__in=Payment.PW_PAID_STATUSES,
)
- school_schedules_purchased = school_payments.annotate(
- joined_weekdays=Func(F('weekdays'), function='unnest', )
- ).values_list('joined_weekdays', flat=True).distinct()
- weekdays = list(set(map(int, weekdays)) - set(school_schedules_purchased))
prev_school_payment = school_payments.filter(add_days=False).last()
- add_days = bool(prev_school_payment)
- else:
- add_days = False
- school_schedules = SchoolSchedule.objects.filter(
- weekday__in=weekdays,
- is_camp=False,
- )
- if add_days:
- date_end = prev_school_payment.date_end
- weekdays_count = weekdays_in_date_range(date_start, prev_school_payment.date_end)
- all_weekdays_count = weekdays_in_date_range(prev_school_payment.date_start, prev_school_payment.date_end)
- for ss in school_schedules:
- price += ss.month_price // all_weekdays_count.get(ss.weekday, 0) * weekdays_count.get(
- ss.weekday, 0)
+ if prev_school_payment:
+ date_start, date_end = Payment.get_date_range(prev_school_payment.date_end + timedelta(1),
+ months=package.duration, is_camp=False)
+ school_schedules = SchoolSchedule.objects.filter(is_camp=False).exclude(title='')
+ weekdays = list(school_schedules.values_list('weekday', flat=True))
+ # FIXME после мая 2019 убрать?
+ # Если хотят купить школу в мае, то оплатить ее можно только до 31 мая, потом школа закроется
+ if date_start.month == 5:
+ weekdays_count = weekdays_in_date_range(date_start, date_end)
+ weekdays_count = sum(weekdays_count[wd] for wd in weekdays)
+ all_weekdays_count = weekdays_in_date_range(date_start.replace(day=1), date_end)
+ all_weekdays_count = sum(all_weekdays_count[wd] for wd in weekdays)
+ price = package.price // all_weekdays_count * weekdays_count
else:
- # FIXME после мая 2019 убрать?
- # Если хотят купить школу в мае, то оплатить ее можно только до 31 мая, потом школа закроется
- if date_start.month == 5:
- weekdays_count = weekdays_in_date_range(date_start, date_end)
- all_weekdays_count = weekdays_in_date_range(date_start.replace(day=1), date_end)
- for ss in school_schedules:
- price += ss.month_price // all_weekdays_count.get(ss.weekday, 0) * weekdays_count.get(
- ss.weekday, 0)
- else:
- price = school_schedules.aggregate(
- models.Sum('month_price'),
- ).get('month_price__sum', 0)
- if not (payment and payment.id) and price >= config.SERVICE_DISCOUNT_MIN_AMOUNT:
- discount = config.SERVICE_DISCOUNT
+ price = package.price
amount = price - discount
referral_bonus = round(amount * referral_bonus / 100)
referrer_bonus = round(amount * referrer_bonus / 100)
@@ -337,6 +329,7 @@ class CoursePayment(Payment):
class SchoolPayment(Payment):
weekdays = ArrayField(models.IntegerField(), size=7, verbose_name='Дни недели')
add_days = models.BooleanField('Докупленные дни', default=False)
+ package = models.ForeignKey(Package, null=True, blank=True, on_delete=models.SET_NULL)
date_start = models.DateField('Дата начала подписки', null=True, blank=True)
date_end = models.DateField('Дата окончания подписки', null=True, blank=True)
diff --git a/apps/payment/templates/payment/package_payment_success.html b/apps/payment/templates/payment/package_payment_success.html
new file mode 100644
index 00000000..c4bb469a
--- /dev/null
+++ b/apps/payment/templates/payment/package_payment_success.html
@@ -0,0 +1,12 @@
+{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %}
+
+
+
+
Вы успешно приобрели подписку с {{ package.date_start }} по {{ package.date_end }}!
+
+
+
+
+{% endblock content %}
diff --git a/apps/payment/templates/payment/payment_success.html b/apps/payment/templates/payment/payment_success.html
index 5686ee03..c9b8d6fb 100644
--- a/apps/payment/templates/payment/payment_success.html
+++ b/apps/payment/templates/payment/payment_success.html
@@ -1,9 +1,10 @@
-{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %}
+{% extends "templates/lilcity/index.html" %} {% load static %}{% load plural %}
+{% block content %}
{% if school %}
-
Вы успешно приобрели доступ к урокам онлайн-школы!
+
Вы успешно приобрели доступ на {{ duration|rupluralize:"месяц,месяца,месяцев" }}!
diff --git a/apps/payment/views.py b/apps/payment/views.py
index 9aca10df..8eb30777 100644
--- a/apps/payment/views.py
+++ b/apps/payment/views.py
@@ -20,6 +20,7 @@ from django.utils.timezone import now
from paymentwall import Pingback, Product, Widget
+from apps.content.models import Package
from apps.course.models import Course
from apps.payment.tasks import transaction_to_mixpanel, product_payment_to_mixpanel, transaction_to_roistat
from apps.notification.utils import send_email
@@ -48,7 +49,12 @@ class SchoolBuySuccessView(TemplateView):
template_name = 'payment/payment_success.html'
def get(self, request, pk=None, is_camp=False, *args, **kwargs):
- return self.render_to_response(context={'camp': True} if is_camp else {'school': True})
+ context = {
+ 'duration': request.GET.get('duration'),
+ 'camp': is_camp,
+ 'school': not is_camp
+ }
+ return self.render_to_response(context=context)
@method_decorator(login_required, name='dispatch')
@@ -116,56 +122,23 @@ class SchoolBuyView(TemplateView):
template_name = 'payment/paymentwall_widget.html'
def get(self, request, *args, **kwargs):
- raise Http404() # FIXME
host = urlsplit(self.request.META.get('HTTP_REFERER'))
host = str(host[0]) + '://' + str(host[1])
- weekdays = set(request.GET.getlist('weekdays', []))
use_bonuses = request.GET.get('use_bonuses')
roistat_visit = request.COOKIES.get('roistat_visit', None)
date_start = request.GET.get('date_start')
+ duration = request.GET.get('duration')
+ package = get_object_or_404(Package, duration=duration)
date_start = date_start and datetime.datetime.strptime(date_start, '%Y-%m-%d').date() or now().date()
- date_start, date_end = Payment.get_date_range(date_start, months=1)
- if not weekdays:
- messages.error(request, 'Выберите несколько дней недели.')
- return redirect('school:school')
- try:
- weekdays = [int(weekday) for weekday in weekdays]
- except ValueError:
- messages.error(request, 'Ошибка выбора дней недели.')
- return redirect('school:school')
- prev_school_payment = SchoolPayment.objects.filter(
+ amount_data = SchoolPayment.calc_amount(package=package, user=request.user, date_start=date_start)
+ school_payment = SchoolPayment.objects.create(
user=request.user,
- date_start__lte=date_start,
- date_end__gte=date_start,
- add_days=False,
- status__in=[
- Pingback.PINGBACK_TYPE_REGULAR,
- Pingback.PINGBACK_TYPE_GOODWILL,
- Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
- ],
- ).last()
- add_days = bool(prev_school_payment)
- if add_days:
- school_payment = SchoolPayment.objects.create(
- user=request.user,
- weekdays=weekdays,
- date_start=date_start,
- date_end=prev_school_payment.date_end,
- add_days=True,
- roistat_visit=roistat_visit,
- )
- # Если произойдет ошибка и оплату бонусами повторят еще раз на те же дни, то вернет ошибку
- if school_payment.amount <= 0:
- messages.error(request, 'Выбранные дни отсутствуют в оставшемся периоде подписки')
- return redirect(reverse_lazy('school:school'))
- else:
- school_payment = SchoolPayment.objects.create(
- user=request.user,
- weekdays=weekdays,
- roistat_visit=roistat_visit,
- date_start=date_start,
- date_end=date_end,
- )
+ weekdays=amount_data.get('weekdays'),
+ roistat_visit=roistat_visit,
+ date_start=amount_data.get('date_start'),
+ date_end=amount_data.get('date_end'),
+ package=package,
+ )
if use_bonuses and request.user.bonus:
if request.user.bonus >= school_payment.amount:
bonus = UserBonus.objects.create(amount= -school_payment.amount, user=request.user, payment=school_payment)
@@ -183,7 +156,7 @@ class SchoolBuyView(TemplateView):
f'school_{school_payment.id}',
school_payment.amount,
'RUB',
- 'Школа',
+ 'Подписка',
)
widget = Widget(
str(request.user.id),
@@ -195,7 +168,7 @@ class SchoolBuyView(TemplateView):
'evaluation': 1,
'demo': 1,
'test_mode': 1,
- 'success_url': host + str(reverse_lazy('payment-success')),
+ 'success_url': host + str(reverse_lazy('payment-success')) + '?duration=%s' % duration,
'failure_url': host + str(reverse_lazy('payment-error')),
}
)
@@ -362,6 +335,7 @@ class PaymentwallCallbackView(View):
'created_at': payment.created_at,
'update_at': payment.update_at,
}
+
payment.save()
product_payment_to_mixpanel.delay(
diff --git a/apps/school/views.py b/apps/school/views.py
index bd32cb7b..c737f199 100644
--- a/apps/school/views.py
+++ b/apps/school/views.py
@@ -70,7 +70,7 @@ class DrawingCampLessonsView(ListView):
def get_queryset(self):
date_start = (now() - timedelta(days=7)).date()
- date_start, date_end = DrawingCampPayment.ajust_date_bounds(date_start, date_start + timedelta(days=23), is_camp=True)
+ date_start, date_end = DrawingCampPayment.adjust_date_bounds(date_start, date_start + timedelta(days=23), is_camp=True)
date_range = Q(
date__range=[date_start, date_end]
)
@@ -106,15 +106,15 @@ class LiveLessonsDetailView(DetailView):
is_purchased = DrawingCampPayment.objects.all()
else:
is_purchased = SchoolPayment.objects.filter(weekdays__contains=[self.object.date.weekday() + 1],)
- is_purchased = is_purchased.filter(
+ is_purchased = is_purchased.paid().filter(
user=request.user,
date_start__lte=now(),
date_end__gte=now() - timedelta(days=7),
- status__in=[
- Pingback.PINGBACK_TYPE_REGULAR,
- Pingback.PINGBACK_TYPE_GOODWILL,
- Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
- ],
+ ).exists() or is_purchased.paid().filter(
+ user=request.user,
+ date_start__lte=now(),
+ date_end__gte=now() - timedelta(days=30),
+ package__duration__lte=9,
).exists()
if not is_purchased and request.user.role not in [User.ADMIN_ROLE, User.TEACHER_ROLE]:
raise Http404
@@ -221,7 +221,10 @@ class SchoolView(TemplateView):
# берем все подписки, которые были в периоде
for sp in prev_school_payments:
# берем все уроки в оплаченном промежутке
- date_range = [max(sp.date_start, prev_range[0]), min(sp.date_end, prev_range[1])]
+ if sp.package and sp.package.duration >= 9:
+ date_range = [max(sp.date_start, date_now - timedelta(30)), min(sp.date_end, prev_range[1])]
+ else:
+ date_range = [max(sp.date_start, prev_range[0]), min(sp.date_end, prev_range[1])]
prev_live_lessons += list(LiveLesson.objects.filter(
date__range=date_range,
deactivated_at__isnull=True,
diff --git a/apps/user/admin.py b/apps/user/admin.py
index 63398df0..50915f33 100644
--- a/apps/user/admin.py
+++ b/apps/user/admin.py
@@ -12,7 +12,8 @@ User = get_user_model()
class UserAdmin(BaseUserAdmin):
fieldsets = (
(None, {'fields': ('username', 'password')}),
- (_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'gender', 'about', 'photo')}),
+ (_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'gender', 'about', 'photo',)}),
+ ('Teacher', {'fields': ('show_in_mainpage', 'trial_lesson', 'instagram_hashtag',)}),
('Facebook Auth data', {'fields': ('fb_id', 'fb_data', 'is_email_proved')}),
(_('Permissions'), {'fields': ('role', 'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions', 'show_in_mainpage')}),
diff --git a/project/templates/blocks/about.html b/project/templates/blocks/about.html
index e2c8b3c4..d24701e3 100644
--- a/project/templates/blocks/about.html
+++ b/project/templates/blocks/about.html
@@ -1,12 +1,23 @@
+{% load static %}
-
+
-
-
Вы житель мегаполиса и у вас нет времени дополнительно развивать своего ребенка?
- Или вы живете в маленьком городе,
- где нет качественных школ и секций для детей?
-
Lil School это решение для тех родителей, кто стремится дать лучшее своему ребенку.
- Учитесь не выходя из дома!
+
+
+
+
Для кого?

+
+
Вы житель мегаполиса и у вас нет времени дополнительно развивать своего ребенка?
+ Или вы живете в маленьком городе,
+ где нет качественных школ и секций для детей?
+
Lil-School это решение для тех родителей, кто стремится дать лучшее своему ребенку.
+ Учитесь, не выходя из дома!
+
+
diff --git a/project/templates/blocks/banner.html b/project/templates/blocks/banner.html
index f047c2cf..83d9cda0 100644
--- a/project/templates/blocks/banner.html
+++ b/project/templates/blocks/banner.html
@@ -1,5 +1,5 @@
diff --git a/project/templates/blocks/banners.html b/project/templates/blocks/banners.html
index ed3f0e55..662cae95 100644
--- a/project/templates/blocks/banners.html
+++ b/project/templates/blocks/banners.html
@@ -16,5 +16,7 @@
{% else %}
- {% include 'templates/blocks/banner.html' with banner=banners.0 %}
+
+ {% include 'templates/blocks/banner.html' with banner=banners.0 %}
+
{% endif %}
diff --git a/project/templates/blocks/counters.html b/project/templates/blocks/counters.html
index 40198a07..2fc03840 100644
--- a/project/templates/blocks/counters.html
+++ b/project/templates/blocks/counters.html
@@ -1,11 +1,10 @@
{% load static %}
{% load ruplural from plural %}
-
+
-

-
Lil School в цифрах
+
@@ -32,6 +31,5 @@
со всего мира со счастливыми учениками Lil School
-
diff --git a/project/templates/blocks/footer.html b/project/templates/blocks/footer.html
index 21c8df71..021faff8 100644
--- a/project/templates/blocks/footer.html
+++ b/project/templates/blocks/footer.html
@@ -1,7 +1,17 @@
{% load static %}