Merge remote-tracking branch 'origin/dev' into dev

remotes/origin/hasaccess
Vitaly Baev 8 years ago
commit 0590e7aa42
  1. 1
      .dockerignore
  2. 25
      api/v1/serializers/payment.py
  3. 2
      api/v1/serializers/user.py
  4. 2
      api/v1/urls.py
  5. 14
      api/v1/views.py
  6. 24
      apps/course/templates/course/course.html
  7. 10
      apps/course/views.py
  8. 60
      apps/payment/admin.py
  9. 16
      apps/payment/migrations/0002_delete_purchase.py
  10. 71
      apps/payment/migrations/0003_auto_20180220_1912.py
  11. 32
      apps/payment/migrations/0004_auto_20180221_1022.py
  12. 25
      apps/payment/migrations/0005_auto_20180221_1120.py
  13. 29
      apps/payment/migrations/0006_auto_20180221_1126.py
  14. 24
      apps/payment/migrations/0007_auto_20180221_1258.py
  15. 20
      apps/payment/migrations/0008_auto_20180221_1335.py
  16. 19
      apps/payment/migrations/0009_auto_20180222_0955.py
  17. 23
      apps/payment/migrations/0010_auto_20180227_0933.py
  18. 119
      apps/payment/models.py
  19. 12
      apps/payment/templates/payment/payment_error.html
  20. 12
      apps/payment/templates/payment/payment_success.html
  21. 9
      apps/payment/templates/payment/paymentwall_widget.html
  22. 153
      apps/payment/views.py
  23. 18
      apps/school/migrations/0004_auto_20180221_1120.py
  24. 18
      apps/school/migrations/0005_schoolschedule_day_discount.py
  25. 6
      apps/school/models.py
  26. 10
      apps/user/models.py
  27. 24
      apps/user/templates/user/payment-history.html
  28. 5
      apps/user/views.py
  29. 9
      project/settings.py
  30. 59
      project/templates/lilcity/index.html
  31. 129
      project/templates/lilcity/main.html
  32. 17
      project/urls.py
  33. 3
      requirements.txt

@ -0,0 +1 @@
venv*/

@ -0,0 +1,25 @@
from rest_framework import serializers
from apps.payment.models import AuthorBalance
class AuthorBalanceSerializer(serializers.ModelSerializer):
class Meta:
model = AuthorBalance
fields = (
'id',
'author',
'type',
'amount',
'commission',
'status',
'payment',
)
read_only_fields = (
'id',
'author',
'type',
'payment',
)

@ -35,6 +35,7 @@ class UserSerializer(serializers.ModelSerializer):
'fb_data',
'is_email_proved',
'photo',
'balance',
)
read_only_fields = (
@ -44,6 +45,7 @@ class UserSerializer(serializers.ModelSerializer):
'is_staff',
'fb_id',
'fb_data',
'balance',
)

