Merge branch 'master' into dev

remotes/origin/feature/effective_amount
Ivlev Denis 8 years ago
commit d99078868b
  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. 2
      apps/course/templates/course/course_only_lessons.html
  7. 2
      apps/course/views.py
  8. 4
      apps/payment/views.py
  9. 2
      apps/school/templates/blocks/online.html
  10. 108
      apps/school/templates/summer/_schedule_purchased_item.html
  11. 2
      apps/school/templates/summer/open_lesson.html
  12. 14
      apps/user/templates/user/profile.html
  13. 1
      apps/user/views.py
  14. 22
      docker/.env.review
  15. 1
      docker/Dockerfile
  16. 10
      docker/conf/nginx/conf.d/default.conf
  17. 1
      docker/conf/nginx/nginx.conf
  18. 1
      docker/conf/nginx/sites-enabled/default
  19. 7
      docker/docker-compose-prod.yml
  20. 58
      docker/docker-compose-review.yml
  21. 1
      docker/entrypoint_app.sh
  22. 4
      project/templates/blocks/popup_buy.html
  23. 31
      project/templates/blocks/promo.html
  24. 41
      project/templates/lilcity/index.html
  25. 140
      web/src/components/CourseRedactor.vue
  26. 8
      web/src/js/modules/api.js
  27. 38
      web/src/sass/_common.sass

@ -1,16 +1,68 @@
stages:
- deploy
- deploy
- db
- stop
variables:
REVIEW_DOMAIN: back-review.lil.school
deploy_prod:
stage: deploy
script:
- rsync -a --stats --delete --exclude="docker/data/" --exclude="docker/.env" ./ /work/www/lil.school/
- 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:
name: prod/site
name: prod
url: https://lil.school
only:
- master
tags:
- 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):
def has_permission(self, request, view):
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_superuser
)

@ -184,6 +184,7 @@ class LessonCreateSerializer(DispatchContentMixin, serializers.ModelSerializer):
'created_at',
'update_at',
'deactivated_at',
'position',
)
read_only_fields = (
@ -196,6 +197,9 @@ class LessonCreateSerializer(DispatchContentMixin, serializers.ModelSerializer):
def create(self, validated_data):
content = validated_data.pop('content', [])
lesson = super().create(validated_data)
if not validated_data.get('position'):
lesson.set_last_position()
lesson.save()
self.dispatch_content(lesson, content)
return lesson
@ -226,6 +230,7 @@ class LessonSerializer(serializers.ModelSerializer):
'created_at',
'update_at',
'deactivated_at',
'position',
)
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)
update_at = models.DateTimeField(auto_now=True)
position = models.PositiveSmallIntegerField(
'Положение на странице',
default=1,
)
def __str__(self):
return self.title
def set_last_position(self):
if self.course:
self.position = self.course.lessons.count()
def save(self, *args, **kwargs):
if not self.author and self.course and self.course.author:
self.author = self.course.author

@ -173,7 +173,7 @@
<div class="lessons">
<div class="lessons__title title">Содержание курса</div>
<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 %}
<a href="{% url 'lesson' pk=lesson.id %}">
{% else %}

@ -203,6 +203,8 @@ class CourseView(DetailView):
status=Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW,
).exists()
context['only_lessons'] = self.only_lessons
if self.only_lessons:
context['lessons'] = self.object.lessons.order_by('position')
return context
def get_queryset(self):

@ -222,7 +222,9 @@ class PaymentwallCallbackView(View):
date_start = arrow.get(school_payment.date_end).shift(days=1).datetime
date_end = arrow.get(date_start).shift(months=1).datetime
else:
month = 0 if now().day >= 1 and now().day <= 10 else 1
#month = 0 if now().day >= 1 and now().day <= 10 else 1
# Логика июльского лагеря: до конца июля приобретаем только на текущий месяц
month = 0
date_start = self.add_months(sourcedate=now().replace(hour=0, minute=0, day=1), months=month)
date_end = arrow.get(date_start).shift(months=1, minutes=-1).datetime
payment.date_start = date_start

@ -3,7 +3,7 @@
<div class="online__center center">
<div class="online__type">ПРЯМОЙ ЭФИР</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">
<svg class="icon icon-play">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-play"></use>

