Merge branch 'feature/api' of gitlab.com:lilcity/backend into feature/create-edit-courses

remotes/origin/hasaccess
Vitaly Baev 8 years ago
commit aa5a820ce7
  1. 45
      api/v1/auth.py
  2. 10
      api/v1/serializers/content.py
  3. 12
      api/v1/serializers/course.py
  4. 4
      api/v1/urls.py
  5. 1
      apps/course/admin.py
  6. 11
      apps/course/fixtures/course.json
  7. 17
      apps/course/migrations/0027_remove_course_url.py
  8. 19
      apps/course/migrations/0028_course_slug.py
  9. 19
      apps/course/migrations/0029_auto_20180209_0911.py
  10. 53
      apps/course/models.py
  11. 2
      apps/user/fixtures/superuser.json
  12. 1
      project/urls.py

@ -0,0 +1,45 @@
from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.compat import authenticate
User = get_user_model()
class AuthTokenSerializer(serializers.Serializer):
email = serializers.CharField(label=_("Email"))
password = serializers.CharField(
label=_("Password"),
style={'input_type': 'password'},
trim_whitespace=False
)
def validate(self, attrs):
email = attrs.get('email')
password = attrs.get('password')
if email and password:
user = authenticate(request=self.context.get('request'),
email=email, password=password)
# The authenticate call simply returns None for is_active=False
# users. (Assuming the default ModelBackend authentication
# backend.)
if not user:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg, code='authorization')
elif user.role != User.ADMIN_ROLE:
msg = _('Only admin have permission to login admin page.')
raise serializers.ValidationError(msg, code='authorization')
else:
msg = _('Must include "email" and "password".')
raise serializers.ValidationError(msg, code='authorization')
attrs['user'] = user
return attrs
class ObtainToken(ObtainAuthToken):
serializer_class = AuthTokenSerializer

@ -6,7 +6,6 @@ from apps.content.models import (
) )
from . import Base64ImageField from . import Base64ImageField
# from .course import CourseSerializer
class ContentCreateSerializer(serializers.Serializer): class ContentCreateSerializer(serializers.Serializer):
@ -156,6 +155,15 @@ class ContentSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Content model = Content
fields = (
'id',
'course',
'lesson',
'title',
'position',
'created_at',
'update_at',
)
def to_representation(self, obj): def to_representation(self, obj):
if isinstance(obj, Image): if isinstance(obj, Image):

