Merge remote-tracking branch 'origin/master' into hasaccess

# Conflicts:
#	apps/school/models.py
#	apps/school/views.py
#	project/views.py
remotes/origin/hasaccess
nikita 8 years ago
commit 9ad0724fa6
  1. 58
      .gitlab-ci.yml
  2. 2
      api/v1/permissions.py
  3. 5
      api/v1/serializers/course.py
  4. 18
      apps/course/migrations/0039_lesson_position.py
  5. 8
      apps/course/models.py
  6. 6
      apps/course/templates/course/content/imagetext.html
  7. 56
      apps/course/templates/course/course.html
  8. 13
      apps/course/templates/course/course_only_lessons.html
  9. 1
      apps/course/templates/course/courses.html
  10. 9
      apps/course/templates/course/lesson.html
  11. 6
      apps/course/views.py
  12. 3
      apps/payment/admin.py
  13. 18
      apps/payment/migrations/0019_payment_roistat_visit.py
  14. 1
      apps/payment/models.py
  15. 3
      apps/payment/tasks.py
  16. 2
      apps/payment/templates/payment/payment_success.html
  17. 6
      apps/payment/views.py
  18. 18
      apps/school/migrations/0017_auto_20180628_2000.py
  19. 18
      apps/school/migrations/0018_auto_20180629_1501.py
  20. 6
      apps/school/models.py
  21. 14
      apps/school/templates/blocks/_schedule_purchased_item.html
  22. 2
      apps/school/templates/blocks/online.html
  23. 1
      apps/school/templates/blocks/schedule_purchased.html
  24. 2
      apps/school/templates/school/livelesson_detail.html
  25. 1
      apps/school/templates/school/summer_school.html
  26. 26
      apps/school/templates/summer/_schedule_purchased_item.html
  27. 1
      apps/school/templates/summer/schedule_purchased.html
  28. 8
      apps/user/templates/user/profile.html
  29. 22
      docker/.env.review
  30. 1
      docker/Dockerfile
  31. 10
      docker/conf/nginx/conf.d/default.conf
  32. 1
      docker/conf/nginx/nginx.conf
  33. 1
      docker/conf/nginx/sites-enabled/default
  34. 7
      docker/docker-compose-prod.yml
  35. 58
      docker/docker-compose-review.yml
  36. 1
      docker/entrypoint_app.sh
  37. 2
      project/settings.py
  38. 14
      project/templates/blocks/about.html
  39. 32
      project/templates/blocks/promo.html
  40. 2
      project/templates/blocks/user_menu.html
  41. 39
      project/templates/lilcity/index.html
  42. 15
      requirements.txt
  43. 148
      web/src/components/CourseRedactor.vue
  44. 36
      web/src/components/LessonRedactor.vue
  45. BIN
      web/src/img/og_blog.jpg
  46. BIN
      web/src/img/og_courses.jpg
  47. BIN
      web/src/img/og_main.jpg
  48. BIN
      web/src/img/og_summer_school.jpg
  49. 11
      web/src/js/modules/api.js
  50. 15
      web/src/sass/_common.sass

@ -1,16 +1,68 @@
stages: stages:
- deploy - deploy
- db
- stop
variables:
REVIEW_DOMAIN: back-review.lil.school
deploy_prod: deploy_prod:
stage: deploy stage: deploy
script: script:
- rsync -a --stats --delete --exclude="docker/data/" --exclude="docker/.env" ./ /work/www/lil.school/ - rsync -a --stats --delete --exclude="docker/data/" --exclude="docker/.env" ./ /work/www/lil.school/
- cd /work/www/lil.school/docker/ - cd /work/www/lil.school/docker/
- docker-compose -f docker-compose-prod.yml up --build -d - docker-compose -f docker-compose-prod.yml -p back up --build -d
environment: environment:
name: prod/site name: prod
url: https://lil.school url: https://lil.school
only: only:
- master - master
tags: tags:
- prod - prod
deploy_review:
stage: deploy
script:
- export REVIEW_HOST=$CI_COMMIT_REF_SLUG-$REVIEW_DOMAIN
- cd docker
- cp .env.review .env
- docker-compose -f docker-compose-review.yml -p back$CI_COMMIT_REF_NAME up --build -d
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_COMMIT_REF_SLUG-$REVIEW_DOMAIN
on_stop: stop-review
tags:
- review
only:
- branches
stop-review:
stage: stop
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
script:
- export REVIEW_HOST=$CI_COMMIT_REF_SLUG-$REVIEW_DOMAIN
- cd docker
- docker-compose -f docker-compose-review.yml -p back$CI_COMMIT_REF_NAME down
- rm -rf /work/data/back_${CI_COMMIT_REF_NAME}/
when: manual
only:
- branches
tags:
- review
prod-db:
stage: db
script:
- export REVIEW_HOST=$CI_COMMIT_REF_SLUG-$REVIEW_DOMAIN
- cd docker
- cp .env.review .env
- docker-compose -f docker-compose-review.yml -p back$CI_COMMIT_REF_NAME restart db
- echo 'DROP DATABASE IF EXISTS lilcity; CREATE DATABASE lilcity' | docker-compose -f docker-compose-review.yml -p back$CI_COMMIT_REF_NAME exec -T -u postgres db psql postgres
- /work/scripts/get_prod_db.sh | docker-compose -f docker-compose-review.yml -p back$CI_COMMIT_REF_NAME exec -T -u postgres db psql lilcity
when: manual
only:
- branches
tags:
- review

@ -15,7 +15,7 @@ class IsAdmin(BasePermission):
class IsTeacherOrAdmin(BasePermission): class IsTeacherOrAdmin(BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
return request.user.is_authenticated and ( return request.user.is_authenticated and (
request.user.role > User.TEACHER_ROLE or request.user.role >= User.TEACHER_ROLE or
request.user.is_staff or request.user.is_staff or
request.user.is_superuser request.user.is_superuser
) )

@ -184,6 +184,7 @@ class LessonCreateSerializer(DispatchContentMixin, serializers.ModelSerializer):
'created_at', 'created_at',
'update_at', 'update_at',
'deactivated_at', 'deactivated_at',
'position',
) )
read_only_fields = ( read_only_fields = (
@ -196,6 +197,9 @@ class LessonCreateSerializer(DispatchContentMixin, serializers.ModelSerializer):
def create(self, validated_data): def create(self, validated_data):
content = validated_data.pop('content', []) content = validated_data.pop('content', [])
lesson = super().create(validated_data) lesson = super().create(validated_data)
if not validated_data.get('position'):
lesson.set_last_position()
lesson.save()
self.dispatch_content(lesson, content) self.dispatch_content(lesson, content)
return lesson return lesson
@ -226,6 +230,7 @@ class LessonSerializer(serializers.ModelSerializer):
'created_at', 'created_at',
'update_at', 'update_at',
'deactivated_at', 'deactivated_at',
'position',
) )
read_only_fields = ( read_only_fields = (

@ -0,0 +1,18 @@
# Generated by Django 2.0.6 on 2018-07-02 13:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0038_lesson_author'),
]
operations = [
migrations.AddField(
model_name='lesson',
name='position',
field=models.PositiveSmallIntegerField(default=1, verbose_name='Положение на странице'),
),
]

@ -177,10 +177,18 @@ class Lesson(BaseModel, DeactivatedMixin):
) )
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True) update_at = models.DateTimeField(auto_now=True)
position = models.PositiveSmallIntegerField(
'Положение на странице',
default=1,
)
def __str__(self): def __str__(self):
return self.title return self.title
def set_last_position(self):
if self.course:
self.position = self.course.lessons.count()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.author and self.course and self.course.author: if not self.author and self.course and self.course.author:
self.author = self.course.author self.author = self.course.author

@ -4,7 +4,11 @@
<div class="lessons__item"> <div class="lessons__item">
<div class="lessons__row"> <div class="lessons__row">
<div class="lessons__preview"><img class="lessons__pic" style="display: block;border-radius:50%;width:130px;height: 130px" src="{{ content.img.image.url }}"></div> <div class="lessons__preview">
<div class="lessons__pic-wrapper">
<img class="lessons__pic" src="{{ content.img.image.url }}">
</div>
</div>
<div class="lessons__content">{{ content.txt | safe }}</div> <div class="lessons__content">{{ content.txt | safe }}</div>
</div> </div>
</div> </div>

