Merge branch 'master' of https://gitlab.com/lilcity/backend into feature/lil-583
commit
167a6470b3
69 changed files with 1903 additions and 223 deletions
@ -0,0 +1,68 @@ |
|||||||
|
from rest_framework import serializers |
||||||
|
|
||||||
|
from api.v1.serializers.content import ContentSerializer, ContentCreateSerializer, ImageObjectSerializer |
||||||
|
from api.v1.serializers.mixins import DispatchContentMixin |
||||||
|
|
||||||
|
from apps.content.models import (Contest, ContestWork) |
||||||
|
|
||||||
|
|
||||||
|
class ContestSerializer(serializers.ModelSerializer): |
||||||
|
cover = ImageObjectSerializer() |
||||||
|
content = ContentSerializer(many=True) |
||||||
|
|
||||||
|
class Meta: |
||||||
|
model = Contest |
||||||
|
fields = '__all__' |
||||||
|
|
||||||
|
|
||||||
|
class ContestCreateSerializer(DispatchContentMixin, serializers.ModelSerializer): |
||||||
|
content = serializers.ListSerializer( |
||||||
|
child=ContentCreateSerializer(), |
||||||
|
required=False, |
||||||
|
) |
||||||
|
|
||||||
|
class Meta: |
||||||
|
model = Contest |
||||||
|
fields = '__all__' |
||||||
|
|
||||||
|
def create(self, validated_data): |
||||||
|
content = validated_data.pop('content', []) |
||||||
|
contest = super().create(validated_data) |
||||||
|
self.dispatch_content(contest, content) |
||||||
|
return contest |
||||||
|
|
||||||
|
def update(self, instance, validated_data): |
||||||
|
content = validated_data.pop('content', []) |
||||||
|
contest = super().update(instance, validated_data) |
||||||
|
self.dispatch_content(contest, content) |
||||||
|
return contest |
||||||
|
|
||||||
|
def to_representation(self, instance): |
||||||
|
return ContestSerializer(instance=instance, context=self.context).to_representation(instance) |
||||||
|
|
||||||
|
|
||||||
|
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', '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 Meta: |
||||||
|
model = ContestWork |
||||||
|
fields = '__all__' |
||||||
|
|
||||||
|
def to_representation(self, instance): |
||||||
|
return ContestWorkSerializer(instance=instance, context=self.context).to_representation(instance) |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
from django.contrib.auth.backends import ModelBackend |
||||||
|
from django.contrib.auth import get_user_model |
||||||
|
|
||||||
|
User = get_user_model() |
||||||
|
|
||||||
|
|
||||||
|
class CaseInsensitiveModelBackend(ModelBackend): |
||||||
|
|
||||||
|
def authenticate(self, request, username=None, password=None, **kwargs): |
||||||
|
if username is None: |
||||||
|
username = kwargs.get(User.USERNAME_FIELD) |
||||||
|
try: |
||||||
|
user = User.objects.get(**{f'{User.USERNAME_FIELD}__iexact': username}) |
||||||
|
if user.check_password(password) and self.user_can_authenticate(user): |
||||||
|
return user |
||||||
|
except User.DoesNotExist: |
||||||
|
return None |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
# Generated by Django 2.0.6 on 2018-08-13 13:06 |
||||||
|
|
||||||
|
from django.conf import settings |
||||||
|
from django.db import migrations, models |
||||||
|
import django.db.models.deletion |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('course', '0040_course_age'), |
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||||
|
('content', '0020_auto_20180424_1607'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.CreateModel( |
||||||
|
name='Contest', |
||||||
|
fields=[ |
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||||
|
('title', models.CharField(max_length=255)), |
||||||
|
('description', models.TextField(blank=True, default='', max_length=1000)), |
||||||
|
('slug', models.SlugField(allow_unicode=True, blank=True, max_length=100, null=True, unique=True)), |
||||||
|
('date_start', models.DateField(blank=True, null=True, verbose_name='Дата начала')), |
||||||
|
('date_end', models.DateField(blank=True, null=True, verbose_name='Дата окончания')), |
||||||
|
('active', models.BooleanField(default=True)), |
||||||
|
('cover', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='contest_covers', to='content.ImageObject', verbose_name='Фоновая картинка')), |
||||||
|
], |
||||||
|
), |
||||||
|
migrations.CreateModel( |
||||||
|
name='ContestWork', |
||||||
|
fields=[ |
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||||
|
('child_full_name', models.CharField(max_length=255)), |
||||||
|
('age', models.SmallIntegerField()), |
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)), |
||||||
|
('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.Contest')), |
||||||
|
('image', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contest_work_images', to='content.ImageObject', verbose_name='Работа участника')), |
||||||
|
('likes', models.ManyToManyField(blank=True, to='course.Like')), |
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), |
||||||
|
], |
||||||
|
), |
||||||
|
migrations.AddField( |
||||||
|
model_name='content', |
||||||
|
name='contest', |
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='content', to='content.Contest', verbose_name='Конкурс'), |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
# Generated by Django 2.0.6 on 2018-08-15 21:29 |
||||||
|
|
||||||
|
from django.db import migrations |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('content', '0021_auto_20180813_1306'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.AlterModelOptions( |
||||||
|
name='contestwork', |
||||||
|
options={'ordering': ('-created_at',), 'verbose_name': 'Конкурсная работа', 'verbose_name_plural': 'Конкурсные работы'}, |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,47 @@ |
|||||||
|
{% extends "templates/lilcity/index.html" %} |
||||||
|
|
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<upload-contest-work contest-id="{{ contest.id }}"></upload-contest-work> |
||||||
|
<div class="main main_default" {% if contest.cover %}style="background-image: url({{ contest.cover.image.url }});"{% endif %}> |
||||||
|
<div class="main__center center"> |
||||||
|
<div class="main__title"> |
||||||
|
{{ contest.title }} |
||||||
|
</div> |
||||||
|
<div class="main__subtitle"> |
||||||
|
{{ contest.description }} |
||||||
|
</div> |
||||||
|
<div class="main__actions"> |
||||||
|
{% if not contest_work_uploaded %} |
||||||
|
<a class="main__btn btn" href="" |
||||||
|
{% if request.user.is_authenticated %}data-show-upload-contest-work |
||||||
|
{% else %}data-popup=".js-popup-auth"{% endif %}>Загрузить свою работу</a> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div style="text-align: center;"> |
||||||
|
{% for content in contest.content.all %} |
||||||
|
|
||||||
|
{% with template="content/blocks/"|add:content.ctype|add:".html" %} |
||||||
|
{% include template %} |
||||||
|
{% endwith %} |
||||||
|
|
||||||
|
{% endfor %} |
||||||
|
<div class="section"> |
||||||
|
<div class="section__center center"> |
||||||
|
<a id="gallery" name="gallery"> |
||||||
|
<div class="title title_center">Галерея</div> |
||||||
|
</a> |
||||||
|
<div class="text"> |
||||||
|
{% if not contest_work_uploaded %} |
||||||
|
<a class="btn" href="" |
||||||
|
{% if request.user.is_authenticated %}data-show-upload-contest-work |
||||||
|
{% else %}data-popup=".js-popup-auth"{% endif %}>Загрузить свою работу</a> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
<contest-works contest-id="{{ contest.id }}" autoload="true"></contest-works> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endblock content %} |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
{% extends "templates/lilcity/edit_index.html" %} |
||||||
|
{% load static %} |
||||||
|
|
||||||
|
{% block title %} |
||||||
|
{% if object %} |
||||||
|
Редактирование конкурса {{ object.title }} |
||||||
|
{% else %} |
||||||
|
Создание конкурса |
||||||
|
{% endif %} |
||||||
|
{% endblock title %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<contest-redactor {% if object and object.id %}:contest-id="{{ object.id }}"{% endif %}></contest-redactor> |
||||||
|
{% endblock content %} |
||||||
|
{% block foot %} |
||||||
|
<script type="text/javascript" src="{% static 'contestRedactor.js' %}"></script> |
||||||
|
<link rel="stylesheet" href="{% static 'contestRedactor.css' %}" /> |
||||||
|
{% endblock foot %} |
||||||
@ -0,0 +1,112 @@ |
|||||||
|
{% extends "templates/lilcity/index.html" %} |
||||||
|
{% load static %} |
||||||
|
{% block title %}{{ contest_work.child_full_name }}, {{ contest_work.age }} лет{% endblock title %} |
||||||
|
|
||||||
|
{% block ogimage %}http://{{request.META.HTTP_HOST}}{{ contest_work.image.image.url }}{% endblock ogimage %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<div class="section" style="padding-bottom: 25px;"> |
||||||
|
<div class="section__center center center_sm"> |
||||||
|
<div class="go"> |
||||||
|
<a class="go__item" href="{% url 'contest' contest_work.contest.slug %}"> |
||||||
|
<div class="go__arrow"> |
||||||
|
<svg class="icon icon-arrow-left"> |
||||||
|
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-left"></use> |
||||||
|
</svg> |
||||||
|
</div> |
||||||
|
<div class="go__title">Вернуться к галерее</div> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="contest-work section__center center center_sm"> |
||||||
|
<div class="contest-work__img-wrap"> |
||||||
|
<img class="contest-work__img" src="{{ contest_work.image.image.url }}"> |
||||||
|
</div> |
||||||
|
<div class="contest-work__info"> |
||||||
|
<div class="contest-work__bio"> |
||||||
|
<div>{{ contest_work.child_full_name }}</div> |
||||||
|
<div class="contest-work__age">{{ contest_work.age }} {% if contest_work.age < 5 %}года{% else %}лет{% endif %}</div> |
||||||
|
</div> |
||||||
|
<div class="contest-work__likes"> |
||||||
|
<likes obj-type="contest_work" obj-id="{{ contest_work.id }}" |
||||||
|
{% if user_liked %}:user-liked="true"{% endif %} likes="{{ contest_work.likes.count }}"></likes> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="section" style="padding: 0;"> |
||||||
|
<div class="section__center center center_sm"> |
||||||
|
<div class="go"> |
||||||
|
{% if prev_contest_work %} |
||||||
|
<a class="go__item" href="{% url 'contest_work' prev_contest_work.id %}"> |
||||||
|
<div class="go__arrow"> |
||||||
|
<svg class="icon icon-arrow-left"> |
||||||
|
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-left"></use> |
||||||
|
</svg> |
||||||
|
</div> |
||||||
|
<div class="go__title">Предыдущая работа</div> |
||||||
|
</a> |
||||||
|
{% 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__arrow"> |
||||||
|
<svg class="icon icon-arrow-right"> |
||||||
|
<use xlink:href="{% static 'img/sprite.svg' %}#icon-arrow-right"></use> |
||||||
|
</svg> |
||||||
|
</div> |
||||||
|
</a> |
||||||
|
{% else %} |
||||||
|
<div class="go__item"></div> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="section"> |
||||||
|
<div class="section__center center center_sm"> |
||||||
|
{% include 'templates/blocks/share.html' with share_object_name='работой' %} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="section section_gray"> |
||||||
|
<div class="section__center center center_sm"> |
||||||
|
<div class="title">Оставьте комментарий:</div> |
||||||
|
<div class="questions"> |
||||||
|
{% if request.user.is_authenticated %} |
||||||
|
<form class="questions__form" method="post" action="{% url 'contest_work_comment' contest_work_id=contest_work.id %}"> |
||||||
|
<input type="hidden" name="reply_id"> |
||||||
|
<div class="questions__ava ava"> |
||||||
|
<img |
||||||
|
class="ava__pic" |
||||||
|
{% if request.user.photo %} |
||||||
|
src="{{ request.user.photo.url }}" |
||||||
|
{% else %} |
||||||
|
src="{% static 'img/user_default.jpg' %}" |
||||||
|
{% endif %} |
||||||
|
> |
||||||
|
</div> |
||||||
|
<div class="questions__wrap"> |
||||||
|
<div class="questions__reply-info">В ответ на |
||||||
|
<a href="" class="questions__reply-anchor">этот комментарий</a>. |
||||||
|
<a href="#" class="questions__reply-cancel grey-link">Отменить</a> |
||||||
|
</div> |
||||||
|
<div class="questions__field"> |
||||||
|
<textarea class="questions__textarea"></textarea> |
||||||
|
</div> |
||||||
|
<button class="questions__btn btn btn_light">ОТПРАВИТЬ</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
{% else %} |
||||||
|
<div>Только зарегистрированные пользователи могут оставлять комментарии.</div> |
||||||
|
{% endif %} |
||||||
|
<div class="questions__list"> |
||||||
|
{% include "templates/blocks/comments.html" with object=contest_work %} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endblock content %} |
||||||
@ -1,2 +1,110 @@ |
|||||||
from django.shortcuts import render |
from django.contrib.auth.decorators import login_required |
||||||
|
from django.http import JsonResponse |
||||||
|
from django.shortcuts import get_object_or_404 |
||||||
|
from django.template import loader |
||||||
|
from django.utils.decorators import method_decorator |
||||||
|
from django.views.decorators.csrf import csrf_exempt |
||||||
|
from django.views.decorators.http import require_http_methods |
||||||
|
from django.views.generic import TemplateView, DetailView |
||||||
|
|
||||||
|
from apps.content.models import Contest, ContestWork |
||||||
|
from apps.course.models import ContestWorkComment |
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch') |
||||||
|
class ContestEditView(TemplateView): |
||||||
|
template_name = 'content/contest_edit.html' |
||||||
|
|
||||||
|
def get(self, request, pk=None, lesson=None): |
||||||
|
if pk: |
||||||
|
self.object = get_object_or_404(Contest, pk=pk) |
||||||
|
else: |
||||||
|
self.object = Contest() |
||||||
|
|
||||||
|
return super().get(request) |
||||||
|
|
||||||
|
def get_context_data(self): |
||||||
|
context = super().get_context_data() |
||||||
|
context['object'] = self.object |
||||||
|
return context |
||||||
|
|
||||||
|
|
||||||
|
class ContestView(DetailView): |
||||||
|
model = Contest |
||||||
|
context_object_name = 'contest' |
||||||
|
template_name = 'content/contest.html' |
||||||
|
query_pk_and_slug = True |
||||||
|
|
||||||
|
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 |
||||||
|
|
||||||
|
|
||||||
|
@login_required |
||||||
|
@csrf_exempt |
||||||
|
@require_http_methods(['POST']) |
||||||
|
def contest_work_comment(request, contest_work_id): |
||||||
|
try: |
||||||
|
contest_work = ContestWork.objects.get(id=contest_work_id) |
||||||
|
except ContestWork.DoesNotExist: |
||||||
|
return JsonResponse({ |
||||||
|
'success': False, |
||||||
|
'errors': ['Contest_work with id f{contest_work_id} not found'] |
||||||
|
}, status=400) |
||||||
|
else: |
||||||
|
reply_to = request.POST.get('reply_id', 0) |
||||||
|
comment = request.POST.get('comment', '') |
||||||
|
if not comment: |
||||||
|
return JsonResponse({ |
||||||
|
'success': False, |
||||||
|
'errors': ['Comment can not be empty'] |
||||||
|
}, status=400) |
||||||
|
|
||||||
|
if not int(reply_to): |
||||||
|
contest_work_comment = ContestWorkComment.objects.create( |
||||||
|
author=request.user, |
||||||
|
content=comment, |
||||||
|
contest_work=contest_work, |
||||||
|
) |
||||||
|
else: |
||||||
|
try: |
||||||
|
_contest_work_comment = ContestWorkComment.objects.get(id=reply_to) |
||||||
|
except ContestWorkComment.DoesNotExist: |
||||||
|
return JsonResponse({ |
||||||
|
'success': False, |
||||||
|
'errors': ['LessonComment with id f{reply_to} not found'] |
||||||
|
}, status=400) |
||||||
|
else: |
||||||
|
contest_work_comment = ContestWorkComment.objects.create( |
||||||
|
author=request.user, |
||||||
|
content=comment, |
||||||
|
contest_work=contest_work, |
||||||
|
parent=_contest_work_comment, |
||||||
|
) |
||||||
|
ctx = {'node': contest_work_comment, 'user': request.user} |
||||||
|
html = loader.render_to_string('templates/blocks/comment.html', ctx) |
||||||
|
return JsonResponse({ |
||||||
|
'success': True, |
||||||
|
'comment': html, |
||||||
|
}) |
||||||
|
|||||||
@ -0,0 +1,18 @@ |
|||||||
|
# Generated by Django 2.0.6 on 2018-08-08 01:54 |
||||||
|
|
||||||
|
from django.db import migrations, models |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('course', '0039_lesson_position'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.AddField( |
||||||
|
model_name='course', |
||||||
|
name='age', |
||||||
|
field=models.SmallIntegerField(choices=[(0, ''), (1, 'до 5'), (2, '5-7'), (3, '7-9'), (4, '9-12'), (5, '12-15'), (6, '15-18'), (7, 'от 18')], default=0), |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,36 @@ |
|||||||
|
# Generated by Django 2.0.6 on 2018-08-13 13:06 |
||||||
|
|
||||||
|
from django.db import migrations, models |
||||||
|
import django.db.models.deletion |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('content', '0021_auto_20180813_1306'), |
||||||
|
('course', '0040_course_age'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.CreateModel( |
||||||
|
name='ContestWorkComment', |
||||||
|
fields=[ |
||||||
|
('comment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='course.Comment')), |
||||||
|
('contest_work', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='content.ContestWork')), |
||||||
|
], |
||||||
|
options={ |
||||||
|
'ordering': ('tree_id', 'lft'), |
||||||
|
'abstract': False, |
||||||
|
'base_manager_name': 'objects', |
||||||
|
}, |
||||||
|
bases=('course.comment',), |
||||||
|
), |
||||||
|
migrations.AlterModelOptions( |
||||||
|
name='course', |
||||||
|
options={'ordering': ['-is_featured'], 'verbose_name': 'Курс', 'verbose_name_plural': 'Курсы'}, |
||||||
|
), |
||||||
|
migrations.RemoveField( |
||||||
|
model_name='course', |
||||||
|
name='age', |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
# Generated by Django 2.0.6 on 2018-08-16 16:45 |
||||||
|
|
||||||
|
from django.db import migrations, models |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('course', '0041_auto_20180813_1306'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.AddField( |
||||||
|
model_name='like', |
||||||
|
name='ip', |
||||||
|
field=models.GenericIPAddressField(blank=True, null=True), |
||||||
|
), |
||||||
|
] |
||||||
@ -1,5 +1,6 @@ |
|||||||
{% for cat in category_items %} |
{% for cat in category_items %} |
||||||
<div class="select__option js-select-option{% if category and category.0 == cat.title %} active{% endif %}" data-category-option data-category-name="{{ cat.title }}" data-category-url="{% url 'courses' %}?category={{ cat.title }}"> |
<div class="select__option js-select-option{% if category == cat.id %} active{% endif %}" |
||||||
|
data-category-option data-category-name="{{ cat.title }}" data-category-url="{% url 'courses' %}?category={{ cat.id }}"> |
||||||
<div class="select__title">{{ cat.title }}</div> |
<div class="select__title">{{ cat.title }}</div> |
||||||
</div> |
</div> |
||||||
{% endfor %} |
{% endfor %} |
||||||
@ -1,4 +1,4 @@ |
|||||||
<a |
<a |
||||||
class="timing__btn btn btn_light" |
class="timing__btn btn btn_light" |
||||||
href="{% url 'school:lesson-detail' live_lesson.id %}" |
href="{% url 'school:lesson-detail' live_lesson.id %}" |
||||||
>смотреть урок</a> |
>подробнее</a> |
||||||
|
|||||||
@ -0,0 +1,10 @@ |
|||||||
|
{% load static %} |
||||||
|
<script> |
||||||
|
window.LIL_STORE = { |
||||||
|
staticUrl: '{% static "" %}', |
||||||
|
accessToken: '{{ request.user.auth_token }}', |
||||||
|
user: { |
||||||
|
id: {{ request.user.id|default:'null' }}, |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
@ -0,0 +1,361 @@ |
|||||||
|
<template> |
||||||
|
<form @submit.prevent="save"> |
||||||
|
<div class="info"> |
||||||
|
<div class="info__section" :style="coverBackgroundStyle"> |
||||||
|
<div class="info__main"> |
||||||
|
<div class="info__head"> |
||||||
|
<div class="info__upload upload"> |
||||||
|
Загрузить фон |
||||||
|
<input type="file" class="upload__file" @change="onCoverImageSelected"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="info__title"> |
||||||
|
<div class="info__field field field_info" |
||||||
|
v-bind:class="{ error: $v.contest.title.$dirty && $v.contest.title.$invalid }"> |
||||||
|
<div class="field__label">Название</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<textarea class="field__textarea" |
||||||
|
rows="1" |
||||||
|
v-autosize="contest.title" |
||||||
|
@change="onTitleInput" |
||||||
|
v-model="contest.title"></textarea> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="info__field field field_info field_short_description"> |
||||||
|
<div class="field__label">Описание</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<textarea class="field__textarea" |
||||||
|
v-autosize="contest.description" |
||||||
|
v-model="contest.description"></textarea> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="info__foot"> |
||||||
|
<div class="info__field field"> |
||||||
|
<div class="field__label field__label_gray">ССЫЛКА</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input type="text" class="field__input" v-model="contest.slug" @input="slugChanged = true"> |
||||||
|
</div> |
||||||
|
<div class="field__wrap field__wrap--additional">{{ contestFullUrl }}</div> |
||||||
|
</div> |
||||||
|
<div class="info__field field"> |
||||||
|
<div class="field__label">ДАТА НАЧАЛА</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<vue-datepicker input-class="field__input" v-model="contest.date_start" language="ru" format="dd/MM/yyyy"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="info__field field"> |
||||||
|
<div class="field__label">ДАТА ОКОНЧАНИЯ</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<vue-datepicker input-class="field__input" v-model="contest.date_end" language="ru" format="dd/MM/yyyy"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<button type="submit">Save</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="info__sidebar"> |
||||||
|
<div class="info__wrap"> |
||||||
|
<div class="info__fieldset"> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="section"> |
||||||
|
<div class="section__center center"> |
||||||
|
<div class="kit"> |
||||||
|
<div class="kit__body"> |
||||||
|
<vue-draggable v-model="contest.content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }"> |
||||||
|
<div v-for="(block, index) in contest.content" :key="block.data.id ? block.data.id : block.data.guid"> |
||||||
|
<block-text v-if="block.type === 'text'" |
||||||
|
:index="index" |
||||||
|
:title.sync="block.data.title" |
||||||
|
:text.sync="block.data.text" |
||||||
|
v-on:remove="onBlockRemoved" |
||||||
|
:access-token="accessToken"/> |
||||||
|
<block-image-text v-if="block.type === 'image-text'" |
||||||
|
:index="index" |
||||||
|
:title.sync="block.data.title" |
||||||
|
:text.sync="block.data.text" |
||||||
|
:image-id.sync="block.data.image_id" |
||||||
|
:image-url.sync="block.data.image_url" |
||||||
|
v-on:remove="onBlockRemoved" |
||||||
|
:access-token="accessToken"/> |
||||||
|
<block-image v-if="block.type === 'image'" |
||||||
|
:index="index" |
||||||
|
:title.sync="block.data.title" |
||||||
|
:image-id.sync="block.data.image_id" |
||||||
|
:image-url.sync="block.data.image_url" |
||||||
|
v-on:remove="onBlockRemoved" |
||||||
|
:access-token="accessToken"/> |
||||||
|
<block-images v-if="block.type === 'images'" |
||||||
|
:index="index" |
||||||
|
:title.sync="block.data.title" |
||||||
|
:text.sync="block.data.text" |
||||||
|
:images.sync="block.data.images" |
||||||
|
v-on:remove="onBlockRemoved" |
||||||
|
:access-token="accessToken"/> |
||||||
|
<block-video v-if="block.type === 'video'" |
||||||
|
:index="index" |
||||||
|
:title.sync="block.data.title" |
||||||
|
v-on:remove="onBlockRemoved" |
||||||
|
:video-url.sync="block.data.video_url"/> |
||||||
|
</div> |
||||||
|
</vue-draggable> |
||||||
|
|
||||||
|
<block-add v-on:added="onBlockAdded"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import BlockText from './blocks/BlockText' |
||||||
|
import BlockImage from './blocks/BlockImage' |
||||||
|
import BlockImages from './blocks/BlockImages' |
||||||
|
import BlockImageText from './blocks/BlockImageText' |
||||||
|
import BlockVideo from './blocks/BlockVideo' |
||||||
|
import BlockAdd from "./blocks/BlockAdd"; |
||||||
|
import {api} from "../js/modules/api"; |
||||||
|
import DatePicker from 'vuejs-datepicker'; |
||||||
|
import Draggable from 'vuedraggable'; |
||||||
|
import slugify from 'slugify'; |
||||||
|
import {required, minValue, numeric, url } from 'vuelidate/lib/validators' |
||||||
|
import _ from 'lodash'; |
||||||
|
import moment from 'moment' |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'contest-redactor', |
||||||
|
props: ["contestId"], |
||||||
|
data() { |
||||||
|
return { |
||||||
|
loading: false, |
||||||
|
slugChanged: false, |
||||||
|
contest: { |
||||||
|
coverImage: '', |
||||||
|
coverImageId: null, |
||||||
|
|
||||||
|
title: '', |
||||||
|
description: '', |
||||||
|
content: [], |
||||||
|
date_start: '', |
||||||
|
date_end: '', |
||||||
|
slug: '', |
||||||
|
active: true, |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
accessToken() { |
||||||
|
return window.LIL_STORE.accessToken; |
||||||
|
}, |
||||||
|
coverBackgroundStyle() { |
||||||
|
return this.contest.coverImage ? `background-image: url(${this.contest.coverImage});` : ''; |
||||||
|
}, |
||||||
|
contestFullUrl() { |
||||||
|
return `https://lil.city/contest/${this.contest.slug}`; |
||||||
|
}, |
||||||
|
}, |
||||||
|
|
||||||
|
validations() { |
||||||
|
return { |
||||||
|
contest: { |
||||||
|
title: { |
||||||
|
required |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
if (this.contestId) { |
||||||
|
this.load(); |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
onBlockRemoved(blockIndex) { |
||||||
|
const blockToRemove = this.contest.content[blockIndex]; |
||||||
|
// Удаляем блок из Vue |
||||||
|
this.contest.content.splice(blockIndex, 1); |
||||||
|
// Если блок уже был записан в БД, отправляем запрос на сервер на удаление блока из БД |
||||||
|
if (blockToRemove.data.id) { |
||||||
|
api.removeContentBlock(blockToRemove, this.accessToken); |
||||||
|
} |
||||||
|
}, |
||||||
|
onBlockAdded(blockData) { |
||||||
|
this.contest.content.push(blockData); |
||||||
|
}, |
||||||
|
onTitleInput() { |
||||||
|
this.$v.contest.title.$touch(); |
||||||
|
if (!this.slugChanged) { |
||||||
|
this.contest.slug = slugify(this.contest.title); |
||||||
|
} |
||||||
|
}, |
||||||
|
onCoverImageSelected(event) { |
||||||
|
let file = event.target.files[0]; |
||||||
|
let reader = new FileReader(); |
||||||
|
reader.onload = () => { |
||||||
|
this.$set(this.contest, 'coverImage', reader.result); |
||||||
|
api.uploadImage(reader.result, this.accessToken) |
||||||
|
.then((response) => { |
||||||
|
this.contest.coverImageId = response.data.id; |
||||||
|
}) |
||||||
|
.catch((error) => { |
||||||
|
//console.log('error', error); |
||||||
|
}); |
||||||
|
}; |
||||||
|
if (file) { |
||||||
|
reader.readAsDataURL(file); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
processContestJson(data) { |
||||||
|
this.contest = { |
||||||
|
coverImage: data.cover && data.cover.image || null, |
||||||
|
coverImageId: data.cover && data.cover.id || null, |
||||||
|
|
||||||
|
id: data.id, |
||||||
|
title: data.title, |
||||||
|
description: data.description, |
||||||
|
content: api.convertContentResponse(data.content), |
||||||
|
date_start: data.date_start, |
||||||
|
date_end: data.date_end, |
||||||
|
slug: data.slug, |
||||||
|
active: data.active, |
||||||
|
|
||||||
|
}; |
||||||
|
}, |
||||||
|
load() { |
||||||
|
this.loading = true; |
||||||
|
const request = api.get(`/api/v1/contests/${this.contestId}/`, { |
||||||
|
headers: { |
||||||
|
'Authorization': `Token ${window.LIL_STORE.accessToken}`, |
||||||
|
} |
||||||
|
}); |
||||||
|
request |
||||||
|
.then((response) => { |
||||||
|
this.processContestJson(response.data); |
||||||
|
this.$nextTick(() => { |
||||||
|
this.loading = false; |
||||||
|
}); |
||||||
|
}) |
||||||
|
.catch((err) => { |
||||||
|
this.loading = false; |
||||||
|
}); |
||||||
|
|
||||||
|
return request; |
||||||
|
}, |
||||||
|
save() { |
||||||
|
let data = _.pick(this.contest, ['title', 'description', 'slug', 'active']); |
||||||
|
data.date_start = data.date_start ? moment(data.date_start).format('MM-DD-YYYY') : null; |
||||||
|
data.date_end = data.date_end ? moment(data.date_end).format('MM-DD-YYYY') : null; |
||||||
|
data.cover = this.contest.coverImageId || ''; |
||||||
|
data.content = this.contest.content.map((block, index) => { |
||||||
|
if (block.type === 'text') { |
||||||
|
return { |
||||||
|
'type': 'text', |
||||||
|
'data': { |
||||||
|
'id': block.data.id ? block.data.id : null, |
||||||
|
'uuid': block.uuid, |
||||||
|
'position': ++index, |
||||||
|
'title': block.data.title, |
||||||
|
'txt': block.data.text, |
||||||
|
} |
||||||
|
} |
||||||
|
} else if (block.type === 'image') { |
||||||
|
return { |
||||||
|
'type': 'image', |
||||||
|
'data': { |
||||||
|
'id': block.data.id ? block.data.id : null, |
||||||
|
'uuid': block.uuid, |
||||||
|
'position': ++index, |
||||||
|
'title': block.data.title, |
||||||
|
'img': block.data.image_id, |
||||||
|
} |
||||||
|
} |
||||||
|
} else if (block.type === 'image-text') { |
||||||
|
return { |
||||||
|
'type': 'image-text', |
||||||
|
'data': { |
||||||
|
'id': block.data.id ? block.data.id : null, |
||||||
|
'uuid': block.uuid, |
||||||
|
'position': ++index, |
||||||
|
'title': block.data.title, |
||||||
|
'img': block.data.image_id, |
||||||
|
'txt': block.data.text, |
||||||
|
} |
||||||
|
} |
||||||
|
} else if (block.type === 'images') { |
||||||
|
return { |
||||||
|
'type': 'images', |
||||||
|
'data': { |
||||||
|
'id': block.data.id ? block.data.id : null, |
||||||
|
'uuid': block.uuid, |
||||||
|
'position': ++index, |
||||||
|
'title': block.data.title, |
||||||
|
'images': block.data.images.map((galleryImage) => { |
||||||
|
return { |
||||||
|
'id': galleryImage.id ? galleryImage.id : null, |
||||||
|
'img': galleryImage.img, |
||||||
|
} |
||||||
|
}), |
||||||
|
} |
||||||
|
} |
||||||
|
} else if (block.type === 'video') { |
||||||
|
return { |
||||||
|
'type': 'video', |
||||||
|
'data': { |
||||||
|
'id': block.data.id ? block.data.id : null, |
||||||
|
'uuid': block.uuid, |
||||||
|
'position': ++index, |
||||||
|
'title': block.data.title, |
||||||
|
'url': block.data.video_url, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
const request = this.contest.id |
||||||
|
? api.put(`/api/v1/contests/${this.contest.id}/`, data, { |
||||||
|
headers: { |
||||||
|
'Authorization': `Token ${window.LIL_STORE.accessToken}`, |
||||||
|
} |
||||||
|
}) |
||||||
|
: api.post('/api/v1/contests/', data, { |
||||||
|
headers: { |
||||||
|
'Authorization': `Token ${window.LIL_STORE.accessToken}`, |
||||||
|
} |
||||||
|
}); |
||||||
|
request.then((response) => { |
||||||
|
if(this.contest.id) { |
||||||
|
this.contest = this.processContestJson(response.data); |
||||||
|
} |
||||||
|
else { |
||||||
|
window.location.href = `/contest/${response.data.id}/edit`; |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch((err) => { |
||||||
|
//this.contestSaving = false; |
||||||
|
}); |
||||||
|
|
||||||
|
} |
||||||
|
}, |
||||||
|
components: { |
||||||
|
BlockAdd, |
||||||
|
'vue-datepicker': DatePicker, |
||||||
|
'block-text': BlockText, |
||||||
|
'block-image': BlockImage, |
||||||
|
'block-image-text': BlockImageText, |
||||||
|
'block-images': BlockImages, |
||||||
|
'block-video': BlockVideo, |
||||||
|
'vue-draggable': Draggable, |
||||||
|
|
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
|
||||||
|
</style> |
||||||
@ -0,0 +1,86 @@ |
|||||||
|
<template> |
||||||
|
<div class="contest-works"> |
||||||
|
<div class="contest-works__works"> |
||||||
|
<contest-work v-for="contestWork in contestWorks" :key="contestWork.id" :contest-work="contestWork"></contest-work> |
||||||
|
</div> |
||||||
|
<div v-show="loading" class="contest-works__loader"><div class="loading-loader"></div></div> |
||||||
|
<div v-if="loaded && !contestWorks.length" class="contest-works__no-works">Здесь вы сможете увидеть работы участников после их добавления</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import {api} from "../js/modules/api"; |
||||||
|
import $ from 'jquery'; |
||||||
|
import ContestWork from "./blocks/ContestWork.vue"; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "contest-works", |
||||||
|
props: ['contestId', 'autoload'], |
||||||
|
data(){ |
||||||
|
return { |
||||||
|
page: 1, |
||||||
|
lastPage: null, |
||||||
|
loading: false, |
||||||
|
loaded: false, |
||||||
|
contestWorks: [], |
||||||
|
}; |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.load(); |
||||||
|
if(this.autoload) { |
||||||
|
$(window).scroll(() => { |
||||||
|
const pos = $(this.$el).offset().top + this.$el.clientHeight - 100; |
||||||
|
if($(window).scrollTop() + $(window).height() >= pos |
||||||
|
&& !this.loading && !this.lastPage) { |
||||||
|
this.page += 1; |
||||||
|
this.load(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
load() { |
||||||
|
this.loading = true; |
||||||
|
api.get(`/api/v1/contest-works/?contest=${this.contestId}&page=${this.page}`) |
||||||
|
.then((response) => { |
||||||
|
this.loading = false; |
||||||
|
this.loaded = true; |
||||||
|
if(this.page > 1){ |
||||||
|
this.contestWorks = this.contestWorks.concat(response.data.results); |
||||||
|
} |
||||||
|
else{ |
||||||
|
this.contestWorks = response.data.results; |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch((response) => { |
||||||
|
this.loading = false; |
||||||
|
this.loaded = true; |
||||||
|
if(response.response.status == 404){ |
||||||
|
this.lastPage = this.page; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
}, |
||||||
|
components: {ContestWork}, |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
.contest-works { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
.contest-works__works { |
||||||
|
column-width: 300px; |
||||||
|
column-gap: 20px; |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
.contest-works__loader { |
||||||
|
width: 100%; |
||||||
|
height: 30px; |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
.contest-works__no-works { |
||||||
|
text-align: center; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,193 @@ |
|||||||
|
<template> |
||||||
|
<div ref="popup" class="upload-contest-work popup" @click.prevent="hide"> |
||||||
|
<div class="popup__wrap popup__wrap_md" @click.stop> |
||||||
|
<button class="popup__close" @click.prevent="hide"> |
||||||
|
<svg class="icon icon-close"> |
||||||
|
<use v-bind="{'xlink:href': $root.store.staticUrl + 'img/sprite.svg' + '#icon-close' }"></use> |
||||||
|
</svg> |
||||||
|
</button> |
||||||
|
<div class="popup__body"> |
||||||
|
<form class="form"> |
||||||
|
<div class="title"> |
||||||
|
Чтобы принять участие<br>в конкурсе, заполните поля<br>и прикрепите изображение |
||||||
|
<img src="/static/img/curve-1.svg" class="text__curve"> |
||||||
|
</div> |
||||||
|
<div class="field" |
||||||
|
v-bind:class="{ error: $v.contestWork.child_full_name.$dirty && $v.contestWork.child_full_name.$invalid }"> |
||||||
|
<div class="field__label">ИМЯ И ФАМИЛИЯ РЕБЕНКА</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input class="field__input" type="text" v-model="contestWork.child_full_name"> |
||||||
|
</div> |
||||||
|
<div class="field__error"></div> |
||||||
|
</div> |
||||||
|
<div class="field" |
||||||
|
v-bind:class="{ error: $v.contestWork.age.$dirty && $v.contestWork.age.$invalid }"> |
||||||
|
<div class="field__label">ВОЗРАСТ</div> |
||||||
|
<div class="field__wrap"> |
||||||
|
<input class="field__input" type="number" v-model="contestWork.age" style="width: 100px;"> |
||||||
|
</div> |
||||||
|
<div class="field__error"></div> |
||||||
|
</div> |
||||||
|
<div class="field"> |
||||||
|
<lil-image :image-id.sync="contestWork.imageId" :image-url.sync="contestWork.imageUrl" |
||||||
|
v-on:update:imageId="onUpdateImageId" :access-token="$root.store.accessToken" /> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="field" style="text-align: center;"> |
||||||
|
<button class="btn" tabindex="3" @click.prevent="save">Отправить на конкурс</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import {api} from "../js/modules/api"; |
||||||
|
import LilImage from './blocks/Image'; |
||||||
|
import {showNotification} from "../js/modules/notification"; |
||||||
|
import {required, minValue, numeric, url } from 'vuelidate/lib/validators'; |
||||||
|
import $ from 'jquery'; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'upload-contest-work', |
||||||
|
props: ['contestId'], |
||||||
|
data() { |
||||||
|
return { |
||||||
|
fields: { |
||||||
|
image: "Изображение", |
||||||
|
child_full_name: "Имя и фамилия ребенка", |
||||||
|
age: 'Возраст', |
||||||
|
}, |
||||||
|
contestWork: { |
||||||
|
contest: this.contestId, |
||||||
|
image: null, |
||||||
|
imageId: null, |
||||||
|
imageUrl: '', |
||||||
|
child_full_name: '', |
||||||
|
age: '', |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
validations() { |
||||||
|
return { |
||||||
|
contestWork: { |
||||||
|
image: { |
||||||
|
required |
||||||
|
}, |
||||||
|
child_full_name: { |
||||||
|
required |
||||||
|
}, |
||||||
|
age: { |
||||||
|
required, numeric |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
$('[data-show-upload-contest-work]').click((e) => { |
||||||
|
e.preventDefault(); |
||||||
|
e.stopPropagation(); |
||||||
|
this.clear(); |
||||||
|
this.show(); |
||||||
|
}); |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
clear() { |
||||||
|
Object.assign(this.$data, this.$options.data.apply(this)) |
||||||
|
}, |
||||||
|
onUpdateImageId(imageId) { |
||||||
|
this.contestWork.image = imageId; |
||||||
|
}, |
||||||
|
show() { |
||||||
|
const $popup = $(this.$refs.popup) |
||||||
|
$('body').addClass('no-scroll'); |
||||||
|
$popup.addClass('visible'); |
||||||
|
setTimeout(() => { |
||||||
|
$popup.addClass('open'); |
||||||
|
}, 300); |
||||||
|
}, |
||||||
|
hide() { |
||||||
|
const $popup = $(this.$refs.popup) |
||||||
|
$('body').removeClass('no-scroll'); |
||||||
|
$popup.removeClass('visible'); |
||||||
|
setTimeout(() => { |
||||||
|
$popup.removeClass('open'); |
||||||
|
}, 300); |
||||||
|
}, |
||||||
|
validate() { |
||||||
|
if (this.$v.contestWork.$invalid) { |
||||||
|
for(let i in this.$v.contestWork) { |
||||||
|
if(this.$v.contestWork[i].$invalid) { |
||||||
|
showNotification("error", "Ошибка валидации поля " + this.fields[i]); |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
}, |
||||||
|
save() { |
||||||
|
if(! this.validate()) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
let data = this.contestWork; |
||||||
|
data.contest = this.contestId; |
||||||
|
data.user = this.$root.store.user.id; |
||||||
|
const request = api.post(`/api/v1/contest-works/`, data, { |
||||||
|
headers: { |
||||||
|
'Authorization': `Token ${this.$root.store.accessToken}`, |
||||||
|
} |
||||||
|
}); |
||||||
|
request.then((response) => { |
||||||
|
if(+response.data.id){ |
||||||
|
this.$emit('add:contest-work', response.data); |
||||||
|
this.hide(); |
||||||
|
window.location.reload(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
}, |
||||||
|
components: { |
||||||
|
'lil-image': LilImage, |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.upload-contest-work { |
||||||
|
|
||||||
|
.popup__wrap { |
||||||
|
padding: 35px 35px 0; |
||||||
|
} |
||||||
|
|
||||||
|
.title { |
||||||
|
text-align: center; font-size: 24px; |
||||||
|
|
||||||
|
.text__curve { |
||||||
|
right: 55px; |
||||||
|
width: 170px; |
||||||
|
bottom: -40px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.kit__photo { |
||||||
|
height: 400px; |
||||||
|
} |
||||||
|
|
||||||
|
.kit__photo.has-image { |
||||||
|
border: none; |
||||||
|
} |
||||||
|
|
||||||
|
.kit__photo-image { |
||||||
|
max-height: 400px; |
||||||
|
height: auto; |
||||||
|
width: auto; |
||||||
|
} |
||||||
|
|
||||||
|
.kit__file { |
||||||
|
bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,62 @@ |
|||||||
|
<template> |
||||||
|
<div class="contest-work-item"> |
||||||
|
<a :href="`/contest-work/${contestWork.id}/`"> |
||||||
|
<img class="contest-work-item__img" :src="contestWork.image.image_thumbnail" /> |
||||||
|
</a> |
||||||
|
<div class="contest-work-item__info"> |
||||||
|
<div class="contest-work-item__bio"> |
||||||
|
<div>{{ contestWork.child_full_name }}</div> |
||||||
|
<div class="contest-work-item__age">{{ contestWork.age }} {{ contestWork.age < 5 ? 'года' : 'лет' }}</div> |
||||||
|
</div> |
||||||
|
<div class="contest-work-item__likes"> |
||||||
|
<likes obj-type="contest_work" :obj-id="contestWork.id" :user-liked="contestWork.user_liked" |
||||||
|
:likes="contestWork.likes"></likes> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import Likes from './Likes.vue'; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "contest-work", |
||||||
|
props: ['contestWork'], |
||||||
|
components: {Likes}, |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.contest-work-item { |
||||||
|
break-inside: avoid; |
||||||
|
border-radius: 8px; |
||||||
|
overflow: hidden; |
||||||
|
margin-bottom: 20px; |
||||||
|
transition: opacity .4s ease-in-out; |
||||||
|
text-transform: uppercase; |
||||||
|
font-weight: bold; |
||||||
|
color: black; |
||||||
|
border: 1px solid #ececec; |
||||||
|
display: block; |
||||||
|
} |
||||||
|
.contest-work-item__img { |
||||||
|
width: 100%; |
||||||
|
height: auto; |
||||||
|
} |
||||||
|
.contest-work-item__info { |
||||||
|
display: flex; |
||||||
|
padding: 5px 10px; |
||||||
|
} |
||||||
|
.contest-work-item__age { |
||||||
|
color: #919191; |
||||||
|
} |
||||||
|
.contest-work-item__bio { |
||||||
|
flex: calc(100% - 70px); |
||||||
|
} |
||||||
|
|
||||||
|
@media only screen and (min-width: 1023px) { |
||||||
|
.contest-works:hover .contest-work-item:not(:hover) { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,74 @@ |
|||||||
|
<template> |
||||||
|
<div class="likes" :class="{ 'likes_liked': userLikedProp }"> |
||||||
|
<span>{{ likesProp }}</span><span class="likes__like" @click="addLike" |
||||||
|
v-bind="{ 'data-popup': $root.store.user.id ? '' : '.js-popup-auth' }"> |
||||||
|
<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(event) { |
||||||
|
if(this.userLikedProp){ |
||||||
|
return; |
||||||
|
} |
||||||
|
if(this.$root.store.user.id) { |
||||||
|
event.stopPropagation(); |
||||||
|
api.post('/api/v1/likes/', { |
||||||
|
user: this.$root.store.user.id, // FIXME |
||||||
|
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> |
||||||
|
After Width: | Height: | Size: 33 KiB |
@ -0,0 +1,19 @@ |
|||||||
|
import Vue from 'vue' |
||||||
|
import Vuelidate from 'vuelidate' |
||||||
|
import VueAutosize from '../components/directives/autosize' |
||||||
|
import ContestRedactor from '../components/ContestRedactor.vue' |
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') { |
||||||
|
// Enable vue-devtools
|
||||||
|
Vue.config.devtools = true; |
||||||
|
} |
||||||
|
|
||||||
|
Vue.use(VueAutosize); |
||||||
|
Vue.use(Vuelidate); |
||||||
|
|
||||||
|
let app = new Vue({ |
||||||
|
el: '#lilcity-vue-app', |
||||||
|
components: { |
||||||
|
'contest-redactor': ContestRedactor, |
||||||
|
} |
||||||
|
}); |
||||||
Loading…
Reference in new issue