From ae0160fc9c356c252bbfeda3b8f6eda028d6fb85 Mon Sep 17 00:00:00 2001 From: gzbender Date: Tue, 3 Jul 2018 01:44:05 +0500 Subject: [PATCH 1/6] =?UTF-8?q?LIL-523=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B2=D0=BE=D0=B7=D0=BC?= =?UTF-8?q?=D0=BE=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BA=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20=D1=83=D1=80=D0=BE=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B8=20=D0=BA=D1=83=D1=80=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/serializers/course.py | 5 + .../course/migrations/0039_lesson_position.py | 18 +++ apps/course/models.py | 8 ++ .../templates/course/course_only_lessons.html | 2 +- apps/course/views.py | 2 + web/src/components/CourseRedactor.vue | 113 ++++++++++++------ web/src/js/modules/api.js | 4 +- 7 files changed, 111 insertions(+), 41 deletions(-) create mode 100644 apps/course/migrations/0039_lesson_position.py diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index d07e58b5..1a9695bb 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -181,6 +181,7 @@ class LessonCreateSerializer(DispatchContentMixin, serializers.ModelSerializer): 'created_at', 'update_at', 'deactivated_at', + 'position', ) read_only_fields = ( @@ -193,6 +194,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 @@ -223,6 +227,7 @@ class LessonSerializer(serializers.ModelSerializer): 'created_at', 'update_at', 'deactivated_at', + 'position', ) read_only_fields = ( diff --git a/apps/course/migrations/0039_lesson_position.py b/apps/course/migrations/0039_lesson_position.py new file mode 100644 index 00000000..7a26d8e5 --- /dev/null +++ b/apps/course/migrations/0039_lesson_position.py @@ -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='Положение на странице'), + ), + ] diff --git a/apps/course/models.py b/apps/course/models.py index 7d0300c7..942a0593 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -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 diff --git a/apps/course/templates/course/course_only_lessons.html b/apps/course/templates/course/course_only_lessons.html index a919e08f..f0217bcf 100644 --- a/apps/course/templates/course/course_only_lessons.html +++ b/apps/course/templates/course/course_only_lessons.html @@ -173,7 +173,7 @@
Содержание курса
- {% 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 %} {% else %} diff --git a/apps/course/views.py b/apps/course/views.py index 60b7e308..8cff720a 100644 --- a/apps/course/views.py +++ b/apps/course/views.py @@ -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): diff --git a/web/src/components/CourseRedactor.vue b/web/src/components/CourseRedactor.vue index 3ba9359e..4407671c 100644 --- a/web/src/components/CourseRedactor.vue +++ b/web/src/components/CourseRedactor.vue @@ -215,24 +215,31 @@
Содержание курса
-
-
- - -
-
{{ lesson.title }}
-
-
{{ lesson.short_description }}
-
-
+ +
+
+ + + +
+
{{ lesson.title }}
+
+
{{ lesson.short_description }}
+
+
+
Загрузка...
@@ -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) { @@ -668,6 +669,15 @@ 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) => { this.courseLoading = false; @@ -794,17 +804,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 +860,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 +889,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; diff --git a/web/src/js/modules/api.js b/web/src/js/modules/api.js index 36dc5f99..3b48837b 100644 --- a/web/src/js/modules/api.js +++ b/web/src/js/modules/api.js @@ -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) => { From 7a931aadcd2df63516685b564b0e31ff10d86232 Mon Sep 17 00:00:00 2001 From: gzbender Date: Tue, 3 Jul 2018 14:23:01 +0500 Subject: [PATCH 2/6] =?UTF-8?q?LIL-467=20=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=BF=D1=83=D1=81=D1=82=D0=BE=D0=B9=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=BA=D0=BE=D0=B9,=20=D0=B0=20=D0=BD=D0=B5?= =?UTF-8?q?=20'0'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/CourseRedactor.vue | 29 ++++++++++----------------- web/src/js/modules/api.js | 4 ++-- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/web/src/components/CourseRedactor.vue b/web/src/components/CourseRedactor.vue index 3ba9359e..1a9a0779 100644 --- a/web/src/components/CourseRedactor.vue +++ b/web/src/components/CourseRedactor.vue @@ -619,6 +619,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 +636,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,21 +657,10 @@ } 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); - }); }) .catch((err) => { this.courseLoading = false; @@ -1025,7 +1018,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; diff --git a/web/src/js/modules/api.js b/web/src/js/modules/api.js index 36dc5f99..31787852 100644 --- a/web/src/js/modules/api.js +++ b/web/src/js/modules/api.js @@ -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, From 7782042377d98759070f4afb17b48df835615ebd Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 6 Jul 2018 07:55:56 +0300 Subject: [PATCH 3/6] LIL-563. Add roistat visit field to payment --- apps/payment/admin.py | 3 ++- .../migrations/0019_payment_roistat_visit.py | 18 ++++++++++++++++++ apps/payment/models.py | 1 + apps/payment/views.py | 5 +++++ 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 apps/payment/migrations/0019_payment_roistat_visit.py diff --git a/apps/payment/admin.py b/apps/payment/admin.py index 47dde452..19166563 100644 --- a/apps/payment/admin.py +++ b/apps/payment/admin.py @@ -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',) diff --git a/apps/payment/migrations/0019_payment_roistat_visit.py b/apps/payment/migrations/0019_payment_roistat_visit.py new file mode 100644 index 00000000..4f7fa40e --- /dev/null +++ b/apps/payment/migrations/0019_payment_roistat_visit.py @@ -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'), + ), + ] diff --git a/apps/payment/models.py b/apps/payment/models.py index b1368dc4..32d7a0e9 100644 --- a/apps/payment/models.py +++ b/apps/payment/models.py @@ -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) diff --git a/apps/payment/views.py b/apps/payment/views.py index 34d35a0b..6facfaee 100644 --- a/apps/payment/views.py +++ b/apps/payment/views.py @@ -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}', From d377668b52e4362054344b7efc6991f78a3766fc Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 6 Jul 2018 08:03:49 +0300 Subject: [PATCH 4/6] LIL-563. Add roistat visit to transaction_to_roistat --- apps/payment/tasks.py | 3 ++- apps/payment/views.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/payment/tasks.py b/apps/payment/tasks.py index 911235a3..f6140af6 100644 --- a/apps/payment/tasks.py +++ b/apps/payment/tasks.py @@ -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 or None, 'price': str(amount), 'client_id': str(user_id), 'fields': { diff --git a/apps/payment/views.py b/apps/payment/views.py index 6facfaee..4f4a958a 100644 --- a/apps/payment/views.py +++ b/apps/payment/views.py @@ -264,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: From 918d70272ade784f46c97f298b4cc965e807c0ae Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 6 Jul 2018 08:05:32 +0300 Subject: [PATCH 5/6] fix --- apps/payment/tasks.py | 2 +- requirements.txt | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/payment/tasks.py b/apps/payment/tasks.py index f6140af6..e30f4e19 100644 --- a/apps/payment/tasks.py +++ b/apps/payment/tasks.py @@ -44,7 +44,7 @@ def transaction_to_roistat(user_id, payment_id, event_name, amount, time, status 'name': event_name, 'date_create': time, 'status': str(status), - 'roistat': str(roistat_visit) if roistat_visit or None, + 'roistat': str(roistat_visit) if roistat_visit else None, 'price': str(amount), 'client_id': str(user_id), 'fields': { diff --git a/requirements.txt b/requirements.txt index 68723b36..63995574 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 From 5a01145ea9365a1ce7e0fa1996151500a9cab54d Mon Sep 17 00:00:00 2001 From: nikita Date: Fri, 6 Jul 2018 13:52:26 +0300 Subject: [PATCH 6/6] Add google, yandex snippets --- project/templates/lilcity/index.html | 39 +++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index 30b5541a..5d7b9d2d 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -76,7 +76,44 @@ &noscript=1"/> - + + + + + + + {% include "templates/blocks/mixpanel.html" %}