From f10cd4e6b5d650332af495d0bc4a843cade8aea8 Mon Sep 17 00:00:00 2001 From: gzbender Date: Sat, 17 Aug 2019 02:09:16 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=B4=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D0=B9=D0=BD=20/=20=D0=9A=D1=83=D1=80=D1=81=D1=8B?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/serializers/course.py | 12 +- api/v1/urls.py | 3 +- api/v1/views.py | 9 +- .../migrations/0050_auto_20190815_1537.py | 38 ++++++ apps/course/models.py | 24 +++- apps/course/templates/course/_items.html | 1 - apps/course/templates/course/courses.html | 39 ++++-- apps/course/views.py | 23 +++- web/package.json | 11 +- web/src/components/CourseRedactor.vue | 56 +++++++- web/src/sass/_common.sass | 120 ++++++++++++++---- 11 files changed, 284 insertions(+), 52 deletions(-) create mode 100644 apps/course/migrations/0050_auto_20190815_1537.py diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index 718dad8c..c8be42e9 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -9,7 +9,7 @@ from apps.course.models import ( Comment, CourseComment, LessonComment, Material, Lesson, Like, - LiveLessonComment) + LiveLessonComment, Tag) from .content import ( ImageObjectSerializer, ContentSerializer, ContentCreateSerializer, GallerySerializer, ) @@ -19,6 +19,12 @@ from .user import UserSerializer User = get_user_model() +class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = ('tag',) + + class MaterialCreateSerializer(serializers.ModelSerializer): class Meta: @@ -116,6 +122,7 @@ class CourseCreateSerializer(DispatchContentMixin, ) materials = MaterialSerializer(many=True, required=False) gallery = GallerySerializer() + tags = TagSerializer(many=True, required=False) class Meta: model = Course @@ -145,6 +152,7 @@ class CourseCreateSerializer(DispatchContentMixin, 'content', 'gallery', 'lessons', + 'tags', ) read_only_fields = ( @@ -269,6 +277,7 @@ class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer): gallery = GallerySerializer() content = ContentSerializer(many=True) lessons = LessonSerializer(many=True) + tags = TagSerializer(many=True, required=False) class Meta: model = Course @@ -298,6 +307,7 @@ class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer): 'content', 'gallery', 'lessons', + 'tags', ) read_only_fields = ( diff --git a/api/v1/urls.py b/api/v1/urls.py index 08434dfc..99921937 100644 --- a/api/v1/urls.py +++ b/api/v1/urls.py @@ -19,7 +19,7 @@ from .views import ( SchoolScheduleViewSet, LiveLessonViewSet, PaymentViewSet, ObjectCommentsViewSet, ContestViewSet, ContestWorkViewSet, NotifiedAboutBonuses, - AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet, UserGalleryViewSet, BonusesViewSet) + AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet, UserGalleryViewSet, BonusesViewSet, TagViewSet) router = DefaultRouter() router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests') @@ -48,6 +48,7 @@ router.register(r'users', UserViewSet, base_name='users') router.register(r'user-gallery', UserGalleryViewSet, base_name='user-gallery') router.register(r'contests', ContestViewSet, base_name='contests') router.register(r'contest-works', ContestWorkViewSet, base_name='contest_works') +router.register(r'tags', TagViewSet, base_name='tags') # router.register(r'configs', ConfigViewSet, base_name='configs') diff --git a/api/v1/views.py b/api/v1/views.py index 4bde8660..6123f9e2 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -21,7 +21,7 @@ from .serializers.course import ( MaterialSerializer, MaterialCreateSerializer, LessonSerializer, LessonCreateSerializer, LikeCreateSerializer, CourseCommentSerializer, LessonCommentSerializer, - LiveLessonCommentSerializer,) + LiveLessonCommentSerializer, TagSerializer) from .serializers.content import ( BannerSerializer, ImageSerializer, ImageCreateSerializer, @@ -60,7 +60,7 @@ from apps.course.models import ( Comment, CourseComment, LessonComment, Material, Lesson, Like, - LiveLessonComment) + LiveLessonComment, Tag) from apps.config.models import Config from apps.content.models import ( Banner, Image, Text, ImageText, Video, @@ -774,3 +774,8 @@ class NotifiedAboutBonuses(views.APIView): b.save() return Response({'status': 'ok'}) + +class TagViewSet(ExtendedModelViewSet): + queryset = Tag.objects.all() + serializer_class = TagSerializer + search_fields = ('tag',) diff --git a/apps/course/migrations/0050_auto_20190815_1537.py b/apps/course/migrations/0050_auto_20190815_1537.py new file mode 100644 index 00000000..9cab27c1 --- /dev/null +++ b/apps/course/migrations/0050_auto_20190815_1537.py @@ -0,0 +1,38 @@ +# Generated by Django 2.0.7 on 2019-08-15 15:37 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0049_auto_20190207_1551'), + ] + + operations = [ + migrations.CreateModel( + name='CourseTags', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('course_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.Course')), + ], + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tag', models.CharField(max_length=20)), + ], + ), + migrations.AddField( + model_name='coursetags', + name='tag_id', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.Tag'), + ), + migrations.AddField( + model_name='course', + name='tags', + field=models.ManyToManyField(blank=True, through='course.CourseTags', to='course.Tag'), + ), + ] diff --git a/apps/course/models.py b/apps/course/models.py index f7ef5f20..bf1383be 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -21,6 +21,17 @@ from apps.content.models import ImageObject, Gallery, Video, ContestWork User = get_user_model() +def default_slug(): + return str(uuid4()) + + +def deferred_start_at_validator(value): + if value < now(): + raise ValidationError( + 'Дата и время начала курса не может быть меньше текущих.', + ) + + class Like(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) ip = models.GenericIPAddressField(blank=True, null=True) @@ -29,15 +40,13 @@ class Like(models.Model): update_at = models.DateTimeField(auto_now=True) -def default_slug(): - return str(uuid4()) +class Tag(models.Model): + tag = models.CharField(max_length=20,) -def deferred_start_at_validator(value): - if value < now(): - raise ValidationError( - 'Дата и время начала курса не может быть меньше текущих.', - ) +class CourseTags(models.Model): + tag_id = models.ForeignKey(Tag, on_delete=models.CASCADE) + course_id = models.ForeignKey('Course', on_delete=models.CASCADE) class Course(BaseModel, DeactivatedMixin): @@ -110,6 +119,7 @@ class Course(BaseModel, DeactivatedMixin): on_delete=models.CASCADE, null=True, blank=True, related_name='results_gallery', ) + tags = models.ManyToManyField('Tag', through=CourseTags, blank=True) created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) diff --git a/apps/course/templates/course/_items.html b/apps/course/templates/course/_items.html index 070f5dbc..7730e543 100644 --- a/apps/course/templates/course/_items.html +++ b/apps/course/templates/course/_items.html @@ -13,7 +13,6 @@ {% empty %} {% endthumbnail %} -
Подробнее
{% if course.is_featured %}
{% endif %} diff --git a/apps/course/templates/course/courses.html b/apps/course/templates/course/courses.html index 10a16ff9..45f3f749 100644 --- a/apps/course/templates/course/courses.html +++ b/apps/course/templates/course/courses.html @@ -12,17 +12,40 @@ {% endif %}
+ +
+
-
-

