Merge branch 'master' of https://gitlab.com/lilcity/backend into feature/LIL-559

remotes/origin/hotfix/LIL-661
gzbender 8 years ago
commit 8fd70682fa
  1. 5
      api/v1/serializers/course.py
  2. 18
      apps/course/migrations/0039_lesson_position.py
  3. 8
      apps/course/models.py
  4. 2
      apps/course/templates/course/course_only_lessons.html
  5. 2
      apps/course/views.py
  6. 3
      apps/payment/admin.py
  7. 18
      apps/payment/migrations/0019_payment_roistat_visit.py
  8. 1
      apps/payment/models.py
  9. 3
      apps/payment/tasks.py
  10. 6
      apps/payment/views.py
  11. 39
      project/templates/lilcity/index.html
  12. 11
      requirements.txt
  13. 140
      web/src/components/CourseRedactor.vue
  14. 8
      web/src/js/modules/api.js

@ -184,6 +184,7 @@ class LessonCreateSerializer(DispatchContentMixin, serializers.ModelSerializer):
'created_at',
'update_at',
'deactivated_at',
'position',
)
read_only_fields = (
@ -196,6 +197,9 @@ class LessonCreateSerializer(DispatchContentMixin, serializers.ModelSerializer):
def create(self, validated_data):
content = validated_data.pop('content', [])
lesson = super().create(validated_data)
if not validated_data.get('position'):
lesson.set_last_position()
lesson.save()
self.dispatch_content(lesson, content)
return lesson
@ -226,6 +230,7 @@ class LessonSerializer(serializers.ModelSerializer):
'created_at',
'update_at',
'deactivated_at',
'position',
)
read_only_fields = (

@ -0,0 +1,18 @@
# Generated by Django 2.0.6 on 2018-07-02 13:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0038_lesson_author'),
]
operations = [
migrations.AddField(
model_name='lesson',
name='position',
field=models.PositiveSmallIntegerField(default=1, verbose_name='Положение на странице'),
),
]

@ -177,10 +177,18 @@ class Lesson(BaseModel, DeactivatedMixin):
)
created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
position = models.PositiveSmallIntegerField(
'Положение на странице',
default=1,
)
def __str__(self):
return self.title
def set_last_position(self):
if self.course:
self.position = self.course.lessons.count()
def save(self, *args, **kwargs):
if not self.author and self.course and self.course.author:
self.author = self.course.author

@ -173,7 +173,7 @@
<div class="lessons">
<div class="lessons__title title">Содержание курса</div>
<div class="lessons__list">
{% for lesson in course.lessons.all %}
{% for lesson in lessons %}
{% if course.author == request.user or request.user.role >= request.user.TEACHER_ROLE or paid %}
<a href="{% url 'lesson' pk=lesson.id %}">
{% else %}

@ -214,6 +214,8 @@ class CourseView(DetailView):
status=Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW,
).exists()
context['only_lessons'] = self.only_lessons
if self.only_lessons:
context['lessons'] = self.object.lessons.order_by('position')
return context
def get_queryset(self):

@ -28,10 +28,11 @@ class PaymentChildAdmin(PolymorphicChildModelAdmin):
'user',
'amount',
'status',
'roistat_visit',
'created_at',
)
base_fieldsets = (
(None, {'fields': ('user', 'amount', 'status', 'data',)}),
(None, {'fields': ('user', 'amount', 'status', 'data', 'roistat_visit',)}),
)
readonly_fields = ('amount', 'data',)

@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2018-07-06 07:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payment', '0018_auto_20180512_1202'),
]
operations = [
migrations.AddField(
model_name='payment',
name='roistat_visit',
field=models.PositiveIntegerField(editable=False, null=True, verbose_name='Номер визита Roistat'),
),
]

@ -97,6 +97,7 @@ class Payment(PolymorphicModel):
amount = models.DecimalField('Итого', max_digits=8, decimal_places=2, default=0, editable=False)
status = models.PositiveSmallIntegerField('Статус платежа', choices=PW_STATUS_CHOICES, null=True)
data = JSONField('Данные платежа от провайдера', default={}, editable=False)
roistat_visit = models.PositiveIntegerField('Номер визита Roistat', null=True, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)

