Новый дизайн / Курсы поиск

remotes/origin/features/course-search-16-08-19
gzbender 7 years ago
parent 4aa6febd5e
commit 2264809584
  1. 16
      api/v1/views.py
  2. 29
      apps/course/templates/course/course_items.html
  3. 1
      apps/course/views.py
  4. 17
      web/src/components/CourseRedactor.vue
  5. 3
      web/src/js/modules/api.js
  6. 2
      web/src/js/modules/courses.js
  7. 36
      web/src/sass/_common.sass

@ -11,6 +11,7 @@ from rest_framework.response import Response
from rest_framework.settings import api_settings
from django.utils.timezone import now
from project.utils.db import format_sql, execute_sql
from . import ExtendedModelViewSet, BothListFormatMixin
from .serializers.config import ConfigSerializer
from .serializers.course import (
@ -60,7 +61,7 @@ from apps.course.models import (
Comment, CourseComment, LessonComment,
Material, Lesson,
Like,
LiveLessonComment, Tag)
LiveLessonComment, Tag, CourseTags)
from apps.config.models import Config
from apps.content.models import (
Banner, Image, Text, ImageText, Video,
@ -784,3 +785,16 @@ class TagViewSet(ExtendedModelViewSet):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(methods=['get'], detail=False, url_path='popular', authentication_classes=[], permission_classes=[])
def popular(self, *args, **kwargs):
sql = format_sql('''
select {ct.tag_id}
from {ct}
group by {ct.tag_id}
order by count(*) desc
limit 15''', ct=CourseTags)
tags = [t[0] for t in execute_sql(sql)]
tags = Tag.objects.filter(id__in=tags).order_by('tag')
serializer = self.get_serializer(tags, many=True)
return Response(serializer.data)

@ -1,4 +1,29 @@
{% for course in course_items %}
{% for course in course_items %} {% load static %}
{% cycle '' 'theme_green' 'theme_violet' as theme_color silent %}
{% if forloop.counter == 6 and search_query and new_courses.count >= 4 %}
<div class="courses__item new-courses">
<div class="new-courses__block-title">Новые курсы</div>
<div>
{% for new_course in new_courses %}
<div class="new-courses__item">
<a class="new-courses__image" href="{{ course.url }}">
{% if new_course.cover %}
<img src="{{ new_course.cover.image_thumbnail.url }}" width="80px" />
{% else %}
<img src="{% static 'img/no_cover.png' %}" width="80px" />
{% endif %}
</a>
<div class="new-courses__details">
<a class="new-courses__title" href="{{ course.url }}">
{{ new_course.title }}
</a>
<a class="new-courses__author"
href="{{ new_course.author.url }}">{{ new_course.author.get_full_name }}</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% include "course/_items.html" %}
{% endfor %}
{% endfor %}

@ -340,6 +340,7 @@ class CoursesView(ListView):
context['search_query'] = self.request.GET.get('q', '')
context['banners'] = Banner.get_for_page(Banner.PAGE_COURSES)
context['course_items'] = Course.shuffle(context.get('course_items'))
context['new_courses'] = Course.objects.filter(status=Course.PUBLISHED).order_by('-created_at')[:4]
context['ages'] = Course.AGE_CHOICES[1:]
age = context.get('age')
if age and age[0]:

@ -130,9 +130,10 @@
<div class="field__wrap">
<div class="autocomplete">
<input class="autocomplete__query" type="text" v-model="tagSearchQuery" placeholder="добавить тег"
:class="{loading: tagsLoading}" @keyup.enter="addTag" @keyup.down="selectFirstTag" />
:class="{loading: tagsLoading}" @keyup.enter="addTag" @keyup.down="selectFirstTag"
@click="tagsFocused = true"/>
<select ref="tags_options" class="autocomplete__options" multiple="multiple"
v-show="tags.length && tagSearchQuery" style="display: none;">
v-show="tags.length && tagsFocused" style="display: none;">
<option v-for="tag in tags" :key="tag.id" @click="selectTag(tag)"
@keyup.enter.stop.prevent="selectTag(tag)">{{ tag.tag }}</option>
</select>
@ -289,6 +290,7 @@
props: ["authorName", "authorPicture", "accessToken", "courseId", "live", "camp"],
data() {
return {
tagsFocused: false,
tagsLoading: false,
tagSearchXhr: null,
tagSearchQuery: '',
@ -486,10 +488,11 @@
},
methods: {
searchTags(){
this.tagsLoading = true;
if(this.tagSearchTimeout){
clearTimeout(this.tagSearchTimeout);
}
this.tagsLoading = true;
this.tagsFocused = false;
this.tagSearchTimeout = setTimeout(() => {
api.getTags({search: this.tagSearchQuery}).then(response => {
this.tagsLoading = false;
@ -505,6 +508,7 @@
$opts.focus();
},
selectTag(tag){
this.tagsFocused = false;
if(this.course.tags.findIndex(t => t.id == tag.id) > -1){
return;
}
@ -961,6 +965,11 @@
let promises = [];
api.getPopularTags().then(response => {
const tags = this.course.tags.map(t => t.id);
this.tags = response.data.filter(t => tags.indexOf(t.id) == -1);
});
let cats = api.getCategories(this.accessToken);
promises.push(cats);
cats.then((response) => {
@ -1052,7 +1061,7 @@
$(window).click(e => {
const cssClass = $(e.target).attr('class');
if(!cssClass || cssClass.indexOf('autocomplete') == -1){
this.tags = [];
this.tagsFocused = false;
}
});
},

@ -518,6 +518,9 @@ export const api = {
getTags: params => {
return api.get('/api/v1/tags/', {params});
},
getPopularTags: params => {
return api.get('/api/v1/tags/popular/', {params});
},
addTag: data => {
return api.post('/api/v1/tags/', data, {
headers: {

@ -40,7 +40,7 @@ $(document).ready(function () {
$('.course-search__search').click(function(e){
e.preventDefault();
page = 1;
loadCourses();
loadCourses(true);
})
// Обработчик выбора категории

@ -1860,6 +1860,42 @@ a.grey-link
padding-right: 15px
flex: 0 0 235px
.new-courses
&__block-title
font-size: 25px
margin-bottom: 20px
margin-top: -6px
+m
margin-bottom: 10px
&__item
display: flex
margin-bottom: 10px
&__image
width: 80px
height: 57px
border-radius: 2px
&__details
margin-left: 15px
&__title
font-size: 15px
color: #333333
margin-bottom: 5px
display: block
+m
font-size: 14px
&__author
color: #888888
font-size: 10px
text-transform: uppercase
display: block
+m
font-size: 9px
.load
margin-top: 30px
+m

Loading…
Cancel
Save