@ -9,7 +9,7 @@
{% block ogtitle %}{{ course.title }} - {{ block.super }}{% endblock ogtitle %} {% block ogtitle %}{{ course.title }} - {{ block.super }}{% endblock ogtitle %}
{% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %} {% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %}
{% if course.cover and course.cover.image %} {% if course.cover and course.cover.image %}
{% block ogimage %}http://{{request.META.HTTP_HOST}}{{ course.cover.image.url }}{% endblock ogimage %} {% block ogimage %}http://{{request.META.HTTP_HOST}}{% if course.cover %}{{ course.cover.image.url }}{% else %}{% static 'img/og_courses.jpg' %}{% endif %}{% endblock ogimage %}
{% endif %} {% endif %}
{% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %} {% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %}
@ -141,37 +141,45 @@
<div class="course__actions"> <div class="course__actions">
<a href="{% url 'course' course.id %}" class="course__action btn btn_lg{% if not only_lessons %} btn_stroke{% else %} btn_gray{% endif %}">Описание курса</a> <a href="{% url 'course' course.id %}" class="course__action btn btn_lg{% if not only_lessons %} btn_stroke{% else %} btn_gray{% endif %}">Описание курса</a>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
{% if course.author == request.user and request.user.role >= request.user.AUTHOR_ROLE %} {% if course.author == request.user and request.user.role >= request.user.AUTHOR_ROLE %}
<a <a
href="{% url 'course-only-lessons' course.id %}" href="{% url 'course-only-lessons' course.id %}"
class="course__action btn btn_lg{% if only_lessons %} btn_stroke{% else %} btn_gray{% endif %}" class="course__action btn btn_lg{% if only_lessons %} btn_stroke{% else %} btn_gray{% endif %}"
{% if not user.is_authenticated %}data-popup=".js-popup-auth"{% endif %} {% if not user.is_authenticated %}data-popup=".js-popup-auth"{% endif %}
>УРОКИ >УРОКИ
</a> </a>
{% elif request.user.role == request.user.ADMIN_ROLE %} {% elif request.user.role == request.user.ADMIN_ROLE %}
<a <a
href="{% url 'course-only-lessons' course.id %}" href="{% url 'course-only-lessons' course.id %}"
class="course__action btn btn_lg{% if only_lessons %} btn_stroke{% else %} btn_gray{% endif %}" class="course__action btn btn_lg{% if only_lessons %} btn_stroke{% else %} btn_gray{% endif %}"
{% if not user.is_authenticated %}data-popup=".js-popup-auth"{% endif %} {% if not user.is_authenticated %}data-popup=".js-popup-auth"{% endif %}
>УРОКИ >УРОКИ
</a> </a>
{% else %}
<a
class="course__action btn btn_lg{% if only_lessons %} btn_stroke{% else %} btn_gray{% endif %}"
{% if paid %}
href="{% url 'course-only-lessons' course.id %}"
{% else %}
data-popup=".js-popup-course-lock"
{% endif %}
>УРОКИ
{% if not paid %}
<svg class="icon icon-lock">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-lock"></use>
</svg>
{% endif %}
</a>
{% endif %}
{% else %} {% else %}
<a <a
class="course__action btn btn_lg{% if only_lessons %} btn_stroke{% else %} btn_gray{% endif %}" class="course__action btn btn_lg{% if only_lessons %} btn_stroke{% else %} btn_gray{% endif %}"
{% if paid %} data-popup=".js-popup-auth">УРОКИ
href="{% url 'course-only-lessons' course.id %}"
{% else %}
data-popup=".js-popup-course-lock"
{% endif %}
>УРОКИ
{% if not paid %}
<svg class="icon icon-lock"> <svg class="icon icon-lock">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-lock"></use> <use xlink:href="{% static 'img/sprite.svg' %}#icon-lock"></use>
</svg> </svg>
{% endif %}
</a> </a>
{% endif %} {% endif %}
{% endif %}
</div> </div>
<a class="course__video video" href="#"> <a class="course__video video" href="#">
{% if course.cover %} {% if course.cover %}

@ -9,7 +9,7 @@
{% block ogtitle %}{{ course.title }} - {{ block.super }}{% endblock ogtitle %} {% block ogtitle %}{{ course.title }} - {{ block.super }}{% endblock ogtitle %}
{% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %} {% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %}
{% if course.cover %} {% if course.cover %}
{% block ogimage %}{{ request.build_absolute_uri }}{{ course.cover.url }}{% endblock ogimage %} {% block ogimage %}http://{{request.META.HTTP_HOST}}{% if course.cover %}{{ course.cover.image.url }}{% else %}{% static 'img/og_courses.jpg' %}{% endif %}{% endblock ogimage %}
{% endif %} {% endif %}
{% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %} {% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %}
@ -173,7 +173,7 @@
<div class="lessons"> <div class="lessons">
<div class="lessons__title title">Содержание курса</div> <div class="lessons__title title">Содержание курса</div>
<div class="lessons__list"> <div class="lessons__list">
{% for lesson in course.lessons.all %} {% for lesson in lessons %}
{% if course.author == request.user or request.user.role >= request.user.TEACHER_ROLE or paid %} {% if course.author == request.user or request.user.role >= request.user.TEACHER_ROLE or paid %}
<a href="{% url 'lesson' pk=lesson.id %}"> <a href="{% url 'lesson' pk=lesson.id %}">
{% else %} {% else %}
@ -184,10 +184,15 @@
<div class="lessons__row"> <div class="lessons__row">
{% if lesson.cover %} {% if lesson.cover %}
<div class="lessons__preview"> <div class="lessons__preview">
<img class="lessons__pic" src="{{ lesson.cover.url }}"> <div class="lessons__pic-wrapper">
<img class="lessons__pic" src="{{ lesson.cover.image.url }}">
</div>
</div> </div>
{% endif %} {% endif %}
<div class="lessons__content">{{ lesson.short_description | safe }}</div> <div class="lessons__content">{{ lesson.short_description | truncatechars_html:800 | safe }}</div>
</div>
<div class="lessons__row">
<a href="{% url 'lesson' pk=lesson.id %}" class="btn btn_stroke">Перейти к уроку</a>
</div> </div>
</div> </div>
</a> </a>

@ -2,6 +2,7 @@
{% load static %} {% load static %}
{% load category_items from lilcity_category %} {% load category_items from lilcity_category %}
{% block ogimage %}http://{{request.META.HTTP_HOST}}{% static 'img/og_courses.jpg' %}{% endblock ogimage %}
{% block content %} {% block content %}
<div class="main" style="background-image: url({% static 'img/bg-1.jpg' %});"> <div class="main" style="background-image: url({% static 'img/bg-1.jpg' %});">
<div class="main__center center"> <div class="main__center center">

@ -2,11 +2,12 @@
{% load static %} {% load static %}
{% block title %}{{ lesson.title }} - {{ block.super }}{% endblock title %} {% block title %}{{ lesson.title }} - {{ block.super }}{% endblock title %}
{% block ogimage %}http://{{request.META.HTTP_HOST}}{% if lesson.course.cover %}{{ lesson.course.cover.image.url }}{% else %}{% static 'img/og_courses.jpg' %}{% endif %}{% endblock ogimage %}
{% block content %} {% block content %}
<div class="section" style="margin-bottom:0;padding-bottom:0"> <div class="section" style="margin-bottom:0;padding-bottom:0">
<div class="section__center center center_sm"> <div class="section__center center center_sm">
<div class="go"> <div class="go">
<a class="go__item" href="{% if next %}{{next}}{% else %}{% url 'course' lesson.course.id %}{% endif %}"> <a class="go__item" href="{% if next %}{{next}}{% else %}{% url 'course-only-lessons' lesson.course.id %}{% endif %}">
<div class="go__arrow"> <div class="go__arrow">
<svg class="icon icon-arrow-left"> <svg class="icon icon-arrow-left">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-left"></use> <use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-left"></use>
@ -14,8 +15,8 @@
</div> </div>
<div class="go__title">Вернуться к&nbsp;списку уроков</div> <div class="go__title">Вернуться к&nbsp;списку уроков</div>
</a> </a>
{% comment %} {% if next_lesson %} {% if next_lesson %}
<a class="go__item" href="{{ next_lesson }}"> <a class="go__item" href="{% url 'lesson' pk=next_lesson %}">
<div class="go__title">Перейти к&nbsp;следующему уроку</div> <div class="go__title">Перейти к&nbsp;следующему уроку</div>
<div class="go__arrow"> <div class="go__arrow">
<svg class="icon icon-arrow-right"> <svg class="icon icon-arrow-right">
@ -23,7 +24,7 @@
</svg> </svg>
</div> </div>
</a> </a>
{% endif %} {% endcomment %} {% endif %}
</div> </div>
<div class="lesson"> <div class="lesson">
<div class="lesson__subtitle subtitle">{{ lesson.title }}</div> <div class="lesson__subtitle subtitle">{{ lesson.title }}</div>