@ -1,6 +1,6 @@
{% load static %} {% load thumbnail %}
<div class="timing__item{% if school_schedule.weekday > 5 %} timing__item_bg{% endif %} js-timing-item js-timing-toggle{% if not school_schedule.weekday in school_schedules_purchased %} disable{% endif %}">
<div class="timing__cell">
<div class="timing__cell timing__cell--info">
<div class="timing__info">
<div class="timing__day{% if school_schedule.is_online %} active{% endif %}">
{{ school_schedule }}
@ -8,16 +8,16 @@
{% if live_lesson %}
<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 live_lesson.title %}
{% include './open_lesson.html' %}
<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 %}
{% else %}
{% include './day_pay_btn.html' %}
{% endif %}
</div>
</div>
{% comment %}
<!-- это нужно чтобы в попапе продления школы всегда знать какие дни выбраны(куплены) -->
@ -26,53 +26,55 @@
<span style="display: none;" data-purchased="{{ school_schedule.weekday }}"></span>
{% endif %}
</div>
<div class="timing__cell">
<div class="timing__preview">
{% thumbnail live_lesson.cover.image "70x70" crop="center" as im %}
<img class="timing__pic" src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}" />
{% empty %}
<img class="timing__pic" src="{% static 'img/no_cover.png' %}" width="70px" height="70px" />
{% endthumbnail %}
<div style="display: flex;">
<div class="timing__cell timing__cell--preview">
<div class="timing__preview">
{% thumbnail live_lesson.cover.image "70x70" crop="center" as im %}
<img class="timing__pic" src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}" />
{% empty %}
<img class="timing__pic" src="{% static 'img/no_cover.png' %}" style="width: 70px; height: 70px;" />
{% endthumbnail %}
</div>
</div>
</div>
<div class="timing__cell">
<div class="timing__title">{{ school_schedule.title }}{% if live_lesson and live_lesson.title %},
<span class="bold">{{ live_lesson.title }}</span>
<div class="timing__cell timing__cell--content">
<div class="timing__title">{{ school_schedule.title }}{% if live_lesson and live_lesson.title %},
<span class="bold">{{ live_lesson.title }}</span>
{% endif %}
</div>
<div class="timing__content">
{% if live_lesson and live_lesson.short_description %}
{{ live_lesson.short_description }}
{% else %}
{{ school_schedule.description }}
{% endif %}
</div>
<div class="timing__content">
{% if live_lesson and live_lesson.short_description %}
{{ live_lesson.short_description }}
{% else %}
{{ school_schedule.description }}
{% endif %}
</div>
<div class="timing__more">
<div class="timing__head">Материалы</div>
<div class="timing__row">
<div class="timing__text">
{{ school_schedule.materials }}
</div>
</div>
{% if school_schedule.schoolschedule_images.exists %}
<div class="timing__head">Результаты прошлых уроков</div>
<div class="timing__works gallery">
{% for image in school_schedule.schoolschedule_images.all %}
{% thumbnail image.img.image "48x48" crop="center" as im %}
<a class="timing__work" href="{{ image.img.image.url }}">
<img class="timing__pic" src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}"/>
</a>
{% endthumbnail %}
{% endfor %}
<div class="timing__more">
<div class="timing__head">Материалы</div>
<div class="timing__row">
<div class="timing__text">
{{ school_schedule.materials }}
</div>
</div>
{% if school_schedule.schoolschedule_images.exists %}
<div class="timing__head">Результаты прошлых уроков</div>
<div class="timing__works gallery">
{% for image in school_schedule.schoolschedule_images.all %}
{% thumbnail image.img.image "48x48" crop="center" as im %}
<a class="timing__work" href="{{ image.img.image.url }}">
<img class="timing__pic" src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}"/>
</a>
{% endthumbnail %}
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
<div class="timing__cell">
<button class="timing__toggle">
<svg class="icon icon-arrow-down">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-down"></use>
</svg>
</button>
<div class="timing__cell timing__cell--toggle">
<button class="timing__toggle">
<svg class="icon icon-arrow-down">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-down"></use>
</svg>
</button>
</div>
</div>
</div>

@ -1,4 +1,4 @@
<a
class="timing__btn btn btn_light"
href="{% url 'school:lesson-detail' live_lesson.id %}"
>смотреть урок</a>
>подробнее</a>

