Merge branch 'feature/LIL-523' into 'master'

Feature/lil 523

See merge request lilcity/backend!64
remotes/origin/hasaccess^2
cfwme 8 years ago
commit e733858c88
  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. 113
      web/src/components/CourseRedactor.vue
  7. 4
      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):

@ -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) {
@ -661,6 +662,15 @@
this.$nextTick(() => {
this.courseLoading = false;
});
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;
@ -787,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) {
@ -847,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) {
@ -879,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;

@ -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