@ -8,6 +8,7 @@ from drf_yasg import openapi
from .auth import ObtainToken
from .views import (
AuthorBalanceViewSet,
CategoryViewSet, CourseViewSet,
MaterialViewSet, LikeViewSet,
ImageViewSet, TextViewSet,
@ -18,6 +19,7 @@ from .views import (
)
router = DefaultRouter()
router.register(r'author-balance', AuthorBalanceViewSet, base_name='author-balance')
router.register(r'courses', CourseViewSet, base_name='courses')
router.register(r'categories', CategoryViewSet, base_name='categories')
router.register(r'materials', MaterialViewSet, base_name='materials')

@ -24,6 +24,7 @@ from .serializers.content import (
ImageObjectSerializer,
)
from .serializers.school import SchoolScheduleSerializer
from .serializers.payment import AuthorBalanceSerializer
from .serializers.user import (
UserSerializer, UserPhotoSerializer,
)
@ -35,10 +36,23 @@ from apps.content.models import (
Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject,
)
from apps.payment.models import AuthorBalance
from apps.school.models import SchoolSchedule
User = get_user_model()
class AuthorBalanceViewSet(ExtendedModelViewSet):
queryset = AuthorBalance.objects.all()
serializer_class = AuthorBalanceSerializer
permission_classes = (IsAdmin,)
filter_fields = ('status', 'type')
search_fields = (
'author__email',
'author__first_name',
'author__last_name',
)
class ImageObjectViewSet(ExtendedModelViewSet):
queryset = ImageObject.objects.all()
serializer_class = ImageObjectSerializer

@ -25,14 +25,18 @@
</div>
<div class="go__title">Вернуться</div>
</a>
<button
class="go__btn btn btn_md"
{% if not paid %}
<a
class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md"
{% if user.is_authenticated %}
data-popup=".js-popup-buy"
{% if not pending %}
href="{% url 'course-checkout' course.id %}"
{% endif %}
{% else %}
data-popup=".js-popup-auth"
{% endif %}
>КУПИТЬ КУРС</button>
>{% if pending %}ОЖИДАЕТСЯ ПОДТВЕРЖДЕНИЕ ОПЛАТЫ{% else %}КУПИТЬ КУРС{% endif %}</a>
{% endif %}
</div>
<div
class="course"
@ -383,14 +387,18 @@
<div class="meta__title">{{ course.price|floatformat:"-2" }}₽</div>
</div>
</div>
<button
class="course__buy btn btn_md"
{% if not paid %}
<a
class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md"
{% if user.is_authenticated %}
data-popup=".js-popup-buy"
{% if not pending %}
href="{% url 'course-checkout' course.id %}"
{% endif %}
{% else %}
data-popup=".js-popup-auth"
{% endif %}
>КУПИТЬ КУРС</button>
>{% if pending %}ОЖИДАЕТСЯ ПОДТВЕРЖДЕНИЕ ОПЛАТЫ{% else %}КУПИТЬ КУРС{% endif %}</a>
{% endif %}
</div>
</div>
</div>

@ -8,6 +8,8 @@ from django.views.generic import View, CreateView, DetailView, ListView, Templat
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from apps.payment.models import AuthorBalance
from .models import Course, Like, Lesson, CourseComment, LessonComment
from .filters import CourseFilter
@ -187,6 +189,14 @@ class CourseView(DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['next'] = self.request.GET.get('next', None)
context['paid'] = self.object.payments.filter(
user=self.request.user,
authorbalance__status=AuthorBalance.ACCEPTED,
).exists()
context['pending'] = self.object.payments.filter(
user=self.request.user,
authorbalance__status=AuthorBalance.PENDING,
).exists()
return context
def get_queryset(self):

@ -1,3 +1,61 @@
from django.contrib import admin
from polymorphic.admin import (
PolymorphicParentModelAdmin,
PolymorphicChildModelAdmin,
PolymorphicChildModelFilter,
)
# Register your models here.
from .models import AuthorBalance, CoursePayment, SchoolPayment, Payment
@admin.register(AuthorBalance)
class AuthorBalanceAdmin(admin.ModelAdmin):
list_display = (
'author',
'type',
'amount',
'commission',
'status',
'payment',
)
class PaymentChildAdmin(PolymorphicChildModelAdmin):
base_model = Payment
show_in_index = True
list_display = (
'id',
'user',
'amount',
'status',
)
base_fieldsets = (
(None, {'fields': ('user', 'amount', 'status', 'data',)}),
)
readonly_fields = ('amount', 'status', 'data',)
@admin.register(CoursePayment)
class CoursePaymentAdmin(PaymentChildAdmin):
base_model = CoursePayment
list_display = PaymentChildAdmin.list_display + ('course',)
@admin.register(SchoolPayment)
class SchoolPaymentAdmin(PaymentChildAdmin):
base_model = SchoolPayment
list_display = PaymentChildAdmin.list_display + (
'weekdays',
'date_start',
'date_end',
)
@admin.register(Payment)
class PaymentAdmin(PolymorphicParentModelAdmin):
base_model = Payment
polymorphic_list = True
child_models = (
CoursePayment,
SchoolPayment,
)

@ -0,0 +1,16 @@
# Generated by Django 2.0.2 on 2018-02-20 17:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('payment', '0001_initial'),
]
operations = [
migrations.DeleteModel(
name='Purchase',
),
]

@ -0,0 +1,71 @@
# Generated by Django 2.0.2 on 2018-02-20 19:12
from django.conf import settings
import django.contrib.postgres.fields
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('course', '0034_auto_20180215_1503'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
('payment', '0002_delete_purchase'),
]
operations = [
migrations.CreateModel(
name='Payment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='Итого')),
('status', models.PositiveSmallIntegerField(choices=[(0, 'regular'), (1, 'goodwill'), (2, 'negative'), (200, 'risk under review'), (201, 'risk reviewed accepted'), (202, 'risk reviewed declined'), (203, 'risk authorization voided'), (12, 'subscription cancelation'), (13, 'subscription expired'), (14, 'subscription payment failed')], verbose_name='Статус платежа')),
('data', django.contrib.postgres.fields.jsonb.JSONField(default={}, verbose_name='Данные платежа от провайдера')),
],
options={
'verbose_name': 'Платеж',
'verbose_name_plural': 'Платежи',
},
),
migrations.CreateModel(
name='CoursePayment',
fields=[
('payment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='payment.Payment')),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.Course', verbose_name='Курс')),
],
options={
'verbose_name': 'Платеж за курс',
'verbose_name_plural': 'Платежи за курсы',
},
bases=('payment.payment',),
),
migrations.CreateModel(
name='SchoolPayment',
fields=[
('payment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='payment.Payment')),
('weekdays', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=7, verbose_name='Дни недели')),
('date_start', models.DateField(verbose_name='Дата начала подписки')),
('date_end', models.DateField(verbose_name='Дата окончания подписки')),
],
options={
'verbose_name': 'Платеж за курс',
'verbose_name_plural': 'Платежи за курсы',
},
bases=('payment.payment',),
),
migrations.AddField(
model_name='payment',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_payment.payment_set+', to='contenttypes.ContentType'),
),
migrations.AddField(
model_name='payment',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь'),
),
]

@ -0,0 +1,32 @@
# Generated by Django 2.0.2 on 2018-02-21 10:22
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('payment', '0003_auto_20180220_1912'),
]
operations = [
migrations.CreateModel(
name='AuthorBalance',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.PositiveSmallIntegerField(choices=[(0, 'In'), (1, 'Out')], default=0, verbose_name='Тип')),
('amount', models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='Итого')),
('commission', models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='Комиссия')),
('status', models.PositiveSmallIntegerField(choices=[(0, 'Pending'), (1, 'Accepted'), (2, 'Declined')], default=0, verbose_name='Статус')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Автор')),
('payment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='payment.Payment', verbose_name='Платёж')),
],
),
migrations.AlterModelOptions(
name='schoolpayment',
options={'verbose_name': 'Платеж за школу', 'verbose_name_plural': 'Платежи за школу'},
),
]

@ -0,0 +1,25 @@
# Generated by Django 2.0.2 on 2018-02-21 11:20
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('payment', '0004_auto_20180221_1022'),
]
operations = [
migrations.AlterField(
model_name='authorbalance',
name='author',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Автор'),
),
migrations.AlterField(
model_name='authorbalance',
name='payment',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='payment.Payment', verbose_name='Платёж'),
),
]