@ -62,22 +62,22 @@
</div>
</div>
{% if not guest %}
<div class="section section_pink-light section_tabs">
<div class="section__center center">
<div class="tabs js-tabs">
<div class="tabs__nav">
{% if owner %}
<button class="tabs__btn js-tabs-btn active">ЛАГЕРЬ</button>
<button class="tabs__btn js-tabs-btn">ПРИОБРЕТЕННЫЕ
<span class="mobile-hide">КУРСЫ</span>
</button>
{% if not simple_user %}
<button class="tabs__btn js-tabs-btn">ОПУБЛИКОВАННЫЕ
{% endif %}
<button class="tabs__btn js-tabs-btn {% if not owner %}active{% endif %}">ОПУБЛИКОВАННЫЕ
<span class="mobile-hide">КУРСЫ</span>
</button>
{% endif %}
</div>
<div class="tabs__container">
{% if owner %}
<div class="tabs__item js-tabs-item" style="display: block;">
{% if is_purchased_future %}
<div class="center center_xs">
@ -126,8 +126,8 @@
</div>
</div>
</div>
{% if not simple_user %}
<div class="tabs__item js-tabs-item">
{% endif %}
<div class="tabs__item js-tabs-item" {% if not owner %}style="display: block;"{% endif %}>
<div class="courses courses_scroll">
<div class="courses__list">
{% if published.exists %}
@ -145,10 +145,8 @@
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
{% endblock content %}

@ -56,6 +56,7 @@ class UserView(DetailView):
else:
context['simple_user'] = True
context['guest'] = True
context['owner'] = self.request.user.id == self.object.id
if context['guest'] and self.object.role <= User.USER_ROLE:
raise Http404()

@ -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/
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/
EXPOSE 80
ENTRYPOINT ["/app/docker/entrypoint_app.sh"]

