diff --git a/api/v1/views.py b/api/v1/views.py index fdfb5a12..ffc240fd 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -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) diff --git a/apps/course/templates/course/course_items.html b/apps/course/templates/course/course_items.html index 26a3377f..7c56ad10 100644 --- a/apps/course/templates/course/course_items.html +++ b/apps/course/templates/course/course_items.html @@ -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 %} +
+
Новые курсы
+
+ {% for new_course in new_courses %} + + {% endfor %} +
+
+ {% endif %} {% include "course/_items.html" %} -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/apps/course/views.py b/apps/course/views.py index 5da37691..d4bd0298 100644 --- a/apps/course/views.py +++ b/apps/course/views.py @@ -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]: diff --git a/web/src/components/CourseRedactor.vue b/web/src/components/CourseRedactor.vue index e529f12d..c4275eb3 100644 --- a/web/src/components/CourseRedactor.vue +++ b/web/src/components/CourseRedactor.vue @@ -130,9 +130,10 @@
+ :class="{loading: tagsLoading}" @keyup.enter="addTag" @keyup.down="selectFirstTag" + @click="tagsFocused = true"/> @@ -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; } }); }, diff --git a/web/src/js/modules/api.js b/web/src/js/modules/api.js index b0fd379c..73c35123 100644 --- a/web/src/js/modules/api.js +++ b/web/src/js/modules/api.js @@ -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: { diff --git a/web/src/js/modules/courses.js b/web/src/js/modules/courses.js index 20c16dba..ce3e4e42 100644 --- a/web/src/js/modules/courses.js +++ b/web/src/js/modules/courses.js @@ -40,7 +40,7 @@ $(document).ready(function () { $('.course-search__search').click(function(e){ e.preventDefault(); page = 1; - loadCourses(); + loadCourses(true); }) // Обработчик выбора категории diff --git a/web/src/sass/_common.sass b/web/src/sass/_common.sass index 5e65d4c2..0f9971f8 100755 --- a/web/src/sass/_common.sass +++ b/web/src/sass/_common.sass @@ -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