Merge branch 'hotfix/LIL-706' into 'master'

Hotfix/lil 706

See merge request lilcity/backend!220
remotes/origin/editis_13-01-19
cfwme 7 years ago
commit 7ed17d4e5a
  1. 14
      api/v1/serializers/mixins.py
  2. 31
      api/v1/serializers/user.py
  3. 3
      api/v1/urls.py
  4. 11
      api/v1/views.py
  5. 4
      apps/school/templates/blocks/schedule.html
  6. 2
      apps/school/templates/school/livelessons_list.html
  7. 41
      apps/school/templates/summer/schedule_purchased.html
  8. 48
      apps/school/views.py
  9. 2
      apps/user/forms.py
  10. 18
      apps/user/migrations/0026_user_site.py
  11. 20
      apps/user/migrations/0027_user_gallery.py
  12. 5
      apps/user/models.py
  13. 35
      apps/user/templates/user/edit-gallery.html
  14. 11
      apps/user/templates/user/profile-settings.html
  15. 25
      apps/user/templates/user/profile.html
  16. 5
      apps/user/views.py
  17. 1
      project/templates/blocks/lil_store_js.html
  18. 2
      project/templates/lilcity/index.html
  19. 3
      project/urls.py
  20. 164
      web/src/components/CourseRedactor.vue
  21. 40
      web/src/components/blocks/BlockContent.vue
  22. 19
      web/src/components/blocks/BlockImage.vue
  23. 17
      web/src/components/blocks/BlockImageText.vue
  24. 40
      web/src/components/blocks/BlockImages.vue
  25. 17
      web/src/components/blocks/BlockText.vue
  26. 19
      web/src/components/blocks/BlockVideo.vue
  27. 4
      web/src/js/app.js
  28. 26
      web/src/js/modules/api.js
  29. 14
      web/src/js/user-gallery-edit.js
  30. 48
      web/src/sass/_common.sass
  31. 5
      web/src/sass/generated/_sprite-svg.scss
  32. 1
      web/webpack.config.js

@ -131,7 +131,7 @@ class DispatchMaterialMixin(object):
class DispatchGalleryMixin(object):
def dispatch_gallery(self, course, gallery):
def dispatch_gallery(self, obj, gallery):
if gallery:
if 'id' in gallery and gallery['id']:
g = Gallery.objects.get(id=gallery['id'])
@ -145,15 +145,19 @@ class DispatchGalleryMixin(object):
)
if 'images' in gallery:
for image in gallery['images']:
if isinstance(image['img'], ImageObject):
img = image['img']
else:
img = ImageObject.objects.get(id=image['img'])
if 'id' in image and image['id']:
gi = GalleryImage.objects.get(id=image['id'])
gi.gallery = g
gi.img = image['img']
gi.img = img
gi.save()
else:
gi = GalleryImage.objects.create(
gallery=g,
img=image['img'],
img=img,
)
course.gallery = g
course.save()
obj.gallery = g
obj.save()

@ -3,8 +3,10 @@ from rest_framework import serializers
from django.contrib.auth import get_user_model
from api.v1.serializers.content import GallerySerializer, GalleryImageSerializer, GalleryImageCreateSerializer
from . import Base64ImageField
from apps.user.models import AuthorRequest
from .mixins import DispatchGalleryMixin
User = get_user_model()
@ -104,3 +106,32 @@ class AuthorRequestSerializer(serializers.ModelSerializer):
'created_at',
'update_at',
)
class UserGallerySerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
'gallery',
)
class UserGalleryUpdateSerializer(DispatchGalleryMixin, serializers.ModelSerializer):
images = serializers.ListField()
class Meta:
model = User
fields = (
'images',
)
def update(self, instance, validated_data):
images = validated_data.pop('images', {})
self.dispatch_gallery(instance, {
'id': instance.gallery_id,
'images': images,
})
return instance
def to_representation(self, instance):
return UserGallerySerializer(instance=instance, context=self.context).to_representation(instance)

@ -19,7 +19,7 @@ from .views import (
SchoolScheduleViewSet, LiveLessonViewSet,
PaymentViewSet, ObjectCommentsViewSet,
ContestViewSet, ContestWorkViewSet,
AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet)
AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet, UserGalleryViewSet)
router = DefaultRouter()
router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests')
@ -45,6 +45,7 @@ router.register(r'faq', FAQViewSet, base_name='faq')
router.register(r'school-schedules', SchoolScheduleViewSet, base_name='school-schedules')
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')

