Merge branch 'master' of https://gitlab.com/lilcity/backend into feature/LIL-523

remotes/origin/hasaccess^2
gzbender 8 years ago
commit 981b5f4751
  1. 58
      .gitlab-ci.yml
  2. 5
      api/v1/serializers/course.py
  3. 3
      apps/payment/admin.py
  4. 0
      apps/payment/management/__init__.py
  5. 0
      apps/payment/management/commands/__init__.py
  6. 50
      apps/payment/management/commands/roistat_set_statuses.py
  7. 18
      apps/payment/migrations/0019_payment_roistat_visit.py
  8. 1
      apps/payment/models.py
  9. 26
      apps/payment/tasks.py
  10. 17
      apps/payment/views.py
  11. 10
      apps/school/models.py
  12. 14
      apps/school/templates/blocks/_schedule_purchased_item.html
  13. 14
      apps/school/templates/summer/_schedule_purchased_item.html
  14. 0
      apps/user/management/__init__.py
  15. 0
      apps/user/management/commands/__init__.py
  16. 32
      apps/user/management/commands/users_to_roistat.py
  17. 23
      apps/user/tasks.py
  18. 2
      docker/.env.example
  19. 22
      docker/.env.review
  20. 1
      docker/Dockerfile
  21. 18
      docker/Dockerfile.dev
  22. 10
      docker/conf/nginx/conf.d/default.conf
  23. 1
      docker/conf/nginx/nginx.conf
  24. 1
      docker/conf/nginx/sites-enabled/default
  25. 26
      docker/docker-compose-dev.yml
  26. 7
      docker/docker-compose-prod.yml
  27. 58
      docker/docker-compose-review.yml
  28. 1
      docker/entrypoint_app.sh
  29. 16
      project/settings.py
  30. 14
      project/templates/blocks/about.html
  31. 64
      project/templates/blocks/promo.html
  32. 32
      project/templates/lilcity/index.html
  33. 12
      project/views.py
  34. 12
      requirements.txt
  35. 29
      web/src/components/CourseRedactor.vue
  36. 4
      web/src/js/modules/api.js

@ -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

@ -1,4 +1,5 @@
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from apps.course.models import (
Category, Course,
@ -88,7 +89,9 @@ class CourseCreateSerializer(DispatchContentMixin,
):
title = serializers.CharField(allow_blank=True)
short_description = serializers.CharField(allow_blank=True)
slug = serializers.SlugField(allow_unicode=True, allow_blank=True, allow_null=True, required=False)
slug = serializers.SlugField(
allow_unicode=True, allow_blank=True, allow_null=True,
required=False, validators=[UniqueValidator(queryset=Course.objects.all())])
content = serializers.ListSerializer(
child=ContentCreateSerializer(),
required=False,

@ -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',)

@ -0,0 +1,50 @@
import requests
from paymentwall import Pingback
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
help = 'Update statuses on Roistat'
def handle(self, *args, **options):
body = [
{
'id': str(Pingback.PINGBACK_TYPE_REGULAR),
'name': 'PINGBACK_TYPE_REGULAR',
'type': 'paid',
},
{
'id': str(Pingback.PINGBACK_TYPE_GOODWILL),
'name': 'PINGBACK_TYPE_GOODWILL',
'type': 'paid',
},
{
'id': str(Pingback.PINGBACK_TYPE_NEGATIVE),
'name': 'PINGBACK_TYPE_NEGATIVE',
'type': 'canceled',
},
{
'id': str(Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW),
'name': 'PINGBACK_TYPE_RISK_UNDER_REVIEW',
'type': 'progress',
},
{
'id': str(Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED),
'name': 'PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED',
'type': 'paid',
},
{
'id': str(Pingback.PINGBACK_TYPE_RISK_REVIEWED_DECLINED),
'name': 'PINGBACK_TYPE_RISK_REVIEWED_DECLINED',
'type': 'canceled',
},
]
project = settings.ROISTAT_PROJECT
key = settings.ROISTAT_KEY
url = settings.ROISTAT_API_URL + f'/project/set-statuses?key={key}&project={project}'
resp = requests.post(url, json=body)
self.stdout.write(str(resp))
self.stdout.write(str(resp.json()))

@ -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)
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)

