Merge branch 'master' of gitlab.com:lilcity/backend into hotfix/LIL-706

remotes/origin/editis_13-01-19
gzbender 7 years ago
commit fb7ffdd873
  1. 8
      api/v1/serializers/content.py
  2. 4
      api/v1/urls.py
  3. 9
      api/v1/views.py
  4. 21
      apps/content/migrations/0023_faq.py
  5. 5
      apps/content/models.py
  6. 18
      apps/content/templates/content/faq.html
  7. 16
      apps/content/views.py
  8. 3
      apps/notification/tasks.py
  9. 2
      project/settings.py
  10. 1
      project/templates/blocks/lil_store_js.html
  11. 3
      project/urls.py
  12. 50
      web/src/components/FAQ.vue
  13. 8
      web/src/components/blocks/BlockImageText.vue
  14. 56
      web/src/js/app.js
  15. 4
      web/src/js/modules/profile.js

@ -4,7 +4,7 @@ from django.conf import settings
from apps.content.models import ( from apps.content.models import (
Baner, Content, Image, Text, ImageText, Video, Baner, Content, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject,) Gallery, GalleryImage, ImageObject, FAQ)
from . import Base64ImageField from . import Base64ImageField
@ -256,3 +256,9 @@ class ContentSerializer(serializers.ModelSerializer):
return GallerySerializer(obj, context=self.context).to_representation(obj) return GallerySerializer(obj, context=self.context).to_representation(obj)
return super(ContentSerializer, self).to_representation(obj) return super(ContentSerializer, self).to_representation(obj)
class FAQSerializer(serializers.ModelSerializer):
class Meta:
model = FAQ
fields = '__all__'

@ -19,7 +19,7 @@ from .views import (
SchoolScheduleViewSet, LiveLessonViewSet, SchoolScheduleViewSet, LiveLessonViewSet,
PaymentViewSet, ObjectCommentsViewSet, PaymentViewSet, ObjectCommentsViewSet,
ContestViewSet, ContestWorkViewSet, ContestViewSet, ContestWorkViewSet,
AuthorBalanceUsersViewSet, CaptureEmail, UserGalleryViewSet) AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet, UserGalleryViewSet)
router = DefaultRouter() router = DefaultRouter()
router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests') router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests')
@ -41,7 +41,7 @@ router.register(r'image-texts', ImageTextViewSet, base_name='image-texts')
router.register(r'videos', VideoViewSet, base_name='videos') router.register(r'videos', VideoViewSet, base_name='videos')
router.register(r'galleries', GalleryViewSet, base_name='galleries') router.register(r'galleries', GalleryViewSet, base_name='galleries')
router.register(r'gallery-images', GalleryImageViewSet, base_name='gallery-images') router.register(r'gallery-images', GalleryImageViewSet, base_name='gallery-images')
router.register(r'faq', FAQViewSet, base_name='faq')
router.register(r'school-schedules', SchoolScheduleViewSet, base_name='school-schedules') router.register(r'school-schedules', SchoolScheduleViewSet, base_name='school-schedules')
router.register(r'users', UserViewSet, base_name='users') router.register(r'users', UserViewSet, base_name='users')

@ -28,7 +28,7 @@ from .serializers.content import (
VideoSerializer, VideoCreateSerializer, VideoSerializer, VideoCreateSerializer,
GallerySerializer, GallerySerializer,
GalleryImageSerializer, GalleryImageCreateSerializer, GalleryImageSerializer, GalleryImageCreateSerializer,
ImageObjectSerializer, ImageObjectSerializer, FAQSerializer,
) )
from .serializers.school import ( from .serializers.school import (
SchoolScheduleSerializer, SchoolScheduleSerializer,
@ -63,7 +63,7 @@ from apps.config.models import Config
from apps.content.models import ( from apps.content.models import (
Baner, Image, Text, ImageText, Video, Baner, Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject, Gallery, GalleryImage, ImageObject,
Contest, ContestWork) Contest, ContestWork, FAQ)
from apps.payment.models import ( from apps.payment.models import (
AuthorBalance, Payment, AuthorBalance, Payment,
CoursePayment, SchoolPayment, CoursePayment, SchoolPayment,
@ -719,3 +719,8 @@ class CaptureEmail(views.APIView):
return Response({'status': 'ok'}) return Response({'status': 'ok'})
class FAQViewSet(ExtendedModelViewSet):
queryset = FAQ.objects.all()
serializer_class = FAQSerializer

@ -0,0 +1,21 @@
# Generated by Django 2.0.7 on 2018-12-04 11:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content', '0022_auto_20180815_2129'),
]
operations = [
migrations.CreateModel(
name='FAQ',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question', models.TextField(max_length=1000)),
('answer', models.TextField(max_length=1000)),
],
),
]

@ -210,3 +210,8 @@ class ContestWork(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy('contest_work', args=[self.id]) return reverse_lazy('contest_work', args=[self.id])
class FAQ(models.Model):
question = models.TextField(max_length=1000,)
answer = models.TextField(max_length=1000,)

@ -0,0 +1,18 @@
{% extends "templates/lilcity/index.html" %}
{% load static %}
{% load jsonify_queryset %}
{% block content %}
<div class="section">
<div class="section__center center center_sm">
<div class="title">Часто задаваемые вопросы</div>
<faq :faqs="$root.store.data.faqs"></faq>
</div>
</div>
{% endblock content %}
{% block pre_app_js %}
<script>
window.LIL_STORE.data.faqs = {{ faqs|safe }};
</script>
{% endblock pre_app_js %}

@ -7,7 +7,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView, DetailView from django.views.generic import TemplateView, DetailView
from apps.content.models import Contest, ContestWork from apps.content.models import Contest, ContestWork, FAQ
from apps.course.models import ContestWorkComment from apps.course.models import ContestWorkComment
@ -110,3 +110,17 @@ def contest_work_comment(request, contest_work_id):
'success': True, 'success': True,
'comment': html, 'comment': html,
}) })
class FAQView(TemplateView):
template_name = 'content/faq.html'
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
context['faqs'] = [{
'question': f.question,
'answer': f.answer,
'opened': 0,
} for f in FAQ.objects.all()]
return self.render_to_response(context)

