Merge branch 'master' of gitlab.com:lilcity/backend into hotfix/LIL-731

remotes/origin/hotfix/LIL-731
gzbender 7 years ago
commit ee8b155c91
  1. 2
      api/v1/serializers/course.py
  2. 67
      api/v1/serializers/payment.py
  3. 5
      api/v1/urls.py
  4. 36
      api/v1/views.py
  5. 18
      apps/course/migrations/0047_course_old_price.py
  6. 6
      apps/course/models.py
  7. 7
      apps/course/templates/course/_items.html
  8. 9
      apps/course/templates/course/course.html
  9. 2
      apps/course/templates/course/course_only_lessons.html
  10. 18
      apps/payment/migrations/0030_auto_20190114_1649.py
  11. 2
      apps/payment/models.py
  12. 2
      apps/payment/templates/payment/course_payment_success.html
  13. 2
      apps/payment/templates/payment/payment_success.html
  14. 6
      apps/school/models.py
  15. 2
      apps/school/templates/blocks/open_lesson.html
  16. 2
      apps/school/templates/school/livelessons_list.html
  17. 2
      apps/school/templates/summer/open_lesson.html
  18. 5
      apps/school/urls.py
  19. 7
      apps/school/views.py
  20. 2
      project/settings.py
  21. 2
      project/templates/blocks/lil_store_js.html
  22. 2
      project/templates/lilcity/index.html
  23. 36
      web/src/components/CourseRedactor.vue
  24. 2
      web/src/js/modules/api.js
  25. 7
      web/src/sass/_common.sass

@ -128,6 +128,7 @@ class CourseCreateSerializer(DispatchContentMixin,
'from_author',
'cover',
'price',
'old_price',
'age',
'is_infinite',
'deferred_start_at',
@ -280,6 +281,7 @@ class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
'from_author',
'cover',
'price',
'old_price',
'age',
'is_infinite',
'deferred_start_at',

@ -3,7 +3,7 @@ from rest_framework import serializers
from apps.payment.models import (
AuthorBalance, Payment,
CoursePayment, SchoolPayment,
GiftCertificatePayment)
GiftCertificatePayment, UserBonus,)
from .user import UserSerializer
from .course import CourseSerializer
@ -200,3 +200,68 @@ class GiftCertificatePaymentSerializer(serializers.ModelSerializer):
'created_at',
'update_at',
)
class UserBonusCreateSerializer(serializers.ModelSerializer):
class Meta:
model = UserBonus
fields = (
'user', 'amount', 'payment', 'referral', 'created_at', 'is_service', 'action_name',
)
read_only_fields = (
'id',
'created_at',
)
def to_representation(self, instance):
return UserBonusSerializer(instance, context=self.context).to_representation(instance)
class UserBonusSerializer(serializers.ModelSerializer):
user = UserSerializer()
payment = serializers.SerializerMethodField()
referral = serializers.SerializerMethodField()
class Meta:
model = UserBonus
fields = (
'user', 'amount', 'payment', 'referral', 'created_at', 'is_service', 'action_name',
)
read_only_fields = (
'id',
'user',
'created_at',
)
def get_payment(self, instance):
try:
p = instance.payment
except Exception:
return None
if not p:
return None
data = {
'id': p.id,
'created_at': p.created_at,
'amount': p.amount,
'data': p.data,
'status': p.status,
}
if isinstance(instance.payment, CoursePayment):
data['course'] = {
'id': p.course.id,
'title': p.course.title,
}
return data
def get_referral(self, instance):
try:
r = instance.referral
except Exception:
return None
if not r:
return None
return {
'id': r.id,
'referral': UserSerializer(instance=r.referral).data,
}

