From b89c4a86d2c24c6989568428fdfafdb4c437c3a6 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 11:18:11 +0300 Subject: [PATCH 01/14] Remove url field from Course model? and use get_absolute_url method --- .../course/migrations/0027_remove_course_url.py | 17 +++++++++++++++++ apps/course/models.py | 10 ++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 apps/course/migrations/0027_remove_course_url.py 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/models.py b/apps/course/models.py index 61cca877..10295cd3 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -2,7 +2,7 @@ import arrow from django.db import models from django.utils import timezone 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 @@ -49,7 +49,6 @@ 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) likes = models.ManyToManyField(Like, blank=True) materials = models.ManyToManyField('Material', blank=True) @@ -61,6 +60,13 @@ class Course(models.Model): created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) + @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: From 8527a8b847a595bf44e8826d172fea4ced9bd598 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 11:18:44 +0300 Subject: [PATCH 02/14] Clean not needed import --- api/v1/serializers/content.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/v1/serializers/content.py b/api/v1/serializers/content.py index 15af6819..b1a8678f 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): From 34c22321f9f54e4b44b640683a5f5c6a7d970f40 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 11:19:24 +0300 Subject: [PATCH 03/14] Set ull field to read only --- api/v1/serializers/course.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index d75e3a13..a91c3032 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -95,7 +95,7 @@ class CourseCreateSerializer(serializers.ModelSerializer): read_only_fields = ( 'id', - # 'content', + 'url', 'created_at', 'update_at', ) From 52481c4bbb22be427406e40b9cb876ff8c4f9515 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 12:45:25 +0300 Subject: [PATCH 04/14] Add slug field --- api/v1/serializers/course.py | 10 +++-- apps/course/migrations/0028_course_slug.py | 19 ++++++++++ .../migrations/0029_auto_20180209_0911.py | 19 ++++++++++ apps/course/models.py | 37 +++++++++++++++---- 4 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 apps/course/migrations/0028_course_slug.py create mode 100644 apps/course/migrations/0029_auto_20180209_0911.py diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index a91c3032..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', @@ -101,8 +103,8 @@ class CourseCreateSerializer(serializers.ModelSerializer): ) 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/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 10295cd3..b03092b6 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -1,6 +1,8 @@ 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 @@ -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, + 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,7 +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) - 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( @@ -60,6 +74,12 @@ class Course(models.Model): created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) + def save(self, *args, **kwargs): + print(self.title) + self.slug = slugify(self.title[:100] + '_' + str(uuid4())[:6], allow_unicode=True) + print(self.slug) + return super().save() + @property def url(self): return self.get_absolute_url() @@ -114,7 +134,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, @@ -179,7 +200,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 = 'Комментарий курса' @@ -187,7 +209,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 = 'Комментарий урока' From 38e0ed1f324ee481e50bbb974edc854e4f3ff835 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 12:45:42 +0300 Subject: [PATCH 05/14] Add fiel list to ContentSerializer --- api/v1/serializers/content.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api/v1/serializers/content.py b/api/v1/serializers/content.py index b1a8678f..9a338c45 100644 --- a/api/v1/serializers/content.py +++ b/api/v1/serializers/content.py @@ -155,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): From 9b607487f2ec60deab8018f65de5634700b9c57f Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 12:46:03 +0300 Subject: [PATCH 06/14] Fix Course admin --- apps/course/admin.py | 1 + 1 file changed, 1 insertion(+) 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) From 7458202bb7a64951eaba573793e0b3c909685d42 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 12:46:27 +0300 Subject: [PATCH 07/14] Add route for retrieve course by slug --- project/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/project/urls.py b/project/urls.py index 5e5d2e8e..5fd683fc 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'), From ed0e0fa34a1be5fc8ce38a496a1566322ca3e3f5 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 12:46:38 +0300 Subject: [PATCH 08/14] Fix fixtures --- apps/course/fixtures/course.json | 11 ----------- 1 file changed, 11 deletions(-) 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", From e24efc7f6d4d18266e1b65e0a14f65a3827ffdbb Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 13:07:44 +0300 Subject: [PATCH 09/14] Fix saving Course model --- apps/course/models.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/course/models.py b/apps/course/models.py index b03092b6..10175b21 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -75,9 +75,15 @@ class Course(models.Model): update_at = models.DateTimeField(auto_now=True) def save(self, *args, **kwargs): - print(self.title) - self.slug = slugify(self.title[:100] + '_' + str(uuid4())[:6], allow_unicode=True) - print(self.slug) + 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 From 81a2f0e5c2e2f1ad7fbf7949753afbac5af057bd Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 13:19:31 +0300 Subject: [PATCH 10/14] Add default value for slug uuid4 --- apps/course/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/course/models.py b/apps/course/models.py index 10175b21..69eff32b 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -35,7 +35,7 @@ class Course(models.Model): (ARCHIVED, 'Archived'), ) slug = models.SlugField( - allow_unicode=True, + allow_unicode=True, default=default_slug, max_length=100, unique=True, db_index=True, ) author = models.ForeignKey( From f07dff7dcd98107e43f08dadf0d8b6d3ffa4cc1d Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 13:25:07 +0300 Subject: [PATCH 11/14] Add api-token-auth endpoint --- api/v1/urls.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/v1/urls.py b/api/v1/urls.py index 846af2ae..0118d09a 100644 --- a/api/v1/urls.py +++ b/api/v1/urls.py @@ -2,6 +2,7 @@ from django.urls import path, include from rest_framework import permissions from rest_framework.routers import DefaultRouter +from rest_framework.authtoken import views as auth_views from drf_yasg.views import get_schema_view from drf_yasg import openapi @@ -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/', auth_views.obtain_auth_token), + path('', include((router.urls, 'api-root')), name='api-root'), ] From 5e7f57857b91503e75437a3eebdbb8f45b8d8db0 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 14:04:34 +0300 Subject: [PATCH 12/14] Add custom view for obtain token --- api/v1/auth.py | 39 +++++++++++++++++++++++++++++++++++++++ api/v1/urls.py | 4 ++-- 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 api/v1/auth.py diff --git a/api/v1/auth.py b/api/v1/auth.py new file mode 100644 index 00000000..2bf643f4 --- /dev/null +++ b/api/v1/auth.py @@ -0,0 +1,39 @@ +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 + + +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') + else: + msg = _('Must include "username" and "password".') + raise serializers.ValidationError(msg, code='authorization') + + attrs['user'] = user + return attrs + + +class ObtainToken(ObtainAuthToken): + serializer_class = AuthTokenSerializer diff --git a/api/v1/urls.py b/api/v1/urls.py index 0118d09a..305b053d 100644 --- a/api/v1/urls.py +++ b/api/v1/urls.py @@ -2,11 +2,11 @@ from django.urls import path, include from rest_framework import permissions from rest_framework.routers import DefaultRouter -from rest_framework.authtoken import views as auth_views from drf_yasg.views import get_schema_view from drf_yasg import openapi +from .auth import ObtainToken from .views import ( CategoryViewSet, CourseViewSet, MaterialViewSet, LikeViewSet, @@ -48,6 +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('api-token-auth/', auth_views.obtain_auth_token), + path('api-token-auth/', ObtainToken.as_view(), name='api-token-auth'), path('', include((router.urls, 'api-root')), name='api-root'), ] From cbdeb0e2bedc9eda3a8ec25be0df8250606abccc Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 14:24:45 +0300 Subject: [PATCH 13/14] Only admin users obtain tocken --- api/v1/auth.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/v1/auth.py b/api/v1/auth.py index 2bf643f4..ea99f40c 100644 --- a/api/v1/auth.py +++ b/api/v1/auth.py @@ -1,9 +1,12 @@ +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")) @@ -27,8 +30,11 @@ class AuthTokenSerializer(serializers.Serializer): 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 "username" and "password".') + msg = _('Must include "email" and "password".') raise serializers.ValidationError(msg, code='authorization') attrs['user'] = user From 35e44ed93ef6370647e45f238d006ed3eb3e9cae Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 9 Feb 2018 15:01:58 +0300 Subject: [PATCH 14/14] Fix user fixture, admin@lil.city now have admin role --- apps/user/fixtures/superuser.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": "",