@ -84,6 +84,8 @@ def sendgrid_update_recipients():
for user in users: for user in users:
last_course_purchase = course_payments.get(user.id) and course_payments.get(user.id).strftime(date_format) last_course_purchase = course_payments.get(user.id) and course_payments.get(user.id).strftime(date_format)
last_school_purchase = school_payments.get(user.id) and school_payments.get(user.id).strftime(date_format) last_school_purchase = school_payments.get(user.id) and school_payments.get(user.id).strftime(date_format)
courses_purchased = CoursePayment.objects.filter(user=user,
status__in=CoursePayment.PW_PAID_STATUSES).values_list('course_id', flat=True)
data.append({ data.append({
'first_name': user.first_name, 'first_name': user.first_name,
'last_name': user.last_name, 'last_name': user.last_name,
@ -94,6 +96,7 @@ def sendgrid_update_recipients():
# 'gender': {User.NOT_DEFINED: '', User.MALE: 'Мужчина', User.FEMALE: 'Женщина'}[user.gender] if user.gender else '', # 'gender': {User.NOT_DEFINED: '', User.MALE: 'Мужчина', User.FEMALE: 'Женщина'}[user.gender] if user.gender else '',
'birthday': user.birthday and user.birthday.strftime(date_format), 'birthday': user.birthday and user.birthday.strftime(date_format),
'date_joined': user.date_joined.strftime(date_format), 'date_joined': user.date_joined.strftime(date_format),
'courses_purchased': ','.join(map(str, courses_purchased)) + ',' if courses_purchased else '',
}) })
sg = get_sendgrid_client() sg = get_sendgrid_client()