@ -203,6 +203,8 @@ class CourseView(DetailView):
status=Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW, status=Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW,
).exists() ).exists()
context['only_lessons'] = self.only_lessons context['only_lessons'] = self.only_lessons
if self.only_lessons:
context['lessons'] = self.object.lessons.order_by('position')
return context return context
def get_queryset(self): def get_queryset(self):
@ -286,7 +288,9 @@ class LessonView(DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['next'] = self.request.GET.get('next', None) context['next'] = self.request.GET.get('next', None)
context['next_lesson'] = self.request.GET.get('next_lesson', 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 return context

@ -28,10 +28,11 @@ class PaymentChildAdmin(PolymorphicChildModelAdmin):
'user', 'user',
'amount', 'amount',
'status', 'status',
'roistat_visit',
'created_at', 'created_at',
) )
base_fieldsets = ( base_fieldsets = (
(None, {'fields': ('user', 'amount', 'status', 'data',)}), (None, {'fields': ('user', 'amount', 'status', 'data', 'roistat_visit',)}),
) )
readonly_fields = ('amount', 'data',) readonly_fields = ('amount', 'data',)

@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2018-07-06 07:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payment', '0018_auto_20180512_1202'),
]
operations = [
migrations.AddField(
model_name='payment',
name='roistat_visit',
field=models.PositiveIntegerField(editable=False, null=True, verbose_name='Номер визита Roistat'),
),
]

@ -97,6 +97,7 @@ class Payment(PolymorphicModel):
amount = models.DecimalField('Итого', max_digits=8, decimal_places=2, default=0, editable=False) amount = models.DecimalField('Итого', max_digits=8, decimal_places=2, default=0, editable=False)
status = models.PositiveSmallIntegerField('Статус платежа', choices=PW_STATUS_CHOICES, null=True) status = models.PositiveSmallIntegerField('Статус платежа', choices=PW_STATUS_CHOICES, null=True)
data = JSONField('Данные платежа от провайдера', default={}, editable=False) data = JSONField('Данные платежа от провайдера', default={}, editable=False)
roistat_visit = models.PositiveIntegerField('Номер визита Roistat', null=True, editable=False)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True) update_at = models.DateTimeField(auto_now=True)

