diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9a2b0562..1d78b5d1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/api/v1/permissions.py b/api/v1/permissions.py index 95a136ee..c0854a4f 100644 --- a/api/v1/permissions.py +++ b/api/v1/permissions.py @@ -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 ) diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index d1e8dcba..bdb1a289 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -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 = ( diff --git a/apps/course/migrations/0039_lesson_position.py b/apps/course/migrations/0039_lesson_position.py new file mode 100644 index 00000000..7a26d8e5 --- /dev/null +++ b/apps/course/migrations/0039_lesson_position.py @@ -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='Положение на странице'), + ), + ] diff --git a/apps/course/models.py b/apps/course/models.py index 7d0300c7..942a0593 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -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 diff --git a/apps/course/templates/course/content/imagetext.html b/apps/course/templates/course/content/imagetext.html index 62ec511b..6385391e 100644 --- a/apps/course/templates/course/content/imagetext.html +++ b/apps/course/templates/course/content/imagetext.html @@ -4,7 +4,11 @@
-
+
+
+ +
+
{{ content.txt | safe }}
diff --git a/apps/course/templates/course/course.html b/apps/course/templates/course/course.html index 630aeb61..f5ba90af 100644 --- a/apps/course/templates/course/course.html +++ b/apps/course/templates/course/course.html @@ -9,7 +9,7 @@ {% block ogtitle %}{{ course.title }} - {{ block.super }}{% endblock ogtitle %} {% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %} {% 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 %} {% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %} @@ -141,37 +141,45 @@
Описание курса {% if request.user.is_authenticated %} - {% if course.author == request.user and request.user.role >= request.user.AUTHOR_ROLE %} - УРОКИ - - {% elif request.user.role == request.user.ADMIN_ROLE %} - УРОКИ - + {% if course.author == request.user and request.user.role >= request.user.AUTHOR_ROLE %} + УРОКИ + + {% elif request.user.role == request.user.ADMIN_ROLE %} + УРОКИ + + {% else %} + УРОКИ + {% if not paid %} + + + + {% endif %} + + {% endif %} {% else %} УРОКИ - {% if not paid %} + data-popup=".js-popup-auth">УРОКИ - {% endif %} {% endif %} - {% endif %}
{% if course.cover %} diff --git a/apps/course/templates/course/course_only_lessons.html b/apps/course/templates/course/course_only_lessons.html index 3ea64dba..f0217bcf 100644 --- a/apps/course/templates/course/course_only_lessons.html +++ b/apps/course/templates/course/course_only_lessons.html @@ -9,7 +9,7 @@ {% block ogtitle %}{{ course.title }} - {{ block.super }}{% endblock ogtitle %} {% block ogurl %}{{ request.build_absolute_uri }}{% endblock ogurl %} {% 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 %} {% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %} @@ -173,7 +173,7 @@
Содержание курса
- {% 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 %} {% else %} @@ -184,10 +184,15 @@
{% if lesson.cover %}
- +
+ +
{% endif %} -
{{ lesson.short_description | safe }}
+
{{ lesson.short_description | truncatechars_html:800 | safe }}
+
+
diff --git a/apps/course/templates/course/courses.html b/apps/course/templates/course/courses.html index 44ee9436..a3277174 100644 --- a/apps/course/templates/course/courses.html +++ b/apps/course/templates/course/courses.html @@ -2,6 +2,7 @@ {% load static %} {% load category_items from lilcity_category %} +{% block ogimage %}http://{{request.META.HTTP_HOST}}{% static 'img/og_courses.jpg' %}{% endblock ogimage %} {% block content %}
diff --git a/apps/course/templates/course/lesson.html b/apps/course/templates/course/lesson.html index 6c9511ae..662a4dd5 100644 --- a/apps/course/templates/course/lesson.html +++ b/apps/course/templates/course/lesson.html @@ -2,11 +2,12 @@ {% load static %} {% 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 %}
{{ lesson.title }}
@@ -32,7 +33,7 @@ {% if lesson.cover %} {% else %} - + {% endif %} diff --git a/apps/course/views.py b/apps/course/views.py index d4598354..b4fd6fd1 100644 --- a/apps/course/views.py +++ b/apps/course/views.py @@ -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): @@ -286,7 +288,9 @@ class LessonView(DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) 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 diff --git a/apps/payment/admin.py b/apps/payment/admin.py index 47dde452..19166563 100644 --- a/apps/payment/admin.py +++ b/apps/payment/admin.py @@ -28,10 +28,11 @@ class PaymentChildAdmin(PolymorphicChildModelAdmin): 'user', 'amount', 'status', + 'roistat_visit', 'created_at', ) base_fieldsets = ( - (None, {'fields': ('user', 'amount', 'status', 'data',)}), + (None, {'fields': ('user', 'amount', 'status', 'data', 'roistat_visit',)}), ) readonly_fields = ('amount', 'data',) diff --git a/apps/payment/migrations/0019_payment_roistat_visit.py b/apps/payment/migrations/0019_payment_roistat_visit.py new file mode 100644 index 00000000..4f7fa40e --- /dev/null +++ b/apps/payment/migrations/0019_payment_roistat_visit.py @@ -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'), + ), + ] diff --git a/apps/payment/models.py b/apps/payment/models.py index b1368dc4..32d7a0e9 100644 --- a/apps/payment/models.py +++ b/apps/payment/models.py @@ -97,6 +97,7 @@ class Payment(PolymorphicModel): amount = models.DecimalField('Итого', max_digits=8, decimal_places=2, default=0, editable=False) status = models.PositiveSmallIntegerField('Статус платежа', choices=PW_STATUS_CHOICES, null=True) data = JSONField('Данные платежа от провайдера', default={}, editable=False) + roistat_visit = models.PositiveIntegerField('Номер визита Roistat', null=True, editable=False) created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) diff --git a/apps/payment/tasks.py b/apps/payment/tasks.py index 911235a3..e30f4e19 100644 --- a/apps/payment/tasks.py +++ b/apps/payment/tasks.py @@ -38,12 +38,13 @@ def product_payment_to_mixpanel(user_id, event_name, time, properties): @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 = [{ 'id': str(payment_id), 'name': event_name, 'date_create': time, 'status': str(status), + 'roistat': str(roistat_visit) if roistat_visit else None, 'price': str(amount), 'client_id': str(user_id), 'fields': { diff --git a/apps/payment/templates/payment/payment_success.html b/apps/payment/templates/payment/payment_success.html index 670678fa..d7821bf9 100644 --- a/apps/payment/templates/payment/payment_success.html +++ b/apps/payment/templates/payment/payment_success.html @@ -5,7 +5,7 @@ {% if school %}
Вы успешно приобрели доступ к урокам онлайн-школы!
{% else %}
Вы успешно приобрели курс!
diff --git a/apps/payment/views.py b/apps/payment/views.py index 34d35a0b..4f4a958a 100644 --- a/apps/payment/views.py +++ b/apps/payment/views.py @@ -58,12 +58,14 @@ class CourseBuyView(TemplateView): host = urlsplit(self.request.META.get('HTTP_REFERER')) host = str(host[0]) + '://' + str(host[1]) course = Course.objects.get(id=pk) + roistat_visit = request.COOKIES.get('roistat_visit', None) if request.user == course.author: messages.error(request, 'Вы не можете приобрести свой курс.') return redirect(reverse_lazy('course', args=[course.id])) course_payment = CoursePayment.objects.create( user=request.user, course=course, + roistat_visit=roistat_visit, ) product = Product( f'course_{course_payment.id}', @@ -96,6 +98,7 @@ class SchoolBuyView(TemplateView): host = str(host[0]) + '://' + str(host[1]) weekdays = set(request.GET.getlist('weekdays', [])) add_days = 'add_days' in request.GET + roistat_visit = request.COOKIES.get('roistat_visit', None) if not weekdays: messages.error(request, 'Выберите несколько дней недели.') return redirect('school:summer-school') @@ -117,6 +120,7 @@ class SchoolBuyView(TemplateView): date_start=now().date(), date_end=_school_payment.date_end, add_days=True, + roistat_visit=roistat_visit, ) if school_payment.amount <= 0: messages.error(request, 'Выбранные дни отсутствуют в оставшемся периоде подписки') @@ -125,6 +129,7 @@ class SchoolBuyView(TemplateView): school_payment = SchoolPayment.objects.create( user=request.user, weekdays=weekdays, + roistat_visit=roistat_visit, ) product = Product( f'school_{school_payment.id}', @@ -259,6 +264,7 @@ class PaymentwallCallbackView(View): now().strftime('%Y-%m-%d %H:%M:%S'), pingback.get_type(), product_type_name, + payment.roistat_visit, ) author_balance = getattr(payment, 'author_balance', None) if author_balance and author_balance.type == AuthorBalance.IN: diff --git a/apps/school/migrations/0017_auto_20180628_2000.py b/apps/school/migrations/0017_auto_20180628_2000.py new file mode 100644 index 00000000..d641cd5e --- /dev/null +++ b/apps/school/migrations/0017_auto_20180628_2000.py @@ -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='Краткое описание урока'), + ), + ] diff --git a/apps/school/migrations/0018_auto_20180629_1501.py b/apps/school/migrations/0018_auto_20180629_1501.py new file mode 100644 index 00000000..0126d518 --- /dev/null +++ b/apps/school/migrations/0018_auto_20180629_1501.py @@ -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='Название урока'), + ), + ] diff --git a/apps/school/models.py b/apps/school/models.py index 1e1c6df6..29ed8795 100644 --- a/apps/school/models.py +++ b/apps/school/models.py @@ -114,8 +114,8 @@ class SchoolScheduleImage(models.Model): class LiveLesson(BaseModel, DeactivatedMixin): - title = models.CharField('Название урока', max_length=100) - short_description = models.TextField('Краткое описание урока') + title = models.CharField('Название урока', max_length=100, default='', blank=True) + short_description = models.TextField('Краткое описание урока', default='', blank=True) stream = models.URLField('Ссылка на VIMEO', default='', blank=True) date = models.DateField(default=now, unique=True) cover = models.ForeignKey( @@ -157,5 +157,5 @@ class LiveLesson(BaseModel, DeactivatedMixin): return False else: 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() diff --git a/apps/school/templates/blocks/_schedule_purchased_item.html b/apps/school/templates/blocks/_schedule_purchased_item.html index 8017a687..b9d1db1f 100644 --- a/apps/school/templates/blocks/_schedule_purchased_item.html +++ b/apps/school/templates/blocks/_schedule_purchased_item.html @@ -6,17 +6,17 @@ {{ school_schedule }}
{% if live_lesson %} - +
{{ live_lesson.date }}
{% endif %}
{{ school_schedule.start_at }} (МСК)
{% 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 %} + {% if live_lesson and live_lesson.title %} + {% include './open_lesson.html' %} + {% endif %} {% else %} - {% include './day_pay_btn.html' %} + {% include './day_pay_btn.html' %} {% endif %}
{% comment %} @@ -36,12 +36,12 @@
-
{{ school_schedule.title }}{% if live_lesson %}, +
{{ school_schedule.title }}{% if live_lesson and live_lesson.title %}, {{ live_lesson.title }} {% endif %}
- {% if live_lesson %} + {% if live_lesson and live_lesson.short_description %} {{ live_lesson.short_description }} {% else %} {{ school_schedule.description }} diff --git a/apps/school/templates/blocks/online.html b/apps/school/templates/blocks/online.html index 1da3e731..c4c82931 100644 --- a/apps/school/templates/blocks/online.html +++ b/apps/school/templates/blocks/online.html @@ -3,7 +3,7 @@
ПРЯМОЙ ЭФИР
Каждый день в 17.00 (по Мск)
-
Кроме выходных. Запись эфира доступна в течение 24-х часов.
+
Кроме выходных. Запись эфира доступна по завершению трансляции
diff --git a/apps/school/templates/blocks/schedule_purchased.html b/apps/school/templates/blocks/schedule_purchased.html index 03e98e92..7f8f035d 100644 --- a/apps/school/templates/blocks/schedule_purchased.html +++ b/apps/school/templates/blocks/schedule_purchased.html @@ -9,7 +9,6 @@
Подписка истекает {{ subscription_ends }}
- {% include './prolong_btn.html' %} {% else %}
Подписка истекла diff --git a/apps/school/templates/school/livelesson_detail.html b/apps/school/templates/school/livelesson_detail.html index 92ca42e1..1bcb3227 100644 --- a/apps/school/templates/school/livelesson_detail.html +++ b/apps/school/templates/school/livelesson_detail.html @@ -10,7 +10,7 @@
{{ livelesson.short_description }}
{% if livelesson.stream_index %} - Если видео не загрузилось обновите страницу diff --git a/apps/school/templates/school/summer_school.html b/apps/school/templates/school/summer_school.html index 133f8d8b..00da0bf8 100644 --- a/apps/school/templates/school/summer_school.html +++ b/apps/school/templates/school/summer_school.html @@ -1,5 +1,6 @@ {% extends "templates/lilcity/index.html" %} {% load static %} {% block title %}Онлайн-школа LilCity{% endblock title%} +{% block ogimage %}http://{{request.META.HTTP_HOST}}{% static 'img/og_summer_school.jpg' %}{% endblock %} {% block content %} {% if not is_purchased %} {% include "../summer/promo.html" %} diff --git a/apps/school/templates/summer/_schedule_purchased_item.html b/apps/school/templates/summer/_schedule_purchased_item.html index 8017a687..4620367c 100644 --- a/apps/school/templates/summer/_schedule_purchased_item.html +++ b/apps/school/templates/summer/_schedule_purchased_item.html @@ -6,18 +6,18 @@ {{ school_schedule }}
{% if live_lesson %} - - {% endif %} -
-
-
{{ school_schedule.start_at }} (МСК)
- {% 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' %} +
{{ live_lesson.date }}
{% endif %} +
+
{{ school_schedule.start_at }} (МСК)
+ {% 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 %} +
{% comment %} @@ -36,12 +36,12 @@
-
{{ school_schedule.title }}{% if live_lesson %}, +
{{ school_schedule.title }}{% if live_lesson and live_lesson.title %}, {{ live_lesson.title }} {% endif %}
- {% if live_lesson %} + {% if live_lesson and live_lesson.short_description %} {{ live_lesson.short_description }} {% else %} {{ school_schedule.description }} diff --git a/apps/school/templates/summer/schedule_purchased.html b/apps/school/templates/summer/schedule_purchased.html index 467be723..63e963fd 100644 --- a/apps/school/templates/summer/schedule_purchased.html +++ b/apps/school/templates/summer/schedule_purchased.html @@ -9,7 +9,6 @@
Подписка истекает {{ subscription_ends }}
- {% include './prolong_btn.html' %} {% else %}
Подписка истекла diff --git a/apps/user/templates/user/profile.html b/apps/user/templates/user/profile.html index cea0b4ee..caddc204 100644 --- a/apps/user/templates/user/profile.html +++ b/apps/user/templates/user/profile.html @@ -67,18 +67,18 @@
- + {% if not simple_user %} - {% endif %}
-
+
{% if is_purchased_future %}
@@ -127,7 +127,7 @@
{% if not simple_user %} -
+
{% if published.exists %} diff --git a/docker/.env.review b/docker/.env.review new file mode 100644 index 00000000..351dc795 --- /dev/null +++ b/docker/.env.review @@ -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 diff --git a/docker/Dockerfile b/docker/Dockerfile index 46722a2f..acaa74c8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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"] diff --git a/docker/conf/nginx/sites-available/default b/docker/conf/nginx/conf.d/default.conf similarity index 71% rename from docker/conf/nginx/sites-available/default rename to docker/conf/nginx/conf.d/default.conf index 1aaa7519..b20174f6 100644 --- a/docker/conf/nginx/sites-available/default +++ b/docker/conf/nginx/conf.d/default.conf @@ -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; } diff --git a/docker/conf/nginx/nginx.conf b/docker/conf/nginx/nginx.conf index c477630e..2889f7ad 100644 --- a/docker/conf/nginx/nginx.conf +++ b/docker/conf/nginx/nginx.conf @@ -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; } diff --git a/docker/conf/nginx/sites-enabled/default b/docker/conf/nginx/sites-enabled/default deleted file mode 120000 index ad35b834..00000000 --- a/docker/conf/nginx/sites-enabled/default +++ /dev/null @@ -1 +0,0 @@ -/etc/nginx/sites-available/default \ No newline at end of file diff --git a/docker/docker-compose-prod.yml b/docker/docker-compose-prod.yml index 440f2cbf..c3b97c9e 100644 --- a/docker/docker-compose-prod.yml +++ b/docker/docker-compose-prod.yml @@ -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 diff --git a/docker/docker-compose-review.yml b/docker/docker-compose-review.yml new file mode 100644 index 00000000..9413f978 --- /dev/null +++ b/docker/docker-compose-review.yml @@ -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 diff --git a/docker/entrypoint_app.sh b/docker/entrypoint_app.sh index a61879d2..a346f89c 100755 --- a/docker/entrypoint_app.sh +++ b/docker/entrypoint_app.sh @@ -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 diff --git a/project/settings.py b/project/settings.py index fe902536..9323745c 100644 --- a/project/settings.py +++ b/project/settings.py @@ -55,6 +55,7 @@ INSTALLED_APPS = [ 'corsheaders', 'sorl.thumbnail', 'raven.contrib.django.raven_compat', + 'django_user_agents', ] + [ 'apps.auth.apps', 'apps.user', @@ -68,6 +69,7 @@ INSTALLED_APPS = [ MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', + 'django_user_agents.middleware.UserAgentMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', diff --git a/project/templates/blocks/about.html b/project/templates/blocks/about.html index 4caf08de..8deec906 100644 --- a/project/templates/blocks/about.html +++ b/project/templates/blocks/about.html @@ -44,21 +44,19 @@
diff --git a/project/templates/blocks/promo.html b/project/templates/blocks/promo.html index 8fa641f4..4e49ee7a 100644 --- a/project/templates/blocks/promo.html +++ b/project/templates/blocks/promo.html @@ -13,7 +13,7 @@
{% if user.is_authenticated and online %}
- Сейчас идёт прямой эфир урока «{{ school_schedule.title }}» + Сейчас идёт прямой эфир урока «{{ school_schedule.title }}, {{ school_schedule.current_live_lesson.title }}»
{% elif user.is_authenticated and online_coming_soon and school_schedule and school_schedule.start_at_humanize %} -
- Урок «{{ school_schedule.title }}» начнётся +
+ Урок «{{ school_schedule.title }}, {{ school_schedule.current_live_lesson.title }}» начнётся
{{ school_schedule.start_at_humanize }} @@ -49,21 +49,21 @@ Присоединяйтесь в Рисовальный лагерь
{% endif %} diff --git a/project/templates/blocks/user_menu.html b/project/templates/blocks/user_menu.html index 54ebf6b6..13f4fbdf 100644 --- a/project/templates/blocks/user_menu.html +++ b/project/templates/blocks/user_menu.html @@ -1,7 +1,7 @@ {% load static %} {% load thumbnail %} {% if request.user.is_authenticated %}
-
ДАТА
-
+
-->
ДОСТУП
@@ -215,24 +215,31 @@
Содержание курса
-
-
- - -
-
{{ lesson.title }}
-
-
{{ lesson.short_description }}
-
-
+ +
+
+ + + +
+
{{ lesson.title }}
+
+
{{ lesson.short_description }}
+
+
+
Загрузка...
@@ -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) { @@ -841,11 +840,11 @@ this.course.id = courseData.id; } - if(this.live && courseData.date) { + /*if(this.live && courseData.date) { this.course.date = _.find(this.scheduleOptions, function(item){ return item.value == courseData.date; }); - } + }*/ this.$nextTick(() => { this.courseSyncHook = false; }); @@ -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; diff --git a/web/src/components/LessonRedactor.vue b/web/src/components/LessonRedactor.vue index 82faaadb..a0a245e2 100644 --- a/web/src/components/LessonRedactor.vue +++ b/web/src/components/LessonRedactor.vue @@ -14,16 +14,22 @@
{{ title }}
-
-
- -
-
-
-
- +
+ +
+
+
+ +
+
+
+
+ +
+
@@ -81,6 +87,7 @@ import BlockImages from './blocks/BlockImages' import BlockImageText from './blocks/BlockImageText' import BlockVideo from './blocks/BlockVideo' + import LilImage from "./blocks/Image" import {api} from "../js/modules/api"; import Draggable from 'vuedraggable'; import _ from 'lodash' @@ -105,7 +112,13 @@ if (blockToRemove.data.id) { api.removeContentBlock(blockToRemove, this.accessToken); } - } + }, + onUpdateCoverUrl(newValue) { + this.lesson.coverImage = newValue; + }, + onUpdateCoverId(newValue) { + this.lesson.coverImageId = newValue; + }, }, computed: { title() { @@ -120,10 +133,11 @@ 'block-images': BlockImages, 'block-video': BlockVideo, 'vue-draggable': Draggable, + 'lil-image': LilImage, } } \ No newline at end of file + diff --git a/web/src/img/og_blog.jpg b/web/src/img/og_blog.jpg new file mode 100644 index 00000000..cabead92 Binary files /dev/null and b/web/src/img/og_blog.jpg differ diff --git a/web/src/img/og_courses.jpg b/web/src/img/og_courses.jpg new file mode 100644 index 00000000..918718fe Binary files /dev/null and b/web/src/img/og_courses.jpg differ diff --git a/web/src/img/og_main.jpg b/web/src/img/og_main.jpg new file mode 100644 index 00000000..d9c04f59 Binary files /dev/null and b/web/src/img/og_main.jpg differ diff --git a/web/src/img/og_summer_school.jpg b/web/src/img/og_summer_school.jpg new file mode 100644 index 00000000..2adc8a7a Binary files /dev/null and b/web/src/img/og_summer_school.jpg differ diff --git a/web/src/js/modules/api.js b/web/src/js/modules/api.js index 59c98939..990ac7b2 100644 --- a/web/src/js/modules/api.js +++ b/web/src/js/modules/api.js @@ -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, @@ -204,9 +204,11 @@ export const api = { const isAdding = (!lessonObject.hasOwnProperty('id') || !lessonObject.hasOwnProperty('id')); const lessonJson = { + cover: lessonObject.coverImageId ? lessonObject.coverImageId : null, 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 { @@ -284,7 +286,10 @@ export const api = { id: lessonJSON.id, title: lessonJSON.title, 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) => { diff --git a/web/src/sass/_common.sass b/web/src/sass/_common.sass index 096e0b1b..033ce477 100755 --- a/web/src/sass/_common.sass +++ b/web/src/sass/_common.sass @@ -2648,8 +2648,15 @@ a.grey-link flex: 0 0 140px +m display: none + &__pic-wrapper + width: 130px; + height: 130px; + border-radius: 50%; + overflow: hidden; &__pic - display: block + top: 50%; + position: relative; + transform: translateY(-50%); width: 100% &__content flex: 0 0 calc(100% - 165px) @@ -3061,8 +3068,6 @@ a.grey-link border-bottom: 1px solid $border align-items: center justify-content: center - +m - margin: 0 -15px 30px &__btn height: 56px border-bottom: 1px solid $border @@ -3072,7 +3077,8 @@ a.grey-link letter-spacing: 1px transition: border-color .2s, color .2s +m - flex: 0 0 50% + flex: 0 0 35% + font-size: 10px &:not(:last-child) margin-right: 40px +m @@ -3846,7 +3852,6 @@ a.grey-link .icon font-size: 8px fill: $pink - &__buy, &__more display: none &__more