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

remotes/origin/hasaccess
Vitaly Baev 8 years ago
commit f758b91392
  1. 38
      api/v1/serializers/config.py
  2. 29
      api/v1/serializers/payment.py
  3. 1
      api/v1/serializers/school.py
  4. 7
      api/v1/urls.py
  5. 31
      api/v1/views.py
  6. 36
      apps/content/tasks.py
  7. 730
      apps/course/fixtures/course.json
  8. 8
      apps/course/templates/course/course.html
  9. 68
      apps/course/templates/course/school.html
  10. 5
      apps/course/templatetags/plural.py
  11. 25
      apps/course/views.py
  12. 8
      apps/notification/templates/notification/email/decline_withdrawal.html
  13. 18
      apps/payment/migrations/0011_authorbalance_cause.py
  14. 18
      apps/payment/migrations/0012_auto_20180302_0740.py
  15. 36
      apps/payment/migrations/0013_auto_20180304_1757.py
  16. 29
      apps/payment/migrations/0014_auto_20180307_0617.py
  17. 18
      apps/payment/migrations/0015_auto_20180307_0911.py
  18. 62
      apps/payment/models.py
  19. 43
      apps/payment/views.py
  20. 93
      apps/school/fixtures/school_schedules.json
  21. 25
      apps/user/fields.py
  22. 66
      apps/user/fixtures/superuser.json
  23. 7
      apps/user/forms.py
  24. 102
      apps/user/templates/user/payment-history.html
  25. 4
      apps/user/templates/user/profile.html
  26. 70
      apps/user/views.py
  27. 0
      media/instagram/results/.gitkeep
  28. 12
      project/settings.py
  29. 6
      project/templates/lilcity/index.html
  30. 28
      project/templates/lilcity/main.html
  31. 15
      project/urls.py
  32. 16
      project/views.py
  33. 4
      requirements.txt
  34. 5
      web/package-lock.json
  35. 1
      web/package.json
  36. 3
      web/src/js/modules/common.js
  37. 43
      web/src/js/modules/popup.js
  38. 20
      web/src/sass/_common.sass
  39. 27
      web/yarn.lock

@ -0,0 +1,38 @@
from constance import config
from constance.admin import get_values, ConstanceForm
from rest_framework import serializers
from rest_framework.fields import SkipField
from collections import OrderedDict
def _set_constance_value(key, value):
form = ConstanceForm(initial=get_values())
field = form.fields[key]
clean_value = field.clean(field.to_python(value))
setattr(config, key, clean_value)
class ConfigSerializer(serializers.Serializer):
INSTAGRAM_CLIENT_ACCESS_TOKEN = serializers.CharField(required=False)
INSTAGRAM_CLIENT_SECRET = serializers.CharField(required=False)
INSTAGRAM_RESULTS_TAG = serializers.CharField(required=False)
INSTAGRAM_RESULTS_PATH = serializers.CharField(required=False)
SERVICE_COMMISSION = serializers.IntegerField(required=False)
def to_representation(self, instance):
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
attribute = instance.get(field.field_name)
ret[field.field_name] = field.to_representation(attribute)
return ret
def to_internal_value(self, data):
ret = OrderedDict(get_values())
for k, v in data.items():
ret[k] = v
return ret
def update(self, instance, validated_data):
for k, v in validated_data.items():
_set_constance_value(k, v)