@ -38,12 +38,13 @@ def product_payment_to_mixpanel(user_id, event_name, time, properties):
@app.task @app.task
def transaction_to_roistat(user_id, payment_id, event_name, amount, time, status, product_type): def transaction_to_roistat(user_id, payment_id, event_name, amount, time, status, product_type, roistat_visit):
body = [{ body = [{
'id': str(payment_id), 'id': str(payment_id),
'name': event_name, 'name': event_name,
'date_create': time, 'date_create': time,
'status': str(status), 'status': str(status),
'roistat': str(roistat_visit) if roistat_visit else None,
'price': str(amount), 'price': str(amount),
'client_id': str(user_id), 'client_id': str(user_id),
'fields': { 'fields': {

@ -5,7 +5,7 @@
{% if school %} {% if school %}
<div class="done__title title">Вы успешно приобрели доступ к урокам онлайн-школы!</div> <div class="done__title title">Вы успешно приобрели доступ к урокам онлайн-школы!</div>
<div class="done__foot"> <div class="done__foot">
<a class="done__btn btn btn_md btn_stroke" href="{% url 'school:summer-school' %}">ПЕРЕЙТИ К ШКОЛЕ</a> <a class="done__btn btn btn_md btn_stroke" href="{% url 'user' request.user.id %}">ПЕРЕЙТИ К ШКОЛЕ</a>
</div> </div>
{% else %} {% else %}
<div class="done__title title">Вы успешно приобрели курс!</div> <div class="done__title title">Вы успешно приобрели курс!</div>

@ -58,12 +58,14 @@ class CourseBuyView(TemplateView):
host = urlsplit(self.request.META.get('HTTP_REFERER')) host = urlsplit(self.request.META.get('HTTP_REFERER'))
host = str(host[0]) + '://' + str(host[1]) host = str(host[0]) + '://' + str(host[1])
course = Course.objects.get(id=pk) course = Course.objects.get(id=pk)
roistat_visit = request.COOKIES.get('roistat_visit', None)
if request.user == course.author: if request.user == course.author:
messages.error(request, 'Вы не можете приобрести свой курс.') messages.error(request, 'Вы не можете приобрести свой курс.')
return redirect(reverse_lazy('course', args=[course.id])) return redirect(reverse_lazy('course', args=[course.id]))
course_payment = CoursePayment.objects.create( course_payment = CoursePayment.objects.create(
user=request.user, user=request.user,
course=course, course=course,
roistat_visit=roistat_visit,
) )
product = Product( product = Product(
f'course_{course_payment.id}', f'course_{course_payment.id}',
@ -96,6 +98,7 @@ class SchoolBuyView(TemplateView):
host = str(host[0]) + '://' + str(host[1]) host = str(host[0]) + '://' + str(host[1])
weekdays = set(request.GET.getlist('weekdays', [])) weekdays = set(request.GET.getlist('weekdays', []))
add_days = 'add_days' in request.GET add_days = 'add_days' in request.GET
roistat_visit = request.COOKIES.get('roistat_visit', None)
if not weekdays: if not weekdays:
messages.error(request, 'Выберите несколько дней недели.') messages.error(request, 'Выберите несколько дней недели.')
return redirect('school:summer-school') return redirect('school:summer-school')
@ -117,6 +120,7 @@ class SchoolBuyView(TemplateView):
date_start=now().date(), date_start=now().date(),
date_end=_school_payment.date_end, date_end=_school_payment.date_end,
add_days=True, add_days=True,
roistat_visit=roistat_visit,
) )
if school_payment.amount <= 0: if school_payment.amount <= 0:
messages.error(request, 'Выбранные дни отсутствуют в оставшемся периоде подписки') messages.error(request, 'Выбранные дни отсутствуют в оставшемся периоде подписки')
@ -125,6 +129,7 @@ class SchoolBuyView(TemplateView):
school_payment = SchoolPayment.objects.create( school_payment = SchoolPayment.objects.create(
user=request.user, user=request.user,
weekdays=weekdays, weekdays=weekdays,
roistat_visit=roistat_visit,
) )
product = Product( product = Product(
f'school_{school_payment.id}', f'school_{school_payment.id}',
@ -259,6 +264,7 @@ class PaymentwallCallbackView(View):
now().strftime('%Y-%m-%d %H:%M:%S'), now().strftime('%Y-%m-%d %H:%M:%S'),
pingback.get_type(), pingback.get_type(),
product_type_name, product_type_name,
payment.roistat_visit,
) )
author_balance = getattr(payment, 'author_balance', None) author_balance = getattr(payment, 'author_balance', None)
if author_balance and author_balance.type == AuthorBalance.IN: if author_balance and author_balance.type == AuthorBalance.IN:

@ -0,0 +1,18 @@
# Generated by Django 2.0.5 on 2018-06-28 20:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('school', '0016_auto_20180429_0818'),
]
operations = [
migrations.AlterField(
model_name='livelesson',
name='short_description',
field=models.TextField(blank=True, default='', verbose_name='Краткое описание урока'),
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.6 on 2018-06-29 15:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('school', '0017_auto_20180628_2000'),
]
operations = [
migrations.AlterField(
model_name='livelesson',
name='title',
field=models.CharField(blank=True, default='', max_length=100, verbose_name='Название урока'),
),
]

@ -114,8 +114,8 @@ class SchoolScheduleImage(models.Model):
class LiveLesson(BaseModel, DeactivatedMixin): class LiveLesson(BaseModel, DeactivatedMixin):
title = models.CharField('Название урока', max_length=100) title = models.CharField('Название урока', max_length=100, default='', blank=True)
short_description = models.TextField('Краткое описание урока') short_description = models.TextField('Краткое описание урока', default='', blank=True)
stream = models.URLField('Ссылка на VIMEO', default='', blank=True) stream = models.URLField('Ссылка на VIMEO', default='', blank=True)
date = models.DateField(default=now, unique=True) date = models.DateField(default=now, unique=True)
cover = models.ForeignKey( cover = models.ForeignKey(
@ -157,5 +157,5 @@ class LiveLesson(BaseModel, DeactivatedMixin):
return False return False
else: else:
start_at = school_schedule.start_at start_at = school_schedule.start_at
end_at = datetime.combine(now().today(), start_at) + timedelta(hours=2) end_at = datetime.combine(now().today(), start_at) + timedelta(hours=1)
return start_at <= now().time() and end_at.time() >= now().time() return start_at <= now().time() and end_at.time() >= now().time()

@ -6,17 +6,17 @@
{{ school_schedule }} {{ school_schedule }}
</div> </div>
{% if live_lesson %} {% if live_lesson %}
<!--<div class="timing__date">{{ live_lesson.date }}</div>--> <div class="timing__date">{{ live_lesson.date }}</div>
{% endif %} {% endif %}
</div> </div>
<div class="timing__buy"> <div class="timing__buy">
<div class="timing__time">{{ school_schedule.start_at }} (МСК)</div> <div class="timing__time">{{ school_schedule.start_at }} (МСК)</div>
{% if school_schedule.weekday in school_schedules_purchased %} {% if school_schedule.weekday in school_schedules_purchased %}
{% if live_lesson and school_schedule.is_online or live_lesson and is_previous and live_lesson in live_lessons %} {% if live_lesson and live_lesson.title %}
{% include './open_lesson.html' %} {% include './open_lesson.html' %}
{% endif %} {% endif %}
{% else %} {% else %}
{% include './day_pay_btn.html' %} {% include './day_pay_btn.html' %}
{% endif %} {% endif %}
</div> </div>
{% comment %} {% comment %}
@ -36,12 +36,12 @@
</div> </div>
</div> </div>
<div class="timing__cell"> <div class="timing__cell">
<div class="timing__title">{{ school_schedule.title }}{% if live_lesson %}, <div class="timing__title">{{ school_schedule.title }}{% if live_lesson and live_lesson.title %},
<span class="bold">{{ live_lesson.title }}</span> <span class="bold">{{ live_lesson.title }}</span>
{% endif %} {% endif %}
</div> </div>
<div class="timing__content"> <div class="timing__content">
{% if live_lesson %} {% if live_lesson and live_lesson.short_description %}
{{ live_lesson.short_description }} {{ live_lesson.short_description }}
{% else %} {% else %}
{{ school_schedule.description }} {{ school_schedule.description }}

@ -3,7 +3,7 @@
<div class="online__center center"> <div class="online__center center">
<div class="online__type">ПРЯМОЙ ЭФИР</div> <div class="online__type">ПРЯМОЙ ЭФИР</div>
<div class="online__title">Каждый день в 17.00 (по Мск) </div> <div class="online__title">Каждый день в 17.00 (по Мск) </div>
<div class="online__text text">Кроме выходных. Запись эфира доступна в&nbsp;течение 24-х&nbsp;часов.</div> <div class="online__text text">Кроме выходных. Запись эфира доступна по завершению трансляции</div>
<div class="online__action"> <div class="online__action">
<svg class="icon icon-play"> <svg class="icon icon-play">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-play"></use> <use xlink:href="{% static 'img/sprite.svg' %}#icon-play"></use>

@ -9,7 +9,6 @@
<div class="casing__msg">Подписка истекает <div class="casing__msg">Подписка истекает
<span class="bold">{{ subscription_ends }}</span> <span class="bold">{{ subscription_ends }}</span>
</div> </div>
{% include './prolong_btn.html' %}
{% else %} {% else %}
<div class="casing__msg">Подписка <div class="casing__msg">Подписка
<span class="bold">истекла</span> <span class="bold">истекла</span>

@ -10,7 +10,7 @@
<div class="lesson__content">{{ livelesson.short_description }}</div> <div class="lesson__content">{{ livelesson.short_description }}</div>
<a class="lesson__video video" href="#"> <a class="lesson__video video" href="#">
{% if livelesson.stream_index %} {% if livelesson.stream_index %}
<iframe class="lesson__video_frame" src="https://player.vimeo.com/video/{{ livelesson.stream_index }}" frameborder="0" webkitallowfullscreen <iframe class="lesson__video_frame" src="https://player.vimeo.com/video/{{ livelesson.stream_index }}?autoplay=1" frameborder="0" webkitallowfullscreen
mozallowfullscreen allowfullscreen> mozallowfullscreen allowfullscreen>
</iframe> </iframe>
<a href="#" onclick="location.reload();">Если видео не загрузилось обновите страницу</a> <a href="#" onclick="location.reload();">Если видео не загрузилось обновите страницу</a>

@ -1,5 +1,6 @@
{% extends "templates/lilcity/index.html" %} {% load static %} {% extends "templates/lilcity/index.html" %} {% load static %}
{% block title %}Онлайн-школа LilCity{% endblock title%} {% block title %}Онлайн-школа LilCity{% endblock title%}
{% block ogimage %}http://{{request.META.HTTP_HOST}}{% static 'img/og_summer_school.jpg' %}{% endblock %}
{% block content %} {% block content %}
{% if not is_purchased %} {% if not is_purchased %}
{% include "../summer/promo.html" %} {% include "../summer/promo.html" %}

@ -6,18 +6,18 @@
{{ school_schedule }} {{ school_schedule }}
</div> </div>
{% if live_lesson %} {% if live_lesson %}
<!--<div class="timing__date">{{ live_lesson.date }}</div>--> <div class="timing__date">{{ live_lesson.date }}</div>
{% endif %}
</div>
<div class="timing__buy">
<div class="timing__time">{{ school_schedule.start_at }} (МСК)</div>
{% if school_schedule.weekday in school_schedules_purchased %}
{% if live_lesson and school_schedule.is_online or live_lesson and is_previous and live_lesson in live_lessons %}
{% include './open_lesson.html' %}
{% endif %}
{% else %}
{% include './day_pay_btn.html' %}
{% endif %} {% endif %}
<div class="timing__buy">
<div class="timing__time">{{ school_schedule.start_at }} (МСК)</div>
{% if school_schedule.weekday in school_schedules_purchased %}
{% if live_lesson and live_lesson.title %}
{% include './open_lesson.html' %}
{% endif %}
{% else %}
{% include './day_pay_btn.html' %}
{% endif %}
</div>
</div> </div>
{% comment %} {% comment %}
<!-- это нужно чтобы в попапе продления школы всегда знать какие дни выбраны(куплены) --> <!-- это нужно чтобы в попапе продления школы всегда знать какие дни выбраны(куплены) -->
@ -36,12 +36,12 @@
</div> </div>
</div> </div>
<div class="timing__cell"> <div class="timing__cell">
<div class="timing__title">{{ school_schedule.title }}{% if live_lesson %}, <div class="timing__title">{{ school_schedule.title }}{% if live_lesson and live_lesson.title %},
<span class="bold">{{ live_lesson.title }}</span> <span class="bold">{{ live_lesson.title }}</span>
{% endif %} {% endif %}
</div> </div>
<div class="timing__content"> <div class="timing__content">
{% if live_lesson %} {% if live_lesson and live_lesson.short_description %}
{{ live_lesson.short_description }} {{ live_lesson.short_description }}
{% else %} {% else %}
{{ school_schedule.description }} {{ school_schedule.description }}

@ -9,7 +9,6 @@
<div class="casing__msg">Подписка истекает <div class="casing__msg">Подписка истекает
<span class="bold">{{ subscription_ends }}</span> <span class="bold">{{ subscription_ends }}</span>
</div> </div>
{% include './prolong_btn.html' %}
{% else %} {% else %}
<div class="casing__msg">Подписка <div class="casing__msg">Подписка
<span class="bold">истекла</span> <span class="bold">истекла</span>

@ -67,18 +67,18 @@
<div class="section__center center"> <div class="section__center center">
<div class="tabs js-tabs"> <div class="tabs js-tabs">
<div class="tabs__nav"> <div class="tabs__nav">
<button class="tabs__btn js-tabs-btn">ЛАГЕРЬ</button> <button class="tabs__btn js-tabs-btn active">ЛАГЕРЬ</button>
<button class="tabs__btn js-tabs-btn">ПРИОБРЕТЕННЫЕ <button class="tabs__btn js-tabs-btn">ПРИОБРЕТЕННЫЕ
<span class="mobile-hide">КУРСЫ</span> <span class="mobile-hide">КУРСЫ</span>
</button> </button>
{% if not simple_user %} {% if not simple_user %}
<button class="tabs__btn js-tabs-btn active">ОПУБЛИКОВАННЫЕ <button class="tabs__btn js-tabs-btn">ОПУБЛИКОВАННЫЕ
<span class="mobile-hide">КУРСЫ</span> <span class="mobile-hide">КУРСЫ</span>
</button> </button>
{% endif %} {% endif %}
</div> </div>
<div class="tabs__container"> <div class="tabs__container">
<div class="tabs__item js-tabs-item"> <div class="tabs__item js-tabs-item" style="display: block;">
{% if is_purchased_future %} {% if is_purchased_future %}
<div class="center center_xs"> <div class="center center_xs">
<div class="done"> <div class="done">
@ -127,7 +127,7 @@
</div> </div>
</div> </div>
{% if not simple_user %} {% if not simple_user %}
<div class="tabs__item js-tabs-item" style="display: block;"> <div class="tabs__item js-tabs-item">
<div class="courses courses_scroll"> <div class="courses courses_scroll">
<div class="courses__list"> <div class="courses__list">
{% if published.exists %} {% if published.exists %}

@ -0,0 +1,22 @@
DEBUG=True
ALLOWED_HOSTS=*
PORT=8000
CORS_ORIGIN_WHITELIST=lilcity.9ev.ru:8080
LANG=ru_RU.UTF-8
POSTGRES_DB=lilcity
POSTGRES_USER=lilcity
POSTGRES_PASSWORD=GPVs/E/{5&qe
DJANGO_SETTINGS_MODULE=project.settings
DATABASE_SERVICE_HOST=db
SECRET_KEY=jelm*91lj(_-o20+6^a+bgv!4s6e_efry^#+f#=1ak&s1xr-2j
MAILGUN_API_KEY=key-ec6af2d43d031d59bff6b1c8fb9390cb
MAILGUN_SENDER_DOMAIN=mail.9ev.ru
DEFAULT_FROM_EMAIL=postmaster@mail.9ev.ru
TWILIO_ACCOUNT=ACdf4a96b776cc764bc3ec0f0e136ba550
TWILIO_TOKEN=559a6b1fce121759c9af2dcbb3f755ea
TWILIO_FROM_PHONE=+37128914409
PAYMENTWALL_APP_KEY=d6f02b90cf6b16220932f4037578aff7
PAYMENTWALL_SECRET_KEY=4ea515bf94e34cf28646c2e12a7b8707
MIXPANEL_TOKEN=79bd6bfd98667ed977737e6810b8abcd
RAVEN_DSN=https://b545dac0ae0545a1bcfc443326fe5850:6f9c900cef7f4c11b63561030b37d15c@sentry.io/1197254
ROISTAT_COUNTER_ID=09db30c750035ae3d70a41d5f10d59ec

@ -18,4 +18,5 @@ ADD . /app/
COPY --from=front /web/build/ /app/web/build/ COPY --from=front /web/build/ /app/web/build/
RUN python manage.py collectstatic --no-input RUN python manage.py collectstatic --no-input
RUN rm -rf /etc/nginx/ && cp -r docker/conf/nginx /etc/ && cp -r docker/conf/supervisor/* /etc/supervisor/conf.d/ && chown -R www-data:www-data /app/ RUN rm -rf /etc/nginx/ && cp -r docker/conf/nginx /etc/ && cp -r docker/conf/supervisor/* /etc/supervisor/conf.d/ && chown -R www-data:www-data /app/
EXPOSE 80
ENTRYPOINT ["/app/docker/entrypoint_app.sh"] ENTRYPOINT ["/app/docker/entrypoint_app.sh"]

@ -19,7 +19,17 @@ server {
location /media/instagram/results/ { location /media/instagram/results/ {
expires 1d; expires 1d;
try_files $uri @prod;
} }
try_files $uri @prod;
}
location @prod {
if ($host = "lil.school") {
return 404;
}
proxy_pass https://lil.school;
proxy_buffering off;
} }
location / { location / {

@ -26,7 +26,6 @@ http {
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
include /etc/nginx/conf.d/*.conf; include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
set_real_ip_from 192.168.0.0/24; set_real_ip_from 192.168.0.0/24;
} }

@ -1 +0,0 @@
/etc/nginx/sites-available/default

@ -10,6 +10,11 @@ services:
- .env - .env
volumes: volumes:
- ./data/postgres:/var/lib/postgresql/data - ./data/postgres:/var/lib/postgresql/data
logging: &logging
driver: "json-file"
options:
max-size: "1m"
max-file: "1"
redis: redis:
image: redis:4.0.9-alpine image: redis:4.0.9-alpine
@ -18,6 +23,7 @@ services:
- "127.0.0.1:6379:6379" - "127.0.0.1:6379:6379"
volumes: volumes:
- ./data/redis:/data - ./data/redis:/data
logging: *logging
app: app:
build: build:
@ -36,3 +42,4 @@ services:
links: links:
- db - db
- redis - redis
logging: *logging

@ -0,0 +1,58 @@
version: '3'
services:
db:
image: postgres:10.3-alpine
restart: always
env_file:
- .env
volumes:
- /work/data/back_${CI_COMMIT_REF_NAME}/postgres:/var/lib/postgresql/data
logging: &logging
driver: "json-file"
options:
max-size: "1m"
max-file: "1"
networks:
- internal
- review
labels:
- traefik.enable=false
redis:
image: redis:4.0.9-alpine
restart: always
volumes:
- /work/data/back_${CI_COMMIT_REF_NAME}/redis:/data
logging: *logging
networks:
- internal
- review
labels:
- traefik.enable=false
app:
build:
context: ../
dockerfile: docker/Dockerfile
restart: always
env_file:
- .env
volumes:
- /work/data/back_${CI_COMMIT_REF_NAME}/media:/app/media
depends_on:
- db
- redis
logging: *logging
networks:
- internal
- review
labels:
- traefik.frontend.rule=Host:${REVIEW_HOST}
- traefik.docker.network=review
networks:
internal:
review:
external:
name: review

@ -1,5 +1,6 @@
#!/bin/sh #!/bin/sh
cd /app cd /app
chown www-data:www-data /app/media
python manage.py migrate python manage.py migrate
#python manage.py loaddata /app/apps/*/fixtures/*.json #python manage.py loaddata /app/apps/*/fixtures/*.json
python2.7 /usr/bin/supervisord -n python2.7 /usr/bin/supervisord -n

@ -55,6 +55,7 @@ INSTALLED_APPS = [
'corsheaders', 'corsheaders',
'sorl.thumbnail', 'sorl.thumbnail',
'raven.contrib.django.raven_compat', 'raven.contrib.django.raven_compat',
'django_user_agents',
] + [ ] + [
'apps.auth.apps', 'apps.auth.apps',
'apps.user', 'apps.user',
@ -68,6 +69,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [ MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', 'corsheaders.middleware.CorsMiddleware',
'django_user_agents.middleware.UserAgentMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',

@ -44,21 +44,19 @@
</div> </div>
</div> </div>
<div class="letsgo"> <div class="letsgo">
<a {% if not is_purchased and not is_purchased_future %}
{% if not is_purchased_future %} <a
{% if not user.is_authenticated %} {% if not user.is_authenticated %}
data-popup=".js-popup-auth" data-popup=".js-popup-auth"
{% else %} {% else %}
data-popup=".js-popup-buy" data-popup=".js-popup-buy"
{% endif %} {% endif %}
{% endif %}
class="main__btn btn" class="main__btn btn"
href="#" href="#"
> >
{% if not is_purchased and not is_purchased_future %}купить доступ от {{ min_school_price }} руб./месяц{% endif %} купить доступ от {{ min_school_price }} руб./месяц
{% if is_purchased_future and not is_purchased %}ваша подписка начинается {{school_purchased_future.date_start}}{% endif %} </a>
{% if is_purchased %}ваша подписка истекает {{ subscription_ends_humanize }}<br/>перейти к оплате{% endif %} {% endif %}
</a>
</div> </div>
</div> </div>
</div> </div>

@ -13,7 +13,7 @@
</div> </div>
{% if user.is_authenticated and online %} {% if user.is_authenticated and online %}
<div class="main__content"> <div class="main__content">
Сейчас идёт прямой эфир урока «{{ school_schedule.title }}» Сейчас идёт прямой эфир урока «{{ school_schedule.title }}, {{ school_schedule.current_live_lesson.title }}»
</div> </div>
<div class="main__actions"> <div class="main__actions">
<a <a
@ -27,8 +27,8 @@
>{% if not is_purchased %}Получить доступ{% else %}Смотреть урок{% endif %}</a> >{% if not is_purchased %}Получить доступ{% else %}Смотреть урок{% endif %}</a>
</div> </div>
{% elif user.is_authenticated and online_coming_soon and school_schedule and school_schedule.start_at_humanize %} {% elif user.is_authenticated and online_coming_soon and school_schedule and school_schedule.start_at_humanize %}
<div class="main__content"> <div class="">
Урок «{{ school_schedule.title }}» начнётся Урок «{{ school_schedule.title }}, {{ school_schedule.current_live_lesson.title }}» начнётся
</div> </div>
<div class="main__time"> <div class="main__time">
{{ school_schedule.start_at_humanize }} {{ school_schedule.start_at_humanize }}
@ -49,21 +49,21 @@
Присоединяйтесь в Рисовальный лагерь Присоединяйтесь в Рисовальный лагерь
</div> </div>
<div class="main__actions"> <div class="main__actions">
<a {% if not is_purchased and not is_purchased_future %}
{% if not is_purchased_future %} <a
{% if not user.is_authenticated %} {% if not is_purchased_future %}
data-popup=".js-popup-auth" {% if not user.is_authenticated %}
{% else %} data-popup=".js-popup-auth"
data-popup=".js-popup-buy" {% else %}
data-popup=".js-popup-buy"
{% endif %}
{% endif %} {% endif %}
{% endif %} class="main__btn btn"
class="main__btn btn" href="#"
href="#"
> >
{% if not is_purchased and not is_purchased_future %}Получить доступ{% endif %} Получить доступ
{% if is_purchased_future and not is_purchased %}ваша подписка начинается {{school_purchased_future.date_start}}{% endif %} </a>
{% if is_purchased %}ваша подписка истекает {{ subscription_ends_humanize }}<br/>перейти к оплате{% endif %} {% endif %}
</a>
<a class="main__btn btn btn_white" href="{% url 'school:summer-school' %}">О лагере</a> <a class="main__btn btn btn_white" href="{% url 'school:summer-school' %}">О лагере</a>
</div> </div>
{% endif %} {% endif %}

@ -1,7 +1,7 @@
{% load static %} {% load thumbnail %} {% load static %} {% load thumbnail %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="header__login"> <div class="header__login">
<a class="header__ava ava" href="{% url 'user' request.user.id %}"> <a class="header__ava ava" href="{% if request.user_agent.is_touch_capable %}#{% else %}{% url 'user' request.user.id %}{% endif %}">
{% thumbnail request.user.photo "48x48" crop="center" as im %} {% thumbnail request.user.photo "48x48" crop="center" as im %}
<img class="ava__pic" src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}" /> <img class="ava__pic" src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}" />
{% empty %} {% empty %}

@ -76,7 +76,44 @@
&noscript=1"/> &noscript=1"/>
</noscript> </noscript>
<!-- End Facebook Pixel Code --> <!-- End Facebook Pixel Code -->
<!-- Global site tag (gtag.js) - Google AdWords: 808701460 --> <script async src="https://www.googletagmanager.com/gtag/js?id=AW-808701460"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'AW-808701460'); </script> <!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-121923960-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-121923960-1');
</script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
(function (d, w, c) {
(w[c] = w[c] || []).push(function() {
try {
w.yaCounter49354039 = new Ya.Metrika2({
id:49354039,
clickmap:true,
trackLinks:true,
accurateTrackBounce:true,
webvisor:true
});
} catch(e) { }
});
var n = d.getElementsByTagName("script")[0],
s = d.createElement("script"),
f = function () { n.parentNode.insertBefore(s, n); };
s.type = "text/javascript";
s.async = true;
s.src = "https://mc.yandex.ru/metrika/tag.js";
if (w.opera == "[object Opera]") {
d.addEventListener("DOMContentLoaded", f, false);
} else { f(); }
})(document, window, "yandex_metrika_callbacks2");
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/49354039" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
{% include "templates/blocks/mixpanel.html" %} {% include "templates/blocks/mixpanel.html" %}
</head> </head>
<body> <body>

@ -1,28 +1,31 @@
# Python-3.6 # Python-3.6
arrow==0.12.1 arrow==0.12.1
celery[redis]==4.2.0 celery[redis]==4.2.0
Django==2.0.6 Django==2.0.7
django-active-link==0.1.5 django-active-link==0.1.5
django-anymail[mailgun]==3.0 django-anymail[mailgun]==3.0
django-cors-headers==2.3.0 django-cors-headers==2.3.0
django_compressor==2.2 django_compressor==2.2
django-filter==2.0.0.dev1 django-filter==2.0.0.dev1
django-mptt==0.9.0 django-mptt==0.9.0
django-silk==3.0.0
django-phonenumber-field==2.0.0 django-phonenumber-field==2.0.0
django-polymorphic-tree==1.5 django-polymorphic-tree==1.5
djangorestframework==3.8.2 djangorestframework==3.8.2
drf-yasg[validation]==1.9.0 drf-yasg==1.9.0
facepy==1.0.9 facepy==1.0.9
gunicorn==19.8.1 gunicorn==19.9.0
mixpanel==4.3.2 mixpanel==4.3.2
psycopg2-binary==2.7.5 psycopg2-binary==2.7.5
Pillow==5.1.0 Pillow==5.2.0
raven==6.9.0 raven==6.9.0
requests==2.19.1 requests==2.19.1
sorl-thumbnail==12.4.1 sorl-thumbnail==12.4.1
twilio==6.14.6 twilio==6.14.7
# paymentwall-python==1.0.7 # paymentwall-python==1.0.7
git+https://github.com/ivlevdenis/paymentwall-python.git git+https://github.com/ivlevdenis/paymentwall-python.git
# python-instagram==1.3.2 # python-instagram==1.3.2
git+https://github.com/ivlevdenis/python-instagram.git git+https://github.com/ivlevdenis/python-instagram.git
django-user-agents==0.3.2
user-agents==1.1.0
ua-parser==0.8.0

@ -84,13 +84,13 @@
</div> </div>
</div> </div>
<div v-if="live" class="info__field field" <!-- <div v-if="live" class="info__field field"
v-bind:class="{ error: ($v.course.date.$dirty || showErrors) && $v.course.date.$invalid }"> v-bind:class="{ error: ($v.course.date.$dirty || showErrors) && $v.course.date.$invalid }">
<div class="field__label">ДАТА</div> <div class="field__label">ДАТА</div>
<div class="field__wrap"> <div class="field__wrap">
<lil-select :value.sync="course.date" :options="scheduleOptions" placeholder="Выберите дату"/> <lil-select :value.sync="course.date" :options="scheduleOptions" placeholder="Выберите дату"/>
</div> </div>
</div> </div> -->
<div v-if="!live" class="info__field field"> <div v-if="!live" class="info__field field">
<div class="field__label field__label_gray">ДОСТУП</div> <div class="field__label field__label_gray">ДОСТУП</div>
@ -215,24 +215,31 @@
<div v-if="viewSection === 'lessons'" class="kit__body"> <div v-if="viewSection === 'lessons'" class="kit__body">
<div class="lessons__title title">Содержание курса</div> <div class="lessons__title title">Содержание курса</div>
<div v-if="!lessonsLoading" class="lessons__list"> <div v-if="!lessonsLoading" class="lessons__list">
<div class="lessons__item" v-for="(lesson, index) in lessons"> <vue-draggable v-model="lessons" @start="drag=true" @end="onLessonsChanged" :options="{ handle: '.sortable__handle' }">
<div class="lessons__actions"> <div class="lessons__item" v-for="(lesson, index) in lessons" :key="lesson.id">
<button type="button" class="lessons__action" @click="removeLesson(index)"> <div class="lessons__actions">
<svg class="icon icon-delete"> <button class="sortable__handle" type="button">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use> <svg class="icon icon-hamburger">
</svg> <use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</button> </svg>
<button type="button" class="lessons__action" @click="editLesson(index)"> </button>
<svg class="icon icon-edit"> <button type="button" class="lessons__action" @click="removeLesson(index)">
<use xlink:href="/static/img/sprite.svg#icon-edit"></use> <svg class="icon icon-delete">
</svg> <use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</button> </svg>
</div> </button>
<div class="lessons__subtitle subtitle">{{ lesson.title }}</div> <button type="button" class="lessons__action" @click="editLesson(index)">
<div class="lessons__row"> <svg class="icon icon-edit">
<div class="lessons__content">{{ lesson.short_description }}</div> <use xlink:href="/static/img/sprite.svg#icon-edit"></use>
</div> </svg>
</div> </button>
</div>
<div class="lessons__subtitle subtitle">{{ lesson.title }}</div>
<div class="lessons__row">
<div class="lessons__content">{{ lesson.short_description }}</div>
</div>
</div>
</vue-draggable>
</div> </div>
<div v-if="lessonsLoading">Загрузка...</div> <div v-if="lessonsLoading">Загрузка...</div>
<div class="lessons__foot"> <div class="lessons__foot">
@ -307,7 +314,7 @@
price: null, price: null,
url: '', url: '',
coverImage: '', coverImage: '',
coverImageId: null, kit__body: null,
is_paid: false, is_paid: false,
is_featured: true, is_featured: true,
is_deferred: false, is_deferred: false,
@ -507,6 +514,7 @@
api.removeCourseLesson(lesson.id, this.accessToken); api.removeCourseLesson(lesson.id, this.accessToken);
} }
this.lessons.splice(lessonIndex, 1); this.lessons.splice(lessonIndex, 1);
this.onLessonsChanged();
}, },
editLesson(lessonIndex) { editLesson(lessonIndex) {
this.currentLesson = this.lessons[lessonIndex]; this.currentLesson = this.lessons[lessonIndex];
@ -581,11 +589,7 @@
}); });
} }
document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНО'; this.changeSavingStatus(true);
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
showNotification("success", 'Урок сохранён'); showNotification("success", 'Урок сохранён');
// this.goToLessons(); // this.goToLessons();
@ -594,10 +598,7 @@
.catch((err) => { .catch((err) => {
this.lessonSaving = false; this.lessonSaving = false;
//console.error(err); //console.error(err);
document.getElementById('course-redactor__saving-status').innerText = 'ОШИБКА'; this.changeSavingStatus(true, true);
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
// alert('Произошло что-то страшное: '+err.toString()); // alert('Произошло что-то страшное: '+err.toString());
console.log(err); console.log(err);
if(err.response) { if(err.response) {
@ -619,6 +620,14 @@
$(window).scrollTop(elementTop); $(window).scrollTop(elementTop);
}); });
}, },
processCourseJson(data) {
this.course = api.convertCourseJson(data);
this.course.live = this.live;
this.lessons = data.lessons.map((lessonJson) => {
return api.convertLessonJson(lessonJson);
});
this.course.duration = this.course.duration || '';
},
loadCourseDraft() { loadCourseDraft() {
//console.log('loadCourseDraft'); //console.log('loadCourseDraft');
if(this.live) { return; } if(this.live) { return; }
@ -628,12 +637,8 @@
response response
.then((response) => { .then((response) => {
this.course = api.convertCourseJson(response.data); this.processCourseJson(response.data);
this.course.live = this.live;
this.courseLoading = false; this.courseLoading = false;
this.lessons = response.data.lessons.map((lessonJson) => {
return api.convertLessonJson(lessonJson);
});
}) })
.catch((err) => { .catch((err) => {
this.courseLoading = false; this.courseLoading = false;
@ -653,20 +658,18 @@
} }
request request
.then((response) => { .then((response) => {
this.course = api.convertCourseJson(response.data); this.processCourseJson(response.data);
this.course.live = this.live;
if (this.live && this.course.date) {
this.course.date = _.find(this.scheduleOptions, (item) => {
return item.value == this.course.date;
})
}
this.$nextTick(() => { this.$nextTick(() => {
this.courseLoading = false; this.courseLoading = false;
}); });
this.lessons = response.data.lessons.map((lessonJson) => { this.lessons.sort((a, b) => {
return api.convertLessonJson(lessonJson); if (a.position > b.position) {
return 1;
}
if (a.position < b.position) {
return -1;
}
return 0;
}); });
}) })
.catch((err) => { .catch((err) => {
@ -794,17 +797,13 @@
} }
this.courseSaving = true; this.courseSaving = true;
clearTimeout(this.savingTimeout); this.changeSavingStatus();
document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНИЕ...';
const courseObject = this.course; const courseObject = this.course;
courseObject.url = (courseObject.url) ? slugify(courseObject.url):courseObject.url; courseObject.url = (courseObject.url) ? slugify(courseObject.url):courseObject.url;
api.saveCourse(courseObject, this.accessToken) api.saveCourse(courseObject, this.accessToken)
.then((response) => { .then((response) => {
this.courseSaving = false; this.courseSaving = false;
document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНО'; this.changeSavingStatus(true);
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
this.courseSyncHook = true; this.courseSyncHook = true;
const courseData = api.convertCourseJson(response.data); const courseData = api.convertCourseJson(response.data);
if (this.course.coverImage) { if (this.course.coverImage) {
@ -841,11 +840,11 @@
this.course.id = courseData.id; this.course.id = courseData.id;
} }
if(this.live && courseData.date) { /*if(this.live && courseData.date) {
this.course.date = _.find(this.scheduleOptions, function(item){ this.course.date = _.find(this.scheduleOptions, function(item){
return item.value == courseData.date; return item.value == courseData.date;
}); });
} }*/
this.$nextTick(() => { this.$nextTick(() => {
this.courseSyncHook = false; this.courseSyncHook = false;
}); });
@ -854,10 +853,7 @@
this.courseSyncHook = false; this.courseSyncHook = false;
this.courseSaving = false; this.courseSaving = false;
//console.error(err); //console.error(err);
document.getElementById('course-redactor__saving-status').innerText = 'ОШИБКА'; this.changeSavingStatus(true, true);
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
// alert('Произошло что-то страшное: '+err.toString()); // alert('Произошло что-то страшное: '+err.toString());
//console.log(err.response.data); //console.log(err.response.data);
if(err.response) { if(err.response) {
@ -886,10 +882,42 @@
this.viewSection = 'lessons-edit'; this.viewSection = 'lessons-edit';
} }
}, },
onLessonsChanged() {
let promises = [];
this.courseSaving = true;
this.lessons.map((lesson, index) => {
lesson.position = index + 1;
lesson.course_id = this.course.id;
let res = api.saveLesson(lesson, this.accessToken);
promises.push(res);
});
Promise.all(promises).then(() => {
this.courseSaving = false;
this.changeSavingStatus(true);
}, () => {
this.courseSaving = false;
this.changeSavingStatus(true, true);
});
},
pluralize(count, words) { pluralize(count, words) {
var cases = [2, 0, 1, 1, 1, 2]; var cases = [2, 0, 1, 1, 1, 2];
return words[ (count % 100 > 4 && count % 100 < 20) ? 2 : cases[ Math.min(count % 10, 5)] ]; return words[ (count % 100 > 4 && count % 100 < 20) ? 2 : cases[ Math.min(count % 10, 5)] ];
} },
changeSavingStatus(saved, error) {
let text = '';
if(error) {
text = 'ОШИБКА';
} else {
text = saved ? 'СОХРАНЕНО' : 'СОХРАНЕНИЕ...';
}
clearTimeout(this.savingTimeout);
document.getElementById('course-redactor__saving-status').innerText = text;
if(saved || error){
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
}
},
}, },
mounted() { mounted() {
this.mounting = true; this.mounting = true;
@ -1025,7 +1053,7 @@
}, },
displayPrice: { displayPrice: {
get: function () { get: function () {
return this.course.is_paid ? (this.course.price || 0) : 0; return this.course.is_paid ? (this.course.price || '') : '';
}, },
set: function (value) { set: function (value) {
this.course.price = value || 0; this.course.price = value || 0;

@ -14,16 +14,22 @@
</div> </div>
<div class="kit__title title">{{ title }}</div> <div class="kit__title title">{{ title }}</div>
<div class="kit__section"> <div class="kit__section">
<div class="kit__field field" <div class="kit__row">
v-bind:class="{ error: $v.currentLesson.title.$invalid }"> <lil-image :image-id.sync="lesson.coverImageId" :image-url.sync="lesson.coverImage"
<div class="field__wrap"> v-on:update:imageUrl="onUpdateCoverUrl" v-on:update:imageId="onUpdateCoverId" :access-token="accessToken"/>
<input type="text" class="field__input" placeholder="Название урока" v-model="lesson.title"> <div class="kit__fieldset">
</div> <div class="kit__field field"
</div> v-bind:class="{ error: $v.currentLesson.title.$invalid }">
<div class="kit__field field" <div class="field__wrap">
v-bind:class="{ error: $v.currentLesson.short_description.$invalid }"> <input type="text" class="field__input" placeholder="Название урока" v-model="lesson.title">
<div class="field__wrap"> </div>
<textarea class="field__input" v-autosize="lesson.short_description" placeholder="Описание урока" v-model="lesson.short_description"></textarea> </div>
<div class="kit__field field"
v-bind:class="{ error: $v.currentLesson.short_description.$invalid }">
<div class="field__wrap">
<textarea class="field__input" v-autosize="lesson.short_description" placeholder="Описание урока" v-model="lesson.short_description"></textarea>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -81,6 +87,7 @@
import BlockImages from './blocks/BlockImages' import BlockImages from './blocks/BlockImages'
import BlockImageText from './blocks/BlockImageText' import BlockImageText from './blocks/BlockImageText'
import BlockVideo from './blocks/BlockVideo' import BlockVideo from './blocks/BlockVideo'
import LilImage from "./blocks/Image"
import {api} from "../js/modules/api"; import {api} from "../js/modules/api";
import Draggable from 'vuedraggable'; import Draggable from 'vuedraggable';
import _ from 'lodash' import _ from 'lodash'
@ -105,7 +112,13 @@
if (blockToRemove.data.id) { if (blockToRemove.data.id) {
api.removeContentBlock(blockToRemove, this.accessToken); api.removeContentBlock(blockToRemove, this.accessToken);
} }
} },
onUpdateCoverUrl(newValue) {
this.lesson.coverImage = newValue;
},
onUpdateCoverId(newValue) {
this.lesson.coverImageId = newValue;
},
}, },
computed: { computed: {
title() { title() {
@ -120,6 +133,7 @@
'block-images': BlockImages, 'block-images': BlockImages,
'block-video': BlockVideo, 'block-video': BlockVideo,
'vue-draggable': Draggable, 'vue-draggable': Draggable,
'lil-image': LilImage,
} }
} }
</script> </script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

@ -107,9 +107,9 @@ export const api = {
author: courseObject.author ? courseObject.author : null, author: courseObject.author ? courseObject.author : null,
short_description: courseObject.short_description, short_description: courseObject.short_description,
category: courseObject.category, category: courseObject.category,
price: courseObject.is_paid ? courseObject.price : 0, price: courseObject.is_paid && courseObject.price || 0,
deferred_start_at: deferredStart, deferred_start_at: deferredStart,
duration: courseObject.duration, duration: courseObject.duration || 0,
is_featured: courseObject.is_featured, is_featured: courseObject.is_featured,
slug: courseObject.url, slug: courseObject.url,
date: (courseObject.date) ? courseObject.date.value:null, date: (courseObject.date) ? courseObject.date.value:null,
@ -204,9 +204,11 @@ export const api = {
const isAdding = (!lessonObject.hasOwnProperty('id') || !lessonObject.hasOwnProperty('id')); const isAdding = (!lessonObject.hasOwnProperty('id') || !lessonObject.hasOwnProperty('id'));
const lessonJson = { const lessonJson = {
cover: lessonObject.coverImageId ? lessonObject.coverImageId : null,
title: lessonObject.title, title: lessonObject.title,
short_description: lessonObject.short_description, short_description: lessonObject.short_description,
course: lessonObject.course_id, course: lessonObject.course_id,
position: lessonObject.position,
content: lessonObject.content.map((block, index) => { content: lessonObject.content.map((block, index) => {
if (block.type === 'text') { if (block.type === 'text') {
return { return {
@ -284,7 +286,10 @@ export const api = {
id: lessonJSON.id, id: lessonJSON.id,
title: lessonJSON.title, title: lessonJSON.title,
short_description: lessonJSON.short_description, short_description: lessonJSON.short_description,
content: api.convertContentResponse(lessonJSON.content) coverImageId: lessonJSON.cover && lessonJSON.cover.id ? lessonJSON.cover.id : null,
coverImage: lessonJSON.cover && lessonJSON.cover.image ? lessonJSON.cover.image : null,
content: api.convertContentResponse(lessonJSON.content),
position: lessonJSON.position,
} }
}, },
convertCourseJson: (courseJSON) => { convertCourseJson: (courseJSON) => {

@ -2648,8 +2648,15 @@ a.grey-link
flex: 0 0 140px flex: 0 0 140px
+m +m
display: none display: none
&__pic-wrapper
width: 130px;
height: 130px;
border-radius: 50%;
overflow: hidden;
&__pic &__pic
display: block top: 50%;
position: relative;
transform: translateY(-50%);
width: 100% width: 100%
&__content &__content
flex: 0 0 calc(100% - 165px) flex: 0 0 calc(100% - 165px)
@ -3061,8 +3068,6 @@ a.grey-link
border-bottom: 1px solid $border border-bottom: 1px solid $border
align-items: center align-items: center
justify-content: center justify-content: center
+m
margin: 0 -15px 30px
&__btn &__btn
height: 56px height: 56px
border-bottom: 1px solid $border border-bottom: 1px solid $border
@ -3072,7 +3077,8 @@ a.grey-link
letter-spacing: 1px letter-spacing: 1px
transition: border-color .2s, color .2s transition: border-color .2s, color .2s
+m +m
flex: 0 0 50% flex: 0 0 35%
font-size: 10px
&:not(:last-child) &:not(:last-child)
margin-right: 40px margin-right: 40px
+m +m
@ -3846,7 +3852,6 @@ a.grey-link
.icon .icon
font-size: 8px font-size: 8px
fill: $pink fill: $pink
&__buy,
&__more &__more
display: none display: none
&__more &__more

Loading…
Cancel
Save