@ -1,9 +1,14 @@
import logging
import requests
from mixpanel import Mixpanel
from django.conf import settings
from project.celery import app
logger = logging.getLogger(__name__)
@app.task
def transaction_to_mixpanel(user_id, amount, time, product_type):
@ -30,3 +35,24 @@ def product_payment_to_mixpanel(user_id, event_name, time, properties):
event_name,
properties=props,
)
@app.task
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': {
'product_type': product_type,
}
}]
project = settings.ROISTAT_PROJECT
key = settings.ROISTAT_KEY
url = settings.ROISTAT_API_URL + f'/project/add-orders?key={key}&project={project}'
resp = requests.post(url, json=body)
logger.info('TRANSACTION_TO_ROISTAT: ' + str(resp))

@ -22,7 +22,7 @@ from paymentwall import Pingback, Product, Widget
from apps.course.models import Course
from apps.school.models import SchoolSchedule
from apps.payment.tasks import transaction_to_mixpanel, product_payment_to_mixpanel
from apps.payment.tasks import transaction_to_mixpanel, product_payment_to_mixpanel, transaction_to_roistat
from .models import AuthorBalance, CoursePayment, SchoolPayment
@ -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}',
@ -251,6 +256,16 @@ class PaymentwallCallbackView(View):
properties,
)
transaction_to_roistat.delay(
payment.user.id,
payment.id,
f'{product_type_name.title()} payment',
payment.amount,
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:
if pingback.is_deliverable():

@ -1,6 +1,9 @@
import arrow
from datetime import datetime, timedelta
from django.conf import settings
from django.db import models
from django.urls import reverse_lazy
from django.utils.timezone import now
from project.mixins import BaseModel, DeactivatedMixin
@ -65,6 +68,10 @@ class SchoolSchedule(models.Model):
).first()
return live_lesson
@property
def start_at_humanize(self):
return arrow.get(datetime.combine(datetime.today(), self.start_at), settings.TIME_ZONE).humanize(locale='ru') if self.start_at else None
class SchoolScheduleImage(models.Model):
schoolschedule = models.ForeignKey(
@ -121,6 +128,9 @@ class LiveLesson(BaseModel, DeactivatedMixin):
self.date = (datetime.combine(self.date, now().time()) + timedelta(days=1)).date()
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse_lazy('school:lesson-detail', args=[str(self.id)])
def stream_index(self):
return self.stream.split('/')[-1]

@ -6,15 +6,17 @@
{{ school_schedule }}
</div>
{% 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 and live_lesson %}
{% include './open_lesson.html' %}
{% 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' %}
{% include './day_pay_btn.html' %}
{% endif %}
</div>
{% comment %}
@ -34,12 +36,12 @@
</div>
</div>
<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>
{% endif %}
</div>
<div class="timing__content">
{% if live_lesson %}
{% if live_lesson and live_lesson.short_description %}
{{ live_lesson.short_description }}
{% else %}
{{ school_schedule.description }}

@ -6,15 +6,17 @@
{{ school_schedule }}
</div>
{% 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 and live_lesson %}
{% include './open_lesson.html' %}
{% 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' %}
{% include './day_pay_btn.html' %}
{% endif %}
</div>
{% comment %}
@ -34,12 +36,12 @@
</div>
</div>
<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>
{% endif %}
</div>
<div class="timing__content">
{% if live_lesson %}
{% if live_lesson and live_lesson.short_description %}
{{ live_lesson.short_description }}
{% else %}
{{ school_schedule.description }}

@ -0,0 +1,32 @@
import requests
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError
# User = get_user_model()
from apps.user.models import User
class Command(BaseCommand):
help = 'Upload users to Roistat'
def handle(self, *args, **options):
users_queryset = User.objects.all()
users = [
{
'id': str(user.id),
'name': user.get_full_name(),
'phone': str(user.phone),
'email': user.email,
'birth_date': user.birthday.strftime('%d%m%Y') if user.birthday else None,
}
for user in users_queryset
]
project = settings.ROISTAT_PROJECT
key = settings.ROISTAT_KEY
url = settings.ROISTAT_API_URL + f'/project/clients/import?key={key}&project={project}'
resp = requests.post(url, json=users)
self.stdout.write(str(resp))
self.stdout.write(str(resp.json()))

@ -1,6 +1,9 @@
import requests
from mixpanel import Mixpanel
from django.conf import settings
from django.contrib.auth import get_user_model
from project.celery import app
@ -20,3 +23,23 @@ def user_to_mixpanel(user_id, email, phone, first_name, last_name, date_joined,
'subscriptions': subscriptions,
}
)
@app.task
def users_to_roistat():
User = get_user_model()
users_queryset = User.objects.all()
users = [
{
'id': str(user.id),
'name': user.get_full_name(),
'phone': str(user.phone),
'email': user.email,
'birth_date': user.birthday.strftime('%d%m%Y') if user.birthday else None,
}
for user in users_queryset
]
project = settings.ROISTAT_PROJECT
key = settings.ROISTAT_KEY
url = settings.ROISTAT_API_URL + f'/project/clients/import?key={key}&project={project}'
resp = requests.post(url, json=users)

@ -20,3 +20,5 @@ PAYMENTWALL_SECRET_KEY=4ea515bf94e34cf28646c2e12a7b8707
MIXPANEL_TOKEN=79bd6bfd98667ed977737e6810b8abcd
RAVEN_DSN=https://b545dac0ae0545a1bcfc443326fe5850:6f9c900cef7f4c11b63561030b37d15c@sentry.io/1197254
ROISTAT_COUNTER_ID=09db30c750035ae3d70a41d5f10d59ec
ROISTAT_PROJECT=84418
ROISTAT_KEY=a2d82a254478c1727adf0eb7ceb669f5

@ -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"]

@ -0,0 +1,18 @@
FROM node:9.11.1-alpine as front
RUN apk update && apk add python alpine-sdk --no-cache
WORKDIR /web/
ADD ./web/yarn.lock /web/
ADD ./web/package.json /web/
RUN yarn install
ADD ./web/ /web/
RUN yarn build
FROM python:3.6
ENV PYTHONUNBUFFERED 1
RUN mkdir /app
WORKDIR /app
ADD requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
ADD . /app/
COPY --from=front /web/build/ /app/web/build/
RUN python manage.py collectstatic --no-input

@ -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

@ -2,27 +2,32 @@ version: '3'
services:
db:
image: postgres:10-alpine
image: postgres:alpine
restart: always
env_file:
- .env
ports:
- "127.0.0.1:5432:5432"
volumes:
- ./postgres_data:/var/lib/postgresql/data/pgdata
- ./data/postgres_dev:/var/lib/postgresql/data
redis:
image: redis:latest
image: redis:alpine
restart: always
ports:
- "127.0.0.1:6379:6379"
volumes:
- ./redis_data:/data
- ./data/redis_dev:/data
web:
build: .
build:
context: ../
dockerfile: docker/Dockerfile.dev
restart: always
volumes:
- .:/lilcity
command: bash -c "python manage.py collectstatic --no-input && python manage.py migrate && python manage.py loaddata /lilcity/apps/*/fixtures/*.json && gunicorn --workers=4 project.wsgi --bind=0.0.0.0:8000 --worker-class=gthread --reload"
- ..:/app
- ./data/media:/app/media
command: bash -c "python manage.py collectstatic --no-input && python manage.py migrate && gunicorn --workers=4 project.wsgi --bind=0.0.0.0:8000 --worker-class=gthread --reload"
env_file:
- .env
ports:
@ -35,10 +40,13 @@ services:
- redis
workers:
build: .
build:
context: ../
dockerfile: docker/Dockerfile.dev
restart: always
volumes:
- .:/lilcity
- ..:/app
- ./data/media:/app/media
command: bash -c "celery worker -A project -B"
env_file:
- .env

@ -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

@ -44,6 +44,7 @@ INSTALLED_APPS = [
] + [
'anymail',
'active_link',
'compressor',
'django_filters',
'polymorphic_tree',
'polymorphic',
@ -163,6 +164,11 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'web/build'),
]
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
]
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
@ -224,6 +230,11 @@ CELERY_BEAT_SCHEDULE = {
'schedule': timedelta(minutes=5) if DEBUG else crontab(minute=0, hour=0),
'args': (),
},
'update_users_in_roistat': {
'task': 'apps.user.tasks.users_to_roistat',
'schedule': timedelta(hours=1),
'args': (),
},
}
try:
@ -260,12 +271,17 @@ RAVEN_CONFIG = {
}
# Roistat counter id
ROISTAT_API_URL = 'https://cloud.roistat.com/api/v1'
ROISTAT_COUNTER_ID = os.getenv('ROISTAT_COUNTER_ID', None)
ROISTAT_PROJECT = os.getenv('ROISTAT_PROJECT', None)
ROISTAT_KEY = os.getenv('ROISTAT_KEY', None)
INSTAGRAM_RESULTS_PATH = 'media/instagram/results/'
DATA_UPLOAD_MAX_MEMORY_SIZE = 20242880
COMPRESS_ENABLED = True
try:
from .local_settings import *
except ImportError:

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