@ -65,13 +65,15 @@ class CategorySerializer(serializers.ModelSerializer):
class CourseCreateSerializer(serializers.ModelSerializer): class CourseCreateSerializer(serializers.ModelSerializer):
content = serializers.ListSerializer(child=ContentCreateSerializer()) slug = serializers.SlugField(allow_unicode=True, required=False)
materials = MaterialSerializer(many=True) content = serializers.ListSerializer(child=ContentCreateSerializer(), required=False)
materials = MaterialSerializer(many=True, required=False)
class Meta: class Meta:
model = Course model = Course
fields = ( fields = (
'id', 'id',
'slug',
'author', 'author',
'title', 'title',
'short_description', 'short_description',
@ -95,14 +97,14 @@ class CourseCreateSerializer(serializers.ModelSerializer):
read_only_fields = ( read_only_fields = (
'id', 'id',
# 'content', 'url',
'created_at', 'created_at',
'update_at', 'update_at',
) )
def create(self, validated_data): def create(self, validated_data):
materials = validated_data.pop('materials') materials = validated_data.pop('materials', [])
content = validated_data.pop('content') content = validated_data.pop('content', [])
course = super().create(validated_data) course = super().create(validated_data)

@ -6,6 +6,7 @@ from rest_framework.routers import DefaultRouter
from drf_yasg.views import get_schema_view from drf_yasg.views import get_schema_view
from drf_yasg import openapi from drf_yasg import openapi
from .auth import ObtainToken
from .views import ( from .views import (
CategoryViewSet, CourseViewSet, CategoryViewSet, CourseViewSet,
MaterialViewSet, LikeViewSet, MaterialViewSet, LikeViewSet,
@ -47,5 +48,6 @@ urlpatterns = [
path('swagger(<str:format>.json|.yaml)', schema_view.without_ui(cache_timeout=None), name='schema-json'), 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('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('redoc/', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'),
path('', include((router.urls, 'api-root')), name='api-root') path('api-token-auth/', ObtainToken.as_view(), name='api-token-auth'),
path('', include((router.urls, 'api-root')), name='api-root'),
] ]

@ -16,6 +16,7 @@ class CourseAdmin(admin.ModelAdmin):
'created_at', 'created_at',
'update_at', 'update_at',
) )
prepopulated_fields = {"slug": ("title",)}
@admin.register(Category) @admin.register(Category)

@ -13,7 +13,6 @@
"category": 2, "category": 2,
"duration": 1, "duration": 1,
"is_featured": false, "is_featured": false,
"url": "https://gitlab.com/",
"status": 0, "status": 0,
"created_at": "2018-01-27T07:04:41.113Z", "created_at": "2018-01-27T07:04:41.113Z",
"update_at": "2018-01-31T15:03:47.118Z", "update_at": "2018-01-31T15:03:47.118Z",
@ -35,7 +34,6 @@
"category": 1, "category": 1,
"duration": 1, "duration": 1,
"is_featured": false, "is_featured": false,
"url": "https://gitlab.com/",
"status": 0, "status": 0,
"created_at": "2018-01-27T07:09:03.437Z", "created_at": "2018-01-27T07:09:03.437Z",
"update_at": "2018-01-31T15:03:47.115Z", "update_at": "2018-01-31T15:03:47.115Z",
@ -57,7 +55,6 @@
"category": 9, "category": 9,
"duration": 1, "duration": 1,
"is_featured": false, "is_featured": false,
"url": "https://gitlab.com/",
"status": 0, "status": 0,
"created_at": "2018-01-27T07:09:03.442Z", "created_at": "2018-01-27T07:09:03.442Z",
"update_at": "2018-01-31T15:03:47.112Z", "update_at": "2018-01-31T15:03:47.112Z",
@ -79,7 +76,6 @@
"category": 8, "category": 8,
"duration": 1, "duration": 1,
"is_featured": false, "is_featured": false,
"url": "https://gitlab.com/",
"status": 0, "status": 0,
"created_at": "2018-01-27T07:09:03.445Z", "created_at": "2018-01-27T07:09:03.445Z",
"update_at": "2018-01-31T15:03:47.108Z", "update_at": "2018-01-31T15:03:47.108Z",
@ -101,7 +97,6 @@
"category": 7, "category": 7,
"duration": 1, "duration": 1,
"is_featured": false, "is_featured": false,
"url": "https://gitlab.com/",
"status": 0, "status": 0,
"created_at": "2018-01-27T07:09:03.449Z", "created_at": "2018-01-27T07:09:03.449Z",
"update_at": "2018-01-31T15:03:47.104Z", "update_at": "2018-01-31T15:03:47.104Z",
@ -123,7 +118,6 @@
"category": 6, "category": 6,
"duration": 1, "duration": 1,
"is_featured": false, "is_featured": false,
"url": "https://gitlab.com/",
"status": 0, "status": 0,
"created_at": "2018-01-27T07:09:03.452Z", "created_at": "2018-01-27T07:09:03.452Z",
"update_at": "2018-01-31T15:03:47.101Z", "update_at": "2018-01-31T15:03:47.101Z",
@ -145,7 +139,6 @@
"category": 5, "category": 5,
"duration": 1, "duration": 1,
"is_featured": false, "is_featured": false,
"url": "https://gitlab.com/",
"status": 0, "status": 0,
"created_at": "2018-01-27T07:09:03.455Z", "created_at": "2018-01-27T07:09:03.455Z",
"update_at": "2018-01-31T15:03:47.097Z", "update_at": "2018-01-31T15:03:47.097Z",
@ -167,7 +160,6 @@
"category": 4, "category": 4,
"duration": 1, "duration": 1,
"is_featured": false, "is_featured": false,
"url": "https://gitlab.com/",
"status": 0, "status": 0,
"created_at": "2018-01-27T07:09:03.458Z", "created_at": "2018-01-27T07:09:03.458Z",
"update_at": "2018-01-31T15:03:47.093Z", "update_at": "2018-01-31T15:03:47.093Z",
@ -189,7 +181,6 @@
"category": 3, "category": 3,
"duration": 1, "duration": 1,
"is_featured": false, "is_featured": false,
"url": "https://gitlab.com/",
"status": 0, "status": 0,
"created_at": "2018-01-27T07:09:03.461Z", "created_at": "2018-01-27T07:09:03.461Z",
"update_at": "2018-01-31T15:03:47.089Z", "update_at": "2018-01-31T15:03:47.089Z",
@ -211,7 +202,6 @@
"category": 2, "category": 2,
"duration": 1, "duration": 1,
"is_featured": true, "is_featured": true,
"url": "https://gitlab.com/",
"status": 1, "status": 1,
"created_at": "2018-01-27T07:09:03.464Z", "created_at": "2018-01-27T07:09:03.464Z",
"update_at": "2018-01-31T15:03:47.086Z", "update_at": "2018-01-31T15:03:47.086Z",
@ -237,7 +227,6 @@
"category": 1, "category": 1,
"duration": 1, "duration": 1,
"is_featured": false, "is_featured": false,
"url": "https://gitlab.com/",
"status": 1, "status": 1,
"created_at": "2018-01-27T07:09:03.467Z", "created_at": "2018-01-27T07:09:03.467Z",
"update_at": "2018-01-31T15:03:47.080Z", "update_at": "2018-01-31T15:03:47.080Z",

@ -0,0 +1,17 @@
# Generated by Django 2.0.2 on 2018-02-09 08:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('course', '0026_auto_20180208_1053'),
]
operations = [
migrations.RemoveField(
model_name='course',
name='url',
),
]

