diff --git a/api/v1/serializers/contest.py b/api/v1/serializers/contest.py index 6288c5fc..930f60f3 100644 --- a/api/v1/serializers/contest.py +++ b/api/v1/serializers/contest.py @@ -43,11 +43,20 @@ class ContestCreateSerializer(DispatchContentMixin, serializers.ModelSerializer) class ContestWorkSerializer(serializers.ModelSerializer): image = ImageObjectSerializer() + likes = serializers.SerializerMethodField() + user_liked = serializers.SerializerMethodField() class Meta: model = ContestWork fields = ['id', 'user', 'contest', 'image', 'child_full_name', 'age', - 'created_at', 'likes', 'img_width', 'img_height'] + 'created_at', 'likes', 'user_liked', 'img_width', 'img_height'] + + def get_likes(self, instance): + return instance.likes.count() + + def get_user_liked(self, instance): + user = self.context['request'].user + return instance.likes.filter(user=user).exists() if user.is_authenticated else False class ContestWorkCreateSerializer(serializers.ModelSerializer): @@ -55,5 +64,7 @@ class ContestWorkCreateSerializer(serializers.ModelSerializer): model = ContestWork fields = '__all__' + # TODO check ContestWork.objects.filter(user=self.request.user).exists() + def to_representation(self, instance): return ContestWorkSerializer(instance=instance, context=self.context).to_representation(instance) diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index bdb1a289..88533c51 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -15,7 +15,7 @@ from .content import ( from apps.content.models import ( Content, Image, Text, ImageText, Video, Gallery, GalleryImage, ImageObject, -) + ContestWork) from .user import UserSerializer from .mixins import DispatchContentMixin, DispatchGalleryMixin, DispatchMaterialMixin @@ -63,6 +63,34 @@ class LikeSerializer(serializers.ModelSerializer): ) +class LikeCreateSerializer(serializers.ModelSerializer): + OBJ_TYPE_CONTEST_WORK = 'contest_work' + + obj_type = serializers.CharField(required=True) + obj_id = serializers.IntegerField(required=True) + + class Meta: + model = Like + fields = ['obj_type', 'obj_id'] + + def create(self, validated_data): + user = self.context['request'].user + if not (user and user.is_authenticated): + return Like() + obj_type = validated_data.pop('obj_type') + obj_id = validated_data.pop('obj_id') + if obj_type == self.OBJ_TYPE_CONTEST_WORK: + contest_work = ContestWork.objects.get(pk=obj_id) + if contest_work.likes.filter(user=user).exists(): + return Like() + like = Like.objects.create(user=user) + contest_work.likes.add(like) + return like + + def to_representation(self, instance): + return LikeSerializer(instance, context=self.context).to_representation(instance) + + class CategorySerializer(serializers.ModelSerializer): class Meta: diff --git a/api/v1/views.py b/api/v1/views.py index 95fea15b..fd7567e6 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -14,7 +14,7 @@ from .serializers.course import ( CommentSerializer, MaterialSerializer, MaterialCreateSerializer, LessonSerializer, LessonCreateSerializer, -) + LikeCreateSerializer) from .serializers.content import ( BanerSerializer, ImageSerializer, ImageCreateSerializer, @@ -130,7 +130,11 @@ class MaterialViewSet(ExtendedModelViewSet): class LikeViewSet(ExtendedModelViewSet): queryset = Like.objects.select_related('user').all() - serializer_class = LikeSerializer + serializer_class = LikeCreateSerializer + serializer_class_map = { + 'list': LikeSerializer, + 'retrieve': LikeSerializer, + } search_fields = ('user__email', 'user__firstname', 'user__lastname',) ordering_fields = ('created_at', 'update_at',) # permission_classes = (IsAdmin,) @@ -448,7 +452,7 @@ class ContestViewSet(ExtendedModelViewSet): class ContestWorkViewSet(ExtendedModelViewSet): - queryset = ContestWork.objects.order_by('-created_at') + queryset = ContestWork.objects.all() serializer_class = ContestWorkCreateSerializer serializer_class_map = { 'list': ContestWorkSerializer, diff --git a/apps/content/models.py b/apps/content/models.py index 378c9995..bbf023d8 100644 --- a/apps/content/models.py +++ b/apps/content/models.py @@ -1,6 +1,7 @@ from urllib.parse import urlparse from django.db import models from django.contrib.auth import get_user_model +from django.urls import reverse_lazy from polymorphic.models import PolymorphicModel @@ -180,6 +181,11 @@ class ContestWork(models.Model): created_at = models.DateTimeField(auto_now_add=True) likes = models.ManyToManyField('course.Like', blank=True) + class Meta: + verbose_name = 'Конкурсная работа' + verbose_name_plural = 'Конкурсные работы' + ordering = ('-created_at',) + @property def img_width(self): return self.image.image.width if self.image and self.image.image else None @@ -187,3 +193,6 @@ class ContestWork(models.Model): @property def img_height(self): return self.image.image.height if self.image and self.image.image else None + + def get_absolute_url(self): + return reverse_lazy('contest_work', args=[self.id]) diff --git a/apps/content/templates/content/contest.html b/apps/content/templates/content/contest.html index 0c3c89a6..4c8a9f2f 100644 --- a/apps/content/templates/content/contest.html +++ b/apps/content/templates/content/contest.html @@ -12,7 +12,9 @@ {{ contest.description }}
+ {% if not contest_work_uploaded %} Загрузить свою работу + {% endif %}
diff --git a/apps/content/templates/content/contest_work.html b/apps/content/templates/content/contest_work.html index 32114301..00f2bca3 100644 --- a/apps/content/templates/content/contest_work.html +++ b/apps/content/templates/content/contest_work.html @@ -2,7 +2,7 @@ {% load static %} {% block title %}{{ contest_work.child_full_name }}, {{ contest_work.age }} лет{% endblock title %} -{% block ogimage %}http://{{request.META.HTTP_HOST}}{{ contest_work.image.url }}{% endblock ogimage %} +{% block ogimage %}http://{{request.META.HTTP_HOST}}{{ contest_work.image.image.url }}{% endblock ogimage %} {% block content %}
@@ -23,14 +23,13 @@
-
+
{{ contest_work.child_full_name }}
-
{{ contest_work.age }} лет
+
{{ contest_work.age }} {% if contest_work.age < 5 %}года{% else %}лет{% endif %}
@@ -39,7 +38,8 @@
+
+ {% include 'templates/blocks/share.html' %} +
{% endblock content %} diff --git a/apps/content/views.py b/apps/content/views.py index dd25a51e..38461f16 100644 --- a/apps/content/views.py +++ b/apps/content/views.py @@ -29,8 +29,27 @@ class ContestView(DetailView): context_object_name = 'contest' template_name = 'content/contest.html' + def get_context_data(self, *args, **kwargs): + context = super().get_context_data() + if self.request.user.is_authenticated: + context['contest_work_uploaded'] = ContestWork.objects.filter(user=self.request.user).exists() + return context + class ContestWorkView(DetailView): model = ContestWork context_object_name = 'contest_work' template_name = 'content/contest_work.html' + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data() + prev_contest_work = ContestWork.objects.filter(created_at__gt=self.object.created_at)[:1] + if prev_contest_work: + context['prev_contest_work'] = prev_contest_work[0] + next_contest_work = ContestWork.objects.filter(created_at__lt=self.object.created_at)[:1] + if next_contest_work: + context['next_contest_work'] = next_contest_work[0] + + context['user_liked'] = self.object.likes.filter(user=self.request.user).exists() \ + if self.request.user.is_authenticated else False + return context diff --git a/web/src/components/UploadContestWork.vue b/web/src/components/UploadContestWork.vue index 346c734b..184225f6 100644 --- a/web/src/components/UploadContestWork.vue +++ b/web/src/components/UploadContestWork.vue @@ -10,6 +10,7 @@
Чтобы принять участие
в конкурсе, заполните поля
и прикрепите изображение +
@@ -138,8 +139,9 @@ }); request.then((response) => { if(response.data.id){ - this.$emit('add:contestWork', response.data); + this.$emit('add:contest-work', response.data); this.hide(); + window.location.reload(); } }); } @@ -159,6 +161,12 @@ .title { text-align: center; font-size: 24px; + + .text__curve { + right: 55px; + width: 170px; + bottom: -40px; + } } .kit__photo { diff --git a/web/src/components/blocks/ContestWork.vue b/web/src/components/blocks/ContestWork.vue index c711cded..3472b11d 100644 --- a/web/src/components/blocks/ContestWork.vue +++ b/web/src/components/blocks/ContestWork.vue @@ -1,24 +1,28 @@ @@ -39,9 +43,6 @@ width: 100%; height: auto; } - .contest-works:hover .contest-work-item:not(:hover) { - opacity: 0.4; - } .contest-work-item__info { display: flex; padding: 5px 10px; @@ -49,15 +50,13 @@ .contest-work-item__age { color: #919191; } - .contest-work-item__likes { - flex: 70px; - text-align: right; + .contest-work-item__bio { + flex: calc(100% - 70px); + } - .icon { - width: 15px; - height: 15px; - margin-left: 5px; - margin-bottom: -3px; + @media only screen and (min-width: 1023px) { + .contest-works:hover .contest-work-item:not(:hover) { + opacity: 0.4; } } diff --git a/web/src/components/blocks/Likes.vue b/web/src/components/blocks/Likes.vue new file mode 100644 index 00000000..10979c93 --- /dev/null +++ b/web/src/components/blocks/Likes.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/web/src/js/app.js b/web/src/js/app.js index c7509536..2eb7a1bb 100644 --- a/web/src/js/app.js +++ b/web/src/js/app.js @@ -22,6 +22,7 @@ import Vue from 'vue'; import Vuelidate from 'vuelidate'; import UploadContestWork from '../components/UploadContestWork.vue'; import ContestWorks from '../components/ContestWorks.vue'; +import Likes from '../components/blocks/Likes.vue'; Vue.use(Vuelidate); @@ -40,5 +41,6 @@ const app = new Vue({ components: { UploadContestWork, ContestWorks, + Likes, } }); diff --git a/web/src/sass/_common.sass b/web/src/sass/_common.sass index c245439c..4077537a 100755 --- a/web/src/sass/_common.sass +++ b/web/src/sass/_common.sass @@ -3886,7 +3886,7 @@ a.grey-link position: relative padding-left: 17px &:before - content: '—' + content: '' position: absolute top: 0 left: 0 @@ -4116,12 +4116,6 @@ a &__age color: #919191; - &__likes - flex: 70px; - text-align: right; + &__bio + flex: calc(100% - 70px); - &__likes .icon - width: 15px; - height: 15px; - margin-left: 5px; - margin-bottom: -3px;