@ -11,31 +11,61 @@
<div class="main__title">
<span class="main__bold">Lil School</span> — первая образовательная онлайн-платформа креативного мышления для детей
</div>
<div class="main__subtitle">
Урок <b>Рисовальный лагерь, Альбрехт Дюрер</b> завершен <br/>
Следующий урок пройдет 4 июля
{% if user.is_authenticated and online %}
<div class="main__content">
Сейчас идёт прямой эфир урока «{{ school_schedule.title }}, {{ school_schedule.current_live_lesson.title }}»
</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 %}
{% if not is_purchased %}
data-popup=".js-popup-buy"
{% endif %}
href='#'
{% else %}
href="{{ school_schedule.current_live_lesson.get_absolute_url }}"
{% endif %}
class="main__btn btn"
>{% 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="">
Урок «{{ school_schedule.title }}, {{ school_schedule.current_live_lesson.title }}» начнётся
</div>
<div class="main__time">
{{ school_schedule.start_at_humanize }}
</div>
<div class="main__actions">
<a
{% if not is_purchased %}
data-popup=".js-popup-buy"
href='#'
{% else %}
href="{{ school_schedule.current_live_lesson.get_absolute_url }}"
{% endif %}
class="main__btn btn"
href="#"
class="main__btn btn"
>{% if not is_purchased %}Получить доступ{% else %}Смотреть урок{% endif %}</a>
</div>
{% else %}
<div class="main__subtitle">
Присоединяйтесь в Рисовальный лагерь
</div>
<div class="main__actions">
{% 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 %}
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 %}
</a>
Получить доступ
</a>
{% endif %}
<a class="main__btn btn btn_white" href="{% url 'school:summer-school' %}">О лагере</a>
</div>
{% endif %}
</div>
</div>