@ -0,0 +1,29 @@
# Generated by Django 2.0.2 on 2018-02-21 11:26
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payment', '0005_auto_20180221_1120'),
]
operations = [
migrations.AlterField(
model_name='payment',
name='amount',
field=models.DecimalField(decimal_places=2, default=0, editable=False, max_digits=8, verbose_name='Итого'),
),
migrations.AlterField(
model_name='payment',
name='data',
field=django.contrib.postgres.fields.jsonb.JSONField(default={}, editable=False, verbose_name='Данные платежа от провайдера'),
),
migrations.AlterField(
model_name='payment',
name='status',
field=models.PositiveSmallIntegerField(choices=[(0, 'regular'), (1, 'goodwill'), (2, 'negative'), (200, 'risk under review'), (201, 'risk reviewed accepted'), (202, 'risk reviewed declined'), (203, 'risk authorization voided'), (12, 'subscription cancelation'), (13, 'subscription expired'), (14, 'subscription payment failed')], editable=False, null=True, verbose_name='Статус платежа'),
),
]

@ -0,0 +1,24 @@
# Generated by Django 2.0.2 on 2018-02-21 12:58
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('payment', '0006_auto_20180221_1126'),
]
operations = [
migrations.AlterModelOptions(
name='authorbalance',
options={'verbose_name': 'Баланс', 'verbose_name_plural': 'Балансы'},
),
migrations.AlterField(
model_name='authorbalance',
name='author',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='balances', to=settings.AUTH_USER_MODEL, verbose_name='Автор'),
),
]

@ -0,0 +1,20 @@
# Generated by Django 2.0.2 on 2018-02-21 13:35
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('payment', '0007_auto_20180221_1258'),
]
operations = [
migrations.AlterField(
model_name='payment',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь'),
),
]

@ -0,0 +1,19 @@
# Generated by Django 2.0.2 on 2018-02-22 09:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('payment', '0008_auto_20180221_1335'),
]
operations = [
migrations.AlterField(
model_name='coursepayment',
name='course',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='course.Course', verbose_name='Курс'),
),
]

@ -0,0 +1,23 @@
# Generated by Django 2.0.2 on 2018-02-27 09:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payment', '0009_auto_20180222_0955'),
]
operations = [
migrations.AlterField(
model_name='schoolpayment',
name='date_end',
field=models.DateField(blank=True, null=True, verbose_name='Дата окончания подписки'),
),
migrations.AlterField(
model_name='schoolpayment',
name='date_start',
field=models.DateField(blank=True, null=True, verbose_name='Дата начала подписки'),
),
]

@ -1,15 +1,112 @@
from django.db import models
from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import ArrayField, JSONField
from constance import config
from paymentwall import Pingback
from polymorphic.models import PolymorphicModel
class Purchase(models.Model):
COMPLETE = 'COMPLETE'
CHARGEBACK = 'CHARGEBACK'
REFUNDED = 'REFUNDED'
ERROR = 'ERROR'
PENDING = 'PENDING'
from apps.course.models import Course
from apps.school.models import SchoolSchedule
transaction_id = models.PositiveIntegerField()
status = models.CharField(max_length=50)
product_id = models.PositiveIntegerField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
User = get_user_model()
class AuthorBalance(models.Model):
IN = 0
OUT = 1
TYPE_CHOICES = (
(IN, 'In'),
(OUT, 'Out'),
)
PENDING = 0
ACCEPTED = 1
DECLINED = 2
STATUS_CHOICES = (
(PENDING, 'Pending'),
(ACCEPTED, 'Accepted'),
(DECLINED, 'Declined'),
)
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='Автор',
null=True, blank=True, related_name='balances')
type = models.PositiveSmallIntegerField('Тип', choices=TYPE_CHOICES, default=0)
amount = models.DecimalField('Итого', max_digits=8, decimal_places=2, default=0)
commission = models.DecimalField('Комиссия', max_digits=8, decimal_places=2, default=0)
status = models.PositiveSmallIntegerField('Статус', choices=STATUS_CHOICES, default=0)
payment = models.OneToOneField('Payment', on_delete=models.CASCADE, null=True, blank=True, verbose_name='Платёж')
class Meta:
verbose_name = 'Баланс'
verbose_name_plural = 'Балансы'
class Payment(PolymorphicModel):
PW_STATUS_CHOICES = (
(Pingback.PINGBACK_TYPE_REGULAR, 'regular',),
(Pingback.PINGBACK_TYPE_GOODWILL, 'goodwill',),
(Pingback.PINGBACK_TYPE_NEGATIVE, 'negative',),
(Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW, 'risk under review',),
(Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, 'risk reviewed accepted',),
(Pingback.PINGBACK_TYPE_RISK_REVIEWED_DECLINED, 'risk reviewed declined',),
(Pingback.PINGBACK_TYPE_RISK_AUTHORIZATION_VOIDED, 'risk authorization voided',),
(Pingback.PINGBACK_TYPE_SUBSCRIPTION_CANCELLATION, 'subscription cancelation',),
(Pingback.PINGBACK_TYPE_SUBSCRIPTION_EXPIRED, 'subscription expired',),
(Pingback.PINGBACK_TYPE_SUBSCRIPTION_PAYMENT_FAILED, 'subscription payment failed',),
)
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='Пользователь', related_name='payments')
amount = models.DecimalField('Итого', max_digits=8, decimal_places=2, default=0, editable=False)
status = models.PositiveSmallIntegerField('Статус платежа', choices=PW_STATUS_CHOICES, null=True, editable=False)
data = JSONField('Данные платежа от провайдера', default={}, editable=False)
class Meta:
verbose_name = 'Платеж'
verbose_name_plural = 'Платежи'
def calc_commission(self):
return self.amount * config.SERVICE_COMMISSION / 100
class CoursePayment(Payment):
course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name='Курс', related_name='payments')
class Meta:
verbose_name = 'Платеж за курс'
verbose_name_plural = 'Платежи за курсы'
def save(self, *args, **kwargs):
self.amount = self.course.price
super().save(*args, **kwargs)
author_balance = getattr(self, 'authorbalance', None)
if not author_balance:
AuthorBalance.objects.create(
author=self.course.author,
amount=self.amount,
payment=self,
commission=self.calc_commission(),
)
else:
author_balance.amount = self.amount
author_balance.commission = self.calc_commission()
author_balance.save()
class SchoolPayment(Payment):
weekdays = ArrayField(models.IntegerField(), size=7, verbose_name='Дни недели')
date_start = models.DateField('Дата начала подписки', null=True, blank=True)
date_end = models.DateField('Дата окончания подписки', null=True, blank=True)
class Meta:
verbose_name = 'Платеж за школу'
verbose_name_plural = 'Платежи за школу'
def save(self, *args, **kwargs):
aggregate = SchoolSchedule.objects.filter(
weekday__in=self.weekdays,
).aggregate(
models.Sum('month_price'),
models.Sum('day_discount'),
)
month_price_sum = aggregate.get('month_price__sum', 0)
day_discount_sum = aggregate.get('day_discount__sum', 0) if len(self.weekdays) == 7 else 0
self.amount = month_price_sum - day_discount_sum
super().save(*args, **kwargs)

