LIL-601, Фильтр по возрастам в курсах, возможность указать возраст, рефакторинг комнонентов

remotes/origin/hotfix/LIL-691
gzbender 8 years ago
parent a502506f59
commit 7267dad3f5
  1. 2
      api/v1/serializers/course.py
  2. 3
      apps/course/filters.py
  3. 18
      apps/course/models.py
  4. 29
      apps/course/templates/course/courses.html
  5. 3
      apps/course/templates/course/inclusion/category_items.html
  6. 9
      apps/course/views.py
  7. 7
      project/templates/lilcity/index.html
  8. 79
      web/src/components/CourseRedactor.vue
  9. 28
      web/src/components/inputs/LilSelect.vue
  10. 2
      web/src/js/modules/api.js
  11. 101
      web/src/js/modules/courses.js

@ -110,6 +110,7 @@ class CourseCreateSerializer(DispatchContentMixin,
'from_author',
'cover',
'price',
'age',
'is_infinite',
'deferred_start_at',
'category',
@ -261,6 +262,7 @@ class CourseSerializer(serializers.ModelSerializer):
'from_author',
'cover',
'price',
'age',
'is_infinite',
'deferred_start_at',
'category',

@ -5,7 +5,8 @@ from .models import Course
class CourseFilter(django_filters.FilterSet):
category = django_filters.CharFilter(field_name='category__title', lookup_expr='iexact')
age = django_filters.ChoiceFilter(field_name='age', choices=Course.AGE_CHOICES)
class Meta:
model = Course
fields = ['category']
fields = ['category', 'age']

@ -50,6 +50,23 @@ class Course(BaseModel, DeactivatedMixin):
(ARCHIVED, 'Archived'),
(DENIED, 'Denied')
)
AGE_LT5 = 1
AGE_57 = 2
AGE_79 = 3
AGE_912 = 4
AGE_1215 = 5
AGE_1518 = 6
AGE_GT18 = 7
AGE_CHOICES = (
(0, 'Любой возраст'),
(AGE_LT5, 'до 5'),
(AGE_57, '5-7'),
(AGE_79, '7-9'),
(AGE_912, '9-12'),
(AGE_1215, '12-15'),
(AGE_1518, '15-18'),
(AGE_GT18, 'от 18'),
)
slug = models.SlugField(
allow_unicode=True, null=True, blank=True,
max_length=100, unique=True, db_index=True,
@ -73,6 +90,7 @@ class Course(BaseModel, DeactivatedMixin):
'Цена курса', help_text='Если цена не выставлена, то курс бесплатный',
max_digits=10, decimal_places=2, null=True, blank=True
)
age = models.SmallIntegerField(choices=AGE_CHOICES, default=0)
is_infinite = models.BooleanField(default=False)
deferred_start_at = models.DateTimeField(
'Отложенный запуск курса', help_text='Заполнить если курс отложенный',

@ -20,18 +20,35 @@
<div class="head__right">
<div class="head__field field">
<div class="field__wrap">
<div class="field__select select js-select{% if category %} selected{% endif %}">
<div class="select__head js-select-head">{% if category %}{{ category.0 }}{% else %}Категории{% endif %}</div>
<div class="field__select select js-select{% if category.0 %} selected{% endif %}">
<div class="select__head js-select-head">{% if category.0 %}{{ category.0 }}{% else %}Категории{% endif %}</div>
<div class="select__drop js-select-drop">
<div class="select__option js-select-option{% if not category %} active{% endif %}"
data-category-option data-category-url="{% url 'courses' %}">
<div class="select__title">Все курсы</div>
<div class="select__option js-select-option{% if not category.0 %} active{% endif %}" data-category-option>
<div class="select__title">Все категории</div>
</div>
{% category_items category %}
</div>
<input class="select__input" type="hidden"></div>
</div>
</div>
<div class="head__field field">
<div class="field__wrap">
<div class="field__select select js-select{% if age.0 %} selected{% endif %}">
<div class="select__head js-select-head">{% if age_name %}{{ age_name }}{% else %}Возраст{% endif %}</div>
<div class="select__drop js-select-drop">
<div class="select__option js-select-option{% if not age.0 %} active{% endif %}" data-age-option>
<div class="select__title">Любой возраст</div>
</div>
{% for a in ages %}
<div class="select__option js-select-option{% if age and age.0 == a.0 %} active{% endif %}" data-age-option
data-age="{{ a.0 }}" data-age-name="{{ a.1 }}">
<div class="select__title">{{ a.1 }}</div>
</div>
{% endfor %}
</div>
<input class="select__input" type="hidden"></div>
</div>
</div>
</div>
</div>
<div class="courses">
@ -40,7 +57,7 @@
</div>
<div class="courses__load load">
{% if page_obj.has_next %}
<button class="load__btn btn" data-next-page-url="{% url 'courses' %}?page={{ page_obj.next_page_number }}">Подгрузить еще</button>
<button class="load__btn btn" data-next-page="{{ page_obj.next_page_number }}">Подгрузить еще</button>
{% endif %}
</div>
</div>

@ -1,5 +1,6 @@
{% for cat in category_items %}
<div class="select__option js-select-option{% if category and category.0 == cat.title %} active{% endif %}" data-category-option data-category-name="{{ cat.title }}" data-category-url="{% url 'courses' %}?category={{ cat.title }}">
<div class="select__option js-select-option{% if category and category.0 == cat.title %} active{% endif %}" data-category-option
data-category-name="{{ cat.title }}">
<div class="select__title">{{ cat.title }}</div>
</div>
{% endfor %}

@ -283,6 +283,15 @@ class CoursesView(ListView):
context = super().get_context_data()
filtered = CourseFilter(self.request.GET)
context.update(filtered.data)
context['ages'] = Course.AGE_CHOICES[1:]
# context['age'] = list(map(int, context.get('age', [])))
age = context.get('age')
if age and age[0]:
age = int(age[0])
context['age'] = [age]
context['age_name'] = dict(Course.AGE_CHOICES).get(age, '')
else:
context['age_name'] = ''
return context
def get_template_names(self):

@ -130,6 +130,13 @@
{% include "templates/blocks/popup_course_lock.html" %}
{% include "templates/blocks/popup_subscribe.html" %}
</div>
<script>
window.LIL_STORE = {
urls: {
courses: "{% url 'courses' %}"
}
};
</script>
<script type="text/javascript" src={% static "app.js" %}></script>
<script>
var schoolDiscount = parseFloat({{ config.SERVICE_DISCOUNT }});

@ -47,7 +47,7 @@
v-bind:class="{ error: ($v.course.category.$dirty || showErrors) && $v.course.category.$invalid }">
<div class="field__label field__label_gray">КАТЕГОРИЯ</div>
<div class="field__wrap">
<lil-select :value.sync="categorySelect" :options="categoryOptions"
<lil-select :value.sync="course.category" :options="categoryOptions"
placeholder="Выберите категорию"/>
</div>
</div>
@ -111,6 +111,13 @@
<button disabled class="field__append">руб.</button>
</div>
</div>
<div v-if="!live" class="info__field field">
<div class="field__label field__label_gray">ВОЗРАСТ</div>
<div class="field__wrap">
<lil-select :value.sync="course.age" :options="ages" value-key="value"
placeholder="Выберите возраст"/>
</div>
</div>
<label v-if="me && !live && me.role === ROLE_ADMIN" class="info__switch switch switch_lg">
<input type="checkbox" class="switch__input" v-model="course.is_featured">
<span class="switch__content">Выделить</span>
@ -311,6 +318,7 @@
duration: null,
author: null,
price: null,
age: 0,
url: '',
coverImage: '',
kit__body: null,
@ -375,6 +383,40 @@
'value': '18:00',
}
],
ages: [
{
'title': 'Любой возраст',
'value': 0,
},
{
'title': 'до 5',
'value': 1,
},
{
'title': '5-7',
'value': 2,
},
{
'title': '7-9',
'value': 3,
},
{
'title': '9-12',
'value': 4,
},
{
'title': '12-15',
'value': 5,
},
{
'title': '15-18',
'value': 6,
},
{
'title': 'от 18',
'value': 7,
},
],
weekdays: [
'',
@ -483,15 +525,6 @@
this.course.url = slugify(this.course.title);
}
},
updateCategory() {
if (this.categoryOptions && Array.isArray(this.categoryOptions) && this.course.category) {
this.categoryOptions.forEach((category) => {
if (category.id === this.course.category) {
this.course.categorySelect = category;
}
});
}
},
onBlockRemoved(blockIndex) {
const blockToRemove = this.course.content[blockIndex];
// Удаляем блок из Vue
@ -940,14 +973,8 @@
promises.push(cats);
cats.then((response) => {
if (response.data) {
this.categoryOptions = response.data.results.map((category) => {
return {
title: category.title,
value: category.id
}
});
this.categoryOptions = response.data.results;
}
this.updateCategory();
});
if(this.live) {
@ -987,7 +1014,6 @@
this.scheduleOptions = _.orderBy(options, (item)=>{return moment(item.value)});
}
this.updateCategory();
});
}
@ -1058,23 +1084,6 @@
this.course.price = value || 0;
}
},
categorySelect: {
get() {
if (!this.categoryOptions || this.categoryOptions.length === 0 || !this.course || !this.course.category) {
return null;
}
let value;
this.categoryOptions.forEach((category) => {
if (category.value === this.course.category) {
value = category;
}
});
return value;
},
set(value) {
this.course.category = value.value;
}
},
// userSelect: {
// get() {
// if (!this.users || this.users.length === 0 || !this.course || !this.course.author) {