@ -19,9 +19,19 @@ server {
location /media/instagram/results/ {
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 / {
try_files $uri @django;
}

@ -26,7 +26,6 @@ http {
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/sites-enabled/*;
set_real_ip_from 192.168.0.0/24;
}

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

@ -10,6 +10,11 @@ services:
- .env
volumes:
- ./data/postgres:/var/lib/postgresql/data
logging: &logging
driver: "json-file"
options:
max-size: "1m"
max-file: "1"
redis:
image: redis:4.0.9-alpine
@ -18,6 +23,7 @@ services:
- "127.0.0.1:6379:6379"
volumes:
- ./data/redis:/data
logging: *logging
app:
build:
@ -36,3 +42,4 @@ services:
links:
- db
- 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
cd /app
chown www-data:www-data /app/media
python manage.py migrate
#python manage.py loaddata /app/apps/*/fixtures/*.json
python2.7 /usr/bin/supervisord -n

@ -37,7 +37,7 @@
{% comment %} dont delete {% endcomment %}
<span class="switch__cell"></span>
<span class="switch__cell">{{ school_schedule.title }}</span>
<span class="switch__cell">{{school_schedule.month_price}}р</span>
<span class="switch__cell">{{school_schedule.month_price}}р в мес.</span>
</span>
</label>
{% endfor %}
@ -58,7 +58,7 @@
{% comment %} dont delete {% endcomment %}
<span class="switch__cell"></span>
<span class="switch__cell">{{ school_schedule.title }}</span>
<span class="switch__cell">{{school_schedule.month_price}}р</span>
<span class="switch__cell">{{school_schedule.month_price}}р в мес.</span>
</span>
</label>
{% endfor %}

@ -13,7 +13,7 @@
</div>
{% if user.is_authenticated and online %}
<div class="main__content">
Сейчас идёт прямой эфир урока «{{ school_schedule.title }}»
Сейчас идёт прямой эфир урока «{{ school_schedule.title }}, {{ school_schedule.current_live_lesson.title }}»
</div>
<div class="main__actions">
<a
@ -27,8 +27,8 @@
>{% if not is_purchased %}Получить доступ{% else %}Смотреть урок{% endif %}</a>
</div>
{% elif user.is_authenticated and online_coming_soon and school_schedule and school_schedule.start_at_humanize %}
<div class="main__content">
Урок «{{ school_schedule.title }}» начнётся
<div class="">
Урок «{{ school_schedule.title }}, {{ school_schedule.current_live_lesson.title }}» начнётся
</div>
<div class="main__time">
{{ school_schedule.start_at_humanize }}
@ -46,23 +46,20 @@
</div>
{% else %}
<div class="main__subtitle">
Урок <b>Рисовальный лагерь, Альбрехт Дюрер</b> завершен <br/>
Следующий урок пройдет 4 июля
Присоединяйтесь в Рисовальный лагерь
</div>
<div class="main__actions">
{% if is_purchased %}
<a class="main__btn btn" href="/school/lessons/214">Перейти в урок</a>
{% else %}
<a
{% if not is_purchased_future %}
{% if not user.is_authenticated %}
data-popup=".js-popup-auth"
{% else %}
data-popup=".js-popup-buy"
{% if not is_purchased and not is_purchased_future %}
<a
{% if not is_purchased_future %}
{% if not user.is_authenticated %}
data-popup=".js-popup-auth"
{% else %}
data-popup=".js-popup-buy"
{% endif %}
{% endif %}
{% endif %}
class="main__btn btn"
href="#"
class="main__btn btn"
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 %}

@ -25,7 +25,7 @@
<meta property="og:title" content="{% block ogtitle %}Онлайн-курсы LilCity{% endblock ogtitle %}">
{% comment %} <meta property="og:type" content="article"> {% endcomment %}
<meta property="og:url" content="{% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %}">
<meta property="og:image" content="{% block ogimage %}http://{{request.META.HTTP_HOST}}{% static 'img/og_main.jpg' %}{% endblock ogimage %}">
<meta property="og:image" content="{% block ogimage %}{{ request.build_absolute_uri }}{% static 'img/video-1.jpg' %}{% endblock ogimage %}">
<meta property="og:description" content="{% block ogdescription %}Онлайн-курсы LilCity{% endblock ogdescription %}">
<meta property="og:site_name" content="Онлайн-курсы LilCity">
<meta property="og:locale" content="ru_RU">
@ -76,7 +76,44 @@
&noscript=1"/>
</noscript>
<!-- 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" %}
</head>
<body>

@ -215,24 +215,31 @@
<div v-if="viewSection === 'lessons'" class="kit__body">
<div class="lessons__title title">Содержание курса</div>
<div v-if="!lessonsLoading" class="lessons__list">
<div class="lessons__item" v-for="(lesson, index) in lessons">
<div class="lessons__actions">
<button type="button" class="lessons__action" @click="removeLesson(index)">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
<button type="button" class="lessons__action" @click="editLesson(index)">
<svg class="icon icon-edit">
<use xlink:href="/static/img/sprite.svg#icon-edit"></use>
</svg>
</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 v-model="lessons" @start="drag=true" @end="onLessonsChanged" :options="{ handle: '.sortable__handle' }">
<div class="lessons__item" v-for="(lesson, index) in lessons" :key="lesson.id">
<div class="lessons__actions">
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<button type="button" class="lessons__action" @click="removeLesson(index)">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
<button type="button" class="lessons__action" @click="editLesson(index)">
<svg class="icon icon-edit">
<use xlink:href="/static/img/sprite.svg#icon-edit"></use>
</svg>
</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 v-if="lessonsLoading">Загрузка...</div>
<div class="lessons__foot">
@ -307,7 +314,7 @@
price: null,
url: '',
coverImage: '',
coverImageId: null,
kit__body: null,
is_paid: false,
is_featured: true,
is_deferred: false,
@ -507,6 +514,7 @@
api.removeCourseLesson(lesson.id, this.accessToken);
}
this.lessons.splice(lessonIndex, 1);
this.onLessonsChanged();
},
editLesson(lessonIndex) {
this.currentLesson = this.lessons[lessonIndex];
@ -581,11 +589,7 @@
});
}
document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНО';
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
this.changeSavingStatus(true);
showNotification("success", 'Урок сохранён');
// this.goToLessons();
@ -594,10 +598,7 @@
.catch((err) => {
this.lessonSaving = false;
//console.error(err);
document.getElementById('course-redactor__saving-status').innerText = 'ОШИБКА';
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
this.changeSavingStatus(true, true);
// alert('Произошло что-то страшное: '+err.toString());
console.log(err);
if(err.response) {
@ -619,6 +620,14 @@
$(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() {
//console.log('loadCourseDraft');
if(this.live) { return; }
@ -628,12 +637,8 @@
response
.then((response) => {
this.course = api.convertCourseJson(response.data);
this.course.live = this.live;
this.processCourseJson(response.data);
this.courseLoading = false;
this.lessons = response.data.lessons.map((lessonJson) => {
return api.convertLessonJson(lessonJson);
});
})
.catch((err) => {
this.courseLoading = false;
@ -653,20 +658,18 @@
}
request
.then((response) => {
this.course = api.convertCourseJson(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.processCourseJson(response.data);
this.$nextTick(() => {
this.courseLoading = false;
});
this.lessons = response.data.lessons.map((lessonJson) => {
return api.convertLessonJson(lessonJson);
this.lessons.sort((a, b) => {
if (a.position > b.position) {
return 1;
}
if (a.position < b.position) {
return -1;
}
return 0;
});
})
.catch((err) => {
@ -794,17 +797,13 @@
}
this.courseSaving = true;
clearTimeout(this.savingTimeout);
document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНИЕ...';
this.changeSavingStatus();
const courseObject = this.course;
courseObject.url = (courseObject.url) ? slugify(courseObject.url):courseObject.url;
api.saveCourse(courseObject, this.accessToken)
.then((response) => {
this.courseSaving = false;
document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНО';
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
this.changeSavingStatus(true);
this.courseSyncHook = true;
const courseData = api.convertCourseJson(response.data);
if (this.course.coverImage) {
@ -854,10 +853,7 @@
this.courseSyncHook = false;
this.courseSaving = false;
//console.error(err);
document.getElementById('course-redactor__saving-status').innerText = 'ОШИБКА';
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
this.changeSavingStatus(true, true);
// alert('Произошло что-то страшное: '+err.toString());
//console.log(err.response.data);
if(err.response) {
@ -886,10 +882,42 @@
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) {
var cases = [2, 0, 1, 1, 1, 2];
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() {
this.mounting = true;
@ -1025,7 +1053,7 @@
},
displayPrice: {
get: function () {
return this.course.is_paid ? (this.course.price || 0) : 0;
return this.course.is_paid ? (this.course.price || '') : '';
},
set: function (value) {
this.course.price = value || 0;

@ -107,9 +107,9 @@ export const api = {
author: courseObject.author ? courseObject.author : null,
short_description: courseObject.short_description,
category: courseObject.category,
price: courseObject.is_paid ? courseObject.price : 0,
price: courseObject.is_paid && courseObject.price || 0,
deferred_start_at: deferredStart,
duration: courseObject.duration,
duration: courseObject.duration || 0,
is_featured: courseObject.is_featured,
slug: courseObject.url,
date: (courseObject.date) ? courseObject.date.value:null,
@ -208,6 +208,7 @@ export const api = {
title: lessonObject.title,
short_description: lessonObject.short_description,
course: lessonObject.course_id,
position: lessonObject.position,
content: lessonObject.content.map((block, index) => {
if (block.type === 'text') {
return {
@ -287,7 +288,8 @@ export const api = {
short_description: lessonJSON.short_description,
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)
content: api.convertContentResponse(lessonJSON.content),
position: lessonJSON.position,
}
},
convertCourseJson: (courseJSON) => {

@ -1832,17 +1832,17 @@ a.grey-link
padding: 0
text-align: right
flex: 0 0 calc(100% - 110px)
&:nth-child(2),
&:last-child
&:nth-child(2)
flex: 0 0 60px
&:nth-child(3)
flex: 0 0 calc(100% - 230px)
flex: 0 0 calc(100% - 290px)
+t
padding: 0
flex: 0 0 calc(100% - 60px)
flex: 0 0 calc(100% - 120px)
&:last-child
padding: 0
text-align: right
flex: 0 0 120px
&_blue &__content
&:after
background: #4A90E2
@ -3731,6 +3731,8 @@ a.grey-link
box-shadow: -40px 0 0 0 $pink-light, 40px 0 0 0 $pink-light
+nf
border-color: transparent
+m
display: block
&__item.open
padding-bottom: 40px
&__item.open &__toggle
@ -3750,14 +3752,8 @@ a.grey-link
&__item.disable &__title,
&__item.disable &__content
opacity: .4
&__item.disable &__cell
&:nth-child(1)
justify-content: center
&:nth-child(3)
+m
padding: 0 0 50px
&__cell
&:nth-child(1)
&--info
display: flex
padding-right: 20px
flex-direction: column
@ -3766,15 +3762,15 @@ a.grey-link
+m
padding: 0
flex: 0 0 0
&:nth-child(2)
margin-bottom: 15px
&--preview
padding-right: 20px
flex: 0 0 90px
&:nth-child(3)
&--content
flex: 0 0 calc(100% - 254px)
+m
padding-top: 20px
flex: 0 0 calc(100% - 114px)
&:nth-child(4)
&--toggle
flex: 0 0 34px
&__unlock
+m
@ -3785,11 +3781,6 @@ a.grey-link
margin-bottom: auto
+m
display: flex
position: absolute
top: 20px
left: 90px
right: 0
padding: 0
flex-direction: row
&__day
position: relative
@ -3816,10 +3807,8 @@ a.grey-link
margin: -3px 0 0 auto
&__buy
+m
position: absolute
left: 0
bottom: 40px
align-items: center
display: flex
margin-left: 20px
&__time
margin: 15px 0
opacity: .5
@ -3852,7 +3841,6 @@ a.grey-link
.icon
font-size: 8px
fill: $pink
&__buy,
&__more
display: none
&__more

Loading…
Cancel
Save