@ -0,0 +1,19 @@
# Generated by Django 2.0.2 on 2018-02-09 08:59
from uuid import uuid4
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0027_remove_course_url'),
]
operations = [
migrations.AddField(
model_name='course',
name='slug',
field=models.SlugField(allow_unicode=True, default=str(uuid4()), max_length=100, unique=True),
preserve_default=False,
),
]

@ -0,0 +1,19 @@
# Generated by Django 2.0.2 on 2018-02-09 09:11
import apps.course.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0028_course_slug'),
]
operations = [
migrations.AlterField(
model_name='course',
name='slug',
field=models.SlugField(allow_unicode=True, default=apps.course.models.default_slug, max_length=100, unique=True),
),
]

@ -1,8 +1,10 @@
import arrow import arrow
from uuid import uuid4
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.text import slugify
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.urls import reverse_lazy
from polymorphic_tree.models import PolymorphicMPTTModel, PolymorphicTreeForeignKey from polymorphic_tree.models import PolymorphicMPTTModel, PolymorphicTreeForeignKey
from .manager import CategoryQuerySet from .manager import CategoryQuerySet
@ -19,6 +21,10 @@ class Like(models.Model):
update_at = models.DateTimeField(auto_now=True) update_at = models.DateTimeField(auto_now=True)
def default_slug():
return str(uuid4())
class Course(models.Model): class Course(models.Model):
PENDING = 0 PENDING = 0
PUBLISHED = 1 PUBLISHED = 1
@ -28,10 +34,17 @@ class Course(models.Model):
(PUBLISHED, 'Published'), (PUBLISHED, 'Published'),
(ARCHIVED, 'Archived'), (ARCHIVED, 'Archived'),
) )
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) slug = models.SlugField(
allow_unicode=True, default=default_slug,
max_length=100, unique=True, db_index=True,
)
author = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, blank=True)
title = models.CharField('Название курса', max_length=100, db_index=True) title = models.CharField('Название курса', max_length=100, db_index=True)
short_description = models.TextField('Краткое описание курса', db_index=True) short_description = models.TextField(
from_author = models.TextField('От автора', default='', null=True, blank=True) 'Краткое описание курса', db_index=True)
from_author = models.TextField(
'От автора', default='', null=True, blank=True)
cover = models.ForeignKey( cover = models.ForeignKey(
ImageObject, related_name='course_covers', ImageObject, related_name='course_covers',
verbose_name='Обложка курса', on_delete=models.CASCADE, verbose_name='Обложка курса', on_delete=models.CASCADE,
@ -49,8 +62,8 @@ class Course(models.Model):
category = models.ForeignKey('Category', on_delete=models.PROTECT) category = models.ForeignKey('Category', on_delete=models.PROTECT)
duration = models.IntegerField('Продолжительность курса', default=0) duration = models.IntegerField('Продолжительность курса', default=0)
is_featured = models.BooleanField(default=False) is_featured = models.BooleanField(default=False)
url = models.URLField('Ссылка', default='') status = models.PositiveSmallIntegerField(
status = models.PositiveSmallIntegerField('Статус', default=0, choices=STATUS_CHOICES) 'Статус', default=0, choices=STATUS_CHOICES)
likes = models.ManyToManyField(Like, blank=True) likes = models.ManyToManyField(Like, blank=True)
materials = models.ManyToManyField('Material', blank=True) materials = models.ManyToManyField('Material', blank=True)
gallery = models.ForeignKey( gallery = models.ForeignKey(
@ -61,6 +74,25 @@ class Course(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True) update_at = models.DateTimeField(auto_now=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(
self.title[:100],
allow_unicode=True
)
if Course.objects.filter(slug=self.slug).exists():
self.slug += '_' + str(uuid4())[:6]
return super().save()
@property
def url(self):
return self.get_absolute_url()
def get_absolute_url(self):
return reverse_lazy('course', args=[self.id])
@property @property
def is_free(self): def is_free(self):
if self.price: if self.price:
@ -108,7 +140,8 @@ class Category(models.Model):
class Lesson(models.Model): class Lesson(models.Model):
title = models.CharField('Название урока', max_length=100) title = models.CharField('Название урока', max_length=100)
short_description = models.TextField('Краткое описание урока') short_description = models.TextField('Краткое описание урока')
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='lessons') course = models.ForeignKey(
Course, on_delete=models.CASCADE, related_name='lessons')
cover = models.ForeignKey( cover = models.ForeignKey(
ImageObject, related_name='lesson_covers', ImageObject, related_name='lesson_covers',
verbose_name='Обложка урока', on_delete=models.CASCADE, verbose_name='Обложка урока', on_delete=models.CASCADE,
@ -173,7 +206,8 @@ class Comment(PolymorphicMPTTModel):
class CourseComment(Comment): class CourseComment(Comment):
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='comments') course = models.ForeignKey(
Course, on_delete=models.CASCADE, related_name='comments')
class Meta(Comment.Meta): class Meta(Comment.Meta):
verbose_name = 'Комментарий курса' verbose_name = 'Комментарий курса'
@ -181,7 +215,8 @@ class CourseComment(Comment):
class LessonComment(Comment): class LessonComment(Comment):
lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE, related_name='comments') lesson = models.ForeignKey(
Lesson, on_delete=models.CASCADE, related_name='comments')
class Meta(Comment.Meta): class Meta(Comment.Meta):
verbose_name = 'Комментарий урока' verbose_name = 'Комментарий урока'

@ -13,7 +13,7 @@
"is_active": true, "is_active": true,
"date_joined": "2018-01-28T08:41:19Z", "date_joined": "2018-01-28T08:41:19Z",
"email": "admin@lil.city", "email": "admin@lil.city",
"role": 0, "role": 2,
"gender": "n", "gender": "n",
"country": "", "country": "",
"city": "", "city": "",

@ -31,6 +31,7 @@ urlpatterns = [
path('auth/', include(('apps.auth.urls', 'lilcity'))), path('auth/', include(('apps.auth.urls', 'lilcity'))),
path('courses/', CoursesView.as_view(), name='courses'), path('courses/', CoursesView.as_view(), name='courses'),
path('course/<int:pk>/', CourseView.as_view(), name='course'), path('course/<int:pk>/', CourseView.as_view(), name='course'),
path('course/<str:slug>/', CourseView.as_view(), name='course'),
path('course/<int:course_id>/like', likes, name='likes'), path('course/<int:course_id>/like', likes, name='likes'),
path('course/<int:course_id>/comment', coursecomment, name='coursecomment'), path('course/<int:course_id>/comment', coursecomment, name='coursecomment'),
path('lesson/<int:pk>/', LessonView.as_view(), name='lesson'), path('lesson/<int:pk>/', LessonView.as_view(), name='lesson'),

Loading…
Cancel
Save