@ -1,9 +1,37 @@
from rest_framework import serializers from rest_framework import serializers
from apps.payment.models import AuthorBalance from apps.payment.models import AuthorBalance
from .user import UserSerializer
class AuthorBalanceCreateSerializer(serializers.ModelSerializer):
class Meta:
model = AuthorBalance
fields = (
'id',
'author',
'type',
'amount',
'commission',
'status',
'payment',
'cause',
)
read_only_fields = (
'id',
'author',
'type',
'payment',
)
def to_representation(self, instance):
return AuthorBalanceSerializer(instance, context=self.context).to_representation(instance)
class AuthorBalanceSerializer(serializers.ModelSerializer): class AuthorBalanceSerializer(serializers.ModelSerializer):
author = UserSerializer()
class Meta: class Meta:
model = AuthorBalance model = AuthorBalance
@ -15,6 +43,7 @@ class AuthorBalanceSerializer(serializers.ModelSerializer):
'commission', 'commission',
'status', 'status',
'payment', 'payment',
'cause',
) )
read_only_fields = ( read_only_fields = (

@ -15,6 +15,7 @@ class SchoolScheduleSerializer(serializers.ModelSerializer):
'materials', 'materials',
'age', 'age',
'month_price', 'month_price',
'day_discount',
) )
read_only_fields = ( read_only_fields = (

@ -8,7 +8,7 @@ from drf_yasg import openapi
from .auth import ObtainToken from .auth import ObtainToken
from .views import ( from .views import (
AuthorBalanceViewSet, AuthorBalanceViewSet, ConfigViewSet,
CategoryViewSet, CourseViewSet, CategoryViewSet, CourseViewSet,
MaterialViewSet, LikeViewSet, MaterialViewSet, LikeViewSet,
ImageViewSet, TextViewSet, ImageViewSet, TextViewSet,
@ -38,6 +38,8 @@ router.register(r'school-schedules', SchoolScheduleViewSet, base_name='school-sc
router.register(r'users', UserViewSet, base_name='users') router.register(r'users', UserViewSet, base_name='users')
# router.register(r'configs', ConfigViewSet, base_name='configs')
schema_view = get_schema_view( schema_view = get_schema_view(
openapi.Info( openapi.Info(
@ -51,9 +53,10 @@ schema_view = get_schema_view(
) )
urlpatterns = [ urlpatterns = [
path('api-token-auth/', ObtainToken.as_view(), name='api-token-auth'),
path('configs/', ConfigViewSet.as_view(), name='configs'),
path('swagger(<str:format>.json|.yaml)', schema_view.without_ui(cache_timeout=None), name='schema-json'), path('swagger(<str:format>.json|.yaml)', schema_view.without_ui(cache_timeout=None), name='schema-json'),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'), path('swagger/', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'), path('redoc/', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'),
path('api-token-auth/', ObtainToken.as_view(), name='api-token-auth'),
path('', include((router.urls, 'api-root')), name='api-root'), path('', include((router.urls, 'api-root')), name='api-root'),
] ]

@ -1,12 +1,16 @@
from constance.admin import get_values
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework import status from rest_framework import status
from rest_framework import viewsets from rest_framework import views, viewsets
from rest_framework import generics
from rest_framework.decorators import detail_route, list_route from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response from rest_framework.response import Response
from . import ExtendedModelViewSet from . import ExtendedModelViewSet
from .serializers.config import ConfigSerializer
from .serializers.course import ( from .serializers.course import (
CategorySerializer, LikeSerializer, CategorySerializer, LikeSerializer,
CourseSerializer, CourseCreateSerializer, CourseSerializer, CourseCreateSerializer,
@ -24,7 +28,7 @@ from .serializers.content import (
ImageObjectSerializer, ImageObjectSerializer,
) )
from .serializers.school import SchoolScheduleSerializer from .serializers.school import SchoolScheduleSerializer
from .serializers.payment import AuthorBalanceSerializer from .serializers.payment import AuthorBalanceSerializer, AuthorBalanceCreateSerializer
from .serializers.user import ( from .serializers.user import (
UserSerializer, UserPhotoSerializer, UserSerializer, UserPhotoSerializer,
) )
@ -38,14 +42,18 @@ from apps.content.models import (
) )
from apps.payment.models import AuthorBalance from apps.payment.models import AuthorBalance
from apps.school.models import SchoolSchedule from apps.school.models import SchoolSchedule
User = get_user_model() User = get_user_model()
class AuthorBalanceViewSet(ExtendedModelViewSet): class AuthorBalanceViewSet(ExtendedModelViewSet):
queryset = AuthorBalance.objects.all() queryset = AuthorBalance.objects.all()
serializer_class = AuthorBalanceSerializer serializer_class = AuthorBalanceCreateSerializer
serializer_class_map = {
'list': AuthorBalanceSerializer,
}
permission_classes = (IsAdmin,) permission_classes = (IsAdmin,)
filter_fields = ('status', 'type') filter_fields = ('author', 'status', 'type')
search_fields = ( search_fields = (
'author__email', 'author__email',
'author__first_name', 'author__first_name',
@ -306,3 +314,18 @@ class SchoolScheduleViewSet(ExtendedModelViewSet):
queryset = SchoolSchedule.objects.all() queryset = SchoolSchedule.objects.all()
serializer_class = SchoolScheduleSerializer serializer_class = SchoolScheduleSerializer
permission_classes = (IsAdmin,) permission_classes = (IsAdmin,)
class ConfigViewSet(generics.RetrieveUpdateAPIView):
serializer_class = ConfigSerializer
permission_classes = (IsAdmin,)
def retrieve(self, request, *args, **kwargs):
serializer = ConfigSerializer(get_values())
return Response(serializer.data)
def patch(self, request, *args, **kwargs):
serializer = ConfigSerializer(data=request.data)
if serializer.is_valid():
serializer.update(get_values(), serializer.validated_data)
return Response(serializer.data)

@ -1,19 +1,31 @@
import os
import json import json
from time import sleep import requests
from project.celery import app import shutil
from constance import config from constance import config
from InstagramAPI import InstagramAPI from instagram.client import InstagramAPI
from project.celery import app
from time import sleep
from django.conf import settings
@app.task @app.task
def retrieve_photos(): def retrieve_photos():
instagram = InstagramAPI( api = InstagramAPI(
config.INSTAGRAM_CLIENT_LOGIN, access_token=config.INSTAGRAM_CLIENT_ACCESS_TOKEN,
config.INSTAGRAM_CLIENT_PASSWORD, client_secret=config.INSTAGRAM_CLIENT_SECRET,
) )
instagram.login() recent_media, next_ = api.user_recent_media(user_id='self', count=20)
sleep(1) path = os.path.join(settings.BASE_DIR, config.INSTAGRAM_RESULTS_PATH)
if instagram.isLoggedIn and instagram.getHashtagFeed(config.INSTAGRAM_RESULTS_TAG): for idx, media in enumerate(recent_media):
with open('s.json', 'w') as f: try:
f.write(json.dumps(instagram.LastJson)) fname = os.path.join(path, f'{idx}.jpg')
return instagram.LastJson r = requests.get(media.images['standard_resolution'].url, stream=True)
if r.status_code == 200:
with open(fname, 'wb') as f:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
except AttributeError:
pass

@ -1,367 +1,367 @@
[ [
{ {
"model": "course.course", "model": "course.course",
"pk": 1, "pk": 1,
"fields": { "fields": {
"author": 1, "author": 1,
"title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438", "title": "Базовый курс для детей по основам иллюстрации",
"short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.", "short_description": "Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.",
"from_author": "", "from_author": "",
"price": "1500.00", "price": "1500.00",
"is_infinite": false, "is_infinite": false,
"deferred_start_at": null, "deferred_start_at": null,
"category": 2, "category": 2,
"duration": 1, "duration": 1,
"is_featured": false, "is_featured": false,
"status": 1, "status": 1,
"created_at": "2018-01-27T07:04:41.113Z", "created_at": "2018-01-27T07:04:41.113Z",
"update_at": "2018-01-31T15:03:47.118Z", "update_at": "2018-01-31T15:03:47.118Z",
"likes": [], "likes": [],
"materials": [] "materials": []
}
},
{
"model": "course.course",
"pk": 2,
"fields": {
"author": 1,
"title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438",
"short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.",
"from_author": "",
"price": "1900.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 1,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.437Z",
"update_at": "2018-01-31T15:03:47.115Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 3,
"fields": {
"author": 1,
"title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438",
"short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.",
"from_author": "",
"price": "100.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 9,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.442Z",
"update_at": "2018-01-31T15:03:47.112Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 4,
"fields": {
"author": 1,
"title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438",
"short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.",
"from_author": "",
"price": "400.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 8,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.445Z",
"update_at": "2018-01-31T15:03:47.108Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 5,
"fields": {
"author": 1,
"title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438",
"short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.",
"from_author": "",
"price": "1800.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 7,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.449Z",
"update_at": "2018-01-31T15:03:47.104Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 6,
"fields": {
"author": 1,
"title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438",
"short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.",
"from_author": "",
"price": "100.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 6,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.452Z",
"update_at": "2018-01-31T15:03:47.101Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 7,
"fields": {
"author": 1,
"title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438",
"short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.",
"from_author": "",
"price": "1600.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 5,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.455Z",
"update_at": "2018-01-31T15:03:47.097Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 8,
"fields": {
"author": 1,
"title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438",
"short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.",
"from_author": "",
"price": "1900.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 4,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.458Z",
"update_at": "2018-01-31T15:03:47.093Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 9,
"fields": {
"author": 1,
"title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438",
"short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.",
"from_author": "",
"price": "200.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 3,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.461Z",
"update_at": "2018-01-31T15:03:47.089Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 10,
"fields": {
"author": 1,
"title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438",
"short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.",
"from_author": "",
"price": "800.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 2,
"duration": 1,
"is_featured": true,
"status": 1,
"created_at": "2018-01-27T07:09:03.464Z",
"update_at": "2018-01-31T15:03:47.086Z",
"likes": [],
"materials": [
1,
2,
3
]
}
},
{
"model": "course.course",
"pk": 11,
"fields": {
"author": 1,
"title": "\u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u0443\u0440\u0441 \u0434\u043b\u044f \u0434\u0435\u0442\u0435\u0439 \u043f\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043c \u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u0438",
"short_description": "\u042d\u0442\u043e\u0442 \u043a\u0443\u0440\u0441 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0442\u044f\u043c \u0443\u0437\u043d\u0430\u0442\u044c \u043e \u0442\u043e\u043c \u043a\u0430\u043a \u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u0444\u043e\u0440\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0435\u0441\u0435\u043b\u044b\u0439 \u0438 \u0445\u0430\u0440\u0438\u0437\u043c\u0430\u0442\u0438\u0447\u043d\u044b\u0445 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0435\u0439.",
"from_author": "",
"price": "100.00",
"is_infinite": false,
"deferred_start_at": "2018-02-28T12:00:00Z",
"category": 1,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.467Z",
"update_at": "2018-01-31T15:03:47.080Z",
"likes": [],
"materials": [
1,
2,
3
]
}
},
{
"model": "course.category",
"pk": 1,
"fields": {
"title": "\u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436"
}
},
{
"model": "course.category",
"pk": 2,
"fields": {
"title": "\u0430\u043a\u0432\u0430\u0440\u0435\u043b\u044c"
}
},
{
"model": "course.category",
"pk": 3,
"fields": {
"title": "\u0438\u043b\u043b\u044e\u0441\u0442\u0440\u0430\u0446\u0438\u044f"
}
},
{
"model": "course.category",
"pk": 4,
"fields": {
"title": "\u0430\u043d\u0438\u043c\u0430\u0446\u0438\u044f"
}
},
{
"model": "course.category",
"pk": 5,
"fields": {
"title": "\u043f\u0430\u0441\u0442\u0435\u043b\u044c"
}
},
{
"model": "course.category",
"pk": 6,
"fields": {
"title": "\u043f\u043b\u0430\u0441\u0442\u0438\u043b\u0438\u043d"
}
},
{
"model": "course.category",
"pk": 7,
"fields": {
"title": "\u043a\u0440\u0435\u0430\u0442\u0438\u0432\u043d\u043e\u0435 \u043c\u044b\u0448\u043b\u0435\u043d\u0438\u0435"
}
},
{
"model": "course.category",
"pk": 8,
"fields": {
"title": "\u043c\u043e\u0442\u043e\u0440\u0438\u043a\u0430"
}
},
{
"model": "course.category",
"pk": 9,
"fields": {
"title": "\u0436\u0438\u0432\u043e\u043f\u0438\u0441\u044c"
}
},
{
"model": "course.lesson",
"pk": 1,
"fields": {
"title": "1 \u0423\u0420\u041e\u041a",
"short_description": "\u0412\u044b\u0431\u0438\u0440\u0430\u0435\u043c \u0441\u044e\u0436\u0435\u0442, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043d\u0430 \u043a\u0443\u0440\u0441\u0435 \u0438 \u0433\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u0433\u0435\u0440\u043e\u044f \u0432\u0430\u0448\u0435\u0439 \u0438\u0441\u0442\u043e\u0440\u0438\u0438. \u0421 \u044d\u0442\u0438\u043c \u0433\u0435\u0440\u043e\u0435\u043c \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043d\u0430 \u043f\u0440\u043e\u0442\u044f\u0436\u0435\u043d\u0438\u0438 \u0432\u0441\u0435\u0433\u043e \u043a\u0443\u0440\u0441\u0430.",
"course": 11,
"created_at": "2018-01-31T15:06:14.830Z",
"update_at": "2018-01-31T15:06:14.830Z"
}
},
{
"model": "course.lesson",
"pk": 2,
"fields": {
"title": "2 \u0423\u0420\u041e\u041a",
"short_description": "\u0421\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b \u0438 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0438\u0440\u0443\u0435\u043c \u0441 \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u0433\u0435\u0440\u043e\u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u043d\u044b\u0445 \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e\u0441\u0442\u0435\u0439 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0430. \u041f\u043e \u043a\u0430\u0436\u0434\u043e\u043c\u0443 \u043f\u0440\u0438\u0437\u043d\u0430\u043a\u0443 \u043d\u0443\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u00ab\u0440\u0435\u0444\u0435\u0440\u0435\u043d\u0441\u044b\u00bb. \u0420\u0438\u0441\u0443\u0435\u043c \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u043e\u0431\u0440\u0430\u0437\u0430 \u0432 \u0441\u0432\u043e\u0435\u043c \u0441\u0442\u0438\u043b\u0435.\r\n\r\n\u0421\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u0438\u0437 \u043d\u0438\u0445 \u043d\u0430\u0448\u0435\u0433\u043e \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0430. \u0412\u044b\u0431\u0438\u0440\u0430\u0435\u043c \u0441\u0430\u043c\u044b\u0435 \u0443\u0434\u0430\u0447\u043d\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b, \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0435\u043c \u0438\u0445 \u0432 \u043e\u0434\u043d\u043e\u043c \u043d\u0430\u0431\u0440\u043e\u0441\u043a\u0435.",
"course": 11,
"created_at": "2018-01-31T15:06:46.772Z",
"update_at": "2018-01-31T15:06:46.772Z"
}
},
{
"model": "course.lesson",
"pk": 3,
"fields": {
"title": "3 \u0423\u0420\u041e\u041a",
"short_description": "\u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0435\u043c \u043e\u0436\u0438\u0432\u0438\u0442\u044c \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0430. \u0412\u044b\u0431\u0438\u0440\u0430\u0435\u043c 5 \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u043d\u044b\u0445 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0434\u043b\u044f \u0432\u0430\u0448\u0435\u0433\u043e \u0433\u0435\u0440\u043e\u044f \u0438 \u0442\u043e\u0433\u043e \u0441\u044e\u0436\u0435\u0442\u0430, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043e\u043d \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0443\u0447\u0430\u0441\u0442\u0438\u0435, \u0440\u0438\u0441\u0443\u0435\u043c \u044d\u0441\u043a\u0438\u0437\u044b \u0432\u0430\u0448\u0435\u0433\u043e \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u0436\u0430 \u0432 \u0440\u0430\u0437\u043d\u044b\u0445 \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u043d\u044b\u0445 \u0434\u043b\u044f \u043d\u0435\u0433\u043e \u043f\u043e\u0437\u0430\u0445 \u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0438.\r\n\r\n\u0412\u044b\u0434\u0435\u043b\u044f\u0435\u043c 5 \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u043d\u044b\u0445 \u044d\u043c\u043e\u0446\u0438\u0439 \u0434\u043b\u044f \u0433\u0435\u0440\u043e\u044f, \u043d\u0430\u0434 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u043c. \u041f\u043e \u044d\u043c\u043e\u0446\u0438\u044f\u043c \u043f\u043e\u0434\u0431\u0438\u0440\u0430\u0435\u043c \u0440\u0435\u0444\u0435\u0440\u0435\u043d\u0441\u044b \u0438 \u0441\u0442\u0438\u043b\u0438\u0437\u0443\u0435\u043c \u0438\u0445 \u0432 \u0441\u0432\u043e\u0435\u043c \u0441\u0442\u0438\u043b\u0435.",
"course": 11,
"created_at": "2018-01-31T15:07:08.979Z",
"update_at": "2018-01-31T15:07:08.979Z"
}
},
{
"model": "course.material",
"pk": 1,
"fields": {
"title": "\u0411\u0443\u043c\u0430\u0433\u0430 \u0430\u043a\u0432\u0430\u0440\u0435\u043b\u044c\u043d\u0430\u044f",
"short_description": "\u0411\u0443\u043c\u0430\u0433\u0430 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0430\u043a\u0432\u0430\u0440\u0435\u043b\u044c\u044e \u0438\u043c\u0435\u0435\u0442 \u0431\u043e\u043b\u044c\u0448\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435. \u042d\u0442\u043e \u043e\u0431\u044a\u044f\u0441\u043d\u044f\u0435\u0442\u0441\u044f \u0442\u0435\u043c, \u0447\u0442\u043e \u0430\u043a\u0432\u0430\u0440\u0435\u043b\u044c \u2014 \u043a\u0440\u0430\u0441\u043a\u0430 \u043f\u0440\u043e\u0437\u0440\u0430\u0447\u043d\u0430\u044f, \u0430 \u0437\u043d\u0430\u0447\u0438\u0442 \u0444\u0430\u043a\u0442\u0443\u0440\u0430 \u0431\u0443\u043c\u0430\u0433\u0438 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0441\u0438\u043b\u044c\u043d\u043e \u0432\u043b\u0438\u044f\u0442\u044c \u043d\u0430 \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0432\u0438\u0434 \u043a\u0440\u0430\u0441\u043e\u0447\u043d\u043e\u0433\u043e \u0441\u043b\u043e\u044f.",
"created_at": "2018-01-31T14:55:48.394Z",
"update_at": "2018-01-31T14:55:48.394Z"
}
},
{
"model": "course.material",
"pk": 2,
"fields": {
"title": "\u041a\u0438\u0441\u0442\u043e\u0447\u043a\u0438 \u0434\u043b\u044f \u0440\u0438\u0441\u043e\u0432\u0430\u043d\u0438\u044f",
"short_description": "\u041a\u0438\u0441\u0442\u044c \u2014 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442 \u0434\u043b\u044f \u043f\u043e\u043a\u0440\u0430\u0441\u043a\u0438 \u0438 \u0436\u0438\u0432\u043e\u043f\u0438\u0441\u0438. \u041a\u0438\u0441\u0442\u0438 \u0434\u0435\u043b\u0430\u044e\u0442\u0441\u044f \u0438\u0437 \u0449\u0435\u0442\u0438\u043d\u044b \u0438 \u0445\u0432\u043e\u0441\u0442\u043e\u0432\u044b\u0445 \u0432\u043e\u043b\u043e\u0441\u043a\u043e\u0432 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u0436\u0438\u0432\u043e\u0442\u043d\u044b\u0445.",
"created_at": "2018-01-31T14:57:37.751Z",
"update_at": "2018-01-31T14:57:37.751Z"
}
},
{
"model": "course.material",
"pk": 3,
"fields": {
"title": "\u041a\u0440\u0430\u0441\u043a\u0438 \u0430\u043a\u0432\u0430\u0440\u0435\u043b\u044c\u043d\u044b\u0435",
"short_description": "\u0417\u0430\u0432\u043e\u0434 \u0445\u0443\u0434\u043e\u0436\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0445 \u043a\u0440\u0430\u0441\u043e\u043a \u00ab\u041d\u0435\u0432\u0441\u043a\u0430\u044f \u043f\u0430\u043b\u0438\u0442\u0440\u0430\u00bb \u0432\u044b\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u0430\u043a\u0432\u0430\u0440\u0435\u043b\u044c 80 \u043b\u0435\u0442, \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044f \u0442\u0440\u0430\u0434\u0438\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0441\u0442\u0432\u0430 \u043f\u0440\u043e\u0434\u0443\u043a\u0446\u0438\u0438 \u0432\u044b\u0441\u043e\u0447\u0430\u0439\u0448\u0435\u0433\u043e \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430. \u041f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u044b\u0435 \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c \u0440\u0435\u0446\u0435\u043f\u0442\u0443\u0440\u044b, \u043e\u0442\u043b\u0430\u0436\u0435\u043d\u043d\u0430\u044f \u0442\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u044f \u0438\u0437\u0433\u043e\u0442\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0434\u0435\u043b\u0430\u043b\u0438 \u0430\u043a\u0432\u0430\u0440\u0435\u043b\u044c\u043d\u044b\u0435 \u043a\u0440\u0430\u0441\u043a\u0438 \u0432\u0438\u0437\u0438\u0442\u043d\u043e\u0439 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u043e\u0439 \u043f\u0440\u0435\u0434\u043f\u0440\u0438\u044f\u0442\u0438\u044f \u0432 \u0420\u043e\u0441\u0441\u0438\u0438",
"created_at": "2018-01-31T14:58:46.209Z",
"update_at": "2018-01-31T14:58:46.209Z"
}
} }
] },
{
"model": "course.course",
"pk": 2,
"fields": {
"author": 1,
"title": "Базовый курс для детей по основам иллюстрации",
"short_description": "Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.",
"from_author": "",
"price": "1900.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 1,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.437Z",
"update_at": "2018-01-31T15:03:47.115Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 3,
"fields": {
"author": 1,
"title": "Базовый курс для детей по основам иллюстрации",
"short_description": "Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.",
"from_author": "",
"price": "100.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 9,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.442Z",
"update_at": "2018-01-31T15:03:47.112Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 4,
"fields": {
"author": 1,
"title": "Базовый курс для детей по основам иллюстрации",
"short_description": "Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.",
"from_author": "",
"price": "400.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 8,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.445Z",
"update_at": "2018-01-31T15:03:47.108Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 5,
"fields": {
"author": 1,
"title": "Базовый курс для детей по основам иллюстрации",
"short_description": "Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.",
"from_author": "",
"price": "1800.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 7,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.449Z",
"update_at": "2018-01-31T15:03:47.104Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 6,
"fields": {
"author": 1,
"title": "Базовый курс для детей по основам иллюстрации",
"short_description": "Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.",
"from_author": "",
"price": "100.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 6,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.452Z",
"update_at": "2018-01-31T15:03:47.101Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 7,
"fields": {
"author": 1,
"title": "Базовый курс для детей по основам иллюстрации",
"short_description": "Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.",
"from_author": "",
"price": "1600.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 5,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.455Z",
"update_at": "2018-01-31T15:03:47.097Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 8,
"fields": {
"author": 1,
"title": "Базовый курс для детей по основам иллюстрации",
"short_description": "Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.",
"from_author": "",
"price": "1900.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 4,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.458Z",
"update_at": "2018-01-31T15:03:47.093Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 9,
"fields": {
"author": 1,
"title": "Базовый курс для детей по основам иллюстрации",
"short_description": "Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.",
"from_author": "",
"price": "200.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 3,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.461Z",
"update_at": "2018-01-31T15:03:47.089Z",
"likes": [],
"materials": []
}
},
{
"model": "course.course",
"pk": 10,
"fields": {
"author": 1,
"title": "Базовый курс для детей по основам иллюстрации",
"short_description": "Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.",
"from_author": "",
"price": "800.00",
"is_infinite": false,
"deferred_start_at": null,
"category": 2,
"duration": 1,
"is_featured": true,
"status": 1,
"created_at": "2018-01-27T07:09:03.464Z",
"update_at": "2018-01-31T15:03:47.086Z",
"likes": [],
"materials": [
1,
2,
3
]
}
},
{
"model": "course.course",
"pk": 11,
"fields": {
"author": 1,
"title": "Базовый курс для детей по основам иллюстрации",
"short_description": "Этот курс поможет детям узнать о том как из простых форм создавать веселый и харизматичных персонажей.",
"from_author": "",
"price": "100.00",
"is_infinite": false,
"deferred_start_at": "2018-02-28T12:00:00Z",
"category": 1,
"duration": 1,
"is_featured": false,
"status": 1,
"created_at": "2018-01-27T07:09:03.467Z",
"update_at": "2018-01-31T15:03:47.080Z",
"likes": [],
"materials": [
1,
2,
3
]
}
},
{
"model": "course.category",
"pk": 1,
"fields": {
"title": "персонаж"
}
},
{
"model": "course.category",
"pk": 2,
"fields": {
"title": "акварель"
}
},
{
"model": "course.category",
"pk": 3,
"fields": {
"title": "иллюстрация"
}
},
{
"model": "course.category",
"pk": 4,
"fields": {
"title": "анимация"
}
},
{
"model": "course.category",
"pk": 5,
"fields": {
"title": "пастель"
}
},
{
"model": "course.category",
"pk": 6,
"fields": {
"title": "пластилин"
}
},
{
"model": "course.category",
"pk": 7,
"fields": {
"title": "креативное мышление"
}
},
{
"model": "course.category",
"pk": 8,
"fields": {
"title": "моторика"
}
},
{
"model": "course.category",
"pk": 9,
"fields": {
"title": "живопись"
}
},
{
"model": "course.lesson",
"pk": 1,
"fields": {
"title": "1 УРОК",
"short_description": "Выбираем сюжет, с которым мы будем работать на курсе и главного героя вашей истории. С этим героем мы будем работать на протяжении всего курса.",
"course": 11,
"created_at": "2018-01-31T15:06:14.830Z",
"update_at": "2018-01-31T15:06:14.830Z"
}
},
{
"model": "course.lesson",
"pk": 2,
"fields": {
"title": "2 УРОК",
"short_description": "Собираем материал и экспериментируем с образом героя, используя полученный список характерных особенностей персонажа. По каждому признаку нужно будет собрать «референсы». Рисуем элементы образа в своем стиле.\\r\\n\\r\\nСобираем из них нашего персонажа. Выбираем самые удачные элементы, объединяем их в одном наброске.",
"course": 11,
"created_at": "2018-01-31T15:06:46.772Z",
"update_at": "2018-01-31T15:06:46.772Z"
}
},
{
"model": "course.lesson",
"pk": 3,
"fields": {
"title": "3 УРОК",
"short_description": "Попробуем оживить персонажа. Выбираем 5 характерных действий для вашего героя и того сюжета, в котором он принимает участие, рисуем эскизы вашего персонажа в разных характерных для него позах и движении.\\r\\n\\r\\nВыделяем 5 характерных эмоций для героя, над которым работаем. По эмоциям подбираем референсы и стилизуем их в своем стиле.",
"course": 11,
"created_at": "2018-01-31T15:07:08.979Z",
"update_at": "2018-01-31T15:07:08.979Z"
}
},
{
"model": "course.material",
"pk": 1,
"fields": {
"title": "Бумага акварельная",
"short_description": "Бумага для работы с акварелью имеет большое значение. Это объясняется тем, что акварель — краска прозрачная, а значит фактура бумаги будет достаточно сильно влиять на внешний вид красочного слоя.",
"created_at": "2018-01-31T14:55:48.394Z",
"update_at": "2018-01-31T14:55:48.394Z"
}
},
{
"model": "course.material",
"pk": 2,
"fields": {
"title": "Кисточки для рисования",
"short_description": "Кисть — инструмент для покраски и живописи. Кисти делаются из щетины и хвостовых волосков различных животных.",
"created_at": "2018-01-31T14:57:37.751Z",
"update_at": "2018-01-31T14:57:37.751Z"
}
},
{
"model": "course.material",
"pk": 3,
"fields": {
"title": "Краски акварельные",
"short_description": "Завод художественных красок «Невская палитра» выпускает акварель 80 лет, сохраняя традиции производства продукции высочайшего качества. Проверенные временем рецептуры, отлаженная технология изготовления сделали акварельные краски визитной карточкой предприятия в России",
"created_at": "2018-01-31T14:58:46.209Z",
"update_at": "2018-01-31T14:58:46.209Z"
}
}
]

@ -25,7 +25,7 @@
</div> </div>
<div class="go__title">Вернуться</div> <div class="go__title">Вернуться</div>
</a> </a>
{% if not paid %} {% if not paid and course.price %}
<a <a
class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md" class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md"
{% if user.is_authenticated %} {% if user.is_authenticated %}
@ -106,6 +106,7 @@
</div> </div>
<div class="meta__title">{{ course.created_at | date:"d F Yг." }}</div> <div class="meta__title">{{ course.created_at | date:"d F Yг." }}</div>
</div> </div>
{% if course.price %}
<div class="meta__item"> <div class="meta__item">
<div class="meta__icon"> <div class="meta__icon">
<svg class="icon icon-money"> <svg class="icon icon-money">
@ -114,6 +115,7 @@
</div> </div>
<div class="meta__title">{{ course.price|floatformat:"-2" }}₽</div> <div class="meta__title">{{ course.price|floatformat:"-2" }}₽</div>
</div> </div>
{% endif %}
</div> </div>
<div class="course__meta meta"> <div class="course__meta meta">
<div class="meta__item"> <div class="meta__item">
@ -378,6 +380,7 @@
</div> </div>
<div class="meta__title">{{ course.created_at | date:"d F Yг." }}</div> <div class="meta__title">{{ course.created_at | date:"d F Yг." }}</div>
</div> </div>
{% if course.price %}
<div class="meta__item"> <div class="meta__item">
<div class="meta__icon"> <div class="meta__icon">
<svg class="icon icon-money"> <svg class="icon icon-money">
@ -386,8 +389,9 @@
</div> </div>
<div class="meta__title">{{ course.price|floatformat:"-2" }}₽</div> <div class="meta__title">{{ course.price|floatformat:"-2" }}₽</div>
</div> </div>
{% endif %}
</div> </div>
{% if not paid %} {% if not paid and course.price %}
<a <a
class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md" class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md"
{% if user.is_authenticated %} {% if user.is_authenticated %}

@ -1,34 +1,44 @@
{% load static %}
{% load rupluralize from plural %}
{% if school_payment %}
<div class="school__page"> <div class="school__page">
<div class="school__block"> <div class="school__block">
<div class="school__month"> <div class="school__month">
<div class="school__month__wrap"> <div class="school__month__wrap">
<div class="school__month__title">Январь</div> <div class="school__month__title">
<div class="school__month__subtitle">Осталось 14 дней</div> Дата окончания:
<div class="school__month__image"> <p>{{ school_payment.date_end }}</p>
<img class="school__month__image__pic" src="/static/img/order.jpg">
</div>
</div>
</div> </div>
<div class="school__btn"> <div class="school__month__subtitle">Осталось: {{ school_days_left | rupluralize:"день,дня,дней" }}</div>
<a class="buy__btn btn btn_md" href="#" {% if user.is_authenticated %} <div class="school__month__image">
data-popup=".js-popup-buy" <img class="school__month__image__pic" src="{% static 'img/order.jpg' %}">
{% else %}
data-popup=".js-popup-auth"
{% endif %}>ПРОДЛИТЬ ШКОЛУ</a>
</div>
<div class="school__btn__prev">
<a class="school__prev__btn btn" href="#">ПОСМОТРЕТЬ ПРЕДЫДУЩИЕ УРОКИ</a>
</div> </div>
</div>
</div> </div>
<div class="school__block school__list"> <div class="school__btn">
{% for course in course_items %} <a
<div class="school__item"> class="buy__btn btn btn_md"
<div class="school__title">Понедельник</div> href="#"
<div class="school__subtitle">Пластилиновая живопись.</div> {% if user.is_authenticated %}
<div class="school__content"> data-popup=".js-popup-buy"
<p>Научимся смешивать цвета, получать красивые оттенки и создавать картины из пластилина разными техниками. Узнаем как хранить и ухаживать за такими работами.</p> {% else %}
</div> data-popup=".js-popup-auth"
</div> {% endif %}>ПРОДЛИТЬ ШКОЛУ</a>
{% endfor %} </div>
<div class="school__btn__prev">
<a class="school__prev__btn btn" href="#">ПОСМОТРЕТЬ ПРЕДЫДУЩИЕ УРОКИ</a>
</div>
</div>
<div class="school__block school__list">
{% for school_schedule in school_schedules %}
<div class="school__item">
<div class="school__title">{{ school_schedule }}</div>
<div class="school__subtitle">{{ school_schedule.title }}</div>
<div class="school__content">
<p>{{ school_schedule.description }}</p>
</div>
</div> </div>
</div> {% endfor %}
</div>
</div>
{% endif %}

@ -9,8 +9,9 @@ register = template.Library()
def rupluralize(value, arg): def rupluralize(value, arg):
args = arg.split(',') args = arg.split(',')
try: try:
value = str(0 if not value or int(value) <= 0 else value)[-1:] _value = str(0 if not value or int(value) <= 0 else value)[-1:]
return value + ' ' + args[0 if value == '1' else (1 if value in '234' else 2)] value = value + ' ' + args[0 if _value == '1' else (1 if _value in '234' else 2)]
return value
except: except:
raise template.TemplateSyntaxError raise template.TemplateSyntaxError
return '' return ''

@ -1,3 +1,5 @@
from paymentwall import Pingback
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Q from django.db.models import Q
@ -188,15 +190,20 @@ class CourseView(DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['next'] = self.request.GET.get('next', None) if not self.request.user.is_anonymous:
context['paid'] = self.object.payments.filter( context['next'] = self.request.GET.get('next', None)
user=self.request.user, context['paid'] = self.object.payments.filter(
authorbalance__status=AuthorBalance.ACCEPTED, user=self.request.user,
).exists() status__in=[
context['pending'] = self.object.payments.filter( Pingback.PINGBACK_TYPE_REGULAR,
user=self.request.user, Pingback.PINGBACK_TYPE_GOODWILL,
authorbalance__status=AuthorBalance.PENDING, Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
).exists() ],
).exists()
context['pending'] = self.object.payments.filter(
user=self.request.user,
status=Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW,
).exists()
return context return context
def get_queryset(self): def get_queryset(self):

@ -0,0 +1,8 @@
{% extends "notification/email/_base.html" %}
{% block content %}
<p style="margin: 0 0 20px">К сожалению вам отказано в выводе средств!</p>
<div style="margin-bottom: 10px;">
<p>{{ author_balance.cause }}</p>
</div>
{% endblock content %}

@ -0,0 +1,18 @@
# Generated by Django 2.0.2 on 2018-02-28 11:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payment', '0010_auto_20180227_0933'),
]
operations = [
migrations.AddField(
model_name='authorbalance',
name='cause',
field=models.TextField(default='', verbose_name='Причина отказа'),
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.2 on 2018-03-02 07:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payment', '0011_authorbalance_cause'),
]
operations = [
migrations.AlterField(
model_name='authorbalance',
name='cause',
field=models.TextField(blank=True, null=True, verbose_name='Причина отказа'),
),
]

@ -0,0 +1,36 @@
# Generated by Django 2.0.2 on 2018-03-04 17:57
import django.core.validators
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('payment', '0012_auto_20180302_0740'),
]
operations = [
migrations.AddField(
model_name='authorbalance',
name='card',
field=models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(regex='^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\\\d{3})\\d{11})$')]),
),
migrations.AddField(
model_name='authorbalance',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='authorbalance',
name='declined_send_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='authorbalance',
name='update_at',
field=models.DateTimeField(auto_now=True),
),
]

@ -0,0 +1,29 @@
# Generated by Django 2.0.2 on 2018-03-07 06:17
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('payment', '0013_auto_20180304_1757'),
]
operations = [
migrations.AlterModelOptions(
name='payment',
options={'ordering': ('created_at',), 'verbose_name': 'Платеж', 'verbose_name_plural': 'Платежи'},
),
migrations.AddField(
model_name='payment',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='payment',
name='update_at',
field=models.DateTimeField(auto_now=True),
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.2 on 2018-03-07 09:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payment', '0014_auto_20180307_0617'),
]
operations = [
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')], null=True, verbose_name='Статус платежа'),
),
]

@ -1,16 +1,21 @@
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 constance import config
from paymentwall import Pingback from paymentwall import Pingback
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
from django.db import models
from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import ArrayField, JSONField
from django.core.validators import RegexValidator
from django.utils.timezone import now
from apps.course.models import Course from apps.course.models import Course
from apps.school.models import SchoolSchedule from apps.school.models import SchoolSchedule
from apps.notification.utils import send_email
User = get_user_model() User = get_user_model()
CREDIT_CARD_RE = r'^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\d{11})$'
class AuthorBalance(models.Model): class AuthorBalance(models.Model):
IN = 0 IN = 0
@ -34,11 +39,33 @@ class AuthorBalance(models.Model):
commission = 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) status = models.PositiveSmallIntegerField('Статус', choices=STATUS_CHOICES, default=0)
payment = models.OneToOneField('Payment', on_delete=models.CASCADE, null=True, blank=True, verbose_name='Платёж') payment = models.OneToOneField('Payment', on_delete=models.CASCADE, null=True, blank=True, verbose_name='Платёж')
cause = models.TextField('Причина отказа', null=True, blank=True)
card = models.CharField(max_length=20, validators=[RegexValidator(regex=CREDIT_CARD_RE)], null=True, blank=True)
declined_send_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
class Meta: class Meta:
verbose_name = 'Баланс' verbose_name = 'Баланс'
verbose_name_plural = 'Балансы' verbose_name_plural = 'Балансы'
def save(self, *args, **kwargs):
self.commission = self.calc_commission()
if self.type == self.OUT:
if self.status == self.DECLINED and not self.declined_send_at:
send_email(
'Отказ вывода средств',
self.author.email,
'notification/email/decline_withdrawal.html',
author_balance=self,
)
self.declined_send_at = now()
super().save(*args, **kwargs)
def calc_commission(self):
return self.amount * config.SERVICE_COMMISSION / 100
class Payment(PolymorphicModel): class Payment(PolymorphicModel):
PW_STATUS_CHOICES = ( PW_STATUS_CHOICES = (
@ -55,16 +82,35 @@ class Payment(PolymorphicModel):
) )
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='Пользователь', related_name='payments') 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) amount = models.DecimalField('Итого', max_digits=8, decimal_places=2, default=0, editable=False)
status = models.PositiveSmallIntegerField('Статус платежа', choices=PW_STATUS_CHOICES, null=True, editable=False) status = models.PositiveSmallIntegerField('Статус платежа', choices=PW_STATUS_CHOICES, null=True)
data = JSONField('Данные платежа от провайдера', default={}, editable=False) data = JSONField('Данные платежа от провайдера', default={}, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
class Meta: class Meta:
verbose_name = 'Платеж' verbose_name = 'Платеж'
verbose_name_plural = 'Платежи' verbose_name_plural = 'Платежи'
ordering = ('created_at',)
def calc_commission(self): def calc_commission(self):
return self.amount * config.SERVICE_COMMISSION / 100 return self.amount * config.SERVICE_COMMISSION / 100
def is_deliverable(self):
return self.status in [
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
]
def is_under_review(self):
return self.status == Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW
def is_cancelable(self):
return self.status in [
Pingback.PINGBACK_TYPE_NEGATIVE,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_DECLINED,
]
class CoursePayment(Payment): class CoursePayment(Payment):
course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name='Курс', related_name='payments') course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name='Курс', related_name='payments')
@ -82,11 +128,9 @@ class CoursePayment(Payment):
author=self.course.author, author=self.course.author,
amount=self.amount, amount=self.amount,
payment=self, payment=self,
commission=self.calc_commission(),
) )
else: else:
author_balance.amount = self.amount author_balance.amount = self.amount
author_balance.commission = self.calc_commission()
author_balance.save() author_balance.save()
@ -99,6 +143,10 @@ class SchoolPayment(Payment):
verbose_name = 'Платеж за школу' verbose_name = 'Платеж за школу'
verbose_name_plural = 'Платежи за школу' verbose_name_plural = 'Платежи за школу'
def __str__(self):
days = ', '.join([dict(SchoolSchedule.WEEKDAY_CHOICES).get(weekday, '') for weekday in sorted(self.weekdays)])
return days
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
aggregate = SchoolSchedule.objects.filter( aggregate = SchoolSchedule.objects.filter(
weekday__in=self.weekdays, weekday__in=self.weekdays,

@ -1,3 +1,8 @@
import logging
import json
from datetime import timedelta
from django.contrib import messages from django.contrib import messages
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
@ -5,6 +10,7 @@ from django.views.generic import View, TemplateView
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.timezone import now
from paymentwall import Pingback, Product, Widget from paymentwall import Pingback, Product, Widget
@ -13,6 +19,8 @@ from apps.school.models import SchoolSchedule
from .models import AuthorBalance, CoursePayment, SchoolPayment from .models import AuthorBalance, CoursePayment, SchoolPayment
logger = logging.getLogger('django')
class CourseBuyView(TemplateView): class CourseBuyView(TemplateView):
template_name = 'payment/paymentwall_widget.html' template_name = 'payment/paymentwall_widget.html'
@ -32,16 +40,17 @@ class CourseBuyView(TemplateView):
) )
widget = Widget( widget = Widget(
str(request.user.id), str(request.user.id),
'p1', 'p1_1',
[product], [product],
extra_params={ extra_params={
'lang': 'ru', 'lang': 'ru',
'evaluation': 1, 'evaluation': 1,
'demo': 1,
'test_mode': 1,
'success_url': host + str(reverse_lazy('payment-success')), 'success_url': host + str(reverse_lazy('payment-success')),
'failure_url': host + str(reverse_lazy('payment-error')), 'failure_url': host + str(reverse_lazy('payment-error')),
} }
) )
return self.render_to_response(context={'widget': widget.get_html_code()}) return self.render_to_response(context={'widget': widget.get_html_code()})
@ -71,16 +80,17 @@ class SchoolBuyView(TemplateView):
) )
widget = Widget( widget = Widget(
str(request.user.id), str(request.user.id),
'p1', 'p1_1',
[product], [product],
extra_params={ extra_params={
'lang': 'ru', 'lang': 'ru',
'evaluation': 1, 'evaluation': 1,
'demo': 1,
'test_mode': 1,
'success_url': host + str(reverse_lazy('payment-success')), 'success_url': host + str(reverse_lazy('payment-success')),
'failure_url': host + str(reverse_lazy('payment-error')), 'failure_url': host + str(reverse_lazy('payment-error')),
} }
) )
return self.render_to_response(context={'widget': widget.get_html_code()}) return self.render_to_response(context={'widget': widget.get_html_code()})
@ -113,13 +123,34 @@ class PaymentwallCallbackView(View):
payment = product_payment_class.objects.get(pk=payment_id) payment = product_payment_class.objects.get(pk=payment_id)
except product_payment_class.DoesNotExist: except product_payment_class.DoesNotExist:
return HttpResponse(status=403) return HttpResponse(status=403)
logger.info(
json.dumps(payment_raw_data),
)
payment.status = pingback.get_type() payment.status = pingback.get_type()
payment.data = payment_raw_data payment.data = payment_raw_data
if pingback.is_deliverable() and product_type_name == 'school':
school_payment = SchoolPayment.objects.filter(
user=payment.user,
date_start__lte=now(),
date_end__gt=now(),
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
).last()
if school_payment:
date_start = school_payment.date_end + timedelta(days=1)
date_end = date_start + timedelta(days=30)
else:
date_start = now()
date_end = now() + timedelta(days=30)
payment.date_start = date_start
payment.date_end = date_end
payment.save() payment.save()
author_balance = getattr(payment, 'author_balance', None) author_balance = getattr(payment, 'author_balance', None)
if author_balance: if author_balance and author_balance.type == AuthorBalance.IN:
if pingback.is_deliverable(): if pingback.is_deliverable():
payment.author_balance.status = AuthorBalance.ACCEPTED payment.author_balance.status = AuthorBalance.ACCEPTED
elif pingback.is_under_review(): elif pingback.is_under_review():

@ -0,0 +1,93 @@
[
{
"model": "school.schoolschedule",
"pk": 1,
"fields": {
"weekday": 1,
"title": "Акварель",
"description": "Сальвадор Дали родился в Испании 11 мая 1904 года в городе Фигерасе, провинция Жирона, в семье зажиточного нотариуса. По национальности был каталонцем, воспринимал себя в этом качестве и настаивал на этой своей особенности.",
"materials": "У него была сестра, Анна Мария Дали (исп. Anna Maria Dalí, 6 января 1908 — 16 мая 1989), и старший брат (12 октября 1901 — 1 августа 1903), который умер от менингита.",
"age": 0,
"month_price": "500.00",
"day_discount": "0.00"
}
},
{
"model": "school.schoolschedule",
"pk": 2,
"fields": {
"weekday": 2,
"title": "Живопись",
"description": "Позднее в возрасте 5 лет на его могиле родители сказали Сальвадору, что он — реинкарнация своего старшего брата.",
"materials": "В детстве Дали был сообразительным, но заносчивым и неуправляемым ребёнком.",
"age": 0,
"month_price": "500.00",
"day_discount": "0.00"
}
},
{
"model": "school.schoolschedule",
"pk": 3,
"fields": {
"weekday": 3,
"title": "Пластелин",
"description": "Однажды он затеял скандал на торговой площади ради леденца, вокруг собралась толпа, и полицейские попросили хозяина лавки открыть её во время сиесты и подарить мальчику сладость.",
"materials": "Он добивался своего капризами и симуляцией, всегда стремился выделиться и привлечь к себе внимание.",
"age": 0,
"month_price": "500.00",
"day_discount": "0.00"
}
},
{
"model": "school.schoolschedule",
"pk": 4,
"fields": {
"weekday": 4,
"title": "Моторика",
"description": "Многочисленные комплексы[какие?] и фобии, например, страх перед кузнечиками[9], мешали ему включиться в обычную школьную жизнь, завести с детьми обычные связи дружбы и симпатии.",
"materials": "Но, как и любой человек, испытывая сенсорный голод, он искал эмоциональный контакт с детьми любыми способами, стараясь вжиться в их коллектив если не в роли товарища, то в любой другой роли, а точнее той единственной, на которую был способен, — в роли эпатажного и непослушного ребёнка, странного, чудаковатого, всегда поступающего вопреки чужим мнениям.",
"age": 0,
"month_price": "500.00",
"day_discount": "0.00"
}
},
{
"model": "school.schoolschedule",
"pk": 5,
"fields": {
"weekday": 5,
"title": "Акварель",
"description": "Проигрывая в школьных азартных играх, он вёл себя так, будто выиграл, и торжествовал. Иногда без причины затевал драки.",
"materials": "Одноклассники относились к «странному» ребёнку довольно нетерпимо, использовали его страх перед кузнечиками, подсовывали ему за шиворот этих насекомых, чем доводили Сальвадора до истерики, о чём он позднее поведал в своей книге «Тайная жизнь Сальвадора Дали, рассказанная им самим».",
"age": 0,
"month_price": "500.00",
"day_discount": "0.00"
}
},
{
"model": "school.schoolschedule",
"pk": 6,
"fields": {
"weekday": 6,
"title": "Анимация",
"description": "Обучаться изобразительному искусству Дали начал в муниципальной художественной школе. С 1914 по 1918 год воспитывался в Академии братьев ордена маристов в Фигерасе.",
"materials": "Одним из друзей детства был будущий футболист ФК «Барселона» Хосеп Самитьер. В 1916 году, с семьей Рамона Пишó, отправился на каникулы в город Кадакéс, где познакомился с современным искусством.",
"age": 0,
"month_price": "500.00",
"day_discount": "0.00"
}
},
{
"model": "school.schoolschedule",
"pk": 7,
"fields": {
"weekday": 7,
"title": "Персонаж",
"description": "После прихода к власти Каудильо Фрáнко в 1936 году Дали ссорится с сюрреалистами, стоящими на левых позициях, и его исключают из группы.",
"materials": "В ответ Дали: «Сюрреализм — это я».",
"age": 0,
"month_price": "500.00",
"day_discount": "0.00"
}
}
]

@ -0,0 +1,25 @@
import re
from django import forms
from django.utils.translation import ugettext_lazy as _
CREDIT_CARD_RE = r'^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\d{11})$'
class CreditCardField(forms.CharField):
"""
Form field that validates credit card numbers.
"""
default_error_messages = {
'required': _(u'Номер карты обязателен.'),
'invalid': _(u'Неверный номер карты.'),
}
def clean(self, value):
value = value.replace(' ', '').replace('-', '')
if self.required and not value:
raise forms.utils.ValidationError(self.error_messages['required'])
if value and not re.match(CREDIT_CARD_RE, value):
raise forms.utils.ValidationError(self.error_messages['invalid'])
return value

@ -1,35 +1,35 @@
[ [
{ {
"model": "user.user", "model": "user.user",
"pk": 1, "pk": 1,
"fields": { "fields": {
"password": "pbkdf2_sha256$100000$HoGxjmAQy4yo$3u+CrUiFtooddpg16OEUaSSjqUlwgFUBP1gt75hEoJs=", "password": "pbkdf2_sha256$100000$HoGxjmAQy4yo$3u+CrUiFtooddpg16OEUaSSjqUlwgFUBP1gt75hEoJs=",
"last_login": "2018-01-31T14:51:34Z", "last_login": "2018-01-31T14:51:34Z",
"is_superuser": true, "is_superuser": true,
"username": "admin@lil.city", "username": "admin@lil.city",
"first_name": "Lil", "first_name": "Lil",
"last_name": "City", "last_name": "City",
"is_staff": true, "is_staff": true,
"is_active": true, "is_active": true,
"date_joined": "2018-01-28T08:41:19Z", "date_joined": "2018-01-28T08:41:19Z",
"email": "admin@lil.city", "email": "admin@lil.city",
"role": 2, "role": 2,
"gender": "n", "gender": "n",
"country": "", "country": "",
"city": "", "city": "",
"about": "\u0417\u0430\u043a\u043e\u043d\u0447\u0438\u043b\u0430 \u041f\u0425\u0423 \u0438\u043c \u041a.\u0410.\u0421\u0430\u0432\u0438\u0446\u043a\u043e\u0433\u043e \u0445\u0443\u0434\u043e\u0436\u043d\u0438\u043a \u0442\u0435\u0430\u0442\u0440\u0430 \u0438 \u043a\u0438\u043d\u043e. \u0420\u0430\u0431\u043e\u0442\u0430\u043b\u0430 \u0441 \u043a\u0440\u0443\u043f\u043d\u0435\u0439\u0448\u0438\u043c\u0438 \u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u0438\u043c\u0438 \u0438 \u0437\u0430\u0440\u0443\u0431\u0435\u0436\u043d\u044b\u043c\u0438 \u0438\u0437\u0434\u0430\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u0430\u043c\u0438. \u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a \u0438 \u043f\u043e\u0431\u0435\u0434\u0438\u0442\u0435\u043b\u044c \u043c\u0435\u0436\u0434\u0443\u043d\u0430\u0440\u043e\u0434\u043d\u044b\u0445 \u0432\u044b\u0441\u0442\u0430\u0432\u043e\u043a. \u041e\u0441\u043d\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \"Lil City\".", "about": "Закончила ПХУ им К.А.Савицкого художник театра и кино. Работала с крупнейшими российскими и зарубежными издательствами. Участник и победитель международных выставок. Основатель компании \"Lil City\".",
"instagram": null, "instagram": null,
"facebook": null, "facebook": null,
"twitter": null, "twitter": null,
"pinterest": null, "pinterest": null,
"youtube": null, "youtube": null,
"vkontakte": null, "vkontakte": null,
"fb_id": null, "fb_id": null,
"fb_data": {}, "fb_data": {},
"is_email_proved": true, "is_email_proved": true,
"photo": "", "photo": "",
"groups": [], "groups": [],
"user_permissions": [] "user_permissions": []
}
} }
] }
]

@ -1,6 +1,8 @@
from django import forms from django import forms
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from .fields import CreditCardField
User = get_user_model() User = get_user_model()
@ -47,3 +49,8 @@ class UserEditForm(forms.ModelForm):
'vkontakte', 'vkontakte',
'photo', 'photo',
) )
class WithdrawalForm(forms.Form):
amount = forms.DecimalField(required=True, min_value=2000)
card = CreditCardField(required=True)

@ -22,64 +22,114 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<div class="section"> {% 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 %}
{% if request.user.role == 1 or request.user.role == 2 %}
<div class="section section_gray">
<div class="section__center center center_xs"> <div class="section__center center center_xs">
<div class="title title_sm">Вывести деньги со счета</div> <div class="title title_sm">Вывести деньги со счета</div>
<div class="form"> <form class="form" method="POST">{% csrf_token %}
<div class="form__group"> <div class="form__group">
<div class="form__content">На вашем счету {{ request.user.balance }} руб.</div> <div class="form__content">На вашем счету {{ request.user.balance }} руб.</div>
<div class="form__field field"> <div class="form__field field{% if form.amount.errors %} error{% endif %}">
<div class="field__label">СУММА</div> <div class="field__label">СУММА</div>
<div class="field__wrap"> <div class="field__wrap">
<input class="field__input" type="text" placeholder="{{ request.user.balance }}"> <input class="field__input" name="amount" required="" id="id_amount" type="number" pattern="\d+(,\d{2})?" placeholder="{{ request.user.balance }}">
</div> </div>
<div class="field__error">Размер выводимой суммы не должно быть менее 2000 рублей.</div> <div class="field__error">Размер выводимой суммы не должно быть менее 2000 рублей.</div>
{% if form.amount.errors %}
<div class="field__error">{{ form.amount.errors }}</div>
{% endif %}
</div> </div>
<div class="form__field field"> <div class="form__field field{% if form.card.errors %} error{% endif %}">
<div class="field__label">НОМЕР КРЕДИТНОЙ КАРТЫ <div class="field__label">НОМЕР КРЕДИТНОЙ КАРТЫ
<span class="field__required">*</span> <span class="field__required">*</span>
</div> </div>
<div class="field__wrap"> <div class="field__wrap">
<input class="field__input" type="email" placeholder="0000 0000 0000 0000"> <input class="field__input" name="card" required="" id="id_card" type="text" autocomplete="off" placeholder="0000 0000 0000 0000">
<div class="field__preview"> <div class="field__preview">
<img class="field__pic" src="{% static 'img/paysys.png' %}"> <img class="field__pic" src="{% static 'img/paysys.png' %}">
</div> </div>
</div> </div>
{% if form.card.errors %}
<div class="field__error">{{ form.card.errors }}</div>
{% endif %}
</div> </div>
</div> </div>
<div class="form__foot"> <div class="form__foot">
<button class="form__btn btn btn_md">СОХРАНИТЬ</button> <button class="form__btn btn btn_md">СОХРАНИТЬ</button>
</div> </div>
</div> </form>
</div> </div>
</div> </div>
<div class="section"> {% endif %}
<div class="section section_gray">
<div class="section__center center center_xs"> <div class="section__center center center_xs">
<div class="title title_sm">История платежей</div> <div class="title title_sm">История платежей</div>
<div class="transactions"> <div class="transactions">
<div class="transactions__wrap"> <div class="transactions__wrap">
{% if request.user.role == 1 or request.user.role == 2 %} {% if request.user.payments.all|length %}
{% for balance in request.user.balances.all %} {% for payment in request.user.payments.all %}
<div class="transactions__row"> <div class="transactions__row">
<div class="transactions__cell">{{balance.payment.course.title}}</div> {% if payment.course %}
<div class="transactions__cell">{{balance.amount}}</div> <div class="transactions__cell">Курс. {{payment.course.title}}</div>
<div class="transactions__cell">Получено</div> {% else %}
</div> <div class="transactions__cell">
{% endfor %} Школа. {% if payment.date_start and payment.date_end %}{{ payment.date_start }} - {{ payment.date_end }}{% endif %}
{{ payment }}
</div>
{% endif %}
{% if payment.balance %}
<div class="transactions__cell">{{payment.balance.amount}}</div>
{% else %}
<div class="transactions__cell">{{payment.amount}}</div>
{% endif %}
{% if payment.balance.type == 1 %}
<div class="transactions__cell">
{% if payment.balance.status == 0 %}
<span class="transactions__cell__pending">Ожидается подтверждение выплаты</span>
{% elif payment.balance.status == 1 %}
<span class="transactions__cell__success">Выплачено</span>
{% else %}
<span class="transactions__cell__error">Выплата отменена</span>
Причина: "{{ payment.balance.cause }}
{% endif %}
</div>
{% else %}
<div class="transactions__cell">
{% if payment.is_deliverable %}
<span class="transactions__cell__success">Получено</span>
{% elif payment.is_under_review %}
<span class="transactions__cell__pending">Ожидается подтверждение оплаты</span>
{% else %}
<span class="transactions__cell__error">Ошибка оплаты</span>
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
{% else %} {% else %}
{% for payment in request.user.payments.all %} Оплат еще не было
<div class="transactions__row">
<div class="transactions__cell">{{payment.course.title}}</div>
<div class="transactions__cell">{{payment.amount}}</div>
<div class="transactions__cell">Получено</div>
</div>
{% endfor %}
{% endif %} {% endif %}
</div> </div>
<div class="transactions__load load"> <!--<div class="transactions__load load">-->
<button class="load__btn btn">еще</button> <!--<button class="load__btn btn">еще</button>-->
</div> <!--</div>-->
</div> </div>
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}
{% block foot %}
<script>
(new Inputmask('9999 9999 9999 9999', {placeholder: "0"})).mask(document.querySelector('#id_card'));
</script>
{% endblock foot %}

@ -98,7 +98,7 @@
<div class="tabs__item js-tabs-item"> <div class="tabs__item js-tabs-item">
<div class="courses courses_scroll"> <div class="courses courses_scroll">
<div class="courses__list"> <div class="courses__list">
{% include "course/school.html" with course_items=published %} {% include "course/school.html" %}
</div> </div>
</div> </div>
</div> </div>
@ -120,4 +120,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

@ -1,9 +1,15 @@
import arrow
from io import BytesIO from io import BytesIO
from PIL import Image from PIL import Image
from os.path import splitext from os.path import splitext
from datetime import timedelta
from paymentwall import Pingback
from django.contrib.auth import login from django.contrib.auth import login
from django.core.exceptions import ValidationError
from django.shortcuts import render, reverse, redirect from django.shortcuts import render, reverse, redirect
from django.views.generic import DetailView, UpdateView, TemplateView from django.views.generic import DetailView, UpdateView, TemplateView, FormView
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
@ -11,13 +17,15 @@ from django.contrib.auth.hashers import check_password, make_password
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.timezone import now
from apps.auth.tokens import verification_email_token from apps.auth.tokens import verification_email_token
from apps.course.models import Course from apps.course.models import Course
from apps.payment.models import CoursePayment
from apps.notification.utils import send_email from apps.notification.utils import send_email
from apps.school.models import SchoolSchedule
from apps.payment.models import AuthorBalance, CoursePayment, SchoolPayment
from .forms import UserEditForm from .forms import UserEditForm, WithdrawalForm
User = get_user_model() User = get_user_model()
@ -31,6 +39,7 @@ def resend_email_verify(request):
return redirect('user-edit-profile', request.user.id) return redirect('user-edit-profile', request.user.id)
@method_decorator(login_required, name='dispatch')
class UserView(DetailView): class UserView(DetailView):
model = User model = User
template_name = 'user/profile.html' template_name = 'user/profile.html'
@ -47,11 +56,35 @@ class UserView(DetailView):
author=self.object, status=Course.DRAFT author=self.object, status=Course.DRAFT
) )
context['paid'] = Course.objects.filter( context['paid'] = Course.objects.filter(
payments__in=CoursePayment.objects.filter(user=self.object), payments__in=CoursePayment.objects.filter(
user=self.object,
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
),
).distinct() ).distinct()
school_payment = SchoolPayment.objects.filter(
user=self.object,
date_start__lte=now(),
date_end__gt=now(),
status__in=[
Pingback.PINGBACK_TYPE_REGULAR,
Pingback.PINGBACK_TYPE_GOODWILL,
Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,
],
).last()
context['school_payment'] = school_payment
if school_payment and school_payment.date_end:
context['school_days_left'] = (school_payment.date_end - now().date()).days
context['school_schedules'] = SchoolSchedule.objects.filter(
weekday__in=school_payment.weekdays if school_payment else [],
)
return context return context
@method_decorator(login_required, name='dispatch')
class NotificationEditView(TemplateView): class NotificationEditView(TemplateView):
template_name = 'user/notification-settings.html' template_name = 'user/notification-settings.html'
@ -59,13 +92,40 @@ class NotificationEditView(TemplateView):
return super().get(request) return super().get(request)
class PaymentHistoryView(TemplateView): @method_decorator(login_required, name='dispatch')
class PaymentHistoryView(FormView):
template_name = 'user/payment-history.html' template_name = 'user/payment-history.html'
form_class = WithdrawalForm
def get(self, request, pk=None): def get(self, request, pk=None):
return super().get(request) return super().get(request)
def post(self, request, pk=None):
form = self.get_form()
if AuthorBalance.objects.filter(created_at__gte=now() - timedelta(days=30)).exists():
messages.error(request, 'Запрос на вывод средств можно сделать только один раз в 30 дней.')
return self.form_invalid(form)
if form.is_valid():
if request.user.balance < form.cleaned_data['amount']:
form.errors['amount'] = 'Сумма для вывода не может быть меньше средств на счету'
return self.form_invalid(form)
AuthorBalance.objects.create(
author=request.user,
type=AuthorBalance.OUT,
amount=form.cleaned_data['amount'],
status=AuthorBalance.PENDING,
card=form.cleaned_data['amount'],
)
return self.form_valid(form)
else:
return self.form_invalid(form)
def get_success_url(self):
success_url = reverse_lazy('user-edit-payments', args=[self.request.user.id])
return success_url
@method_decorator(login_required, name='dispatch')
class UserEditView(UpdateView): class UserEditView(UpdateView):
model = User model = User
template_name = 'user/profile-settings.html' template_name = 'user/profile-settings.html'

@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/2.0/ref/settings/
import os import os
from collections import OrderedDict from collections import OrderedDict
from datetime import timedelta
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -215,13 +216,20 @@ CELERY_BROKER_URL = 'redis://redis:6379/0'
CELERY_RESULT_BACKEND = 'redis://redis:6379/1' CELERY_RESULT_BACKEND = 'redis://redis:6379/1'
CELERY_TASK_SERIALIZER = 'json' CELERY_TASK_SERIALIZER = 'json'
CELERY_BEAT_SCHEDULE = {
'retrieve_photos_from_instagram': {
'task': 'apps.content.tasks.retrieve_photos',
'schedule': timedelta(minutes=2),
'args': (),
},
}
# Dynamic settings # Dynamic settings
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
CONSTANCE_CONFIG = OrderedDict(( CONSTANCE_CONFIG = OrderedDict((
('INSTAGRAM_CLIENT_LOGIN', ('', '')), ('INSTAGRAM_CLIENT_ACCESS_TOKEN', ('7145314808.f6fa114.ce354a5d876041fc9d3db04b0045587d', '')),
('INSTAGRAM_CLIENT_PASSWORD', ('', '')), ('INSTAGRAM_CLIENT_SECRET', ('2334a921425140ccb180d145dcd35b25', '')),
('INSTAGRAM_RESULTS_TAG', ('#lil_акварель', 'Тэг результатов работ.')), ('INSTAGRAM_RESULTS_TAG', ('#lil_акварель', 'Тэг результатов работ.')),
('INSTAGRAM_RESULTS_PATH', ('media/instagram/results/', 'Путь до результатов работ.')), ('INSTAGRAM_RESULTS_PATH', ('media/instagram/results/', 'Путь до результатов работ.')),
('SERVICE_COMMISSION', (10, 'Комиссия сервиса в процентах.')) ('SERVICE_COMMISSION', (10, 'Комиссия сервиса в процентах.'))

@ -440,7 +440,7 @@
<div class="buy__list"> <div class="buy__list">
{% for school_schedule in school_schedules %} {% for school_schedule in school_schedules %}
<label class="switch switch_lesson"> <label class="switch switch_lesson">
<input class="switch__input" type="checkbox"> <input class="switch__input" type="checkbox" data-day="{{school_schedule.weekday}}" data-price="{{school_schedule.month_price}}" data-discount="{{school_schedule.day_discount}}" autocomplete="off">
<span class="switch__content"> <span class="switch__content">
<span class="switch__cell">{{ school_schedule }}</span> <span class="switch__cell">{{ school_schedule }}</span>
{% comment %} dont delete {% endcomment %} {% comment %} dont delete {% endcomment %}
@ -463,7 +463,7 @@
</div> </div>
<div class="order__foot"> <div class="order__foot">
<div class="order__subtitle">Итого, за месяц:</div> <div class="order__subtitle">Итого, за месяц:</div>
<div class="order__total">1800р.</div> <div class="order__total order_price_text">1800р.</div>
</div> </div>
</div> </div>
</div> </div>
@ -471,7 +471,7 @@
</div> </div>
<div class="buy__foot"> <div class="buy__foot">
{% comment %}В ссылке, в параметре запроса weekdays, нужно указать выбранные дни недели{% endcomment %} {% comment %}В ссылке, в параметре запроса weekdays, нужно указать выбранные дни недели{% endcomment %}
<a class="buy__btn btn btn_md" href="{% url 'school-checkout' %}?weekdays=1&weekdays=2">ПЕРЕЙТИ К ОПЛАТЕ</a> <a class="buy__btn btn btn_md but_btn_popup" data-link="{% url 'school-checkout' %}" href="{% url 'school-checkout' %}?weekdays=1&weekdays=2">ПЕРЕЙТИ К ОПЛАТЕ</a>
</div> </div>
</div> </div>
</div> </div>

@ -6,7 +6,11 @@
<div class="main__center center"> <div class="main__center center">
<div class="main__title">Первая онлайн-школа креативного мышления для детей! 5+</div> <div class="main__title">Первая онлайн-школа креативного мышления для детей! 5+</div>
<a <a
{% if not user.is_authenticated %}
data-popup=".js-popup-auth"
{% else %}
data-popup=".js-popup-buy" data-popup=".js-popup-buy"
{% endif %}
class="main__btn btn" class="main__btn btn"
href="#" href="#"
>КУПИТЬ ДОСТУП ОТ 2000р. в мес.</a> >КУПИТЬ ДОСТУП ОТ 2000р. в мес.</a>
@ -192,42 +196,42 @@
<div class="gallery"> <div class="gallery">
<div class="gallery__grid"> <div class="gallery__grid">
<div class="gallery__item gallery__item_lg"> <div class="gallery__item gallery__item_lg">
<img class="gallery__pic" src="{% static 'img/box.jpg' %}"> <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/0.jpg" onerror="this.style.display='none'">
</div> </div>
<div class="gallery__item"> <div class="gallery__item">
<img class="gallery__pic" src="{% static 'img/box.jpg' %}"> <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/1.jpg" onerror="this.style.display='none'">
</div> </div>
<div class="gallery__item"> <div class="gallery__item">
<img class="gallery__pic" src="{% static 'img/box.jpg' %}"> <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/2.jpg" onerror="this.style.display='none'">
</div> </div>
<div class="gallery__item"> <div class="gallery__item">
<img class="gallery__pic" src="{% static 'img/box.jpg' %}"> <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/3.jpg" onerror="this.style.display='none'">
</div> </div>
<div class="gallery__item"> <div class="gallery__item">
<img class="gallery__pic" src="{% static 'img/box.jpg' %}"> <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/4.jpg" onerror="this.style.display='none'">
</div> </div>
<div class="gallery__item"> <div class="gallery__item">
<img class="gallery__pic" src="{% static 'img/box.jpg' %}"> <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/5.jpg" onerror="this.style.display='none'">
</div> </div>
</div> </div>
<div class="gallery__grid"> <div class="gallery__grid">
<div class="gallery__item"> <div class="gallery__item">
<img class="gallery__pic" src="{% static 'img/box.jpg' %}"> <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/6.jpg" onerror="this.style.display='none'">
</div> </div>
<div class="gallery__item"> <div class="gallery__item">
<img class="gallery__pic" src="{% static 'img/box.jpg' %}"> <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/7.jpg" onerror="this.style.display='none'">
</div> </div>
<div class="gallery__item"> <div class="gallery__item">
<img class="gallery__pic" src="{% static 'img/box.jpg' %}"> <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/8.jpg" onerror="this.style.display='none'">
</div> </div>
<div class="gallery__item gallery__item_lg"> <div class="gallery__item gallery__item_lg">
<img class="gallery__pic" src="{% static 'img/box.jpg' %}"> <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/9.jpg" onerror="this.style.display='none'">
</div> </div>
<div class="gallery__item"> <div class="gallery__item">
<img class="gallery__pic" src="{% static 'img/box.jpg' %}"> <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/10.jpg" onerror="this.style.display='none'">
</div> </div>
<div class="gallery__item"> <div class="gallery__item">
<img class="gallery__pic" src="{% static 'img/box.jpg' %}"> <img class="gallery__pic" src="{% get_media_prefix %}instagram/results/11.jpg" onerror="this.style.display='none'">
</div> </div>
</div> </div>
</div> </div>

@ -24,13 +24,13 @@ from apps.course.views import (
lessoncomment, CourseEditView, lessoncomment, CourseEditView,
CourseOnModerationView, CourseOnModerationView,
) )
from apps.course.models import Course
from apps.user.views import ( from apps.user.views import (
UserView, UserEditView, NotificationEditView, UserView, UserEditView, NotificationEditView,
PaymentHistoryView, resend_email_verify, PaymentHistoryView, resend_email_verify,
) )
from apps.payment.views import CourseBuyView, PaymentwallCallbackView, SchoolBuyView from apps.payment.views import CourseBuyView, PaymentwallCallbackView, SchoolBuyView
from apps.school.models import SchoolSchedule
from .views import IndexView
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
@ -48,6 +48,7 @@ urlpatterns = [
path('lesson/<int:pk>/', LessonView.as_view(), name='lesson'), path('lesson/<int:pk>/', LessonView.as_view(), name='lesson'),
path('lesson/<int:lesson_id>/comment', lessoncomment, name='lessoncomment'), path('lesson/<int:lesson_id>/comment', lessoncomment, name='lessoncomment'),
path('payments/ping', PaymentwallCallbackView.as_view(), name='payment-ping'), path('payments/ping', PaymentwallCallbackView.as_view(), name='payment-ping'),
path('paymentwall/pingback', PaymentwallCallbackView.as_view(), name='payment-ping-second'),
path('payments/success', TemplateView.as_view(template_name='payment/payment_success.html'), name='payment-success'), 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('payments/error', TemplateView.as_view(template_name='payment/payment_error.html'), name='payment-error'),
path('school/checkout', SchoolBuyView.as_view(), name='school-checkout'), path('school/checkout', SchoolBuyView.as_view(), name='school-checkout'),
@ -60,15 +61,7 @@ urlpatterns = [
path('privacy', TemplateView.as_view(template_name="templates/lilcity/privacy_policy.html"), name='privacy'), 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('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('refund-policy', TemplateView.as_view(template_name="templates/lilcity/refund_policy.html"), name='refund_policy'),
path('', path('', IndexView.as_view(), name='index'),
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('api/v1/', include(('api.v1.urls', 'api_v1'))),
path('test', TemplateView.as_view(template_name="templates/lilcity/test.html"), name='test'), path('test', TemplateView.as_view(template_name="templates/lilcity/test.html"), name='test'),
] ]

@ -0,0 +1,16 @@
from django.views.generic import TemplateView
from apps.course.models import Course
from apps.school.models import SchoolSchedule
class IndexView(TemplateView):
template_name = 'templates/lilcity/main.html'
def get_context_data(self):
context = super().get_context_data()
context.update({
'course_items': Course.objects.filter(status=Course.PUBLISHED)[:3],
'school_schedules': SchoolSchedule.objects.all(),
})
return context

@ -1,4 +1,5 @@
# Python-3.6 # Python-3.6
requests==2.18.4
Django==2.0.2 Django==2.0.2
django-anymail[mailgun]==1.2 django-anymail[mailgun]==1.2
# paymentwall-python==1.0.7 # paymentwall-python==1.0.7
@ -18,4 +19,5 @@ drf-yasg[validation]==1.4.0
django-silk==2.0.0 django-silk==2.0.0
django-cors-headers==2.1.0 django-cors-headers==2.1.0
django-constance[database]==2.1.0 django-constance[database]==2.1.0
InstagramAPI==1.0.2 # python-instagram==1.3.2
git+https://github.com/ivlevdenis/python-instagram.git

@ -6161,6 +6161,11 @@
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"dev": true "dev": true
}, },
"inputmask": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/inputmask/-/inputmask-3.3.11.tgz",
"integrity": "sha1-FCHJSuKMPc0bTSYze1CLs0mY4tg="
},
"interpret": { "interpret": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",

@ -57,6 +57,7 @@
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"history": "^4.7.2", "history": "^4.7.2",
"ilyabirman-likely": "^2.3.0", "ilyabirman-likely": "^2.3.0",
"inputmask": "^3.3.11",
"jquery": "^3.3.1", "jquery": "^3.3.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"moment": "^2.20.1", "moment": "^2.20.1",

@ -1,6 +1,7 @@
import $ from 'jquery'; import $ from 'jquery';
import Inputmask from "inputmask";
import SmoothScroll from 'smooth-scroll/dist/js/smooth-scroll'; import SmoothScroll from 'smooth-scroll/dist/js/smooth-scroll';
window.Inputmask = Inputmask;
$(document).ready(function () { $(document).ready(function () {
// Добавляем заголовок X-CSRFToken для всех AJAX запросов JQuery. // Добавляем заголовок X-CSRFToken для всех AJAX запросов JQuery.
$.ajaxSetup({ $.ajaxSetup({

@ -47,4 +47,47 @@ $(document).ready(function () {
popup.removeClass('open'); popup.removeClass('open');
}, 300); }, 300);
} }
var selectedWeekdays = {};
$(document).on('change', '[data-day]', function(){
var weekday = $(this).data('day');
var price = $(this).data('price');
var discount = $(this).data('discount');
if($(this).is(':checked')) {
console.log('checked');
selectedWeekdays[weekday] = {price:price, discount:discount};
} else {
console.log('not checked');
delete selectedWeekdays[weekday];
}
updateCart();
});
function updateCart(){
var days = ['', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'];
var weekdays = [], daysText = [], price = 0, discount = 0;
for(var i in selectedWeekdays) {
price += parseInt(selectedWeekdays[i].price);
discount += parseInt(selectedWeekdays[i].discount);
weekdays.push(i);
daysText.push(days[i]);
}
var text = '';
if(weekdays.length >= 7) {
text = '<del>'+price+'</del> '+(price-discount)+'р.';
} else {
text = price+'p.';
}
$('.order_price_text').html(text);
$('.order__days').html((daysText.length) ? daysText.join(', '):'Ничего не выбрано');
var link = $('.but_btn_popup').data('link');
link = link+'?'+decodeURIComponent($.param({weekdays: weekdays}, true));
$('.but_btn_popup').attr('href', link);
}
updateCart();
}); });

@ -2648,6 +2648,8 @@ a.grey-link
padding: 2px padding: 2px
background-image: linear-gradient(-225deg, #FFE2EB 0%, #D8F5F5 100%) background-image: linear-gradient(-225deg, #FFE2EB 0%, #D8F5F5 100%)
border-radius: 8px border-radius: 8px
&__days
min-height: 58px
&__wrap &__wrap
padding: 20px 30px 30px padding: 20px 30px 30px
background: white background: white
@ -2681,6 +2683,8 @@ a.grey-link
padding: 20px 20px 0 padding: 20px 20px 0
&__total &__total
margin-left: auto margin-left: auto
del
color: #a0a0a0
.lock .lock
padding: 50px 60px 40px padding: 50px 60px 40px
@ -2871,6 +2875,12 @@ a.grey-link
&__cell &__cell
padding: 0 10px padding: 0 10px
font-size: 13px font-size: 13px
&__success
color: $green
&__pending
color: $gray
&__error
color: $pink
&:first-child &:first-child
+fb +fb
font-size: 12px font-size: 12px
@ -3339,10 +3349,14 @@ a
flex-wrap: wrap flex-wrap: wrap
&__block &__block
flex: 0 0 calc(50% - 20px) flex: 0 0 calc(50% - 20px)
+m
flex: 0 0 calc(100% - 20px)
&__list &__list
display: flex display: flex
margin: 0 -10px margin: 0 -10px
flex-wrap: wrap flex-wrap: wrap
+m
margin-top: 20px
&__item &__item
display: block display: block
margin: 0 10px 40px 10px margin: 0 10px 40px 10px
@ -3366,10 +3380,14 @@ a
margin-top: 20px margin-top: 20px
a a
width: 70% width: 70%
+m
width: 100%;
&__btn__prev &__btn__prev
margin-top: 12px margin-top: 12px
a a
width: 70% width: 70%
+m
width: 100%;
&__month &__month
margin-bottom: 20px margin-bottom: 20px
flex: 0 0 320px flex: 0 0 320px
@ -3377,6 +3395,8 @@ a
background-image: linear-gradient(-225deg, #FFE2EB 0%, #D8F5F5 100%) background-image: linear-gradient(-225deg, #FFE2EB 0%, #D8F5F5 100%)
width: 70% width: 70%
padding: 2px padding: 2px
+m
width: 100%;
&__wrap &__wrap
padding: 5px 0 10px padding: 5px 0 10px
background: white background: white

@ -347,6 +347,14 @@ autoprefixer@^6.3.1, autoprefixer@^6.3.3:
postcss "^5.2.16" postcss "^5.2.16"
postcss-value-parser "^3.2.3" postcss-value-parser "^3.2.3"
autosize-input@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/autosize-input/-/autosize-input-0.4.0.tgz#a5cdb43110e31f6a1d9fb695ccb747495af0bb5e"
autosize@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.0.tgz#7a0599b1ba84d73bd7589b0d9da3870152c69237"
aws-sign2@~0.6.0: aws-sign2@~0.6.0:
version "0.6.0" version "0.6.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
@ -3282,6 +3290,10 @@ ieee754@^1.1.4:
version "1.1.8" version "1.1.8"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
ilyabirman-likely@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/ilyabirman-likely/-/ilyabirman-likely-2.3.0.tgz#4462becc5dedeb36b74bf4ba339a0ceab820785f"
immutable@3.8.2, immutable@^3.7.6: immutable@3.8.2, immutable@^3.7.6:
version "3.8.2" version "3.8.2"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
@ -3979,6 +3991,10 @@ lodash.clonedeep@^4.3.2:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
lodash.defaults@^4.0.1: lodash.defaults@^4.0.1:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
@ -6768,6 +6784,13 @@ void-elements@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
vue-autosize@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/vue-autosize/-/vue-autosize-1.0.2.tgz#d4159848a7c0f839d49dea45412ce472cdd3a48a"
dependencies:
autosize "^4.0.0"
autosize-input "^0.4.0"
vue-hot-reload-api@^2.2.0: vue-hot-reload-api@^2.2.0:
version "2.2.4" version "2.2.4"
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.2.4.tgz#683bd1d026c0d3b3c937d5875679e9a87ec6cd8f" resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.2.4.tgz#683bd1d026c0d3b3c937d5875679e9a87ec6cd8f"
@ -6823,6 +6846,10 @@ vuejs-datepicker@^0.9.25:
version "0.9.26" version "0.9.26"
resolved "https://registry.yarnpkg.com/vuejs-datepicker/-/vuejs-datepicker-0.9.26.tgz#63e8ab8f3725663f3f90543545d3c097b8ff8f43" resolved "https://registry.yarnpkg.com/vuejs-datepicker/-/vuejs-datepicker-0.9.26.tgz#63e8ab8f3725663f3f90543545d3c097b8ff8f43"
vuelidate@^0.6.1:
version "0.6.2"
resolved "https://registry.yarnpkg.com/vuelidate/-/vuelidate-0.6.2.tgz#b417f5367407b047a2548119b25db56fe2bd5753"
warning@^3.0.0: warning@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"

Loading…
Cancel
Save