diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index d07e58b5..d1e8dcba 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -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, diff --git a/apps/payment/management/__init__.py b/apps/payment/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/payment/management/commands/__init__.py b/apps/payment/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/payment/management/commands/roistat_set_statuses.py b/apps/payment/management/commands/roistat_set_statuses.py new file mode 100644 index 00000000..f3ac3e11 --- /dev/null +++ b/apps/payment/management/commands/roistat_set_statuses.py @@ -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())) diff --git a/apps/payment/tasks.py b/apps/payment/tasks.py index 3f487f76..911235a3 100644 --- a/apps/payment/tasks.py +++ b/apps/payment/tasks.py @@ -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)) diff --git a/apps/payment/views.py b/apps/payment/views.py index c9186a25..34d35a0b 100644 --- a/apps/payment/views.py +++ b/apps/payment/views.py @@ -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(): diff --git a/apps/school/models.py b/apps/school/models.py index 24ce0590..d6d58df7 100644 --- a/apps/school/models.py +++ b/apps/school/models.py @@ -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] diff --git a/apps/user/management/__init__.py b/apps/user/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/user/management/commands/__init__.py b/apps/user/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/user/management/commands/users_to_roistat.py b/apps/user/management/commands/users_to_roistat.py new file mode 100644 index 00000000..84ae366e --- /dev/null +++ b/apps/user/management/commands/users_to_roistat.py @@ -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())) diff --git a/apps/user/tasks.py b/apps/user/tasks.py index a95988be..e35accef 100644 --- a/apps/user/tasks.py +++ b/apps/user/tasks.py @@ -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) diff --git a/docker/.env.example b/docker/.env.example index bc750b18..94fdccb7 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -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 diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev new file mode 100644 index 00000000..9ae8465a --- /dev/null +++ b/docker/Dockerfile.dev @@ -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 diff --git a/docker/docker-compose-dev.yml_notwork b/docker/docker-compose-dev.yml similarity index 57% rename from docker/docker-compose-dev.yml_notwork rename to docker/docker-compose-dev.yml index 4de5b13a..9b636ee2 100644 --- a/docker/docker-compose-dev.yml_notwork +++ b/docker/docker-compose-dev.yml @@ -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 diff --git a/project/settings.py b/project/settings.py index b8b749d2..9323745c 100644 --- a/project/settings.py +++ b/project/settings.py @@ -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: diff --git a/project/templates/blocks/promo.html b/project/templates/blocks/promo.html index 74d66932..8fa641f4 100644 --- a/project/templates/blocks/promo.html +++ b/project/templates/blocks/promo.html @@ -11,13 +11,44 @@