@ -0,0 +1,12 @@
{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %}
<div class="section">
<div class="section__center center center_xs">
<div class="done">
<div class="done__title title">Произошла ошибка!</div>
<div class="done__foot">
<a class="done__btn btn btn_md btn_stroke" href="/">ПЕРЕЙТИ К ГЛАВНОЙ</a>
</div>
</div>
</div>
</div>
{% endblock content %}

@ -0,0 +1,12 @@
{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %}
<div class="section">
<div class="section__center center center_xs">
<div class="done">
<div class="done__title title">Вы успешно приобрели курс!</div>
<div class="done__foot">
<a class="done__btn btn btn_md btn_stroke" href="/">ПЕРЕЙТИ К ГЛАВНОЙ</a>
</div>
</div>
</div>
</div>
{% endblock content %}

@ -0,0 +1,9 @@
{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %}
<div class="section">
<div class="section__center center">
{% autoescape off %}
{{ widget }}
{% endautoescape %}
</div>
</div>
{% endblock content %}

@ -1,27 +1,92 @@
from django.utils.decorators import method_decorator
from django.views.generic import View
from django.views.decorators.csrf import csrf_exempt
from django.contrib import messages
from django.http import HttpResponse
from django.shortcuts import redirect
from django.views.generic import View, TemplateView
from django.views.decorators.csrf import csrf_exempt
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from paymentwall import Pingback, Product, Widget
from apps.course.models import Course
from apps.school.models import SchoolSchedule
from paymentwall.pingback import Pingback
from .models import AuthorBalance, CoursePayment, SchoolPayment
from .models import Purchase
class CourseBuyView(TemplateView):
template_name = 'payment/paymentwall_widget.html'
def get(self, request, pk=None, *args, **kwargs):
host = request.scheme + '://' + request.get_host()
course = Course.objects.get(id=pk)
course_payment = CoursePayment.objects.create(
user=request.user,
course=course,
)
product = Product(
f'course_{course_payment.id}',
course.price,
'RUB',
f'Курс "{course.title}"',
)
widget = Widget(
str(request.user.id),
'p1',
[product],
extra_params={
'lang': 'ru',
'evaluation': 1,
'success_url': host + str(reverse_lazy('payment-success')),
'failure_url': host + str(reverse_lazy('payment-error')),
}
)
return self.render_to_response(context={'widget': widget.get_html_code()})
class SchoolBuyView(TemplateView):
template_name = 'payment/paymentwall_widget.html'
def get(self, request, *args, **kwargs):
host = request.scheme + '://' + request.get_host()
weekdays = set(request.GET.getlist('weekdays', []))
if not weekdays:
messages.error(request, 'Выберите несколько дней недели.')
return redirect('index')
try:
weekdays = [int(weekday) for weekday in weekdays]
except ValueError:
messages.error(request, 'Ошибка выбора дней недели.')
return redirect('index')
school_payment = SchoolPayment.objects.create(
user=request.user,
weekdays=weekdays,
)
product = Product(
f'school_{school_payment.id}',
school_payment.amount,
'RUB',
'Школа',
)
widget = Widget(
str(request.user.id),
'p1',
[product],
extra_params={
'lang': 'ru',
'evaluation': 1,
'success_url': host + str(reverse_lazy('payment-success')),
'failure_url': host + str(reverse_lazy('payment-error')),
}
)
return self.render_to_response(context={'widget': widget.get_html_code()})
@method_decorator(csrf_exempt, name='dispatch')
class PaymentwallCallbackView(View):
CHARGEBACK = '1'
CREDIT_CARD_FRAUD = '2'
ORDER_FRAUD = '3'
BAD_DATA = '4'
FAKE_PROXY_USER = '5'
REJECTED_BY_ADVERTISER = '6'
DUPLICATED_CONVERSIONS = '7'
GOODWILL_CREDIT_TAKEN_BACK = '8'
CANCELLED_ORDER = '9'
PARTIALLY_REVERSED = '10'
def get_request_ip(self):
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
@ -31,46 +96,38 @@ class PaymentwallCallbackView(View):
return ip
def get(self, request, *args, **kwargs):
pingback = Pingback(request.GET.copy(), self.get_request_ip())
payment_raw_data = request.GET.copy()
pingback = Pingback(payment_raw_data, self.get_request_ip())
if pingback.validate():
cart_id = pingback.get_product().get_id()
product_type_name, payment_id = pingback.get_product().get_id().split('_')
# try:
# cart = CartModel.objects.get(pk=cart_id)
# except CartModel.DoesNotExist:
# log.error('Paymentwall pingback: Cant find cart, Paymentwall sent this data: {}'.format(request.GET.copy()))
# return HttpResponse(status=403)
if product_type_name == 'course':
product_payment_class = CoursePayment
elif product_type_name == 'school':
product_payment_class = SchoolPayment
else:
return HttpResponse(status=403)
try:
purchase = Purchase.objects.get(transaction_id=pingback.get_reference_id())
except Purchase.DoesNotExist:
# purchase = cart.create_purchase(transaction_id=pingback.get_reference_id())
pass
if pingback.is_deliverable():
purchase.status = Purchase.COMPLETE
payment = product_payment_class.objects.get(pk=payment_id)
except product_payment_class.DoesNotExist:
return HttpResponse(status=403)
elif pingback.is_cancelable():
reason = pingback.get_parameter('reason')
if reason == self.CHARGEBACK or reason == self.CREDIT_CARD_FRAUD or reason == self.ORDER_FRAUD or reason == self.PARTIALLY_REVERSED:
purchase.status = Purchase.CHARGEBACK
elif reason == self.CANCELLED_ORDER:
purchase.status = Purchase.REFUNDED
else:
purchase.status = Purchase.ERROR
payment.status = pingback.get_type()
payment.data = payment_raw_data
payment.save()
author_balance = getattr(payment, 'author_balance', None)
if author_balance:
if pingback.is_deliverable():
payment.author_balance.status = AuthorBalance.ACCEPTED
elif pingback.is_under_review():
purchase.status = Purchase.PENDING
payment.author_balance.status = AuthorBalance.PENDING
else:
# log.error('Paymentwall pingback: Unknown pingback type, Paymentwall sent this data: {}'.format(request.GET.copy()))
pass
payment.author_balance.status = AuthorBalance.DECLINED
# purchase.save()
return HttpResponse('OK', status=200)
payment.author_balance.save()
return HttpResponse('OK', status=403)
else:
# log.error('Paymentwall pingback: Cant validate pingback, error: {} Paymentwall sent this data: {}'.format(pingback.get_error_summary(), request.GET.copy()))
pass
return

