Merge remote-tracking branch 'origin/dev' into dev

remotes/origin/hasaccess
Vitaly Baev 8 years ago
commit c1e84a4ce5
  1. 21
      .editorconfig
  2. 2
      .gitignore
  3. 5
      api/v1/serializers/course.py
  4. 22
      api/v1/serializers/school.py
  5. 30
      api/v1/urls.py
  6. 47
      api/v1/views.py
  7. 19
      apps/content/tasks.py
  8. 34
      apps/course/templates/course/school.html
  9. 0
      apps/school/__init__.py
  10. 12
      apps/school/admin.py
  11. 5
      apps/school/apps.py
  12. 30
      apps/school/migrations/0001_initial.py
  13. 18
      apps/school/migrations/0002_schoolschedule_age.py
  14. 17
      apps/school/migrations/0003_auto_20180221_0901.py
  15. 0
      apps/school/migrations/__init__.py
  16. 24
      apps/school/models.py
  17. 3
      apps/school/views.py
  18. 8
      apps/user/templates/user/profile.html
  19. 21
      docker-compose.yml
  20. 2
      project/celery.py
  21. 3
      project/celery_settings.py
  22. 27
      project/settings.py
  23. 2
      requirements.txt
  24. 92
      web/src/sass/_common.sass

@ -0,0 +1,21 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{py,rst,ini,ino}]
indent_style = space
indent_size = 4
[*.{html,css,scss,js,json,yml,vue}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

2
.gitignore vendored

@ -86,7 +86,7 @@ celerybeat-schedule
.env
.venv
env/
venv/
venv*/
ENV/
env.bak/
venv.bak/

@ -72,6 +72,11 @@ class CategorySerializer(serializers.ModelSerializer):
)
class CourseBulkChangeCategorySerializer(serializers.Serializer):
old_category = serializers.IntegerField()
new_category = serializers.IntegerField()
class CourseCreateSerializer(DispatchContentMixin,
DispatchGalleryMixin,
DispatchMaterialMixin,

@ -0,0 +1,22 @@
from rest_framework import serializers
from apps.school.models import SchoolSchedule
class SchoolScheduleSerializer(serializers.ModelSerializer):
class Meta:
model = SchoolSchedule
fields = (
'id',
'weekday',
'title',
'description',
'materials',
'age',
'month_price',
)
read_only_fields = (
'id',
)

@ -14,6 +14,7 @@ from .views import (
ImageTextViewSet, VideoViewSet,
GalleryViewSet, GalleryImageViewSet,
UserViewSet, LessonViewSet, ImageObjectViewSet,
SchoolScheduleViewSet,
)
router = DefaultRouter()
@ -30,24 +31,27 @@ router.register(r'image-texts', ImageTextViewSet, base_name='image-texts')
router.register(r'videos', VideoViewSet, base_name='videos')
router.register(r'galleries', GalleryViewSet, base_name='galleries')
router.register(r'gallery-images', GalleryImageViewSet, base_name='gallery-images')
router.register(r'school-schedules', SchoolScheduleViewSet, base_name='school-schedules')
router.register(r'users', UserViewSet, base_name='users')
schema_view = get_schema_view(
openapi.Info(
title="Lil Sity API",
default_version='v1',
description="Routes of Lil City project",
),
validators=['flex', 'ssv'],
public=False,
permission_classes=(permissions.AllowAny,),
openapi.Info(
title="Lil Sity API",
default_version='v1',
description="Routes of Lil City project",
),
validators=['flex', 'ssv'],
public=False,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
path('swagger(<str:format>.json|.yaml)', schema_view.without_ui(cache_timeout=None), name='schema-json'),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'),
path('api-token-auth/', ObtainToken.as_view(), name='api-token-auth'),
path('', include((router.urls, 'api-root')), name='api-root'),
path('swagger(<str:format>.json|.yaml)', schema_view.without_ui(cache_timeout=None), name='schema-json'),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'),
path('api-token-auth/', ObtainToken.as_view(), name='api-token-auth'),
path('', include((router.urls, 'api-root')), name='api-root'),
]

@ -10,6 +10,7 @@ from . import ExtendedModelViewSet
from .serializers.course import (
CategorySerializer, LikeSerializer,
CourseSerializer, CourseCreateSerializer,
CourseBulkChangeCategorySerializer,
MaterialSerializer, MaterialCreateSerializer,
LessonSerializer, LessonCreateSerializer,
)
@ -22,6 +23,7 @@ from .serializers.content import (
GalleryImageSerializer, GalleryImageCreateSerializer,
ImageObjectSerializer,
)
from .serializers.school import SchoolScheduleSerializer
from .serializers.user import (
UserSerializer, UserPhotoSerializer,
)
@ -33,7 +35,7 @@ from apps.content.models import (
Image, Text, ImageText, Video,
Gallery, GalleryImage, ImageObject,
)
from apps.school.models import SchoolSchedule
User = get_user_model()
@ -64,7 +66,7 @@ class LikeViewSet(ExtendedModelViewSet):
class CategoryViewSet(ExtendedModelViewSet):
queryset = Category.objects.all()
queryset = Category.objects.order_by('-id')
serializer_class = CategorySerializer
search_fields = ('title',)
ordering_fields = ('title',)
@ -82,6 +84,7 @@ class CourseViewSet(ExtendedModelViewSet):
'list': CourseSerializer,
'retrieve': CourseSerializer,
'draft': CourseSerializer,
'change_category_bulk': CourseBulkChangeCategorySerializer,
}
filter_fields = ('category', 'status', 'is_infinite', 'is_featured',)
search_fields = ('author__email', 'title', 'category__title',)
@ -99,6 +102,40 @@ class CourseViewSet(ExtendedModelViewSet):
serialized_data = serializer(instance=drafts.last())
return Response(serialized_data.data)
@list_route(methods=['patch'], url_path='change-category-bulk')
def change_category_bulk(self, request):
serializer_class = self.get_serializer_class()
serializer = serializer_class(data=request.data)
if serializer.is_valid():
old_category_id = serializer.validated_data['old_category']
new_category_id = serializer.validated_data['new_category']
try:
old_category = Category.objects.get(id=old_category_id)
except Category.DoesNotExist:
return Response(
{'success': False, 'detail': f'Category with id {old_category_id} not found'},
status=status.HTTP_400_BAD_REQUEST,
)
try:
new_category = Category.objects.get(id=new_category_id)
except Category.DoesNotExist:
return Response(
{'success': False, 'detail': f'Category with id {new_category_id} not found'},
status=status.HTTP_400_BAD_REQUEST,
)
c = Course.objects.filter(
category=old_category,
).update(
category=new_category
)
return Response({'success': True})
else:
return Response(
{'success': False},
status=status.HTTP_400_BAD_REQUEST,
)
class LessonViewSet(ExtendedModelViewSet):
queryset = Lesson.objects.select_related(
@ -249,3 +286,9 @@ class UserViewSet(ExtendedModelViewSet):
return Response({'success': True})
else:
return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST)
class SchoolScheduleViewSet(ExtendedModelViewSet):
queryset = SchoolSchedule.objects.all()
serializer_class = SchoolScheduleSerializer
permission_classes = (IsAdmin,)