@ -4,7 +4,7 @@ from decimal import Decimal
from django.contrib.auth import get_user_model
from django.db.models import Q
from rest_framework import status, views, viewsets, generics
from rest_framework import status, views, viewsets, generics, mixins
from rest_framework.decorators import detail_route, list_route, action, permission_classes, authentication_classes
from rest_framework.response import Response
@ -42,7 +42,7 @@ from .serializers.payment import (
from .serializers.user import (
AuthorRequestSerializer,
UserSerializer, UserPhotoSerializer,
)
UserGallerySerializer, UserGalleryUpdateSerializer)
from .serializers.contest import (
ContestCreateSerializer, ContestSerializer, ContestWorkSerializer, ContestWorkCreateSerializer
)
@ -475,6 +475,13 @@ class UserViewSet(ExtendedModelViewSet):
return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST)
class UserGalleryViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
queryset = User.objects.all()
serializer_class = UserGalleryUpdateSerializer
# FIXME
authentication_classes = []
class SchoolScheduleViewSet(ExtendedModelViewSet):
queryset = SchoolSchedule.objects.all()
serializer_class = SchoolScheduleSerializer

@ -10,7 +10,9 @@
{% endfor %}
</div>
<div class="timing__foot">
{# include './pay_btn.html' #}
{% if not is_purchased and not is_purchased_future %}
<a data-popup=".js-popup-buy" class="btn" href="#">Получить доступ на месяц</a>
{% endif %}
<a target="_blank" class="timing__print" href="{% url 'school:school_schedules-print' %}">Распечатать расписание
<span class="bold">чтобы не забыть</span>
<svg class="icon icon-print">

@ -1,4 +1,4 @@
{% extends "templates/lilcity/index.html" %} {% load jsonify from jsonify_queryset %} {% load static %} {% block content %}
{% extends "templates/lilcity/index.html" %}{% load static %} {% block content %}
<div class="main" style="background-image: url({% static 'img/bg-1.jpg' %});">
<div class="main__center center">
<div class="main__title">Уроки онлайн-школы LilCity</div>

@ -20,38 +20,19 @@
</div>
</div>
<div class="casing__col">
{% if not profile %}
<div class="casing__head">
{% if is_previous %}
<div class="casing__title title">Запись уроков</div>
{% else %}
<div class="casing__title title">Новые уроки</div>
{% endif %}
<label class="casing__switcher switcher">
<span class="switcher__wrap">
<a href="{% url 'school:school' %}?is_previous=true" class="switcher__item{% if is_previous %} active{% endif %}">запись уроков</a>
<a href="{% url 'school:school' %}" class="switcher__item{% if not is_previous %} active{% endif %}">новые уроки</a>
</span>
</label>
</div>
{% endif %}
<div class="casing__timing timing js-timing">
<div class="timing__week">
{% if is_previous and not live_lessons_exists %}
Записей уроков пока нет
{% else %}
{% if is_previous %}
{% for live_lesson in live_lessons %}
{% if live_lesson.school_schedule and live_lesson.title %}
{% include 'blocks/schedule_item.html' with school_schedule=live_lesson.school_schedule live_lesson=live_lesson %}
{% endif %}
{% endfor %}
{% else %}
{% for school_schedule in school_schedules_sorted %}
{% include 'blocks/schedule_item.html' with school_schedule=school_schedule live_lesson=school_schedule.current_live_lesson %}
{% endfor %}
{% endif %}
<div class="subtitle2">Новые уроки</div>
{% for school_schedule in school_schedules_sorted %}
{% include 'blocks/schedule_item.html' with school_schedule=school_schedule live_lesson=school_schedule.current_live_lesson %}
{% endfor %}
{% if prev_live_lessons_exists %}
<div class="subtitle2">Прошедшие уроки</div>
{% for live_lesson in prev_live_lessons %}
{% if live_lesson.school_schedule and live_lesson.title %}
{% include 'blocks/schedule_item.html' with school_schedule=live_lesson.school_schedule live_lesson=live_lesson %}
{% endif %}
{% endfor %}
{% endif %}
</div>
<div class="timing__week">

@ -110,9 +110,7 @@ class SchoolView(TemplateView):
def get_context_data(self):
context = super().get_context_data()
is_previous = 'is_previous' in self.request.GET
date_now = now().date()
yesterday = date_now - timedelta(days=1)
now_time = now()
try:
school_schedule = SchoolSchedule.objects.get(weekday=now_time.isoweekday())
@ -132,8 +130,8 @@ class SchoolView(TemplateView):
key=lambda ss: ss.current_live_lesson and ss.current_live_lesson.date)
except Exception:
school_schedules_sorted = school_schedules
live_lessons = []
live_lessons_exists = False
prev_live_lessons = []
prev_live_lessons_exists = False
subscription_ends = None
school_payment_exists = False
school_schedules_purchased = []
@ -178,31 +176,29 @@ class SchoolView(TemplateView):
weekdays__len__gt=0,
)
if is_previous:
# берем все подписки, которые были в периоде
for sp in prev_school_payments:
# берем все уроки в оплаченном промежутке
date_range = [max(sp.date_start, prev_range[0]), min(sp.date_end, prev_range[1])]
live_lessons += LiveLesson.objects.filter(
date__range=date_range,
deactivated_at__isnull=True,
date__week_day__in=list(map(lambda x: 1 if x == 7 else x+1, sp.weekdays)),
).values_list('id', flat=True)
live_lessons = LiveLesson.objects.filter(id__in=set(live_lessons)).order_by('-date')
live_lessons_exists = live_lessons.exists()
if live_lessons_exists:
school_schedules_dict = {ss.weekday: ss for ss in school_schedules}
school_schedules_dict[0] = school_schedules_dict.get(7)
for ll in live_lessons:
ll.school_schedule = school_schedules_dict.get(ll.date.isoweekday())
else:
live_lessons = []
# берем все подписки, которые были в периоде
for sp in prev_school_payments:
# берем все уроки в оплаченном промежутке
date_range = [max(sp.date_start, prev_range[0]), min(sp.date_end, prev_range[1])]
prev_live_lessons = LiveLesson.objects.filter(
date__range=date_range,
deactivated_at__isnull=True,
date__week_day__in=list(map(lambda x: 1 if x == 7 else x+1, sp.weekdays)),
).values_list('id', flat=True)
prev_live_lessons = LiveLesson.objects.filter(id__in=set(prev_live_lessons)).order_by('-date')
prev_live_lessons_exists = prev_live_lessons.exists()
if prev_live_lessons_exists:
school_schedules_dict = {ss.weekday: ss for ss in school_schedules}
school_schedules_dict[0] = school_schedules_dict.get(7)
for ll in prev_live_lessons:
ll.school_schedule = school_schedules_dict.get(ll.date.isoweekday())
else:
prev_live_lessons = []
context.update({
'online': online,
'live_lessons': live_lessons,
'live_lessons_exists': live_lessons_exists,
'is_previous': is_previous,
'prev_live_lessons': prev_live_lessons,
'prev_live_lessons_exists': prev_live_lessons_exists,
'course_items': Course.objects.filter(status=Course.PUBLISHED)[:6],
'is_purchased': school_payment_exists,
'is_purchased_future': False,

@ -28,6 +28,7 @@ class UserEditForm(forms.ModelForm):
pinterest = forms.URLField(required=False)
youtube = forms.URLField(required=False)
vkontakte = forms.URLField(required=False)
site = forms.URLField(required=False)
photo = forms.ImageField(required=False)
class Meta:
@ -53,6 +54,7 @@ class UserEditForm(forms.ModelForm):
'pinterest',
'youtube',
'vkontakte',
'site',
'photo',
)

@ -0,0 +1,18 @@
# Generated by Django 2.0.6 on 2018-11-15 09:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0025_merge_20180927_2353'),
]
operations = [
migrations.AddField(
model_name='user',
name='site',
field=models.URLField(blank=True, default='', null=True),
),
]