@ -0,0 +1,18 @@
# Generated by Django 2.0.2 on 2018-02-21 11:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('school', '0003_auto_20180221_0901'),
]
operations = [
migrations.AlterField(
model_name='schoolschedule',
name='weekday',
field=models.PositiveSmallIntegerField(choices=[(1, 'понедельник'), (2, 'вторник'), (3, 'среда'), (4, 'четверг'), (5, 'пятница'), (6, 'суббота'), (7, 'воскресенье')], unique=True, verbose_name='День недели'),
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.2 on 2018-02-22 10:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('school', '0004_auto_20180221_1120'),
]
operations = [
migrations.AddField(
model_name='schoolschedule',
name='day_discount',
field=models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='Скидка, в валюте'),
),
]

@ -11,14 +11,18 @@ class SchoolSchedule(models.Model):
(6, 'суббота'),
(7, 'воскресенье'),
)
weekday = models.PositiveSmallIntegerField('День недели', choices=WEEKDAY_CHOICES)
weekday = models.PositiveSmallIntegerField('День недели', choices=WEEKDAY_CHOICES, unique=True)
title = models.CharField('Заголовок', default='', max_length=100, db_index=True)
description = models.TextField('Описание')
materials = models.TextField('Материалы')
age = models.PositiveSmallIntegerField('Возраст', default=0)
month_price = models.DecimalField('Цена', max_digits=8, decimal_places=2, default=0)
day_discount = models.DecimalField('Скидка, в валюте', max_digits=8, decimal_places=2, default=0)
class Meta:
ordering = ('weekday',)
verbose_name = 'Расписание'
verbose_name_plural = 'Расписания'
def __str__(self):
return dict(self.WEEKDAY_CHOICES).get(self.weekday, '')

@ -61,6 +61,16 @@ class User(AbstractUser):
user_data = dumps(user_data, ensure_ascii=False)
return user_data
@property
def balance(self):
aggregate = self.balances.aggregate(
models.Sum('amount'),
models.Sum('commission'),
)
amount = aggregate.get('amount__sum') or 0
commission = aggregate.get('commission__sum') or 0
return amount - commission
@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):

@ -27,11 +27,11 @@
<div class="title title_sm">Вывести деньги со счета</div>
<div class="form">
<div class="form__group">
<div class="form__content">На вашем счету 20. 123 рублей</div>
<div class="form__content">На вашем счету {{ request.user.balance }} руб.</div>
<div class="form__field field">
<div class="field__label">СУММА</div>
<div class="field__wrap">
<input class="field__input" type="text" placeholder="12. 000">
<input class="field__input" type="text" placeholder="{{ request.user.balance }}">
</div>
<div class="field__error">Размер выводимой суммы не должно быть менее 2000 рублей.</div>
</div>
@ -58,21 +58,23 @@
<div class="title title_sm">История платежей</div>
<div class="transactions">
<div class="transactions__wrap">
{% if request.user.role == 1 or request.user.role == 2 %}
{% for balance in request.user.balances.all %}
<div class="transactions__row">
<div class="transactions__cell">Ноябрь. Школа Lil City</div>
<div class="transactions__cell">2000.00</div>
<div class="transactions__cell">{{balance.payment.course.title}}</div>
<div class="transactions__cell">{{balance.amount}}</div>
<div class="transactions__cell">Получено</div>
</div>
{% endfor %}
{% else %}
{% for payment in request.user.payments.all %}
<div class="transactions__row">
<div class="transactions__cell">Общий курс по иллюстрации</div>
<div class="transactions__cell">2000.00</div>
<div class="transactions__cell">Получено</div>
</div>
<div class="transactions__row">
<div class="transactions__cell">Ноябрь. Школа Lil City</div>
<div class="transactions__cell">2000.00</div>
<div class="transactions__cell">{{payment.course.title}}</div>
<div class="transactions__cell">{{payment.amount}}</div>
<div class="transactions__cell">Получено</div>
</div>
{% endfor %}
{% endif %}
</div>
<div class="transactions__load load">
<button class="load__btn btn">еще</button>