@ -0,0 +1,19 @@
import json
from time import sleep
from project.celery import app
from constance import config
from InstagramAPI import InstagramAPI
@app.task
def retrieve_photos():
instagram = InstagramAPI(
config.INSTAGRAM_CLIENT_LOGIN,
config.INSTAGRAM_CLIENT_PASSWORD,
)
instagram.login()
sleep(1)
if instagram.isLoggedIn and instagram.getHashtagFeed(config.INSTAGRAM_RESULTS_TAG):
with open('s.json', 'w') as f:
f.write(json.dumps(instagram.LastJson))
return instagram.LastJson

@ -0,0 +1,34 @@
<div class="school__page">
<div class="school__block">
<div class="school__month">
<div class="school__month__wrap">
<div class="school__month__title">Январь</div>
<div class="school__month__subtitle">Осталось 14 дней</div>
<div class="school__month__image">
<img class="school__month__image__pic" src="/static/img/order.jpg">
</div>
</div>
</div>
<div class="school__btn">
<a class="buy__btn btn btn_md" href="#" {% if user.is_authenticated %}
data-popup=".js-popup-buy"
{% else %}
data-popup=".js-popup-auth"
{% endif %}>ПРОДЛИТЬ ШКОЛУ</a>
</div>
<div class="school__btn__prev">
<a class="school__prev__btn btn" href="#">ПОСМОТРЕТЬ ПРЕДЫДУЩИЕ УРОКИ</a>
</div>
</div>
<div class="school__block school__list">
{% for course in course_items %}
<div class="school__item">
<div class="school__title">Понедельник</div>
<div class="school__subtitle">Пластилиновая живопись.</div>
<div class="school__content">
<p>Научимся смешивать цвета, получать красивые оттенки и создавать картины из пластилина разными техниками. Узнаем как хранить и ухаживать за такими работами.</p>
</div>
</div>
{% endfor %}
</div>
</div>