@ -19,7 +19,7 @@ from .views import (
SchoolScheduleViewSet, LiveLessonViewSet,
PaymentViewSet, ObjectCommentsViewSet,
ContestViewSet, ContestWorkViewSet,
AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet, UserGalleryViewSet)
AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet, UserGalleryViewSet, BonusesViewSet)
router = DefaultRouter()
router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests')
@ -43,10 +43,9 @@ router.register(r'galleries', GalleryViewSet, base_name='galleries')
router.register(r'gallery-images', GalleryImageViewSet, base_name='gallery-images')
router.register(r'faq', FAQViewSet, base_name='faq')
router.register(r'school-schedules', SchoolScheduleViewSet, base_name='school-schedules')
router.register(r'bonuses', BonusesViewSet, base_name='bonuses')
router.register(r'users', UserViewSet, base_name='users')
router.register(r'user-gallery', UserGalleryViewSet, base_name='user-gallery')
router.register(r'contests', ContestViewSet, base_name='contests')
router.register(r'contest-works', ContestWorkViewSet, base_name='contest_works')

@ -39,7 +39,7 @@ from .serializers.school import (
)
from .serializers.payment import (
AuthorBalanceSerializer, AuthorBalanceCreateSerializer,
PaymentSerializer,
PaymentSerializer, UserBonusSerializer, UserBonusCreateSerializer,
CoursePaymentCreateSerializer, SchoolPaymentCreateSerializer)
from .serializers.user import (
AuthorRequestSerializer,
@ -68,7 +68,7 @@ from apps.content.models import (
Contest, ContestWork, FAQ)
from apps.payment.models import (
AuthorBalance, Payment,
CoursePayment, SchoolPayment,
CoursePayment, SchoolPayment, UserBonus,
)
from apps.school.models import SchoolSchedule, LiveLesson
from apps.user.models import AuthorRequest
@ -724,7 +724,37 @@ class CaptureEmail(views.APIView):
return Response({'status': 'ok'})
class FAQViewSet(ExtendedModelViewSet):
queryset = FAQ.objects.all()
serializer_class = FAQSerializer
class BonusesViewSet(ExtendedModelViewSet):
queryset = UserBonus.objects.all()
serializer_class = UserBonusCreateSerializer
serializer_class_map = {
'list': UserBonusSerializer,
}
permission_classes = (IsAdmin,)
filter_fields = ('user', 'referral', 'payment', 'is_service', 'action_name')
search_fields = (
'action_name',
'user__email',
'user__first_name',
'user__last_name',
'referral__referral__email',
'referral__referral__first_name',
'referral__referral__last_name',
)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
if request.query_params.get('page'):
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2019-01-13 23:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0046_auto_20181009_2334'),
]
operations = [
migrations.AddField(
model_name='course',
name='old_price',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Старая цена курса'),
),
]