@ -0,0 +1,20 @@
# Generated by Django 2.0.6 on 2018-11-16 16:36
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('content', '0022_auto_20180815_2129'),
('user', '0026_user_site'),
]
operations = [
migrations.AddField(
model_name='user',
name='gallery',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='content.Gallery', verbose_name='Галерея'),
),
]

@ -71,6 +71,7 @@ class User(AbstractUser):
pinterest = models.URLField(default='', null=True, blank=True)
youtube = models.URLField(default='', null=True, blank=True)
vkontakte = models.URLField('ВКонтакте', default='', null=True, blank=True)
site = models.URLField(default='', null=True, blank=True)
fb_id = models.BigIntegerField(null=True, blank=True, unique=True)
fb_data = pgfields.JSONField(default={}, null=True, blank=True)
is_email_proved = models.BooleanField(
@ -83,6 +84,10 @@ class User(AbstractUser):
allow_unicode=True, null=True, blank=True,
max_length=100, unique=True, db_index=True,
)
gallery = models.ForeignKey(
'content.Gallery', on_delete=models.CASCADE,
verbose_name='Галерея', null=True, blank=True,
)
objects = UserManager()

@ -0,0 +1,35 @@
{% extends "templates/lilcity/index.html" %}
{% load static %}
{% block head %}
{{ block.super }}
<style>
.kit__photo {
width: 140px;
height: 140px;
}
</style>
{% endblock head %}
{% block pre_app_js %}
<script>
window.LIL_STORE.data.user_gallery_images = [{% for img in user.gallery.gallery_images.reverse %}{
id: {{ img.id }},
image_thumbnail_url: '{{ img.img.image_thumbnail.url }}',
image_id: {{ img.img.id }},
},{% endfor %}];
</script>
<script src="{% static 'userGalleryEdit.js' %}"></script>
{% endblock pre_app_js %}
{% block content %}
<div class="section section_gray">
<div class="section__center center center_sm">
<div class="title">Редактировать работы</div>
<div>
<block-images :images="$root.store.data.user_gallery_images" :access-token="$root.store.accessToken" no-title="1"/>
</div>
<a href="#" id="gallery-save" class="btn">Сохранить</a>
</div>
</div>
{% endblock %}