@ -14,6 +14,7 @@ from django.utils.decorators import method_decorator
from apps.auth.tokens import verification_email_token
from apps.course.models import Course
from apps.payment.models import CoursePayment
from apps.notification.utils import send_email
from .forms import UserEditForm
@ -45,7 +46,9 @@ class UserView(DetailView):
context['drafts'] = Course.objects.filter(
author=self.object, status=Course.DRAFT
)
context['paid'] = Course.objects.none()
context['paid'] = Course.objects.filter(
payments__in=CoursePayment.objects.filter(user=self.object),
).distinct()
return context

@ -224,6 +224,7 @@ CONSTANCE_CONFIG = OrderedDict((
('INSTAGRAM_CLIENT_PASSWORD', ('', '')),
('INSTAGRAM_RESULTS_TAG', ('#lil_акварель', 'Тэг результатов работ.')),
('INSTAGRAM_RESULTS_PATH', ('media/instagram/results/', 'Путь до результатов работ.')),
('SERVICE_COMMISSION', (10, 'Комиссия сервиса в процентах.'))
))
try:
@ -231,6 +232,14 @@ try:
except ImportError:
pass
try:
from paymentwall import *
except ImportError:
pass
else:
Paymentwall.set_api_type(Paymentwall.API_GOODS)
Paymentwall.set_app_key('d6f02b90cf6b16220932f4037578aff7')
Paymentwall.set_secret_key('4ea515bf94e34cf28646c2e12a7b8707')
# CORS settings

@ -136,9 +136,9 @@
<div class="header__ava ava"><img class="ava__pic" src="{% static 'img/user.jpg' %}"></div>
{% endif %}
<div class="header__drop">
{% comment %} <a class="header__link header__link_border" href="#">234.120.345 руб.</a> {% endcomment %}
{% if request.user.auth_token %}
{% if request.user.role == 1 or request.user.role == 2 %}
<a class="header__link header__link_border" href="{% url 'user-edit-payments' request.user.id %}">{{ request.user.balance }} руб.</a>
{% if request.user.auth_token %}
<a class="header__link header__link_green" href="{% url 'course_create' %}">
{% comment %} <a class="header__link header__link_gray disabled" href="#"> {% endcomment %}
<div class="header__title">ДОБАВИТЬ КУРС</div>
@ -424,46 +424,32 @@
<div class="buy__col">
<div class="buy__head buy__head_main">
<div class="buy__title">Выбор урока/дня</div>
<div class="buy__content">При записи на 5 уроков скидка 10%.</div>
<!-- <div class="buy__content">При записи на 5 уроков скидка 10%.</div> -->
</div>
</div>
<div class="buy__col">
<div class="buy__head">
<div class="buy__label">Месяц:</div>
<div class="buy__title">Январь</div>
<div class="buy__content">Если вы оплачиваете после 15 числа, доступ к урокам будет с 1-го следующего
<!-- <div class="buy__label">Месяц:</div>
<div class="buy__title">Январь</div> -->
<!-- <div class="buy__content">Если вы оплачиваете после 15 числа, доступ к урокам будет с 1-го следующего
месяца.
</div>
</div> -->
</div>
</div>
<div class="buy__col">
<div class="buy__list"><label class="switch switch_lesson"><input class="switch__input"
type="checkbox"><span
class="switch__content"><span class="switch__cell">ПОНЕДЕЛЬНИК</span><span
class="switch__cell">5+</span><span class="switch__cell">Персонаж</span><span class="switch__cell">600р</span></span></label>
<label
class="switch switch_lesson"><input class="switch__input" type="checkbox" checked><span
class="switch__content"><span class="switch__cell">Вторник</span><span
class="switch__cell">5+</span><span class="switch__cell">Пластилиновая живопись</span><span
class="switch__cell">600р</span></span>
</label><label class="switch switch_lesson"><input class="switch__input" type="checkbox"><span
class="switch__content"><span class="switch__cell">Среда</span><span
class="switch__cell">5+</span><span class="switch__cell">Персонаж</span><span class="switch__cell">600р</span></span></label>
<label
class="switch switch_lesson"><input class="switch__input" type="checkbox" checked><span
class="switch__content"><span class="switch__cell">Четверг</span><span
class="switch__cell">5+</span><span class="switch__cell">Персонаж</span><span class="switch__cell">600р</span></span>
</label><label class="switch switch_lesson"><input class="switch__input" type="checkbox"><span
class="switch__content"><span class="switch__cell">Пятница</span><span
class="switch__cell">5+</span><span class="switch__cell">Развитие креативного мышления</span><span
class="switch__cell">600р</span></span></label>
<label
class="switch switch_lesson"><input class="switch__input" type="checkbox"><span
class="switch__content"><span class="switch__cell">Суббота</span><span
class="switch__cell">7+</span><span class="switch__cell">Персонаж</span><span class="switch__cell">600р</span></span>
</label><label class="switch switch_lesson"><input class="switch__input" type="checkbox" checked><span
class="switch__content"><span class="switch__cell">Воскресенье</span><span
class="switch__cell">7+</span><span class="switch__cell">Персонаж</span><span class="switch__cell">600р</span></span></label>
<div class="buy__list">
{% for school_schedule in school_schedules %}
<label class="switch switch_lesson">
<input class="switch__input" type="checkbox">
<span class="switch__content">
<span class="switch__cell">{{ school_schedule }}</span>
{% comment %} dont delete {% endcomment %}
<span class="switch__cell"></span>
<span class="switch__cell">{{ school_schedule.title }}</span>
<span class="switch__cell">{{school_schedule.month_price}}р</span>
</span>
</label>
{% endfor %}
</div>
</div>
<div class="buy__col">
@ -483,7 +469,10 @@
</div>
</div>
</div>
<div class="buy__foot"><a class="buy__btn btn btn_md" href="#">ПЕРЕЙТИ К ОПЛАТЕ</a></div>
<div class="buy__foot">
{% comment %}В ссылке, в параметре запроса weekdays, нужно указать выбранные дни недели{% endcomment %}
<a class="buy__btn btn btn_md" href="{% url 'school-checkout' %}?weekdays=1&weekdays=2">ПЕРЕЙТИ К ОПЛАТЕ</a>
</div>
</div>
</div>
</div>