@ -1,5 +1,6 @@
{% load static %}
{% load setting from settings %}
{% load compress %}
<!DOCTYPE html>
<html>
@ -24,14 +25,18 @@
<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">
{% comment %} <meta property="fb:admins" content="Facebook numeric ID"> {% endcomment %}
<meta name="csrf-token" content="{{ csrf_token }}">
<link rel="stylesheet" media="all" href={% static "app.css" %}?7>
{% compress css %}
<link rel="stylesheet" media="all" href={% static "app.css" %}>
{% endcompress %}
<link rel="shortcut icon" type="image/png" href="{% static 'img/favicon.png' %}"/>
<script>
var viewportmeta = document.querySelector('meta[name="viewport"]');
@ -52,6 +57,26 @@
COURSE_ID = "{{ course.id }}";
MIXPANEL_CUSTOM_LIB_URL = "/static/mixpanel-2-latest.js";
</script>
<!-- Facebook Pixel Code -->
<script>
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window,document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '194961257900508');
fbq('track', 'PageView');
</script>
<noscript>
<img height="1" width="1"
src="https://www.facebook.com/tr?id=194961257900508&ev=PageView
&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>
{% include "templates/blocks/mixpanel.html" %}
</head>
<body>
@ -75,8 +100,7 @@
</script>
{% comment %} ROISTAT {% endcomment %}
<script>
ROISTAT_COUNTER_ID = "{% setting 'ROISTAT_COUNTER_ID' %}"
(function(w, d, s, h, id) { w.roistatProjectId = id; w.roistatHost = h; var p = d.location.protocol == "https:" ? "https://" : "http://"; var u = /^.*roistat_visit=[^;]+(.*)?$/.test(d.cookie) ? "/dist/module.js" : "/api/site/1.0/"+id+"/init"; var js = d.createElement(s); js.charset="UTF-8"; js.async = 1; js.src = p+h+u; var js2 = d.getElementsByTagName(s)[0]; js2.parentNode.insertBefore(js, js2);})(window, document, 'script', 'cloud.roistat.com', ROISTAT_COUNTER_ID);
(function(w, d, s, h, id) { w.roistatProjectId = id; w.roistatHost = h; var p = d.location.protocol == "https:" ? "https://" : "http://"; var u = /^.*roistat_visit=[^;]+(.*)?$/.test(d.cookie) ? "/dist/module.js" : "/api/site/1.0/"+id+"/init"; var js = d.createElement(s); js.charset="UTF-8"; js.async = 1; js.src = p+h+u; var js2 = d.getElementsByTagName(s)[0]; js2.parentNode.insertBefore(js, js2);})(window, document, 'script', 'cloud.roistat.com', '{% setting "ROISTAT_COUNTER_ID" %}');
</script>
{% block foot %}{% endblock foot %}
</body>