@ -255,7 +255,16 @@
{% for error in form.vkontakte.errors %}
<div class="field__error">{{ error }}</div>
{% endfor %}
</div>
</div>
<div class="form__field field{% if form.site.errors %} error{% endif %}">
<div class="field__label">САЙТ</div>
<div class="field__wrap">
<input name='site' class="field__input" type="text" placeholder="https://site.com" value="{% if user.site %}{{ user.site }}{% endif %}">
</div>
{% for error in form.site.errors %}
<div class="field__error">{{ error }}</div>
{% endfor %}
</div>
</div>
<div class="form__foot">
<button type="submit" class="form__btn btn btn_md">СОХРАНИТЬ</button>

@ -67,12 +67,13 @@
<div class="section__center center">
<div class="tabs js-tabs">
<div class="tabs__nav">
<button class="tabs__btn js-tabs-btn active">МОИ ПОКУПКИ</button>
<a href="#purchases" class="tabs__btn js-tabs-btn active">МОИ ПОКУПКИ</a>
{% if is_author %}
<button class="tabs__btn js-tabs-btn">ОПУБЛИКОВАННЫЕ
<a href="#courses" class="tabs__btn js-tabs-btn">ОПУБЛИКОВАННЫЕ
<span class="mobile-hide">КУРСЫ</span>
</button>
</a>
{% endif %}
<a href="#works" class="tabs__btn js-tabs-btn">МОИ РАБОТЫ</a>
</div>
<div class="tabs__container">
<div class="tabs__item js-tabs-item" style="display: block;">
@ -138,6 +139,24 @@
</div>
</div>
{% endif %}
<div class="tabs__item js-tabs-item">
<div class="center center_sm">
<div style="text-align: right; margin-bottom: 15px;">
<a class="btn btn_md btn_stroke" href="{% url 'user-gallery-edit' %}">Редактировать</a>
</div>
<div class="examples gallery">
{% for image in user.gallery.gallery_images.reverse %}
<div class="examples__item">
<a href="{{ image.img.image.url }}">
{% thumbnail image.img.image "165x165" crop="center" as im %}
<img class="examples__pic" src="{{ im.url }}">
{% endthumbnail %}
</a>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>

@ -313,3 +313,8 @@ class BonusHistoryView(TemplateView):
'Перейдите по ссылке и получите скидку %d%% на первую покупку' \
% (request.user.get_full_name(), config.REFERRAL_BONUS)
return self.render_to_response(context)
@method_decorator(login_required, name='dispatch')
class UserGalleryEditView(TemplateView):
template_name = 'user/edit-gallery.html'

@ -22,6 +22,7 @@
urls: {
courses: "{% url 'courses' %}",
userProfileEdit: "{% url 'user-edit-profile' %}",
userProfile: "{% url 'user-profile' %}",
userBonuses: "{% url 'user-bonuses' %}",
faq: "{% url 'faq' %}",
},

@ -165,6 +165,6 @@
(function(w, d, s, h, id) { w.roistatProjectId = id; w.roistatHost = h; var p = d.location.protocol == "https:" ? "https://" : "http://"; var u = /^.*roistat_visit=[^;]+(.*)?$/.test(d.cookie) ? "/dist/module.js" : "/api/site/1.0/"+id+"/init"; var js = d.createElement(s); js.charset="UTF-8"; js.async = 1; js.src = p+h+u; var js2 = d.getElementsByTagName(s)[0]; js2.parentNode.insertBefore(js, js2);})(window, document, 'script', 'cloud.roistat.com', '{% setting "ROISTAT_COUNTER_ID" %}');
</script>
{% block foot %}{% endblock foot %}
{% block body_js %}{% endblock body_js %}
{% block foot_js %}{% endblock foot_js %}
</body>
</html>