@ -4,8 +4,9 @@
{{ selectedTitle }}
</div>
<div class="select__drop">
<div v-for="option in options" :class="{ select__option: true, active: value && option.value == value.value }" @click.stop.prevent="selectOption(option)">
<div class="select__title">{{ option.title }}</div>
<div v-for="option in options" :class="{ select__option: true, active: option[vk] == selectedValue }"
@click.stop.prevent="selectOption(option)">
<div class="select__title">{{ option[tk] }}</div>
</div>
</div>
</div>
@ -14,11 +15,14 @@
<script>
export default {
name: "lil-select",
props: ["options", "value", "valueKey", "placeholder"],
props: ["options", "value", "titleKey", "valueKey", "isObj", "placeholder"],
data() {
return {
key: this.valueKey ? this.valueKey : 'title',
tk: this.titleKey || 'title',
vk: this.valueKey || 'id',
isOpened: false,
selected: null,
optionsDict: {},
}
},
methods: {
@ -30,22 +34,30 @@
},
selectOption(option) {
this.isOpened = !this.isOpened;
this.$emit('update:value', option);
this.$emit('update:value', this.isObj ? option : option[this.vk]);
this.selected = option;
}
},
mounted() {
document.addEventListener("click", this.clickListener);
this.options.forEach((option) => {
this.optionsDict[option[this.vk]] = option;
});
this.selected = this.optionsDict[this.selectedValue];
},
destroyed() {
document.removeEventListener("click", this.clickListener);
},
computed: {
isSelected() {
return this.value && this.value[this.key] ? true : false;
return !!this.selected;
},
selectedValue() {
return this.isObj ? this.value && this.value[this.vk] : this.value;
},
selectedTitle() {
if (this.isSelected) {
return this.value[this.key];
return this.selected[this.tk];
}
return this.placeholder ? this.placeholder : '';
}
@ -55,4 +67,4 @@
<style scoped>
</style>
</style>

@ -108,6 +108,7 @@ export const api = {
short_description: courseObject.short_description,
category: courseObject.category,
price: courseObject.is_paid && courseObject.price || 0,
age: courseObject.age,
deferred_start_at: deferredStart,
duration: courseObject.duration || 0,
is_featured: courseObject.is_featured,
@ -310,6 +311,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),
age: courseJSON.age,
is_paid: parseFloat(courseJSON.price) > 0,
is_deferred: isDeferred,
date: deferredDate || courseJSON.date,

@ -12,6 +12,8 @@ moment.locale('ru');
const history = createHistory();
$(document).ready(function () {
var category = $('div.js-select-option.active[data-category-option]').data('category-name'),
age = $('div.js-select-option.active[data-age-option]').data('age'), page = 1;
// Обработчик отложенных курсов
setInterval(() => {
$('div[data-future-course]').each((_, element) => {
@ -25,7 +27,8 @@ $(document).ready(function () {
// Обработчик кнопки "Подгрузить еще"
$('.courses').on('click', 'button.load__btn', function () {
load_courses($(this).attr('data-next-page-url'), false);
page = $(this).attr('data-next-page');
loadCourses();
});
// Обработчик выбора категории
@ -34,8 +37,20 @@ $(document).ready(function () {
const currentCategory = $(this).attr('data-category-name');
$('[data-category-name]').removeClass('active');
$(`[data-category-name='${currentCategory}']`).addClass('active');
history.replace($(this).attr('data-category-url'));
load_courses($(this).attr('data-category-url'), true);
category = $(this).attr('data-category-name');
page = 1;
loadCourses(true);
});
// Обработчик выбора возраста
$('div.js-select-option[data-age-option]').on('click', function (e) {
e.preventDefault();
const currentAge = $(this).attr('data-age-name');
$('[data-age-name]').removeClass('active');
$(`[data-age-name='${currentAge}']`).addClass('active');
age = $(this).attr('data-age');
page = 1;
loadCourses(true);
});
// Обработчик лайков
@ -91,42 +106,50 @@ $(document).ready(function () {
}
});
})
});
function load_courses(coursesUrl, fromStart) {
$('.courses__list').css('opacity', '0.9');
const buttonElement = $('.courses').find('button.load__btn');
if (!fromStart) {
buttonElement.addClass('loading');
}
$.ajax(coursesUrl, {
method: 'GET'
})
.done(function (data) {
if (data.success === true) {
if (!fromStart) {
$('.courses__list').append(data.content);
} else {
$('.courses__list').html(data.content);
function loadCourses(replaceHistory) {
$('.courses__list').css('opacity', '0.9');
const buttonElement = $('.courses').find('button.load__btn');
let coursesUrl = window.LIL_STORE.urls.courses + '?' + $.param({
category: category,
age: age,
});
if (page > 1) {
buttonElement.addClass('loading');
}
else{
history.replace(coursesUrl);
}
coursesUrl += `&page=${page}`;
$.ajax(coursesUrl, {
method: 'GET'
})
.done(function (data) {
if (data.success === true) {
if (page > 1) {
$('.courses__list').append(data.content);
} else {
$('.courses__list').html(data.content);
}
if (data.next_url) {
buttonElement.attr('data-next-page-url', data.next_url);
buttonElement.show();
} else {
buttonElement.hide()
}
}
if (data.next_url) {
buttonElement.attr('data-next-page-url', data.next_url);
buttonElement.show();
} else {
buttonElement.hide()
})
.fail(function (xhr) {
if (xhr.status === 404) {
// Нет результатов, скрываем кнопку
buttonElement.hide();
}
}
})
.fail(function (xhr) {
if (xhr.status === 404) {
// Нет результатов, скрываем кнопку
buttonElement.hide();
}
})
.always(function () {
$('.courses__list').css('opacity', '1');
if (buttonElement) {
buttonElement.removeClass('loading');
}
});
}
})
.always(function () {
$('.courses__list').css('opacity', '1');
if (buttonElement) {
buttonElement.removeClass('loading');
}
});
}
});

Loading…
Cancel
Save