Учите и развивайте креативное мышление когда и где угодно. Если вам не совсем удобно заниматься с нами в прямом эфире каждый день, как в - нашей онлайн-школе, специально для вас мы делаем отдельные уроки в записи, которые вы можете проходить, - когда вам будет удобно.

-
Курсы
-
+
Курсы
+
-
+
Категории
@@ -35,7 +58,7 @@
-
+
{% if age_name %}{{ age_name }}{% else %}Возраст{% endif %}
diff --git a/apps/course/views.py b/apps/course/views.py index edd6ffa6..65a5f2c1 100644 --- a/apps/course/views.py +++ b/apps/course/views.py @@ -1,4 +1,5 @@ from datetime import timedelta +from itertools import groupby from paymentwall import Pingback from django.contrib.auth import get_user_model @@ -17,8 +18,9 @@ from django.utils.timezone import now from apps.content.models import Banner from apps.payment.models import AuthorBalance, CoursePayment -from .models import Course, Like, Lesson, CourseComment, LessonComment, Category +from .models import Course, Like, Lesson, CourseComment, LessonComment, Category, CourseTags, Tag from .filters import CourseFilter +from project.utils.db import ModelFieldsNames, format_sql, execute_sql User = get_user_model() @@ -312,6 +314,14 @@ class CoursesView(ListView): ).prefetch_related( 'likes', 'materials', 'content', ).filter(status=Course.PUBLISHED) + q = self.request.GET.get('q') + if q: + if q.startswith('#'): + queryset = queryset.filter(tags__tag__istartswith=q[1:]) + else: + queryset = queryset.filter(Q(tags__tag__icontains=q) | Q(title__icontains=q) | Q(short_description__icontains=q) + | Q(author__first_name__icontains=q) | Q(author__last_name__icontains=q) + | Q(author__email__icontains=q)) filtered = CourseFilter(self.request.GET, queryset=queryset) return filtered.qs @@ -319,6 +329,17 @@ class CoursesView(ListView): context = super().get_context_data() filtered = CourseFilter(self.request.GET) context.update(filtered.data) + sql = format_sql(''' + select {ct.tag_id} + from {ct} + group by {ct.tag_id} + order by count(*) desc + limit 15''', ct=CourseTags) + tags = Tag.objects.filter(id__in=execute_sql(sql)).order_by('tag') + print('tags', tags) + context['tags'] = map(lambda i: i[0], sorted(tags, key=lambda i: len(i[1]))[:15]) + print("context['tags']", context['tags']) + 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['ages'] = Course.AGE_CHOICES[1:] diff --git a/web/package.json b/web/package.json index 9c71cc60..5101212a 100755 --- a/web/package.json +++ b/web/package.json @@ -14,14 +14,14 @@ "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-env": "^1.6.1", "babel-preset-es2015": "^6.24.1", - "browser-sync": "^2.10.0", + "browser-sync": "^2.26.7", "css-loader": "^0.28.9", "css-mqpacker": "^5.0.1", "del": "^2.2.0", "extract-text-webpack-plugin": "^3.0.2", "file-loader": "^1.1.6", - "lodash": "^4.3.0", - "node-sass": "^4.9.0", + "lodash": "^4.17.15", + "node-sass": "^4.12.0", "require-dir": "^0.3.0", "run-sequence": "^1.1.5", "sass-loader": "^7.0.1", @@ -36,7 +36,7 @@ "dependencies": { "autosize": "^4.0.2", "autosize-input": "^1.0.2", - "axios": "^0.17.1", + "axios": "^0.19.0", "babel-polyfill": "^6.26.0", "baguettebox.js": "^1.10.0", "bowser": "^2.1.2", @@ -47,7 +47,7 @@ "history": "^4.7.2", "ilyabirman-likely": "^2.3.0", "inputmask": "^3.3.11", - "jquery": "^3.3.1", + "jquery": "^3.4.1", "js-cookie": "^2.2.0", "lodash.debounce": "^4.0.8", "modal-video": "git+https://github.com/gzbender/modal-video.git", @@ -62,6 +62,7 @@ "vue": "^2.5.13", "vue-autosize": "^1.0.2", "vue-awesome-swiper": "^3.1.3", + "vue-tags-component": "^1.3.0", "vuedraggable": "^2.16.0", "vuejs-datepicker": "^0.9.25", "vuelidate": "^0.6.1" diff --git a/web/src/components/CourseRedactor.vue b/web/src/components/CourseRedactor.vue index 9e5b3fef..655c70db 100644 --- a/web/src/components/CourseRedactor.vue +++ b/web/src/components/CourseRedactor.vue @@ -123,6 +123,20 @@ Выделить + +
+
Теги
+
+ + + + +
+
@@ -238,20 +252,21 @@