@ -5,9 +5,22 @@
<div class="main" style="background-image: url({% static 'img/bg-1.jpg' %});">
<div class="main__center center">
<div class="main__title">Первая онлайн-школа креативного мышления для детей! 5+</div>
<a class="main__btn btn" href="#">КУПИТЬ ДОСТУП ОТ 2000р. в мес.</a>
<a
data-popup=".js-popup-buy"
class="main__btn btn"
href="#"
>КУПИТЬ ДОСТУП ОТ 2000р. в мес.</a>
</div>
</div>
{% if messages %}
<div class="section section_gray section_menu">
<div class="section__center center center_xs">
{% for message in messages %}
<div class="message message_{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="section">
<div class="section__center center">
<div class="text text_lg">
@ -331,125 +344,23 @@
<div class="title title_center">Расписание</div>
</a>
<div class="schedule">
{% for school_schedule in school_schedules %}
<div class="schedule__item">
<div class="schedule__day">Понедельник</div>
<div class="schedule__wrap">
<div class="schedule__title">Персонаж.</div>
<div class="schedule__content">Учимся создавать персонажей из простых форм. Изучаем характеры и эмоции.</div>
<div class="schedule__toggle toggle">
<button class="toggle__head js-toggle-head">Материалы
<svg class="icon icon-arrow-down">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-down"></use>
</svg>
</button>
<div class="toggle__body">Cамое главное - иметь альбом или блокнот с пустыми страницами (без линий и клеток) плотной гладкой бумагой, формат
А4. Рисовать будем цветными карандашами, а также простым, мягкостью B2. Иногда пригодятся вырезки из журналов
и клей-карандаш.</div>
</div>
</div>
</div>
<div class="schedule__item">
<div class="schedule__day">Вторник</div>
<div class="schedule__wrap">
<div class="schedule__title">Персонаж.</div>
<div class="schedule__content">Учимся создавать персонажей из простых форм. Изучаем характеры и эмоции.</div>
<div class="schedule__toggle toggle">
<button class="toggle__head js-toggle-head">Материалы
<svg class="icon icon-arrow-down">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-down"></use>
</svg>
</button>
<div class="toggle__body">Cамое главное - иметь альбом или блокнот с пустыми страницами (без линий и клеток) плотной гладкой бумагой, формат
А4. Рисовать будем цветными карандашами, а также простым, мягкостью B2. Иногда пригодятся вырезки из журналов
и клей-карандаш.</div>
</div>
</div>
</div>
<div class="schedule__item">
<div class="schedule__day">Среда</div>
<div class="schedule__wrap">
<div class="schedule__title">Персонаж.</div>
<div class="schedule__content">Учимся создавать персонажей из простых форм. Изучаем характеры и эмоции.</div>
<div class="schedule__toggle toggle">
<button class="toggle__head js-toggle-head">Материалы
<svg class="icon icon-arrow-down">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-down"></use>
</svg>
</button>
<div class="toggle__body">Cамое главное - иметь альбом или блокнот с пустыми страницами (без линий и клеток) плотной гладкой бумагой, формат
А4. Рисовать будем цветными карандашами, а также простым, мягкостью B2. Иногда пригодятся вырезки из журналов
и клей-карандаш.</div>
</div>
</div>
</div>
<div class="schedule__item">
<div class="schedule__day">Четверг</div>
<div class="schedule__wrap">
<div class="schedule__title">Персонаж.</div>
<div class="schedule__content">Учимся создавать персонажей из простых форм. Изучаем характеры и эмоции.</div>
<div class="schedule__toggle toggle">
<button class="toggle__head js-toggle-head">Материалы
<svg class="icon icon-arrow-down">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-down"></use>
</svg>
</button>
<div class="toggle__body">Cамое главное - иметь альбом или блокнот с пустыми страницами (без линий и клеток) плотной гладкой бумагой, формат
А4. Рисовать будем цветными карандашами, а также простым, мягкостью B2. Иногда пригодятся вырезки из журналов
и клей-карандаш.</div>
</div>
</div>
</div>
<div class="schedule__item">
<div class="schedule__day">Пятница</div>
<div class="schedule__wrap">
<div class="schedule__title">Персонаж.</div>
<div class="schedule__content">Учимся создавать персонажей из простых форм. Изучаем характеры и эмоции.</div>
<div class="schedule__toggle toggle">
<button class="toggle__head js-toggle-head">Материалы
<svg class="icon icon-arrow-down">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-down"></use>
</svg>
</button>
<div class="toggle__body">Cамое главное - иметь альбом или блокнот с пустыми страницами (без линий и клеток) плотной гладкой бумагой, формат
А4. Рисовать будем цветными карандашами, а также простым, мягкостью B2. Иногда пригодятся вырезки из журналов
и клей-карандаш.</div>
</div>
</div>
</div>
<div class="schedule__item">
<div class="schedule__day">Суббота</div>
<div class="schedule__wrap">
<div class="schedule__title">Персонаж.</div>
<div class="schedule__content">Учимся создавать персонажей из простых форм. Изучаем характеры и эмоции.</div>
<div class="schedule__toggle toggle">
<button class="toggle__head js-toggle-head">Материалы
<svg class="icon icon-arrow-down">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-down"></use>
</svg>
</button>
<div class="toggle__body">Cамое главное - иметь альбом или блокнот с пустыми страницами (без линий и клеток) плотной гладкой бумагой, формат
А4. Рисовать будем цветными карандашами, а также простым, мягкостью B2. Иногда пригодятся вырезки из журналов
и клей-карандаш.</div>
</div>
</div>
</div>
<div class="schedule__item">
<div class="schedule__day">Воскресенье</div>
<div class="schedule__day">{{ school_schedule }}</div>
<div class="schedule__wrap">
<div class="schedule__title">Персонаж.</div>
<div class="schedule__content">Учимся создавать персонажей из простых форм. Изучаем характеры и эмоции.</div>
<div class="schedule__title">{{ school_schedule.title }}</div>
<div class="schedule__content">{{ school_schedule.description }}</div>
<div class="schedule__toggle toggle">
<button class="toggle__head js-toggle-head">Материалы
<svg class="icon icon-arrow-down">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-down"></use>
</svg>
</button>
<div class="toggle__body">Cамое главное - иметь альбом или блокнот с пустыми страницами (без линий и клеток) плотной гладкой бумагой, формат
А4. Рисовать будем цветными карандашами, а также простым, мягкостью B2. Иногда пригодятся вырезки из журналов
и клей-карандаш.</div>
<div class="toggle__body">{{ school_schedule.materials }}</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="text text_mb0">
<a href='#'>Распечатать расписание</a> чтобы не забыть</div>