@ -0,0 +1,12 @@
from django.contrib import admin
from .models import SchoolSchedule
@admin.register(SchoolSchedule)
class SchoolScheduleAdmin(admin.ModelAdmin):
list_display = (
'weekday',
'title',
'month_price',
)

@ -0,0 +1,5 @@
from django.apps import AppConfig
class SchoolConfig(AppConfig):
name = 'school'

@ -0,0 +1,30 @@
# Generated by Django 2.0.2 on 2018-02-20 14:21
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='SchoolSchedule',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('weekday', models.PositiveSmallIntegerField(choices=[(1, 'понедельник'), (2, 'вторник'), (3, 'среда'), (4, 'четверг'), (5, 'пятница'), (6, 'суббота'), (7, 'воскресенье')], verbose_name='День недели')),
('title', models.CharField(db_index=True, default='', max_length=100, verbose_name='Заголовок')),
('description', models.TextField(verbose_name='Описание')),
('materials', models.TextField(verbose_name='Материалы')),
('month_price', models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='Цена')),
],
options={
'verbose_name': 'Рассписание',
'verbose_name_plural': 'Рассписания',
'ordering': ('weekday',),
},
),
]

@ -0,0 +1,18 @@
# Generated by Django 2.0.2 on 2018-02-21 08:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('school', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='schoolschedule',
name='age',
field=models.PositiveSmallIntegerField(default=0, verbose_name='Возраст'),
),
]

@ -0,0 +1,17 @@
# Generated by Django 2.0.2 on 2018-02-21 09:01
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('school', '0002_schoolschedule_age'),
]
operations = [
migrations.AlterModelOptions(
name='schoolschedule',
options={'ordering': ('weekday',), 'verbose_name': 'Расписание', 'verbose_name_plural': 'Расписания'},
),
]

@ -0,0 +1,24 @@
from django.db import models
class SchoolSchedule(models.Model):
WEEKDAY_CHOICES = (
(1, 'понедельник'),
(2, 'вторник'),
(3, 'среда'),
(4, 'четверг'),
(5, 'пятница'),
(6, 'суббота'),
(7, 'воскресенье'),
)
weekday = models.PositiveSmallIntegerField('День недели', choices=WEEKDAY_CHOICES)
title = models.CharField('Заголовок', default='', max_length=100, db_index=True)
description = models.TextField('Описание')
materials = models.TextField('Материалы')
age = models.PositiveSmallIntegerField('Возраст', default=0)
month_price = models.DecimalField('Цена', max_digits=8, decimal_places=2, default=0)
class Meta:
ordering = ('weekday',)
verbose_name = 'Расписание'
verbose_name_plural = 'Расписания'

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

@ -71,6 +71,7 @@
<button class="tabs__btn js-tabs-btn active">ОПУБЛИКОВАННЫЕ
<span class="mobile-hide">КУРСЫ</span>
</button>
<button class="tabs__btn js-tabs-btn">ШКОЛА</button>
{% comment %} <button class="tabs__btn js-tabs-btn">
<span class="mobile-hide">КУРСЫ</span>
НА МОДЕРАЦИИ
@ -94,6 +95,13 @@
</div>
</div>
</div>
<div class="tabs__item js-tabs-item">
<div class="courses courses_scroll">
<div class="courses__list">
{% include "course/school.html" with course_items=published %}
</div>
</div>
</div>
{% comment %} <div class="tabs__item js-tabs-item">
<div class="courses courses_scroll">
<div class="courses__list">