@ -80,6 +80,10 @@ class Course(BaseModel, DeactivatedMixin):
verbose_name='Обложка курса', on_delete=models.SET_NULL,
null=True, blank=True,
)
old_price = models.DecimalField(
'Старая цена курса',
max_digits=10, decimal_places=2, null=True, blank=True
)
price = models.DecimalField(
'Цена курса', help_text='Если цена не выставлена, то курс бесплатный',
max_digits=10, decimal_places=2, null=True, blank=True
@ -132,7 +136,7 @@ class Course(BaseModel, DeactivatedMixin):
return self.get_absolute_url()
def get_absolute_url(self):
return reverse_lazy('course', args=[self.id])
return reverse_lazy('course', args=[self.slug or self.id])
@property
def is_free(self):

@ -7,7 +7,7 @@
data-course data-course-id={{ course.id }}
{% if course.is_deferred_start and course.status == 2 %}data-future-course data-future-course-time={{ course.deferred_start_at.timestamp }}{% endif %}
>
<a class="courses__preview" href="{% if course.status <= 1 %}{% url 'course_edit' course.id %}{% else %}{% url 'course' course.id %}?next={{ request.get_full_path }}{% endif %}">
<a class="courses__preview" href="{% if course.status <= 1 %}{% url 'course_edit' course.id %}{% else %}{{ course.url }}{% endif %}">
{% thumbnail course.cover.image "300x200" crop="center" as im %}
<img class="courses__pic" src="{{ im.url }}" width="{{ im.width }}" />
{% empty %}
@ -50,10 +50,13 @@
<a class="courses__theme theme {{ theme_color }}"
href="{% url 'courses' %}?category={{ course.category.id }}">{{ course.category | upper }}</a>
{% if not course.is_free %}
{% if course.old_price %}
<div class="courses__old-price"><s>{{ course.old_price|floatformat:"-2" }}₽</s></div>
{% endif %}
<div class="courses__price">{{ course.price|floatformat:"-2" }}₽</div>
{% endif %}
</div>
<a class="courses__title" href="{% url 'course' course.id %}?next={{ request.get_full_path }}">{{ course.title }}</a>
<a class="courses__title" href="{{ course.url }}">{{ course.title }}</a>
<div class="courses__content">{{ course.short_description | safe | linebreaks | truncatechars_html:300 }}
</div>
<div class="courses__user user">

@ -114,13 +114,16 @@
<div class="meta__title">{{ course.duration | rupluralize:"день,дня,дней" }}</div>
</a>
{% if course.price %}
<div class="meta__item">
<div class="meta__item" title="Цена">
<div class="meta__icon">
<svg class="icon icon-money">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-money"></use>
</svg>
</div>
<div class="meta__title">{{ course.price|floatformat:"-2" }}₽</div>
<div class="meta__title">
{% if course.old_price %}<s>{{ course.old_price|floatformat:"-2" }}₽</s>{% endif %}
{{ course.price|floatformat:"-2" }}₽
</div>
</div>
{% endif %}
<div class="meta__item">
@ -142,7 +145,7 @@
</div>
</div>
<div class="course__actions">
<a href="{% url 'course' course.id %}" class="course__action btn btn_lg{% if not only_lessons %} btn_stroke{% else %} btn_gray{% endif %}">Описание курса</a>
<a href="{{ course.url }}" class="course__action btn btn_lg{% if not only_lessons %} btn_stroke{% else %} btn_gray{% endif %}">Описание курса</a>
{% if request.user.is_authenticated %}
{% if course.author == request.user and request.user.role >= request.user.AUTHOR_ROLE %}
<a

@ -132,7 +132,7 @@
</div>
</div>
<div class="course__actions">
<a href="{% url 'course' course.id %}" class="course__action btn btn_lg{% if not only_lessons %} btn_stroke{% else %} btn_gray{% endif %}">Описание курса</a>
<a href="{{ course.url }}" class="course__action btn btn_lg{% if not only_lessons %} btn_stroke{% else %} btn_gray{% endif %}">Описание курса</a>
{% if course.author == request.user and request.user.role >= request.user.AUTHOR_ROLE %}
<a
href="{% url 'course-only-lessons' course.id %}"

@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2019-01-14 16:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payment', '0029_auto_20181211_1731'),
]
operations = [
migrations.AlterField(
model_name='userbonus',
name='amount',
field=models.DecimalField(decimal_places=2, default=0, max_digits=8),
),
]

@ -304,7 +304,7 @@ class GiftCertificatePayment(Payment):
class UserBonus(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='bonuses')
amount = models.DecimalField(max_digits=8, decimal_places=2, default=0, editable=False)
amount = models.DecimalField(max_digits=8, decimal_places=2, default=0)
payment = models.ForeignKey(Payment, on_delete=models.SET_NULL, null=True)
referral = models.ForeignKey('user.Referral', on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)

@ -4,7 +4,7 @@
<div class="done">
<div class="done__title title">Вы успешно приобрели курс!</div>
<div class="done__foot">
<a class="done__btn btn btn_md btn_stroke" href="{% url 'course' course.id %}">ПЕРЕЙТИ К КУРСУ</a>
<a class="done__btn btn btn_md btn_stroke" href="{{ course.url }}">ПЕРЕЙТИ К КУРСУ</a>
</div>
</div>
</div>

