Merge branch 'dev' into 'master'

Roistat & dev docker & fix roistat counter

See merge request lilcity/backend!58
remotes/origin/hasaccess^2
cfwme 8 years ago
commit 43bfd29782
  1. 5
      api/v1/serializers/course.py
  2. 0
      apps/payment/management/__init__.py
  3. 0
      apps/payment/management/commands/__init__.py
  4. 50
      apps/payment/management/commands/roistat_set_statuses.py
  5. 25
      apps/payment/tasks.py
  6. 11
      apps/payment/views.py
  7. 9
      apps/school/models.py
  8. 0
      apps/user/management/__init__.py
  9. 0
      apps/user/management/commands/__init__.py
  10. 32
      apps/user/management/commands/users_to_roistat.py
  11. 23
      apps/user/tasks.py
  12. 2
      docker/.env.example
  13. 18
      docker/Dockerfile.dev
  14. 26
      docker/docker-compose-dev.yml
  15. 16
      project/settings.py
  16. 43
      project/templates/blocks/promo.html
  17. 31
      project/templates/lilcity/index.html
  18. 12
      project/views.py
  19. 1
      requirements.txt

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

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

@ -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,23 @@ 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):
body = [{
'id': str(payment_id),
'name': event_name,
'date_create': time,
'status': str(status),
'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
@ -251,6 +251,15 @@ 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,
)
author_balance = getattr(payment, 'author_balance', None)
if author_balance and author_balance.type == AuthorBalance.IN:
if pingback.is_deliverable():

@ -1,6 +1,8 @@
import arrow
from datetime import datetime, timedelta
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 +67,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)).humanize(locale='ru') if self.start_at else None
class SchoolScheduleImage(models.Model):
schoolschedule = models.ForeignKey(
@ -121,6 +127,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]

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

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

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

@ -11,13 +11,44 @@
<div class="main__title">
<span class="main__bold">Lil School</span> — первая образовательная онлайн-платформа креативного мышления для детей
</div>
{% if user.is_authenticated and online %}
<div class="main__content">
Сейчас идёт прямой эфир урока «{{ school_schedule.title }}»
</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"
>{% 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>
<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"
>{% if not is_purchased %}Получить доступ{% else %}Смотреть урок{% endif %}</a>
</div>
{% else %}
<div class="main__subtitle">
Урок <b>Рисовальный лагерь, Поль Синьяк</b> пройдет сегодня в 17:00 <br/>
Присоединяйтесь в Рисовальный лагерь
</div>
<div class="main__actions">
{% if is_purchased %}
<a class="main__btn btn" href="/school/lessons/216/">Перейти в урок</a>
{% else %}
<a
{% if not is_purchased_future %}
{% if not user.is_authenticated %}
@ -31,10 +62,10 @@
>
{% if not is_purchased and not is_purchased_future %}Получить доступ{% endif %}
{% if is_purchased_future and not is_purchased %}ваша подписка начинается {{school_purchased_future.date_start}}{% endif %}
{% if is_purchased %}ваша подписка истекает {{ subscription_ends_humanize }}<br/>перейти к оплате{% endif %}
</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,7 +100,7 @@
</script>
{% comment %} ROISTAT {% endcomment %}
<script>
(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' %}');
(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'],

@ -5,6 +5,7 @@ Django==2.0.6
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

Loading…
Cancel
Save