@ -34,4 +34,23 @@ services:
- redis
links:
- db
- redis
- redis
workers:
build: .
restart: always
volumes:
- .:/lilcity
command: bash -c "celery worker -A project -B"
environment:
- DJANGO_SETTINGS_MODULE=project.settings
- DATABASE_SERVICE_HOST=db
- REDIS_SERVICE_HOST=redis
depends_on:
- db
- redis
- web
links:
- db
- redis
- web

@ -10,7 +10,7 @@ app = Celery('project')
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('project.celery_settings')
app.config_from_object('project.settings', namespace='CELERY')
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()

@ -1,3 +0,0 @@
broker_url = 'redis://redis:6379/0'
result_backend = 'redis://redis:6379/1'
task_serializer = 'json'

@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/2.0/ref/settings/
"""
import os
from collections import OrderedDict
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -47,6 +48,8 @@ INSTALLED_APPS = [
'rest_framework.authtoken',
'drf_yasg',
'corsheaders',
'constance',
'constance.backends.database',
] + [
'apps.auth.apps',
'apps.user',
@ -54,6 +57,7 @@ INSTALLED_APPS = [
'apps.payment',
'apps.course',
'apps.content',
'apps.school',
]
if DEBUG:
INSTALLED_APPS += ['silk']
@ -205,6 +209,29 @@ REST_FRAMEWORK = {
'PAGE_SIZE': 10,
}
# Celery settings
CELERY_BROKER_URL = 'redis://redis:6379/0'
CELERY_RESULT_BACKEND = 'redis://redis:6379/1'
CELERY_TASK_SERIALIZER = 'json'
# Dynamic settings
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
CONSTANCE_CONFIG = OrderedDict((
('INSTAGRAM_CLIENT_LOGIN', ('', '')),
('INSTAGRAM_CLIENT_PASSWORD', ('', '')),
('INSTAGRAM_RESULTS_TAG', ('#lil_акварель', 'Тэг результатов работ.')),
('INSTAGRAM_RESULTS_PATH', ('media/instagram/results/', 'Путь до результатов работ.')),
))
try:
from .local_settings import *
except ImportError:
pass
# CORS settings
if DEBUG:

@ -16,3 +16,5 @@ djangorestframework==3.7.7
drf-yasg[validation]==1.4.0
django-silk==2.0.0
django-cors-headers==2.1.0
django-constance[database]==2.1.0
InstagramAPI==1.0.2

@ -3329,4 +3329,94 @@ a
text-align: left
&.pic
position: relative
width: 100%
width: 100%
.school
&__page
display: flex
margin: 0 0 0 10px
flex-wrap: wrap
&__block
flex: 0 0 calc(50% - 20px)
&__list
display: flex
margin: 0 -10px
flex-wrap: wrap
&__item
display: block
margin: 0 10px 40px 10px
flex: 0 0 calc(100% - 20px)
&__title
display: block
margin: 0 0 10px 0
font-weight: bold
letter-spacing: 2px
color: #ff9393
&__subtitle
display: block
margin: 0
color: #333333
font-family: 'ProximaNova-Bold', serif
&__content
font-size: 16px
line-height: 1.25
color: #333333
&__btn
margin-top: 20px
a
width: 70%
&__btn__prev
margin-top: 12px
a
width: 70%
&__month
margin-bottom: 20px
flex: 0 0 320px
border-radius: 8px
background-image: linear-gradient(-225deg, #FFE2EB 0%, #D8F5F5 100%)
width: 70%
padding: 2px
&__wrap
padding: 5px 0 10px
background: white
border-radius: 6px
&__title
margin: 20px 0 0 20px
font-size: 36px
line-height: 0.97
color: #333333
&__subtitle
margin: 10px 0 0 20px
font-size: 16px
line-height: 0.88
color: #888888
&__image
margin: 40px 0 40px 0
&__pic
display: block
width: 100%
&__prev__btn
position: relative
padding: 19px 24px 18px
border-radius: 8px
z-index: 4
font-size: 12px
font-weight: bold
line-height: 1.67
letter-spacing: 1px
text-align: center
color: #333333
&:hover:before
opacity: 0
&:before
content: ''
position: absolute
top: 2px
left: 2px
right: 2px
bottom: 2px
background: #FDF8F9
border-radius: 8px
transition: opacity .2s
z-index: -2

Loading…
Cancel
Save