@ -11,7 +11,7 @@
{% if course %}
<div class="done__title title">Вы успешно приобрели курс!</div>
<div class="done__foot">
<a class="done__btn btn btn_md btn_stroke" href="{% url 'course' course.id %}">ПЕРЕЙТИ К КУРСУ</a>
<a class="done__btn btn btn_md btn_stroke" href="{{ course.url }}">ПЕРЕЙТИ К КУРСУ</a>
</div>
{% endif %}
{% if gift_certificate %}

@ -142,8 +142,12 @@ class LiveLesson(BaseModel, DeactivatedMixin):
self.date = (datetime.combine(self.date, now().time()) + timedelta(days=1)).date()
super().save(*args, **kwargs)
@property
def url(self):
return self.get_absolute_url()
def get_absolute_url(self):
return reverse_lazy('school:lesson-detail', args=[str(self.id)])
return reverse_lazy('school:lesson-detail', kwargs={'lesson_date': self.date.strftime('%d-%m-%y')})
def stream_index(self):
return self.stream.split('/')[-1]

@ -1,4 +1,4 @@
<a
class="timing__btn btn btn_light"
href="{% url 'school:lesson-detail' live_lesson.id %}"
href="{{ live_lesson.url }}"
>смотреть урок</a>

@ -11,7 +11,7 @@
{% for livelesson in livelesson_list %}
<div class="lessons__item">
<div class="lessons__actions lessons__actions__no-hover">
<a target="_blank" class="lessons__action" href="{% url 'school:lesson-detail' livelesson.id %}">
<a target="_blank" class="lessons__action" href="{{ livelesson.url }}">
<svg class="icon icon-eye">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-eye"></use>
</svg>

@ -1,4 +1,4 @@
<a
class="timing__btn btn btn_light"
href="{% url 'school:lesson-detail' live_lesson.id %}"
href="{{ live_lesson.url }}"
>подробнее</a>

@ -1,4 +1,4 @@
from django.urls import path, include
from django.urls import path, re_path
from .views import (
LiveLessonsView, LiveLessonEditView,
@ -12,5 +12,6 @@ urlpatterns = [
path('lessons/', LiveLessonsView.as_view(), name='lessons'),
path('lessons/create', LiveLessonEditView.as_view(), name='lessons-create'),
path('lessons/<int:pk>/edit', LiveLessonEditView.as_view(), name='lessons-edit'),
path('lessons/<int:pk>/', LiveLessonsDetailView.as_view(), name='lesson-detail'),
path('lessons/<int:pk>/', LiveLessonsDetailView.as_view(), name='lesson-detail-id'),
re_path(r'(?P<lesson_date>\d+\-\d+\-\d+)', LiveLessonsDetailView.as_view(), name='lesson-detail'),
]

@ -61,8 +61,11 @@ class LiveLessonsDetailView(DetailView):
model = LiveLesson
template_name = 'school/livelesson_detail.html'
def get(self, request, pk=None):
self.object = self.get_object()
def get(self, request, pk=None, lesson_date=None):
if pk:
self.object = self.get_object()
if lesson_date:
self.object = LiveLesson.objects.get(date=datetime.strptime(lesson_date, '%d-%m-%y'))
if request.user.is_authenticated:
is_purchased = SchoolPayment.objects.filter(
user=request.user,

@ -176,6 +176,8 @@ STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
]
# FIXME
# STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

@ -9,7 +9,7 @@
ADMIN_ROLE: 3,
},
pusherKey: '{% setting "PUSHER_KEY" %}',
staticUrl: '{% static "" %}',
staticUrl: '{% get_static_prefix %}',
accessToken: '{{ request.user.auth_token }}',
isMobile: {{ request.user_agent.is_mobile|yesno:"true,false" }},
defaultUserPhoto: "{% static 'img/user_default.jpg' %}",

