diff --git a/api/v1/auth.py b/api/v1/auth.py new file mode 100644 index 00000000..ea99f40c --- /dev/null +++ b/api/v1/auth.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 diff --git a/api/v1/serializers/content.py b/api/v1/serializers/content.py index 15af6819..9a338c45 100644 --- a/api/v1/serializers/content.py +++ b/api/v1/serializers/content.py @@ -6,7 +6,6 @@ from apps.content.models import ( ) from . import Base64ImageField -# from .course import CourseSerializer class ContentCreateSerializer(serializers.Serializer): @@ -156,6 +155,15 @@ class ContentSerializer(serializers.ModelSerializer): class Meta: model = Content + fields = ( + 'id', + 'course', + 'lesson', + 'title', + 'position', + 'created_at', + 'update_at', + ) def to_representation(self, obj): if isinstance(obj, Image): diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index d75e3a13..f04b81ac 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -65,13 +65,15 @@ class CategorySerializer(serializers.ModelSerializer): class CourseCreateSerializer(serializers.ModelSerializer): - content = serializers.ListSerializer(child=ContentCreateSerializer()) - materials = MaterialSerializer(many=True) + slug = serializers.SlugField(allow_unicode=True, required=False) + content = serializers.ListSerializer(child=ContentCreateSerializer(), required=False) + materials = MaterialSerializer(many=True, required=False) class Meta: model = Course fields = ( 'id', + 'slug', 'author', 'title', 'short_description', @@ -95,14 +97,14 @@ class CourseCreateSerializer(serializers.ModelSerializer): read_only_fields = ( 'id', - # 'content', + 'url', 'created_at', 'update_at', ) def create(self, validated_data): - materials = validated_data.pop('materials') - content = validated_data.pop('content') + materials = validated_data.pop('materials', []) + content = validated_data.pop('content', []) course = super().create(validated_data) diff --git a/api/v1/urls.py b/api/v1/urls.py index 846af2ae..305b053d 100644 --- a/api/v1/urls.py +++ b/api/v1/urls.py @@ -6,6 +6,7 @@ from rest_framework.routers import DefaultRouter from drf_yasg.views import get_schema_view from drf_yasg import openapi +from .auth import ObtainToken from .views import ( CategoryViewSet, CourseViewSet, MaterialViewSet, LikeViewSet, @@ -47,5 +48,6 @@ urlpatterns = [ path('swagger(.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('', 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'), ] diff --git a/apps/course/admin.py b/apps/course/admin.py index dab42062..39d0f54d 100644 --- a/apps/course/admin.py +++ b/apps/course/admin.py @@ -16,6 +16,7 @@ class CourseAdmin(admin.ModelAdmin): 'created_at', 'update_at', ) + prepopulated_fields = {"slug": ("title",)} @admin.register(Category) diff --git a/apps/course/fixtures/course.json b/apps/course/fixtures/course.json index cc3d2a0f..8f9e29d0 100644 --- a/apps/course/fixtures/course.json +++ b/apps/course/fixtures/course.json @@ -13,7 +13,6 @@ "category": 2, "duration": 1, "is_featured": false, - "url": "https://gitlab.com/", "status": 0, "created_at": "2018-01-27T07:04:41.113Z", "update_at": "2018-01-31T15:03:47.118Z", @@ -35,7 +34,6 @@ "category": 1, "duration": 1, "is_featured": false, - "url": "https://gitlab.com/", "status": 0, "created_at": "2018-01-27T07:09:03.437Z", "update_at": "2018-01-31T15:03:47.115Z", @@ -57,7 +55,6 @@ "category": 9, "duration": 1, "is_featured": false, - "url": "https://gitlab.com/", "status": 0, "created_at": "2018-01-27T07:09:03.442Z", "update_at": "2018-01-31T15:03:47.112Z", @@ -79,7 +76,6 @@ "category": 8, "duration": 1, "is_featured": false, - "url": "https://gitlab.com/", "status": 0, "created_at": "2018-01-27T07:09:03.445Z", "update_at": "2018-01-31T15:03:47.108Z", @@ -101,7 +97,6 @@ "category": 7, "duration": 1, "is_featured": false, - "url": "https://gitlab.com/", "status": 0, "created_at": "2018-01-27T07:09:03.449Z", "update_at": "2018-01-31T15:03:47.104Z", @@ -123,7 +118,6 @@ "category": 6, "duration": 1, "is_featured": false, - "url": "https://gitlab.com/", "status": 0, "created_at": "2018-01-27T07:09:03.452Z", "update_at": "2018-01-31T15:03:47.101Z", @@ -145,7 +139,6 @@ "category": 5, "duration": 1, "is_featured": false, - "url": "https://gitlab.com/", "status": 0, "created_at": "2018-01-27T07:09:03.455Z", "update_at": "2018-01-31T15:03:47.097Z", @@ -167,7 +160,6 @@ "category": 4, "duration": 1, "is_featured": false, - "url": "https://gitlab.com/", "status": 0, "created_at": "2018-01-27T07:09:03.458Z", "update_at": "2018-01-31T15:03:47.093Z", @@ -189,7 +181,6 @@ "category": 3, "duration": 1, "is_featured": false, - "url": "https://gitlab.com/", "status": 0, "created_at": "2018-01-27T07:09:03.461Z", "update_at": "2018-01-31T15:03:47.089Z", @@ -211,7 +202,6 @@ "category": 2, "duration": 1, "is_featured": true, - "url": "https://gitlab.com/", "status": 1, "created_at": "2018-01-27T07:09:03.464Z", "update_at": "2018-01-31T15:03:47.086Z", @@ -237,7 +227,6 @@ "category": 1, "duration": 1, "is_featured": false, - "url": "https://gitlab.com/", "status": 1, "created_at": "2018-01-27T07:09:03.467Z", "update_at": "2018-01-31T15:03:47.080Z", diff --git a/apps/course/migrations/0027_remove_course_url.py b/apps/course/migrations/0027_remove_course_url.py new file mode 100644 index 00000000..0646cf90 --- /dev/null +++ b/apps/course/migrations/0027_remove_course_url.py @@ -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', + ), + ] diff --git a/apps/course/migrations/0028_course_slug.py b/apps/course/migrations/0028_course_slug.py new file mode 100644 index 00000000..f9e1a3ce --- /dev/null +++ b/apps/course/migrations/0028_course_slug.py @@ -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, + ), + ] diff --git a/apps/course/migrations/0029_auto_20180209_0911.py b/apps/course/migrations/0029_auto_20180209_0911.py new file mode 100644 index 00000000..daff2a0c --- /dev/null +++ b/apps/course/migrations/0029_auto_20180209_0911.py @@ -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), + ), + ] diff --git a/apps/course/models.py b/apps/course/models.py index 61cca877..69eff32b 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -1,8 +1,10 @@ import arrow +from uuid import uuid4 from django.db import models from django.utils import timezone +from django.utils.text import slugify from django.contrib.auth import get_user_model - +from django.urls import reverse_lazy from polymorphic_tree.models import PolymorphicMPTTModel, PolymorphicTreeForeignKey from .manager import CategoryQuerySet @@ -19,6 +21,10 @@ class Like(models.Model): update_at = models.DateTimeField(auto_now=True) +def default_slug(): + return str(uuid4()) + + class Course(models.Model): PENDING = 0 PUBLISHED = 1 @@ -28,10 +34,17 @@ class Course(models.Model): (PUBLISHED, 'Published'), (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) - short_description = models.TextField('Краткое описание курса', db_index=True) - from_author = models.TextField('От автора', default='', null=True, blank=True) + short_description = models.TextField( + 'Краткое описание курса', db_index=True) + from_author = models.TextField( + 'От автора', default='', null=True, blank=True) cover = models.ForeignKey( ImageObject, related_name='course_covers', verbose_name='Обложка курса', on_delete=models.CASCADE, @@ -49,8 +62,8 @@ class Course(models.Model): category = models.ForeignKey('Category', on_delete=models.PROTECT) duration = models.IntegerField('Продолжительность курса', default=0) is_featured = models.BooleanField(default=False) - url = models.URLField('Ссылка', default='') - status = models.PositiveSmallIntegerField('Статус', default=0, choices=STATUS_CHOICES) + status = models.PositiveSmallIntegerField( + 'Статус', default=0, choices=STATUS_CHOICES) likes = models.ManyToManyField(Like, blank=True) materials = models.ManyToManyField('Material', blank=True) gallery = models.ForeignKey( @@ -61,6 +74,25 @@ class Course(models.Model): created_at = models.DateTimeField(auto_now_add=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 def is_free(self): if self.price: @@ -108,7 +140,8 @@ class Category(models.Model): class Lesson(models.Model): title = models.CharField('Название урока', max_length=100) 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( ImageObject, related_name='lesson_covers', verbose_name='Обложка урока', on_delete=models.CASCADE, @@ -173,7 +206,8 @@ class Comment(PolymorphicMPTTModel): 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): verbose_name = 'Комментарий курса' @@ -181,7 +215,8 @@ class CourseComment(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): verbose_name = 'Комментарий урока' diff --git a/apps/user/fixtures/superuser.json b/apps/user/fixtures/superuser.json index 385896cb..a9118aba 100644 --- a/apps/user/fixtures/superuser.json +++ b/apps/user/fixtures/superuser.json @@ -13,7 +13,7 @@ "is_active": true, "date_joined": "2018-01-28T08:41:19Z", "email": "admin@lil.city", - "role": 0, + "role": 2, "gender": "n", "country": "", "city": "", diff --git a/project/urls.py b/project/urls.py index 5da140a6..28a51a9a 100644 --- a/project/urls.py +++ b/project/urls.py @@ -31,6 +31,7 @@ urlpatterns = [ path('auth/', include(('apps.auth.urls', 'lilcity'))), path('courses/', CoursesView.as_view(), name='courses'), path('course//', CourseView.as_view(), name='course'), + path('course//', CourseView.as_view(), name='course'), path('course//like', likes, name='likes'), path('course//comment', coursecomment, name='coursecomment'), path('lesson//', LessonView.as_view(), name='lesson'),