@ -29,6 +29,8 @@ from apps.user.views import (
UserView, UserEditView, NotificationEditView,
PaymentHistoryView, resend_email_verify,
)
from apps.payment.views import CourseBuyView, PaymentwallCallbackView, SchoolBuyView
from apps.school.models import SchoolSchedule
urlpatterns = [
path('admin/', admin.site.urls),
@ -38,12 +40,17 @@ urlpatterns = [
path('course/on-moderation', CourseOnModerationView.as_view(), name='course-on-moderation'),
path('course/<int:pk>/', CourseView.as_view(), name='course'),
path('course/<str:slug>/', CourseView.as_view(), name='course'),
path('course/<int:pk>/checkout', CourseBuyView.as_view(), name='course-checkout'),
path('course/<int:pk>/edit', CourseEditView.as_view(), name='course_edit'),
path('course/<int:pk>/lessons', CourseView.as_view(template_name='course/course_only_lessons.html'), name='course-only-lessons'),
path('course/<int:course_id>/like', likes, name='likes'),
path('course/<int:course_id>/comment', coursecomment, name='coursecomment'),
path('lesson/<int:pk>/', LessonView.as_view(), name='lesson'),
path('lesson/<int:lesson_id>/comment', lessoncomment, name='lessoncomment'),
path('payments/ping', PaymentwallCallbackView.as_view(), name='payment-ping'),
path('payments/success', TemplateView.as_view(template_name='payment/payment_success.html'), name='payment-success'),
path('payments/error', TemplateView.as_view(template_name='payment/payment_error.html'), name='payment-error'),
path('school/checkout', SchoolBuyView.as_view(), name='school-checkout'),
path('search/', SearchView.as_view(), name='search'),
path('user/<int:pk>/', UserView.as_view(), name='user'),
path('user/<int:pk>/edit', UserEditView.as_view(), name='user-edit-profile'),
@ -53,7 +60,15 @@ urlpatterns = [
path('privacy', TemplateView.as_view(template_name="templates/lilcity/privacy_policy.html"), name='privacy'),
path('terms', TemplateView.as_view(template_name="templates/lilcity/terms.html"), name='terms'),
path('refund-policy', TemplateView.as_view(template_name="templates/lilcity/refund_policy.html"), name='refund_policy'),
path('', TemplateView.as_view(template_name="templates/lilcity/main.html", extra_context={'course_items': Course.objects.all()[:3]}), name='index'),
path('',
TemplateView.as_view(
template_name="templates/lilcity/main.html",
extra_context={
'course_items': Course.objects.filter(status=Course.PUBLISHED)[:3],
'school_schedules': SchoolSchedule.objects.all(),
}),
name='index'
),
path('api/v1/', include(('api.v1.urls', 'api_v1'))),
path('test', TemplateView.as_view(template_name="templates/lilcity/test.html"), name='test'),
]

@ -1,7 +1,8 @@
# Python-3.6
Django==2.0.2
django-anymail[mailgun]==1.2
paymentwall-python==1.0.7
# paymentwall-python==1.0.7
git+https://github.com/ivlevdenis/paymentwall-python.git
twilio==6.10.0
psycopg2==2.7.3.2
facepy==1.0.9

Loading…
Cancel
Save