@ -27,6 +27,8 @@ class IndexView(TemplateView):
school_schedule = SchoolSchedule.objects.get(weekday=now_time.isoweekday())
except SchoolSchedule.DoesNotExist:
online = False
online_coming_soon = False
school_schedule = None
else:
end_at = datetime.combine(now_time.today(), school_schedule.start_at)
online = (
@ -34,6 +36,14 @@ class IndexView(TemplateView):
(end_at + timedelta(hours=1)).time() >= now_time.time() and
school_schedule.current_live_lesson()
)
online_coming_soon = (
school_schedule.start_at > now_time.time() and
(
datetime.combine(datetime.today(), school_schedule.start_at) - timedelta(hours=12)
).time() <= now_time.time() and
school_schedule.current_live_lesson()
)
date_now = now_time.date()
if self.request.user.is_authenticated:
school_payment = SchoolPayment.objects.filter(
@ -62,6 +72,8 @@ class IndexView(TemplateView):
context.update({
'online': online,
'online_coming_soon': online_coming_soon,
'school_schedule': school_schedule,
'course_items': Course.objects.filter(status=Course.PUBLISHED)[:6],
'is_purchased': school_payment_exists,
'min_school_price': SchoolSchedule.objects.aggregate(Min('month_price'))['month_price__min'],

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

@ -620,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; }
@ -629,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;
@ -654,21 +658,10 @@
}
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;
@ -1060,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,

Loading…
Cancel
Save