@ -155,7 +155,7 @@
</div>
{% include 'templates/blocks/lil_store_js.html' %}
{% block pre_app_js %}{% endblock pre_app_js %}
<script type="text/javascript" src="{% static "app.js" %}?2"></script>
<script type="text/javascript" src="{% static "app.js" %}"></script>
<script>
var schoolDiscount = parseFloat({{ config.SERVICE_DISCOUNT }});
var schoolAmountForDiscount = parseFloat({{ config.SERVICE_DISCOUNT_MIN_AMOUNT }});

@ -22,6 +22,7 @@
<lil-select :value.sync="course.category" :options="categoryOptions"
placeholder="Выберите категорию"/>
</div>
<div class="courses__old-price" v-if="course.is_paid && course.old_price"><s>{{ course.old_price }}</s></div>
<div class="courses__price" v-if="course.is_paid && course.price">{{ course.price }}</div>
</div>
<div class="courses__title field field" v-bind:class="{ error: ($v.course.title.$dirty || showErrors) && $v.course.title.$invalid }">
@ -92,7 +93,14 @@
<div v-if="course.is_paid" class="info__field field">
<div class="field__label field__label_gray">СТОИМОСТЬ</div>
<div class="field__wrap field__wrap__appended field__wrap__100px">
<input type="text" class="field__input field__input__appended" v-model.number="displayPrice">
<input type="text" class="field__input field__input__appended" v-model.number.lazy="displayPrice">
<button disabled class="field__append">руб.</button>
</div>
</div>
<div v-if="course.is_paid" class="info__field field">
<div class="field__label field__label_gray">СТОИМОСТЬ БЕЗ СКИДКИ</div>
<div class="field__wrap field__wrap__appended field__wrap__100px">
<input type="text" class="field__input field__input__appended" v-model.number.lazy="displayOldPrice">
<button disabled class="field__append">руб.</button>
</div>
</div>
@ -263,6 +271,7 @@
duration: null,
author: null,
price: null,
old_price: null,
age: 0,
url: '',
coverImage: '',
@ -1011,23 +1020,14 @@
this.course.price = value || 0;
}
},
// userSelect: {
// get() {
// if (!this.users || this.users.length === 0 || !this.course || !this.course.author) {
// return null;
// }
// let value;
// this.users.forEach((user) => {
// if (user.value === this.course.author) {
// value = user;
// }
// });
// return value;
// },
// set(value) {
// this.course.author = value.value;
// }
// },
displayOldPrice: {
get: function () {
return this.course.is_paid && this.course.old_price ? (this.course.old_price || '') : '';
},
set: function (value) {
this.course.old_price = value || 0;
}
},
courseFullUrl() {
if (!this.course.url) {
return `https://lil.city/course/${this.course.id}`;

@ -108,6 +108,7 @@ export const api = {
short_description: courseObject.short_description,
category: courseObject.category,
price: courseObject.is_paid && courseObject.price || 0,
old_price: courseObject.is_paid && courseObject.old_price || 0,
age: courseObject.age,
deferred_start_at: deferredStart,
duration: courseObject.duration || 0,
@ -186,6 +187,7 @@ export const api = {
category: courseJSON.category && courseJSON.category.id ? courseJSON.category.id : courseJSON.category,
author: courseJSON.author && courseJSON.author.id ? courseJSON.author.id : courseJSON.author,
price: parseFloat(courseJSON.price),
old_price: parseFloat(courseJSON.old_price),
is_paid: parseFloat(courseJSON.price) > 0,
is_deferred: isDeferred,
date: deferredDate || courseJSON.date,

@ -1760,12 +1760,14 @@ a.grey-link
&__details
display: flex
margin-bottom: 10px
&__price
margin-left: auto
&__price, &__old-price
margin-left: 20px
+fb
font-size: 12px
letter-spacing: 2px
color: $cl
&__old-price
margin-right: -15px
&__title
display: block
margin-bottom: 10px
@ -1776,6 +1778,7 @@ a.grey-link
line-height: 1.33
&__theme
text-transform: uppercase
flex: 1
&__user
margin-top: 20px
&_two &__item

Loading…
Cancel
Save