remotes/origin/hotfix/LIL-661
gzbender 8 years ago
parent 6630ec34ee
commit 604e2c16f6
  1. 13
      api/v1/serializers/contest.py
  2. 30
      api/v1/serializers/course.py
  3. 10
      api/v1/views.py
  4. 9
      apps/content/models.py
  5. 2
      apps/content/templates/content/contest.html
  6. 26
      apps/content/templates/content/contest_work.html
  7. 19
      apps/content/views.py
  8. 10
      web/src/components/UploadContestWork.vue
  9. 37
      web/src/components/blocks/ContestWork.vue
  10. 69
      web/src/components/blocks/Likes.vue
  11. 2
      web/src/js/app.js
  12. 12
      web/src/sass/_common.sass

@ -43,11 +43,20 @@ class ContestCreateSerializer(DispatchContentMixin, serializers.ModelSerializer)
class ContestWorkSerializer(serializers.ModelSerializer): class ContestWorkSerializer(serializers.ModelSerializer):
image = ImageObjectSerializer() image = ImageObjectSerializer()
likes = serializers.SerializerMethodField()
user_liked = serializers.SerializerMethodField()
class Meta: class Meta:
model = ContestWork model = ContestWork
fields = ['id', 'user', 'contest', 'image', 'child_full_name', 'age', 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): class ContestWorkCreateSerializer(serializers.ModelSerializer):
@ -55,5 +64,7 @@ class ContestWorkCreateSerializer(serializers.ModelSerializer):
model = ContestWork model = ContestWork
fields = '__all__' fields = '__all__'
# TODO check ContestWork.objects.filter(user=self.request.user).exists()
def to_representation(self, instance): def to_representation(self, instance):
return ContestWorkSerializer(instance=instance, context=self.context).to_representation(instance) return ContestWorkSerializer(instance=instance, context=self.context).to_representation(instance)