@ -30,7 +30,7 @@ from apps.user.views import (
ProfileEditView, NotificationEditView,
PaymentHistoryView, resend_email_verify,
SubscribeView, ProfileView,
BonusHistoryView)
BonusHistoryView, UserGalleryEditView)
from apps.payment.views import (
CourseBuySuccessView, CourseBuyView,
PaymentwallCallbackView, SchoolBuySuccessView,
@ -75,6 +75,7 @@ urlpatterns = [
path('user/notifications', NotificationEditView.as_view(), name='user-edit-notifications'),
path('user/payments', PaymentHistoryView.as_view(), name='user-edit-payments'),
path('user/bonuses', BonusHistoryView.as_view(), name='user-bonuses'),
path('user/gallery-edit', UserGalleryEditView.as_view(), name='user-gallery-edit'),
path('user/resend-email-verify', resend_email_verify, name='resend-email-verify'),
path('subscribe', SubscribeView.as_view(), name='subscribe'),
path('subscribe/success', TemplateView.as_view(template_name='templates/lilcity/subscribe_success.html'), name='subscribe-success'),

@ -3,63 +3,38 @@
<div v-if="!courseLoading && !mounting">
<form v-if="viewSection !== 'lessons-edit'" @submit.prevent="onSubmit">
<div class="info">
<div class="info__section" :style="coverBackgroundStyle">
<div class="info__main">
<div class="info__head">
<div class="info__user">
<div class="info__ava ava">
<img :src="authorPicture" alt="Аватар" class="ava__pic">
</div>
<div v-if="me" class="info__group info__field--light">
<div class="info__label">АВТОР</div>
<div class="info__value">{{ authorName }}</div>
</div>
</div>
<div class="info__upload upload">
Загрузить фон
<div class="info__section">
<div class="courses__item">
<div class="courses__preview">
<img class="courses__pic" :src="course.coverImage || defaultCover" width="300px" />
<div class="upload" v-if="! course.coverImage">
<div class="upload__title">Загрузить превью</div>
<input type="file" class="upload__file" @change="onCoverImageSelected">
</div>
<a href="#" title="Удалить превью" class="course-delete-cover" v-if="course.coverImage" @click="removeCover">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</a>
</div>
<div class="info__title">
<div class="info__field field field_info"
v-bind:class="{ error: ($v.course.title.$dirty || showErrors) && $v.course.title.$invalid }">
<div class="field__label">{{titles.courseTitle}}</div>
<div class="field__wrap">
<textarea class="field__textarea"
rows="1"
:title="titles.courseTitle"
v-autosize="course.title"
@change="onCourseNameInput"
v-model="course.title"></textarea>
</div>
</div>
<div class="info__field field field_info field_short_description"
v-bind:class="{ error: ($v.course.short_description.$dirty || showErrors) && $v.course.short_description.$invalid }">
<div class="field__label">{{titles.shortDescription}}</div>
<div class="field__wrap">
<vue-redactor :value="course.short_description"
v-on:update:value="(value) => { this.course.short_description = value; }" />
</div>
</div>
</div>
<div class="info__foot" v-if="!live">
<div class="info__field field field_info info__field--light"
v-bind:class="{ error: ($v.course.category.$dirty || showErrors) && $v.course.category.$invalid }">
<div class="field__label field__label_gray">КАТЕГОРИЯ</div>
<div class="field__wrap">
<lil-select :value.sync="course.category" :options="categoryOptions"
<div class="courses__details">
<div class="courses__theme theme field info__field--light" v-if="!live" v-bind:class="{ error: (!$v.live && $v.course.category.$dirty || showErrors) && $v.course.category.$invalid }">
<lil-select :value.sync="course.category" :options="categoryOptions"
placeholder="Выберите категорию"/>
</div>
</div>
<div class="info__field field field_info"
v-bind:class="{ error: ($v.course.duration.$dirty || showErrors) && $v.course.duration.$invalid }">
<div class="field__label field__label_gray">ПРОДОЛЖИТЕЛЬНОСТЬ</div>
<div class="field__wrap field__wrap__appended">
<input type="text" class="field__input field__input__appended" v-model.number="course.duration"
@input="$v.course.duration.$touch()">
<button disabled class="field__append">{{pluralize(course.duration, ['день', 'дня', 'дней'])}}</button>
</div>
</div>
<div class="courses__price" v-if="course.is_paid && course.price">{{ course.price }}</div>
</div>
<div class="courses__title field field" v-bind:class="{ error: ($v.course.title.$dirty || showErrors) && $v.course.title.$invalid }">
<textarea class="field__textarea"
rows="1"
:title="titles.courseTitle"
v-autosize="course.title"
@change="onCourseNameInput"
v-model="course.title"
placeholder="Добавить заголовок"></textarea>
</div>
<div class="courses__content field" v-bind:class="{ error: ($v.course.short_description.$dirty || showErrors) && $v.course.short_description.$invalid }">
<vue-redactor :value.sync="course.short_description" placeholder="Добавить краткое описание"/>
</div>
</div>
</div>
@ -90,6 +65,16 @@
</div>
</div> -->
<div class="info__field field field_info" v-if="!live"
v-bind:class="{ error: ($v.course.duration.$dirty || showErrors) && $v.course.duration.$invalid }">
<div class="field__label field__label_gray">ПРОДОЛЖИТЕЛЬНОСТЬ</div>
<div class="field__wrap field__wrap__appended">
<input type="text" class="field__input field__input__appended" v-model.number="course.duration"
@input="$v.course.duration.$touch()">
<button disabled class="field__append">{{pluralize(course.duration, ['день', 'дня', 'дней'])}}</button>
</div>
</div>
<div v-if="!live" class="info__field field">
<div class="field__label field__label_gray">ДОСТУП</div>
<div class="field__wrap">
@ -155,7 +140,7 @@
</div>
<div class="section">
<div class="section__center center">
<div class="kit">
<div class="kit" style="margin: 0 auto;">
<div v-if="!live" id="course-redactor__nav" class="kit__nav">
<button class="kit__btn btn btn_lg"
v-bind:class="{ 'btn_stroke': viewSection === 'course', 'btn_gray': viewSection !== 'course' }"
@ -267,6 +252,7 @@
users: null,
ROLE_ADMIN: ROLE_ADMIN,
slugChanged: false,
defaultCover: '/img/no_cover.png',
course: {
title: '',
status: null,
@ -444,6 +430,16 @@
}
},
methods: {
removeCover() {
if(! this.course.coverImageId){
return;
}
api.removeImage(this.course.coverImageId, this.accessToken)
.then(response => {
this.course.coverImageId = null;
this.course.coverImage = null;
});
},
onCoverImageSelected(event) {
let file = event.target.files[0];
let reader = new FileReader();
@ -1005,9 +1001,6 @@
// this.updateViewSection(window.location);
},
computed: {
coverBackgroundStyle() {
return this.course.coverImage ? `background-image: url(${this.course.coverImage});` : '';
},
displayPrice: {
get: function () {
return this.course.is_paid ? (this.course.price || '') : '';
@ -1175,10 +1168,67 @@
overflow: scroll;
}
.field_short_description .redactor-box {
.courses__item {
flex: 0 0 300px;
}
.courses__item .field {
margin-bottom: 0;
}
.courses__content .redactor-box {
overflow-x: visible;
overflow-y: auto;
max-height: 200px;
background: none;
margin-top: 10px;
}
.courses__content .redactor-layer{
background: none;
}
.courses__theme {
flex: 1;
}
.courses__price {
margin-left: 20px;
}
.courses__preview {
.upload {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.upload__title {
color: #888888;
font-size: 16px;
margin-top: 100px;
width: 100%;
text-align: center;
}
.upload__file {
width: 100%;
height: 100%;
}
}
.course-delete-cover {
left: 5px;
position: absolute;
bottom: 3px;
}
.select__head {
font-size: inherit;
line-height: inherit;
height: auto;
}
</style>

@ -1,7 +1,29 @@
<template>
<div>
<vue-draggable :list="content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }">
<div v-for="(block, index) in content" :key="block.id ? block.id : block.uuid">
<div v-for="(block, index) in content" :key="block.id ? block.id : block.uuid" class="kit__section kit__section--block">
<div class="kit__section-remove">
<button if="index != 0" type="button" @click="moveUp(index)">
<svg class="icon icon-arrow-up">
<use xlink:href="/static/img/sprite.svg#icon-arrow-down"></use>
</svg>
</button>
<button if="index < (content.length - 1)" type="button" @click="moveDown(index)">
<svg class="icon icon-arrow-down">
<use xlink:href="/static/img/sprite.svg#icon-arrow-down"></use>
</svg>
</button>
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<button type="button" @click="onBlockRemoved(index)">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
</div>
<block-text v-if="block.type === 'text'"
:index="index"
:title.sync="block.data.title"
@ -55,6 +77,22 @@
name: 'block-content',
props: ['content'],
methods: {
moveUp(blockIndex) {
if(blockIndex <= 0){
return;
}
const block = this.content[blockIndex];
const prevBlock = this.content[blockIndex - 1];
this.content.splice(blockIndex - 1, 2, block, prevBlock);
},
moveDown(blockIndex) {
if(blockIndex >= this.content.length - 1){
return;
}
const block = this.content[blockIndex];
const nextBlock = this.content[blockIndex + 1];
this.content.splice(blockIndex, 2, nextBlock, block);
},
onBlockRemoved(blockIndex) {
const remove = () => {
// Удаляем блок из Vue

@ -1,17 +1,5 @@
<template>
<div class="kit__section kit__section--block">
<div class="kit__section-remove">
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<button type="button" @click="onRemove">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
</div>
<div>
<div class="kit__field field">
<div class="field__wrap field__wrap--title">
<input type="text"
@ -44,9 +32,6 @@
onUpdateImageId(newValue) {
this.$emit('update:imageId', newValue);
},
onRemove() {
this.$emit('remove', this.index);
}
},
components: {
'lil-image': LilImage,
@ -56,4 +41,4 @@
<style scoped>
</style>
</style>

@ -1,17 +1,5 @@
<template>
<div class="kit__section kit__section--block">
<div class="kit__section-remove">
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<button type="button" @click="onRemove">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
</div>
<div>
<div class="kit__row">
<lil-image :image-id="imageId" :image-url="imageUrl" v-on:update:imageUrl="onUpdateImageUrl"
v-on:update:imageId="onUpdateImageId" :access-token="accessToken"/>
@ -55,9 +43,6 @@
onUpdateImageId(newValue) {
this.$emit('update:imageId', newValue);
},
onRemove() {
this.$emit('remove', this.index);
}
},
components: {
'lil-image': LilImage,

@ -1,18 +1,6 @@
<template>
<div class="kit__section kit__section--block">
<div v-if="!readOnly" class="kit__section-remove">
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<button type="button" @click="onRemove">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
</div>
<div class="kit__field field">
<div>
<div v-if="! noTitle" class="kit__field field">
<div class="field__wrap field__wrap--title">
<input :readonly="readOnly" type="text"
:value="title"
@ -24,7 +12,7 @@
<div class="kit__gallery">
<div class="kit__preview" v-for="(image, index) in images" v-bind:class="{ 'kit__preview--loading': image.loading }">
<img :src="image.image_thumbnail_url" class="kit__pic">
<button type="button" @click="onRemoveImage(index)">
<button class="kit__delete-photo" type="button" @click="onRemoveImage(index)">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
@ -45,36 +33,31 @@
export default {
name: "block-images",
props: ["index", "title", "images", "accessToken", "readOnly"],
props: ["index", "title", "images", "accessToken", "readOnly", "longSide", "noTitle"],
methods: {
onTitleChange(event) {
this.$emit('update:title', event.target.value);
},
onImageAdded(event) {
const longSide = +this.longSide || 1600;
Array.from(event.target.files).forEach((file) => {
let reader = new FileReader();
reader.onload = () => {
let images = this.images;
console.log('images before before', JSON.stringify(images));
images.push({
src: reader.result,
loading: true,
});
this.$emit('update:images', images);
const index = images.length - 1;
api.uploadImage(reader.result, this.accessToken)
.then((response) => {
let images = this.images;
images.forEach((image, index) => {
if (image.src === reader.result) {
images[index].image_id = response.data.id;
images[index].loading = false;
images[index].image_url = response.data.image;
images[index].image_thumbnail_url = response.data.image_thumbnail;
}
});
console.log('images after', JSON.stringify(images));
images[index].image_id = response.data.id;
images[index].loading = false;
images[index].image_url = response.data.image;
images[index].image_thumbnail_url = response.data.image_thumbnail;
this.$emit('update:images', images);
})
.catch((error) => {
@ -86,9 +69,6 @@
}
});
},
onRemove() {
this.$emit('remove', this.index);
},
onRemoveImage(index) {
let images = this.images;
let id = images[index].image_id;

@ -1,17 +1,5 @@
<template>
<div class="kit__section kit__section--block">
<div class="kit__section-remove">
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<button type="button" @click="onRemove">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
</div>
<div>
<div class="kit__field field">
<div class="field__wrap field__wrap--title">
<input type="text"
@ -42,9 +30,6 @@
onTextChange(newValue) {
this.$emit('update:text', newValue);
},
onRemove() {
this.$emit('remove', this.index);
}
},
components: {
'vue-redactor': VueRedactor,

@ -1,17 +1,5 @@
<template>
<div class="kit__section kit__section--block">
<div class="kit__section-remove">
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<button type="button" @click="onRemove">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
</div>
<div>
<div class="kit__field field">
<div class="field__wrap field__wrap--title">
<input type="text"
@ -46,13 +34,10 @@
onVideoUrlChange(event) {
this.$emit('update:videoUrl', event.target.value);
},
onRemove() {
this.$emit('remove', this.index);
}
}
}
</script>
<style scoped>
</style>
</style>

@ -20,6 +20,7 @@ import "./modules/mixpanel";
import "../sass/app.sass";
import $ from 'jquery';
import Vue from 'vue';
import Vuelidate from 'vuelidate';
import VueAutosize from '../components/directives/autosize'
@ -63,7 +64,6 @@ const app = new Vue({
data() {
return {
store: window.LIL_STORE,
}
},
} },
components: components
});

@ -199,6 +199,14 @@ export const api = {
gallery: {images: (courseJSON.gallery) ? courseJSON.gallery.gallery_images:[]},
}
},
convertGalleryImagesJson: (images) => {
return images.map((galleryImage) => {
return {
'id': galleryImage.id ? galleryImage.id : null,
'img': galleryImage.image_id,
}
});
},
convertContentJson: (contentJson, forSaving) => {
if(forSaving){
return contentJson.map((block, index) => {
@ -244,12 +252,7 @@ export const api = {
'uuid': block.uuid,
'position': ++index,
'title': block.data.title,
'images': block.data.images.map((galleryImage) => {
return {
'id': galleryImage.id ? galleryImage.id : null,
'img': galleryImage.image_id,
}
}),
'images': api.convertGalleryImagesJson(block.data.images),
}
}
} else if (block.type === 'video') {
@ -486,5 +489,14 @@ export const api = {
'Authorization': `Token ${window.LIL_STORE.accessToken}`,
}
});
}
},
saveUserGallery: (images) => {
return api.patch(`/api/v1/user-gallery/${window.LIL_STORE.user.id}/`, {
images: api.convertGalleryImagesJson(images),
}, {
headers: {
'Authorization': `Token ${window.LIL_STORE.accessToken}`,
}
});
},
};

@ -0,0 +1,14 @@
import BlockImages from '../components/blocks/BlockImages.vue';
import $ from 'jquery';
import {api} from "./modules/api";
window.LIL_STORE.components['block-images'] = BlockImages;
$(document).ready(function () {
$('#gallery-save').click(e => {
e.preventDefault();
api.saveUserGallery(window.LIL_STORE._user_gallery_images).then(response => {
window.location.href = '/user/profile/#works';
});
});
});

@ -2162,11 +2162,13 @@ a.grey-link
color: $pink
border-bottom: 1px $pink solid
.error.field .redactor-box
border-bottom: 1px $pink solid
.info__field--light
.select
.select__head
color: #525252
font-size: 15px
&:after
border-color: #525252 transparent transparent transparent
&.selected
@ -3266,6 +3268,7 @@ a.grey-link
justify-content: center
&__btn
height: 56px
line-height: 56px
border-bottom: 1px solid $border
+fb
font-size: 12px
@ -3476,22 +3479,10 @@ a.grey-link
&__section
display: flex
position: relative
background-position: 50% 50%
background-size: cover
flex: 0 0 calc(50% + 169px)
flex: 0 0 50%
justify-content: flex-end
z-index: 4
&:before
content: ''
position: absolute
top: 0
left: 0
width: 100%
height: 100%
background-image: linear-gradient(135deg, rgba(255, 226, 235, 0.75) 0%, rgba(216, 245, 246, 0.75) 100%)
z-index: -2
&__sidebar
flex: 0 0 calc(50% - 169px)
flex: 0 0 50%
&__main
display: flex
max-width: 675px
@ -3616,19 +3607,23 @@ a.grey-link
&__section
margin-bottom: 60px
&--block
box-shadow: 0 10px 50px 0 rgba(0, 0, 0, 0.06)
border-radius: 10px
/* box-shadow: 0 10px 50px 0 rgba(0, 0, 0, 0.06) */
/* border-radius: 10px */
padding: 15px
position: relative
&-remove
position: absolute
right: 15px
top: 20px
.icon-delete
button:not(:last-child)
margin-right: 10px
.icon-delete, .icon-arrow-up, .icon-arrow-down
width: 1.3em
height: 1.3em
transition: fill 0.3s ease
&:hover
.icon-arrow-up, .icon-arrow-down
fill: #C8C8C8
.icon-delete:hover, .icon-arrow-up:hover, .icon-arrow-down:hover
fill: #000
&__nav
display: flex
@ -3659,6 +3654,10 @@ a.grey-link
flex: 0 0 140px
+m
flex: 0 0 110px
&__delete-photo
position: absolute
bottom: 2px
left: 3px
&__photo
display: flex
position: relative
@ -3704,6 +3703,7 @@ a.grey-link
flex-wrap: wrap
&__gallery &__photo,
&__preview
position: relative
margin: 0 10px 20px
flex: 0 0 calc(25% - 20px)
&--loading
@ -3712,6 +3712,9 @@ a.grey-link
display: block
width: 100%
object-fit: contain
transform: translateY(-50%)
top: 50%
position: relative
&__theme
margin-bottom: 30px
padding-bottom: 5px
@ -3721,6 +3724,8 @@ a.grey-link
text-align: center
&__submit
width: 220px
&__field .field__wrap--title
margin-right: 125px;
.editor
position: relative
@ -4062,6 +4067,11 @@ a.grey-link
letter-spacing: 1px
+m
margin-right: 0
&__item a.timing__btn
opacity: 0;
transition: 0.5s ease;
&__item:hover a.timing__btn
opacity: 1;
&__pic
display: block
width: 100%

@ -8,6 +8,11 @@
height: 1em;
fill: #333;
}
.icon-arrow-up {
width: 1.71em;
height: 1em;
fill: #333;
}
.icon-arrow-left {
width: 1.33em;
height: 1em;

@ -10,6 +10,7 @@ module.exports = {
app: "./src/js/app.js",
courseRedactor: "./src/js/course-redactor.js",
contestRedactor: "./src/js/contest-redactor.js",
userGalleryEdit: "./src/js/user-gallery-edit.js",
mixpanel: "./src/js/third_party/mixpanel-2-latest.js",
sprite: glob('./src/icons/*.svg'),
images: glob('./src/img/*.*'),

Loading…
Cancel
Save