@ -38,12 +38,13 @@ def product_payment_to_mixpanel(user_id, event_name, time, properties):
@app.task
def transaction_to_roistat(user_id, payment_id, event_name, amount, time, status, product_type):
def transaction_to_roistat(user_id, payment_id, event_name, amount, time, status, product_type, roistat_visit):
body = [{
'id': str(payment_id),
'name': event_name,
'date_create': time,
'status': str(status),
'roistat': str(roistat_visit) if roistat_visit else None,
'price': str(amount),
'client_id': str(user_id),
'fields': {

@ -58,12 +58,14 @@ class CourseBuyView(TemplateView):
host = urlsplit(self.request.META.get('HTTP_REFERER'))
host = str(host[0]) + '://' + str(host[1])
course = Course.objects.get(id=pk)
roistat_visit = request.COOKIES.get('roistat_visit', None)
if request.user == course.author:
messages.error(request, 'Вы не можете приобрести свой курс.')
return redirect(reverse_lazy('course', args=[course.id]))
course_payment = CoursePayment.objects.create(
user=request.user,
course=course,
roistat_visit=roistat_visit,
)
product = Product(
f'course_{course_payment.id}',
@ -96,6 +98,7 @@ class SchoolBuyView(TemplateView):
host = str(host[0]) + '://' + str(host[1])
weekdays = set(request.GET.getlist('weekdays', []))
add_days = 'add_days' in request.GET
roistat_visit = request.COOKIES.get('roistat_visit', None)
if not weekdays:
messages.error(request, 'Выберите несколько дней недели.')
return redirect('school:summer-school')
@ -117,6 +120,7 @@ class SchoolBuyView(TemplateView):
date_start=now().date(),
date_end=_school_payment.date_end,
add_days=True,
roistat_visit=roistat_visit,
)
if school_payment.amount <= 0:
messages.error(request, 'Выбранные дни отсутствуют в оставшемся периоде подписки')
@ -125,6 +129,7 @@ class SchoolBuyView(TemplateView):
school_payment = SchoolPayment.objects.create(
user=request.user,
weekdays=weekdays,
roistat_visit=roistat_visit,
)
product = Product(
f'school_{school_payment.id}',
@ -259,6 +264,7 @@ class PaymentwallCallbackView(View):
now().strftime('%Y-%m-%d %H:%M:%S'),
pingback.get_type(),
product_type_name,
payment.roistat_visit,
)
author_balance = getattr(payment, 'author_balance', None)
if author_balance and author_balance.type == AuthorBalance.IN:

@ -76,7 +76,44 @@
&noscript=1"/>
</noscript>
<!-- End Facebook Pixel Code -->
<!-- Global site tag (gtag.js) - Google AdWords: 808701460 --> <script async src="https://www.googletagmanager.com/gtag/js?id=AW-808701460"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'AW-808701460'); </script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-121923960-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-121923960-1');
</script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
(function (d, w, c) {
(w[c] = w[c] || []).push(function() {
try {
w.yaCounter49354039 = new Ya.Metrika2({
id:49354039,
clickmap:true,
trackLinks:true,
accurateTrackBounce:true,
webvisor:true
});
} catch(e) { }
});
var n = d.getElementsByTagName("script")[0],
s = d.createElement("script"),
f = function () { n.parentNode.insertBefore(s, n); };
s.type = "text/javascript";
s.async = true;
s.src = "https://mc.yandex.ru/metrika/tag.js";
if (w.opera == "[object Opera]") {
d.addEventListener("DOMContentLoaded", f, false);
} else { f(); }
})(document, window, "yandex_metrika_callbacks2");
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/49354039" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
{% include "templates/blocks/mixpanel.html" %}
</head>
<body>

@ -1,27 +1,26 @@
# Python-3.6
arrow==0.12.1
celery[redis]==4.2.0
Django==2.0.6
Django==2.0.7
django-active-link==0.1.5
django-anymail[mailgun]==3.0
django-cors-headers==2.3.0
django_compressor==2.2
django-filter==2.0.0.dev1
django-mptt==0.9.0
django-silk==3.0.0
django-phonenumber-field==2.0.0
django-polymorphic-tree==1.5
djangorestframework==3.8.2
drf-yasg[validation]==1.9.0
drf-yasg==1.9.0
facepy==1.0.9
gunicorn==19.8.1
gunicorn==19.9.0
mixpanel==4.3.2
psycopg2-binary==2.7.5
Pillow==5.1.0
Pillow==5.2.0
raven==6.9.0
requests==2.19.1
sorl-thumbnail==12.4.1
twilio==6.14.6
twilio==6.14.7
# paymentwall-python==1.0.7
git+https://github.com/ivlevdenis/paymentwall-python.git
# python-instagram==1.3.2

@ -215,24 +215,31 @@
<div v-if="viewSection === 'lessons'" class="kit__body">
<div class="lessons__title title">Содержание курса</div>
<div v-if="!lessonsLoading" class="lessons__list">
<div class="lessons__item" v-for="(lesson, index) in lessons">
<div class="lessons__actions">
<button type="button" class="lessons__action" @click="removeLesson(index)">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
<button type="button" class="lessons__action" @click="editLesson(index)">
<svg class="icon icon-edit">
<use xlink:href="/static/img/sprite.svg#icon-edit"></use>
</svg>
</button>
</div>
<div class="lessons__subtitle subtitle">{{ lesson.title }}</div>
<div class="lessons__row">
<div class="lessons__content">{{ lesson.short_description }}</div>
</div>
</div>
<vue-draggable v-model="lessons" @start="drag=true" @end="onLessonsChanged" :options="{ handle: '.sortable__handle' }">
<div class="lessons__item" v-for="(lesson, index) in lessons" :key="lesson.id">
<div class="lessons__actions">
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<button type="button" class="lessons__action" @click="removeLesson(index)">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
<button type="button" class="lessons__action" @click="editLesson(index)">
<svg class="icon icon-edit">
<use xlink:href="/static/img/sprite.svg#icon-edit"></use>
</svg>
</button>
</div>
<div class="lessons__subtitle subtitle">{{ lesson.title }}</div>
<div class="lessons__row">
<div class="lessons__content">{{ lesson.short_description }}</div>
</div>
</div>
</vue-draggable>
</div>
<div v-if="lessonsLoading">Загрузка...</div>
<div class="lessons__foot">
@ -307,7 +314,7 @@
price: null,
url: '',
coverImage: '',
coverImageId: null,
kit__body: null,
is_paid: false,
is_featured: true,
is_deferred: false,
@ -507,6 +514,7 @@
api.removeCourseLesson(lesson.id, this.accessToken);
}
this.lessons.splice(lessonIndex, 1);
this.onLessonsChanged();
},
editLesson(lessonIndex) {
this.currentLesson = this.lessons[lessonIndex];
@ -581,11 +589,7 @@
});
}
document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНО';
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
this.changeSavingStatus(true);
showNotification("success", 'Урок сохранён');
// this.goToLessons();
@ -594,10 +598,7 @@
.catch((err) => {
this.lessonSaving = false;
//console.error(err);
document.getElementById('course-redactor__saving-status').innerText = 'ОШИБКА';
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
this.changeSavingStatus(true, true);
// alert('Произошло что-то страшное: '+err.toString());
console.log(err);
if(err.response) {
@ -619,6 +620,14 @@
$(window).scrollTop(elementTop);
});
},
processCourseJson(data) {
this.course = api.convertCourseJson(data);
this.course.live = this.live;
this.lessons = data.lessons.map((lessonJson) => {
return api.convertLessonJson(lessonJson);
});
this.course.duration = this.course.duration || '';
},
loadCourseDraft() {
//console.log('loadCourseDraft');
if(this.live) { return; }
@ -628,12 +637,8 @@
response
.then((response) => {
this.course = api.convertCourseJson(response.data);
this.course.live = this.live;
this.processCourseJson(response.data);
this.courseLoading = false;
this.lessons = response.data.lessons.map((lessonJson) => {
return api.convertLessonJson(lessonJson);
});
})
.catch((err) => {
this.courseLoading = false;
@ -653,20 +658,18 @@
}
request
.then((response) => {
this.course = api.convertCourseJson(response.data);
this.course.live = this.live;
/* if (this.live && this.course.date) {
this.course.date = _.find(this.scheduleOptions, (item) => {
return item.value == this.course.date;
})
} */
this.processCourseJson(response.data);
this.$nextTick(() => {
this.courseLoading = false;
});
this.lessons = response.data.lessons.map((lessonJson) => {
return api.convertLessonJson(lessonJson);
this.lessons.sort((a, b) => {
if (a.position > b.position) {
return 1;
}
if (a.position < b.position) {
return -1;
}
return 0;
});
})
.catch((err) => {
@ -794,17 +797,13 @@
}
this.courseSaving = true;
clearTimeout(this.savingTimeout);
document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНИЕ...';
this.changeSavingStatus();
const courseObject = this.course;
courseObject.url = (courseObject.url) ? slugify(courseObject.url):courseObject.url;
api.saveCourse(courseObject, this.accessToken)
.then((response) => {
this.courseSaving = false;
document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНО';
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
this.changeSavingStatus(true);
this.courseSyncHook = true;
const courseData = api.convertCourseJson(response.data);
if (this.course.coverImage) {
@ -854,10 +853,7 @@
this.courseSyncHook = false;
this.courseSaving = false;
//console.error(err);
document.getElementById('course-redactor__saving-status').innerText = 'ОШИБКА';
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
this.changeSavingStatus(true, true);
// alert('Произошло что-то страшное: '+err.toString());
//console.log(err.response.data);
if(err.response) {
@ -886,10 +882,42 @@
this.viewSection = 'lessons-edit';
}
},
onLessonsChanged() {
let promises = [];
this.courseSaving = true;
this.lessons.map((lesson, index) => {
lesson.position = index + 1;
lesson.course_id = this.course.id;
let res = api.saveLesson(lesson, this.accessToken);
promises.push(res);
});
Promise.all(promises).then(() => {
this.courseSaving = false;
this.changeSavingStatus(true);
}, () => {
this.courseSaving = false;
this.changeSavingStatus(true, true);
});
},
pluralize(count, words) {
var cases = [2, 0, 1, 1, 1, 2];
return words[ (count % 100 > 4 && count % 100 < 20) ? 2 : cases[ Math.min(count % 10, 5)] ];
}
},
changeSavingStatus(saved, error) {
let text = '';
if(error) {
text = 'ОШИБКА';
} else {
text = saved ? 'СОХРАНЕНО' : 'СОХРАНЕНИЕ...';
}
clearTimeout(this.savingTimeout);
document.getElementById('course-redactor__saving-status').innerText = text;
if(saved || error){
this.savingTimeout = setTimeout(() => {
document.getElementById('course-redactor__saving-status').innerText = '';
}, 2000);
}
},
},
mounted() {
this.mounting = true;
@ -1025,7 +1053,7 @@
},
displayPrice: {
get: function () {
return this.course.is_paid ? (this.course.price || 0) : 0;
return this.course.is_paid ? (this.course.price || '') : '';
},
set: function (value) {
this.course.price = value || 0;

@ -107,9 +107,9 @@ export const api = {
author: courseObject.author ? courseObject.author : null,
short_description: courseObject.short_description,
category: courseObject.category,
price: courseObject.is_paid ? courseObject.price : 0,
price: courseObject.is_paid && courseObject.price || 0,
deferred_start_at: deferredStart,
duration: courseObject.duration,
duration: courseObject.duration || 0,
is_featured: courseObject.is_featured,
slug: courseObject.url,
date: (courseObject.date) ? courseObject.date.value:null,
@ -208,6 +208,7 @@ export const api = {
title: lessonObject.title,
short_description: lessonObject.short_description,
course: lessonObject.course_id,
position: lessonObject.position,
content: lessonObject.content.map((block, index) => {
if (block.type === 'text') {
return {
@ -287,7 +288,8 @@ export const api = {
short_description: lessonJSON.short_description,
coverImageId: lessonJSON.cover && lessonJSON.cover.id ? lessonJSON.cover.id : null,
coverImage: lessonJSON.cover && lessonJSON.cover.image ? lessonJSON.cover.image : null,
content: api.convertContentResponse(lessonJSON.content)
content: api.convertContentResponse(lessonJSON.content),
position: lessonJSON.position,
}
},
convertCourseJson: (courseJSON) => {

Loading…
Cancel
Save