@ -252,7 +252,7 @@ CELERY_BEAT_SCHEDULE = {
}, },
'send_certificates': { 'send_certificates': {
'task': 'apps.notification.tasks.send_certificates', 'task': 'apps.notification.tasks.send_certificates',
'schedule': crontab(hour=19), 'schedule': crontab(minute=0, hour=19),
'args': (), 'args': (),
}, },
'sendgrid_update_recipients': { 'sendgrid_update_recipients': {

@ -24,6 +24,7 @@
userProfileEdit: "{% url 'user-edit-profile' %}", userProfileEdit: "{% url 'user-edit-profile' %}",
userProfile: "{% url 'user-profile' %}", userProfile: "{% url 'user-profile' %}",
userBonuses: "{% url 'user-bonuses' %}", userBonuses: "{% url 'user-bonuses' %}",
faq: "{% url 'faq' %}",
}, },
flags: { flags: {
referrer: '{{ referrer.id|default:'' }}', referrer: '{{ referrer.id|default:'' }}',

@ -18,7 +18,7 @@ from django.contrib import admin
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.urls import path, include from django.urls import path, include
from apps.content.views import ContestEditView, ContestView, ContestWorkView, contest_work_comment from apps.content.views import ContestEditView, ContestView, ContestWorkView, contest_work_comment, FAQView
from apps.course.views import ( from apps.course.views import (
CoursesView, likes, coursecomment, CoursesView, likes, coursecomment,
CourseView, LessonView, SearchView, CourseView, LessonView, SearchView,
@ -97,6 +97,7 @@ urlpatterns = [
path('payments/gift-certificate/<int:payment_id>/success', GiftCertificateBuySuccessView.as_view(), path('payments/gift-certificate/<int:payment_id>/success', GiftCertificateBuySuccessView.as_view(),
name='gift-certificate-payment-success'), name='gift-certificate-payment-success'),
path('gift-certificate/<str:slug>/get', GiftCertificateGetView.as_view(), name='gift-certificate-get'), path('gift-certificate/<str:slug>/get', GiftCertificateGetView.as_view(), name='gift-certificate-get'),
path('faq', FAQView.as_view(), name='faq'),
] ]

@ -0,0 +1,50 @@
<template>
<div class="faq">
<div class="faq__item" v-for="faq in faqs" :class="{'faq__item_opened': faq.opened}">
<div class="faq__item-head" @click="open(faq)">
<div class="faq__item-question">{{ faq.question }}</div>
<div class="faq__item-opener">
<svg class="icon" :class="{'icon-arrow-up': faq.opened, 'icon-arrow-down': !faq.opened}">
<use xlink:href="/static/img/sprite.svg#icon-arrow-down"></use>
</svg>
</div>
</div>
<div class="faq__item-answer" v-show="faq.opened" style="display: none;">{{ faq.answer }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'faq',
props: ['faqs'],
methods: {
open(faq){
faq.opened = ! faq.opened;
for(let f of this.faqs){
if(f !== faq){
f.opened = false;
}
}
},
},
}
</script>
<style lang="sass">
.faq
&__item-head
display: flex
background: #f8f8f8
padding: 7px 10px 5px
cursor: pointer
&__item-question
flex: 1
&__item-opener
margin-left: 10px
& .icon
width: 16px
height: 16px
&__item-answer
padding: 10px 10px
</style>

@ -15,11 +15,7 @@
</div> </div>
<div class="kit__field field"> <div class="kit__field field">
<div class="field__wrap"> <div class="field__wrap">
<textarea class="field__textarea field__textarea_sm" <vue-redactor :value="text" v-on:update:value="onTextChange" placeholder="Описание"/>
placeholder="Описание"
:value="text"
v-autosize="text"
@change="onTextChange"></textarea>
</div> </div>
</div> </div>
</div> </div>
@ -29,6 +25,7 @@
<script> <script>
import LilImage from "./Image"; import LilImage from "./Image";
import VueRedactor from '../redactor/VueRedactor';
export default { export default {
name: "block-image-text", name: "block-image-text",
@ -49,6 +46,7 @@
}, },
components: { components: {
'lil-image': LilImage, 'lil-image': LilImage,
'vue-redactor': VueRedactor,
}, },
} }
</script> </script>

@ -15,7 +15,6 @@ import "./modules/courses";
import "./modules/comments"; import "./modules/comments";
import "./modules/comments"; import "./modules/comments";
import "./modules/password-show"; import "./modules/password-show";
import {main as profileMain} from "./modules/profile";
import "./modules/notification"; import "./modules/notification";
import "./modules/mixpanel"; import "./modules/mixpanel";
@ -34,42 +33,37 @@ Vue.use(Vuelidate);
Vue.use(VueAutosize); Vue.use(VueAutosize);
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
// Enable vue-devtools // Enable vue-devtools
Vue.config.devtools = true; Vue.config.devtools = true;
} }
window.urlIs = (urlPatternName) => {
return window.location.pathname.search(window.LIL_STORE.urls[urlPatternName]) > -1;
};
const components = { const components = {
UploadContestWork, UploadContestWork,
ContestWorks, ContestWorks,
Likes, Likes,
Comments, Comments,
}; };
Object.assign(components, window.LIL_STORE.components); Object.assign(components, window.LIL_STORE.components);
if(urlIs('faq')){
const FAQ = require('../components/FAQ.vue');
components['faq'] = FAQ.default;
}
if(urlIs('userProfileEdit') || urlIs('userBonuses')){
const profile = require("./modules/profile");
profile.main();
}
const app = new Vue({ const app = new Vue({
el: '#lilcity-vue-app', el: '#lilcity-vue-app',
data() { data() {
return { return {
store: window.LIL_STORE, store: window.LIL_STORE,
} } },
}, components: components
mounted(){
if(this.urlIs('userProfileEdit') || this.urlIs('userBonuses')){
profileMain(this);
}
if(this.urlIs('userProfile')){
if(window.location.hash){
$(document).ready(() => {
$(`[href="${window.location.hash}"]`).click();
});
}
}
},
methods: {
urlIs(urlPatternName){
return window.location.pathname.search(this.store.urls[urlPatternName]) > -1;
},
},
components: components
}); });

@ -3,8 +3,8 @@ import slugify from 'slugify';
import ClipboardJS from 'clipboard'; import ClipboardJS from 'clipboard';
import {showNotification} from './notification'; import {showNotification} from './notification';
export const main = (app) => { export const main = () => {
if(app.urlIs('userBonuses')){ if(urlIs('userBonuses')){
$('#referrer-url').select().click(function(){ $('#referrer-url').select().click(function(){
$(this).select(); $(this).select();
}); });

Loading…
Cancel
Save