@ -15,7 +15,7 @@ from .content import (
from apps.content.models import ( from apps.content.models import (
Content, Image, Text, ImageText, Video, Content, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject, Gallery, GalleryImage, ImageObject,
) ContestWork)
from .user import UserSerializer from .user import UserSerializer
from .mixins import DispatchContentMixin, DispatchGalleryMixin, DispatchMaterialMixin 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 CategorySerializer(serializers.ModelSerializer):
class Meta: class Meta:

@ -14,7 +14,7 @@ from .serializers.course import (
CommentSerializer, CommentSerializer,
MaterialSerializer, MaterialCreateSerializer, MaterialSerializer, MaterialCreateSerializer,
LessonSerializer, LessonCreateSerializer, LessonSerializer, LessonCreateSerializer,
) LikeCreateSerializer)
from .serializers.content import ( from .serializers.content import (
BanerSerializer, BanerSerializer,
ImageSerializer, ImageCreateSerializer, ImageSerializer, ImageCreateSerializer,
@ -130,7 +130,11 @@ class MaterialViewSet(ExtendedModelViewSet):
class LikeViewSet(ExtendedModelViewSet): class LikeViewSet(ExtendedModelViewSet):
queryset = Like.objects.select_related('user').all() 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',) search_fields = ('user__email', 'user__firstname', 'user__lastname',)
ordering_fields = ('created_at', 'update_at',) ordering_fields = ('created_at', 'update_at',)
# permission_classes = (IsAdmin,) # permission_classes = (IsAdmin,)
@ -448,7 +452,7 @@ class ContestViewSet(ExtendedModelViewSet):
class ContestWorkViewSet(ExtendedModelViewSet): class ContestWorkViewSet(ExtendedModelViewSet):
queryset = ContestWork.objects.order_by('-created_at') queryset = ContestWork.objects.all()
serializer_class = ContestWorkCreateSerializer serializer_class = ContestWorkCreateSerializer
serializer_class_map = { serializer_class_map = {
'list': ContestWorkSerializer, 'list': ContestWorkSerializer,

@ -1,6 +1,7 @@
from urllib.parse import urlparse from urllib.parse import urlparse
from django.db import models from django.db import models
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.urls import reverse_lazy
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
@ -180,6 +181,11 @@ class ContestWork(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
likes = models.ManyToManyField('course.Like', blank=True) likes = models.ManyToManyField('course.Like', blank=True)
class Meta:
verbose_name = 'Конкурсная работа'
verbose_name_plural = 'Конкурсные работы'
ordering = ('-created_at',)
@property @property
def img_width(self): def img_width(self):
return self.image.image.width if self.image and self.image.image else None return self.image.image.width if self.image and self.image.image else None
@ -187,3 +193,6 @@ class ContestWork(models.Model):
@property @property
def img_height(self): def img_height(self):
return self.image.image.height if self.image and self.image.image else None 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])

@ -12,7 +12,9 @@
{{ contest.description }} {{ contest.description }}
</div> </div>
<div class="main__actions"> <div class="main__actions">
{% if not contest_work_uploaded %}
<a class="main__btn btn" href="#" data-show-upload-contest-work>Загрузить свою работу</a> <a class="main__btn btn" href="#" data-show-upload-contest-work>Загрузить свою работу</a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

@ -2,7 +2,7 @@
{% load static %} {% load static %}
{% block title %}{{ contest_work.child_full_name }}, {{ contest_work.age }} лет{% endblock title %} {% 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 %} {% block content %}
<div class="section" style="padding-bottom: 25px;"> <div class="section" style="padding-bottom: 25px;">
@ -23,14 +23,13 @@
<img class="contest-work__img" src="{{ contest_work.image.image.url }}"> <img class="contest-work__img" src="{{ contest_work.image.image.url }}">
</div> </div>
<div class="contest-work__info"> <div class="contest-work__info">
<div> <div class="contest-work__bio">
<div>{{ contest_work.child_full_name }}</div> <div>{{ contest_work.child_full_name }}</div>
<div class="contest-work__age">{{ contest_work.age }} лет</div> <div class="contest-work__age">{{ contest_work.age }} {% if contest_work.age < 5 %}года{% else %}лет{% endif %}</div>
</div> </div>
<div class="contest-work__likes"> <div class="contest-work__likes">
{{ contest_work.likes.count }}<svg class="icon"> <likes obj-type="contest_work" obj-id="{{ contest_work.id }}"
<use v-bind="{'xlink:href': $root.store.staticUrl + 'img/sprite.svg' + '#icon-like' }"></use> {% if user_liked %}:user-liked="true"{% endif %} likes="{{ contest_work.likes.count }}"></likes>
</svg>
</div> </div>
</div> </div>
</div> </div>
@ -39,7 +38,8 @@
<div class="section" style="padding: 0;"> <div class="section" style="padding: 0;">
<div class="section__center center center_sm"> <div class="section__center center center_sm">
<div class="go"> <div class="go">
<a class="go__item" href="{% url 'contest' contest_work.contest.id %}"> {% if prev_contest_work %}
<a class="go__item" href="{% url 'contest_work' prev_contest_work.id %}">
<div class="go__arrow"> <div class="go__arrow">
<svg class="icon icon-arrow-left"> <svg class="icon icon-arrow-left">
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-left"></use> <use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-left"></use>
@ -47,7 +47,11 @@
</div> </div>
<div class="go__title">Предыдущая работа</div> <div class="go__title">Предыдущая работа</div>
</a> </a>
<a class="go__item" href="{% url 'contest' contest_work.contest.id %}"> {% else %}
<div class="go__item"></div>
{% endif %}
{% if next_contest_work %}
<a class="go__item" href="{% url 'contest_work' next_contest_work.id %}">
<div class="go__title">Следующая работа</div> <div class="go__title">Следующая работа</div>
<div class="go__arrow"> <div class="go__arrow">
<svg class="icon icon-arrow-right"> <svg class="icon icon-arrow-right">
@ -55,10 +59,16 @@
</svg> </svg>
</div> </div>
</a> </a>
{% else %}
<div class="go__item"></div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="section"> <div class="section">
<div class="section__center center center_sm">
{% include 'templates/blocks/share.html' %}
</div>
</div> </div>
{% endblock content %} {% endblock content %}

@ -29,8 +29,27 @@ class ContestView(DetailView):
context_object_name = 'contest' context_object_name = 'contest'
template_name = 'content/contest.html' 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): class ContestWorkView(DetailView):
model = ContestWork model = ContestWork
context_object_name = 'contest_work' context_object_name = 'contest_work'
template_name = 'content/contest_work.html' 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

@ -10,6 +10,7 @@
<form class="form"> <form class="form">
<div class="title"> <div class="title">
Чтобы принять участие<br>в конкурсе, заполните поля<br>и прикрепите изображение Чтобы принять участие<br>в конкурсе, заполните поля<br>и прикрепите изображение
<img src="/static/img/curve-1.svg" class="text__curve">
</div> </div>
<div class="field" <div class="field"
v-bind:class="{ error: $v.contestWork.child_full_name.$dirty && $v.contestWork.child_full_name.$invalid }"> v-bind:class="{ error: $v.contestWork.child_full_name.$dirty && $v.contestWork.child_full_name.$invalid }">
@ -138,8 +139,9 @@
}); });
request.then((response) => { request.then((response) => {
if(response.data.id){ if(response.data.id){
this.$emit('add:contestWork', response.data); this.$emit('add:contest-work', response.data);
this.hide(); this.hide();
window.location.reload();
} }
}); });
} }
@ -159,6 +161,12 @@
.title { .title {
text-align: center; font-size: 24px; text-align: center; font-size: 24px;
.text__curve {
right: 55px;
width: 170px;
bottom: -40px;
}
} }
.kit__photo { .kit__photo {

@ -1,24 +1,28 @@
<template> <template>
<a :href="`/contest-work/${contestWork.id}/`" class="contest-work-item"> <div class="contest-work-item">
<img class="contest-work-item__img" :src="contestWork.image.image" /> <a :href="`/contest-work/${contestWork.id}/`">
<img class="contest-work-item__img" :src="contestWork.image.image" />
</a>
<div class="contest-work-item__info"> <div class="contest-work-item__info">
<div> <div class="contest-work-item__bio">
<div>{{ contestWork.child_full_name }}</div> <div>{{ contestWork.child_full_name }}</div>
<div class="contest-work-item__age">{{ contestWork.age }} лет</div> <div class="contest-work-item__age">{{ contestWork.age }} {{ contestWork.age < 5 ? 'года' : 'лет' }}</div>
</div> </div>
<div class="contest-work-item__likes"> <div class="contest-work-item__likes">
{{ contestWork.likes.length }}<svg class="icon"> <likes obj-type="contest_work" :obj-id="contestWork.id" :user-liked="contestWork.user_liked"
<use v-bind="{'xlink:href': $root.store.staticUrl + 'img/sprite.svg' + '#icon-like' }"></use> :likes="contestWork.likes"></likes>
</svg>
</div> </div>
</div> </div>
</a> </div>
</template> </template>
<script> <script>
import Likes from './Likes.vue';
export default { export default {
name: "contest-work", name: "contest-work",
props: ['contestWork'], props: ['contestWork'],
components: {Likes},
} }
</script> </script>
@ -39,9 +43,6 @@
width: 100%; width: 100%;
height: auto; height: auto;
} }
.contest-works:hover .contest-work-item:not(:hover) {
opacity: 0.4;
}
.contest-work-item__info { .contest-work-item__info {
display: flex; display: flex;
padding: 5px 10px; padding: 5px 10px;
@ -49,15 +50,13 @@
.contest-work-item__age { .contest-work-item__age {
color: #919191; color: #919191;
} }
.contest-work-item__likes { .contest-work-item__bio {
flex: 70px; flex: calc(100% - 70px);
text-align: right; }
.icon { @media only screen and (min-width: 1023px) {
width: 15px; .contest-works:hover .contest-work-item:not(:hover) {
height: 15px; opacity: 0.4;
margin-left: 5px;
margin-bottom: -3px;
} }
} }
</style> </style>

@ -0,0 +1,69 @@
<template>
<div class="likes" :class="{ 'likes_liked': userLikedProp }">
<span>{{ likesProp }}</span><span class="likes__like" @click.prevent="addLike">
<svg class="likes__icon icon icon-like">
<use v-bind="{'xlink:href': $root.store.staticUrl + 'img/sprite.svg' + '#icon-like' }"></use>
</svg></span>
</div>
</template>
<script>
import {api} from "../../js/modules/api";
export default {
name: 'likes',
props: ['likes', 'userLiked', 'objType', 'objId'],
data() {
return {
likesProp: +this.likes || 0,
userLikedProp: this.userLiked || false,
}
},
methods: {
addLike() {
if(this._userLiked){
return;
}
api.post('/api/v1/likes/', {
obj_type: this.objType,
obj_id: this.objId,
}, {
headers: {
'Authorization': `Token ${this.$root.store.accessToken}`,
}
})
.then((response) => {
if(response.data && response.data.id) {
this.userLikedProp = true;
this.likesProp += 1;
this.$emit('liked');
}
})
}
}
}
</script>
<style>
.likes {
font-weight: bold;
text-align: right;
}
.likes__like {
cursor: pointer;
margin-left: 5px;
}
.likes_liked .likes__like {
cursor: default;
}
.likes__icon {
margin-bottom: -3px;
}
.likes_liked .likes__icon {
fill: #d40700;
}
</style>

@ -22,6 +22,7 @@ import Vue from 'vue';
import Vuelidate from 'vuelidate'; import Vuelidate from 'vuelidate';
import UploadContestWork from '../components/UploadContestWork.vue'; import UploadContestWork from '../components/UploadContestWork.vue';
import ContestWorks from '../components/ContestWorks.vue'; import ContestWorks from '../components/ContestWorks.vue';
import Likes from '../components/blocks/Likes.vue';
Vue.use(Vuelidate); Vue.use(Vuelidate);
@ -40,5 +41,6 @@ const app = new Vue({
components: { components: {
UploadContestWork, UploadContestWork,
ContestWorks, ContestWorks,
Likes,
} }
}); });

@ -3886,7 +3886,7 @@ a.grey-link
position: relative position: relative
padding-left: 17px padding-left: 17px
&:before &:before
content: '' content: ''
position: absolute position: absolute
top: 0 top: 0
left: 0 left: 0
@ -4116,12 +4116,6 @@ a
&__age &__age
color: #919191; color: #919191;
&__likes &__bio
flex: 70px; flex: calc(100% - 70px);
text-align: right;
&__likes .icon
width: 15px;
height: 15px;
margin-left: 5px;
margin-bottom: -3px;

Loading…
Cancel
Save