From 605c81b33c989bf346248bcd8fa456a185644f7b Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 4 Sep 2019 18:47:06 +0300 Subject: [PATCH 01/51] replace comment model from establishment app, added new app comment --- apps/comment/__init__.py | 0 apps/comment/admin.py | 8 +++ apps/comment/apps.py | 8 +++ apps/comment/migrations/0001_initial.py | 36 +++++++++++ apps/comment/migrations/__init__.py | 0 apps/comment/models.py | 51 ++++++++++++++++ apps/comment/serializers/__init__.py | 0 apps/comment/serializers/common.py | 60 +++++++++++++++++++ apps/comment/serializers/mobile.py | 0 apps/comment/serializers/web.py | 1 + apps/comment/tests.py | 1 + apps/comment/urls/__init__.py | 0 apps/comment/urls/common.py | 12 ++++ apps/comment/urls/mobile.py | 9 +++ apps/comment/urls/web.py | 9 +++ apps/comment/views/__init__.py | 0 apps/comment/views/common.py | 34 +++++++++++ apps/comment/views/mobile.py | 0 apps/comment/views/web.py | 0 apps/establishment/admin.py | 16 ++--- .../migrations/0015_delete_comment.py | 16 +++++ apps/establishment/models.py | 38 +----------- apps/establishment/serializers.py | 30 ++++------ project/settings/base.py | 1 + project/urls/web.py | 1 + 25 files changed, 271 insertions(+), 60 deletions(-) create mode 100644 apps/comment/__init__.py create mode 100644 apps/comment/admin.py create mode 100644 apps/comment/apps.py create mode 100644 apps/comment/migrations/0001_initial.py create mode 100644 apps/comment/migrations/__init__.py create mode 100644 apps/comment/models.py create mode 100644 apps/comment/serializers/__init__.py create mode 100644 apps/comment/serializers/common.py create mode 100644 apps/comment/serializers/mobile.py create mode 100644 apps/comment/serializers/web.py create mode 100644 apps/comment/tests.py create mode 100644 apps/comment/urls/__init__.py create mode 100644 apps/comment/urls/common.py create mode 100644 apps/comment/urls/mobile.py create mode 100644 apps/comment/urls/web.py create mode 100644 apps/comment/views/__init__.py create mode 100644 apps/comment/views/common.py create mode 100644 apps/comment/views/mobile.py create mode 100644 apps/comment/views/web.py create mode 100644 apps/establishment/migrations/0015_delete_comment.py diff --git a/apps/comment/__init__.py b/apps/comment/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/comment/admin.py b/apps/comment/admin.py new file mode 100644 index 00000000..855f6b3e --- /dev/null +++ b/apps/comment/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from . import models + + +@admin.register(models.Comment) +class CommentModelAdmin(admin.ModelAdmin): + """Model admin for model Comment""" diff --git a/apps/comment/apps.py b/apps/comment/apps.py new file mode 100644 index 00000000..d19caa6c --- /dev/null +++ b/apps/comment/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class CommentConfig(AppConfig): + name = 'comment' + verbose_name = _('comment') + verbose_name_plural = _('comments') diff --git a/apps/comment/migrations/0001_initial.py b/apps/comment/migrations/0001_initial.py new file mode 100644 index 00000000..9691d945 --- /dev/null +++ b/apps/comment/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 2.2.4 on 2019-09-04 14:03 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('text', models.TextField(verbose_name='Comment text')), + ('mark', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='Mark')), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'Comment', + 'verbose_name_plural': 'Comments', + }, + ), + ] diff --git a/apps/comment/migrations/__init__.py b/apps/comment/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/comment/models.py b/apps/comment/models.py new file mode 100644 index 00000000..ca6d39ec --- /dev/null +++ b/apps/comment/models.py @@ -0,0 +1,51 @@ +"""Models for app comment.""" +from django.contrib.contenttypes import fields as generic +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from account.models import User +from utils.models import ProjectBaseMixin + + +class CommentQuerySet(models.QuerySet): + """QuerySets for Comment model.""" + + def by_user(self, user: User): + """Return comments by author""" + return self.filter(user=user) + + def annotate_is_mine_status(self, user): + """Annotate belonging status""" + return self.annotate(is_mine=models.Case( + models.When( + models.Q(user=user), + then=True + ), + default=False, + output_field=models.BooleanField(default=False) + )) + + +class Comment(ProjectBaseMixin): + """Comment model.""" + text = models.TextField(verbose_name=_('Comment text')) + mark = models.PositiveIntegerField(blank=True, null=True, default=None, + verbose_name=_('Mark')) + user = models.ForeignKey('account.User', + related_name='comments', + on_delete=models.CASCADE, + verbose_name=_('User')) + content_type = models.ForeignKey(generic.ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + + objects = CommentQuerySet.as_manager() + + class Meta: + """Meta class""" + verbose_name = _('Comment') + verbose_name_plural = _('Comments') + + def __str__(self): + """String representation""" + return str(self.user) diff --git a/apps/comment/serializers/__init__.py b/apps/comment/serializers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/comment/serializers/common.py b/apps/comment/serializers/common.py new file mode 100644 index 00000000..8d11995f --- /dev/null +++ b/apps/comment/serializers/common.py @@ -0,0 +1,60 @@ +"""Common serializers for app comment.""" +from rest_framework import serializers + +from comment import models +from establishment.models import Establishment + + +class CommentBaseMixin(serializers.Serializer): + """Comment base serializer mixin""" + # RESPONSE + nickname = serializers.CharField(read_only=True, + source='user.username') + profile_pic = serializers.ImageField(read_only=True, + source='user.image') + + +class CommentSerializer(CommentBaseMixin, serializers.ModelSerializer): + """Comment serializer""" + is_mine = serializers.BooleanField(read_only=True) + + class Meta: + """Serializer for model Comment""" + model = models.Comment + fields = [ + 'id', + 'user_id', + 'is_mine', + 'created', + 'text', + 'mark', + 'nickname', + 'profile_pic' + ] + + +class EstablishmentCommentCreateSerializer(CommentSerializer): + """Create comment serializer""" + mark = serializers.IntegerField() + establishment_id = serializers.PrimaryKeyRelatedField(queryset=Establishment.objects.all(), + source='content_object', + write_only=True) + + class Meta: + """Serializer for model Comment""" + model = models.Comment + fields = [ + 'created', + 'text', + 'mark', + 'nickname', + 'profile_pic', + 'establishment_id', + ] + + def create(self, validated_data): + """Override create method""" + validated_data.update({ + 'user': self.context.get('request').user + }) + return super().create(validated_data) diff --git a/apps/comment/serializers/mobile.py b/apps/comment/serializers/mobile.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/comment/serializers/web.py b/apps/comment/serializers/web.py new file mode 100644 index 00000000..6db37a03 --- /dev/null +++ b/apps/comment/serializers/web.py @@ -0,0 +1 @@ +"""Serializers for app comment.""" diff --git a/apps/comment/tests.py b/apps/comment/tests.py new file mode 100644 index 00000000..a39b155a --- /dev/null +++ b/apps/comment/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/apps/comment/urls/__init__.py b/apps/comment/urls/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/comment/urls/common.py b/apps/comment/urls/common.py new file mode 100644 index 00000000..7791f4e4 --- /dev/null +++ b/apps/comment/urls/common.py @@ -0,0 +1,12 @@ +"""Comment urlpaths.""" +from django.urls import path + +from comment.views import common as views + +app_name = 'comment' + +urlpatterns = [ + path('', views.CommentListView.as_view(), name='comment-list'), + path('create/', views.CommentCreateView.as_view(), name='comment-create'), + path('/', views.CommentRUD.as_view(), name='comment-rud'), +] diff --git a/apps/comment/urls/mobile.py b/apps/comment/urls/mobile.py new file mode 100644 index 00000000..8d58c5c7 --- /dev/null +++ b/apps/comment/urls/mobile.py @@ -0,0 +1,9 @@ +"""Mobile urlpaths.""" +from comment.urls.common import urlpatterns as common_urlpatterns + +app_name = 'comment' + +urlpatterns_api = [] + +urlpatterns = common_urlpatterns + \ + urlpatterns_api diff --git a/apps/comment/urls/web.py b/apps/comment/urls/web.py new file mode 100644 index 00000000..6141ceed --- /dev/null +++ b/apps/comment/urls/web.py @@ -0,0 +1,9 @@ +"""Web urlpaths.""" +from comment.urls.common import urlpatterns as common_urlpatterns + +app_name = 'comment' + +urlpatterns_api = [] + +urlpatterns = common_urlpatterns + \ + urlpatterns_api diff --git a/apps/comment/views/__init__.py b/apps/comment/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/comment/views/common.py b/apps/comment/views/common.py new file mode 100644 index 00000000..f492f4cb --- /dev/null +++ b/apps/comment/views/common.py @@ -0,0 +1,34 @@ +"""Views for app comment.""" +from rest_framework import generics +from rest_framework.permissions import AllowAny + +from comment.models import Comment +from comment.serializers import common as serializers + + +class CommentViewMixin: + """Mixin for Comment views""" + queryset = Comment.objects.order_by('-created') + serializer_class = serializers.CommentSerializer + + +class CommentListView(CommentViewMixin, generics.ListAPIView): + """View for retrieving list of comments.""" + permission_classes = (AllowAny, ) + + def get_queryset(self): + """Override get_queryset method.""" + return self.queryset.annotate_is_mine_status(user=self.request.user) + + +class CommentCreateView(CommentViewMixin, generics.CreateAPIView): + """View for create new comment.""" + serializer_class = serializers.EstablishmentCommentCreateSerializer + + +class CommentRUD(CommentViewMixin, generics.RetrieveUpdateDestroyAPIView): + """View for retrieve/update/destroy view.""" + + def get_queryset(self): + """Override get_queryset method.""" + return self.queryset.by_user(self.request.user) diff --git a/apps/comment/views/mobile.py b/apps/comment/views/mobile.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/comment/views/web.py b/apps/comment/views/web.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index 05ab36dd..19b34cdd 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -1,10 +1,12 @@ """Establishment admin conf.""" from django.contrib import admin from django.contrib.contenttypes.admin import GenericTabularInline +from django.utils.translation import gettext_lazy as _ + +from comment.models import Comment from establishment import models from main.models import Award, MetaDataContent from review import models as review_models -from django.utils.translation import gettext_lazy as _ @admin.register(models.EstablishmentType) @@ -44,13 +46,18 @@ class ReviewInline(GenericTabularInline): extra = 0 +class CommentInline(GenericTabularInline): + model = Comment + extra = 0 + + @admin.register(models.Establishment) class EstablishmentAdmin(admin.ModelAdmin): """Establishment admin.""" inlines = [ AwardInline, MetaDataContentInline, ContactPhoneInline, ContactEmailInline, - ReviewInline] + ReviewInline, CommentInline] @admin.register(models.EstablishmentSchedule) @@ -58,11 +65,6 @@ class EstablishmentSchedule(admin.ModelAdmin): """Establishment schedule""" -@admin.register(models.Comment) -class EstablishmentComment(admin.ModelAdmin): - """Establishment comments.""" - - @admin.register(models.Position) class PositionAdmin(admin.ModelAdmin): """Position admin.""" diff --git a/apps/establishment/migrations/0015_delete_comment.py b/apps/establishment/migrations/0015_delete_comment.py new file mode 100644 index 00000000..4b1cc007 --- /dev/null +++ b/apps/establishment/migrations/0015_delete_comment.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2.4 on 2019-09-04 13:12 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0014_establishment_website'), + ] + + operations = [ + migrations.DeleteModel( + name='Comment', + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index a27a83f7..7ab4bcfe 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1,11 +1,13 @@ """Establishment models.""" from functools import reduce + from django.contrib.contenttypes import fields as generic from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField + from location.models import Address from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField, TranslatedFieldsMixin, BaseAttributes) @@ -129,6 +131,7 @@ class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin): awards = generic.GenericRelation(to='main.Award') tags = generic.GenericRelation(to='main.MetaDataContent') reviews = generic.GenericRelation(to='review.Review') + comments = generic.GenericRelation(to='comment.Comment') objects = EstablishmentQuerySet.as_manager() @@ -345,38 +348,3 @@ class Menu(TranslatedFieldsMixin, BaseAttributes): class Meta: verbose_name = _('menu') verbose_name_plural = _('menu') - - -class CommentQuerySet(models.QuerySet): - """QuerySets for Comment model.""" - - def by_author(self, author): - """Return comments by author""" - return self.filter(author=author) - - -class Comment(ProjectBaseMixin): - """Comment model.""" - text = models.TextField(verbose_name=_('Comment text')) - mark = models.PositiveIntegerField(blank=True, null=True, - default=None, - verbose_name=_('Mark')) - author = models.ForeignKey('account.User', - related_name='comments', - on_delete=models.CASCADE, - verbose_name=_('Author')) - establishment = models.ForeignKey(Establishment, - related_name='comments', - on_delete=models.CASCADE, - verbose_name=_('Establishment')) - - objects = CommentQuerySet.as_manager() - - class Meta: - """Meta class""" - verbose_name = _('Comment') - verbose_name_plural = _('Comments') - - def __str__(self): - """String representation""" - return str(self.author) diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index ec4fb43e..0e30f402 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -1,6 +1,7 @@ """Establishment serializers.""" from rest_framework import serializers +from comment.serializers.common import CommentSerializer from establishment import models from location.serializers import AddressSerializer from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer @@ -106,23 +107,6 @@ class ReviewSerializer(serializers.ModelSerializer): ) -class CommentSerializer(serializers.ModelSerializer): - """Comment serializer""" - nickname = serializers.CharField(source='author.username') - profile_pic = serializers.ImageField(source='author.image') - - class Meta: - """Serializer for model Comment""" - model = models.Comment - fields = ( - 'created', - 'text', - 'mark', - 'nickname', - 'profile_pic' - ) - - class EstablishmentEmployeeSerializer(serializers.ModelSerializer): """Serializer for actual employees.""" @@ -154,7 +138,8 @@ class EstablishmentSerializer(serializers.ModelSerializer): phones = ContactPhonesSerializer(read_only=True, many=True, ) emails = ContactEmailsSerializer(read_only=True, many=True, ) reviews = ReviewSerializer(source='reviews.last', allow_null=True) - comments = CommentSerializer(many=True, allow_null=True) + # comments = CommentSerializer(many=True, allow_null=True) + comments = serializers.SerializerMethodField() employees = EstablishmentEmployeeSerializer(source='actual_establishment_employees', many=True) menu = MenuSerializers(source='menu_set', many=True, read_only=True) @@ -198,6 +183,15 @@ class EstablishmentSerializer(serializers.ModelSerializer): 'best_price_carte' ) + def get_comments(self, obj): + """Serializer method for comment field""" + request = self.context.get('request') + if request.user.is_authenticated: + return CommentSerializer(obj.comments.all().annotate_is_mine_status(user=request.user), + many=True).data + else: + return CommentSerializer(obj.comments.all(), many=True).data + def get_preview_image(self, obj): """Get preview image""" return obj.get_full_image_url(request=self.context.get('request'), diff --git a/project/settings/base.py b/project/settings/base.py index ff6e6b66..94827649 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -67,6 +67,7 @@ PROJECT_APPS = [ 'configuration.apps.ConfigurationConfig', 'timetable.apps.TimetableConfig', 'review.apps.ReviewConfig', + 'comment.apps.CommentConfig', ] EXTERNAL_APPS = [ diff --git a/project/urls/web.py b/project/urls/web.py index e3b7b230..b7f67722 100644 --- a/project/urls/web.py +++ b/project/urls/web.py @@ -28,4 +28,5 @@ urlpatterns = [ path('location/', include('location.urls')), path('main/', include('main.urls')), path('translation/', include('translation.urls')), + path('comments/', include('comment.urls.web')), ] From c6cbb9bd3633f39f177add59de1cf5138c580f3e Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 5 Sep 2019 09:54:43 +0300 Subject: [PATCH 02/51] fixed return in establishment serializer last published review --- apps/establishment/serializers.py | 10 +++++++--- apps/establishment/views.py | 4 ++-- apps/review/models.py | 4 ++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 0e30f402..9d30f478 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -137,8 +137,7 @@ class EstablishmentSerializer(serializers.ModelSerializer): allow_null=True) phones = ContactPhonesSerializer(read_only=True, many=True, ) emails = ContactEmailsSerializer(read_only=True, many=True, ) - reviews = ReviewSerializer(source='reviews.last', allow_null=True) - # comments = CommentSerializer(many=True, allow_null=True) + review = serializers.SerializerMethodField() comments = serializers.SerializerMethodField() employees = EstablishmentEmployeeSerializer(source='actual_establishment_employees', many=True) @@ -175,7 +174,7 @@ class EstablishmentSerializer(serializers.ModelSerializer): 'booking', 'phones', 'emails', - 'reviews', + 'review', 'comments', 'employees', 'menu', @@ -196,3 +195,8 @@ class EstablishmentSerializer(serializers.ModelSerializer): """Get preview image""" return obj.get_full_image_url(request=self.context.get('request'), thumbnail_key='establishment_preview') + + def get_review(self, obj): + """Serializer method for getting last published review""" + return ReviewSerializer(obj.reviews.by_status(status=review_models.Review.READY) + .order_by('-published_at').first()).data diff --git a/apps/establishment/views.py b/apps/establishment/views.py index 3d3566e8..9ae8679f 100644 --- a/apps/establishment/views.py +++ b/apps/establishment/views.py @@ -1,8 +1,9 @@ """Establishment app views.""" from rest_framework import generics, permissions + +from establishment import filters from establishment import models, serializers from utils.views import JWTGenericViewMixin -from establishment import filters class EstablishmentMixin: @@ -13,7 +14,6 @@ class EstablishmentMixin: def get_queryset(self): """Overrided method 'get_queryset'.""" - # todo: update ordering (last review) return models.Establishment.objects.all().prefetch_actual_employees() diff --git a/apps/review/models.py b/apps/review/models.py index 97eaad8c..d0076f68 100644 --- a/apps/review/models.py +++ b/apps/review/models.py @@ -19,6 +19,10 @@ class ReviewQuerySet(models.QuerySet): """Return reviews by year""" return self.filter(vintage=year) + def by_status(self, status): + """Filter by status""" + return self.filter(status=status) + class Review(BaseAttributes, TranslatedFieldsMixin): """Review model""" From 3cac53c77d5bd43ba2d9dae95759a283714904d0 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 5 Sep 2019 10:32:44 +0300 Subject: [PATCH 03/51] fixed comments in establishment list or detail endpoints --- apps/comment/serializers/common.py | 5 +++++ apps/establishment/serializers.py | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/comment/serializers/common.py b/apps/comment/serializers/common.py index 8d11995f..9c7e91d8 100644 --- a/apps/comment/serializers/common.py +++ b/apps/comment/serializers/common.py @@ -17,6 +17,7 @@ class CommentBaseMixin(serializers.Serializer): class CommentSerializer(CommentBaseMixin, serializers.ModelSerializer): """Comment serializer""" is_mine = serializers.BooleanField(read_only=True) + profile_pic = serializers.SerializerMethodField() class Meta: """Serializer for model Comment""" @@ -32,6 +33,10 @@ class CommentSerializer(CommentBaseMixin, serializers.ModelSerializer): 'profile_pic' ] + def get_profile_pic(self, obj): + """Get profile picture URL""" + return obj.user.get_full_image_url(request=self.context.get('request')) + class EstablishmentCommentCreateSerializer(CommentSerializer): """Create comment serializer""" diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 9d30f478..49209487 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -186,10 +186,13 @@ class EstablishmentSerializer(serializers.ModelSerializer): """Serializer method for comment field""" request = self.context.get('request') if request.user.is_authenticated: - return CommentSerializer(obj.comments.all().annotate_is_mine_status(user=request.user), + return CommentSerializer(obj.comments.annotate_is_mine_status(user=request.user), + context={'request': self.context.get('request')}, many=True).data else: - return CommentSerializer(obj.comments.all(), many=True).data + return CommentSerializer(obj.comments, + context={'request': self.context.get('request')}, + many=True).data def get_preview_image(self, obj): """Get preview image""" From a133cfe1dd8fc1fe581f0ec437f3ac4ef0ca69c8 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 5 Sep 2019 11:53:48 +0300 Subject: [PATCH 04/51] added default value of is_main flag in comment section for establishments --- apps/comment/models.py | 4 ++-- apps/establishment/serializers.py | 11 +++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/apps/comment/models.py b/apps/comment/models.py index ca6d39ec..470c46d9 100644 --- a/apps/comment/models.py +++ b/apps/comment/models.py @@ -18,11 +18,11 @@ class CommentQuerySet(models.QuerySet): """Annotate belonging status""" return self.annotate(is_mine=models.Case( models.When( - models.Q(user=user), + models.Q(user=user if user.is_authenticated else None), then=True ), default=False, - output_field=models.BooleanField(default=False) + output_field=models.BooleanField() )) diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 49209487..78a30131 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -185,14 +185,9 @@ class EstablishmentSerializer(serializers.ModelSerializer): def get_comments(self, obj): """Serializer method for comment field""" request = self.context.get('request') - if request.user.is_authenticated: - return CommentSerializer(obj.comments.annotate_is_mine_status(user=request.user), - context={'request': self.context.get('request')}, - many=True).data - else: - return CommentSerializer(obj.comments, - context={'request': self.context.get('request')}, - many=True).data + return CommentSerializer(obj.comments.annotate_is_mine_status(user=request.user), + context={'request': self.context.get('request')}, + many=True).data def get_preview_image(self, obj): """Get preview image""" From 111e859f74af65f8b73d6d35b9b7ddafbde5f64c Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 5 Sep 2019 15:50:52 +0300 Subject: [PATCH 05/51] refactored establishment serializers --- apps/establishment/serializers.py | 48 +++++++++++++++++++------------ apps/establishment/views.py | 4 +-- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 78a30131..5883ad42 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -122,15 +122,42 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer): fields = ('id', 'name', 'position_translated', 'awards') -class EstablishmentSerializer(serializers.ModelSerializer): +class EstablishmentListSerializer(serializers.ModelSerializer): """Serializer for Establishment model.""" name_translated = serializers.CharField(allow_null=True) - description_translated = serializers.CharField(allow_null=True) type = EstablishmentTypeSerializer(source='establishment_type') subtypes = EstablishmentSubTypeSerializer(many=True) address = AddressSerializer() tags = MetaDataContentSerializer(many=True) + preview_image = serializers.SerializerMethodField() + + class Meta: + """Meta class.""" + + model = models.Establishment + fields = ( + 'id', + 'name_translated', + 'price_level', + 'toque_number', + 'public_mark', + 'type', + 'subtypes', + 'preview_image', + 'address', + 'tags', + ) + + def get_preview_image(self, obj): + """Get preview image""" + return obj.get_full_image_url(request=self.context.get('request'), + thumbnail_key='establishment_preview') + + +class EstablishmentDetailSerializer(EstablishmentListSerializer): + """Serializer for Establishment model.""" + description_translated = serializers.CharField(allow_null=True) awards = AwardSerializer(many=True) schedule = EstablishmentScheduleSerializer(source='schedule.schedule', many=True, @@ -151,20 +178,10 @@ class EstablishmentSerializer(serializers.ModelSerializer): """Meta class.""" model = models.Establishment - fields = ( - 'id', - 'name_translated', + fields = EstablishmentListSerializer.Meta.fields + ( 'description_translated', - 'public_mark', 'price_level', - 'toque_number', - 'price_level', - 'type', - 'subtypes', 'image', - 'preview_image', - 'address', - 'tags', 'awards', 'schedule', 'website', @@ -189,11 +206,6 @@ class EstablishmentSerializer(serializers.ModelSerializer): context={'request': self.context.get('request')}, many=True).data - def get_preview_image(self, obj): - """Get preview image""" - return obj.get_full_image_url(request=self.context.get('request'), - thumbnail_key='establishment_preview') - def get_review(self, obj): """Serializer method for getting last published review""" return ReviewSerializer(obj.reviews.by_status(status=review_models.Review.READY) diff --git a/apps/establishment/views.py b/apps/establishment/views.py index 9ae8679f..36c56fdd 100644 --- a/apps/establishment/views.py +++ b/apps/establishment/views.py @@ -10,7 +10,6 @@ class EstablishmentMixin: """Establishment mixin.""" permission_classes = (permissions.AllowAny,) - serializer_class = serializers.EstablishmentSerializer def get_queryset(self): """Overrided method 'get_queryset'.""" @@ -19,7 +18,7 @@ class EstablishmentMixin: class EstablishmentListView(EstablishmentMixin, JWTGenericViewMixin, generics.ListAPIView): """Resource for getting a list of establishments.""" - + serializer_class = serializers.EstablishmentListSerializer filter_class = filters.EstablishmentFilter def get_queryset(self): @@ -30,6 +29,7 @@ class EstablishmentListView(EstablishmentMixin, JWTGenericViewMixin, generics.Li class EstablishmentRetrieveView(EstablishmentMixin, JWTGenericViewMixin, generics.RetrieveAPIView): """Resource for getting a establishment.""" + serializer_class = serializers.EstablishmentDetailSerializer class EstablishmentTypeListView(JWTGenericViewMixin, generics.ListAPIView): From defe6abf4cd9e86b2f8b7be4367154769aead9a1 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 5 Sep 2019 17:19:10 +0300 Subject: [PATCH 06/51] refactored account and authorization endpoints --- apps/account/admin.py | 5 +- apps/account/serializers/common.py | 147 ++++++++++++++++++++++- apps/account/serializers/web.py | 144 ---------------------- apps/account/urls/common.py | 9 +- apps/account/urls/web.py | 8 -- apps/account/views/common.py | 94 ++++++++++++++- apps/account/views/web.py | 105 ---------------- apps/authorization/serializers/common.py | 40 ++++++ apps/authorization/urls/common.py | 3 +- apps/authorization/views/common.py | 20 +++ 10 files changed, 306 insertions(+), 269 deletions(-) diff --git a/apps/account/admin.py b/apps/account/admin.py index e9c853bb..938be965 100644 --- a/apps/account/admin.py +++ b/apps/account/admin.py @@ -11,8 +11,9 @@ class UserAdmin(BaseUserAdmin): """User model admin settings.""" list_display = ('id', 'username', 'short_name', 'date_joined', 'is_active', - 'is_staff', 'is_superuser',) - list_filter = ('is_active', 'is_staff', 'is_superuser', 'groups',) + 'is_staff', 'is_superuser', 'email_confirmed') + list_filter = ('is_active', 'is_staff', 'is_superuser', 'email_confirmed', + 'groups',) search_fields = ('email', 'first_name', 'last_name') readonly_fields = ('last_login', 'date_joined',) fieldsets = ( diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index f2dae3c2..4a942c52 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -1,20 +1,159 @@ """Common account serializers""" +from django.conf import settings +from django.contrib.auth import password_validation as password_validators from fcm_django.models import FCMDevice -from rest_framework import serializers, exceptions +from rest_framework import exceptions +from rest_framework import serializers -from account import models +from account import models, tasks +from utils import exceptions as utils_exceptions # User serializers class UserSerializer(serializers.ModelSerializer): """User serializer.""" + # RESPONSE + email_confirmed = serializers.BooleanField() + + # REQUEST + image = serializers.ImageField(required=False) + email = serializers.EmailField(required=False) + username = serializers.CharField(required=False) + class Meta: model = models.User fields = [ - 'first_name', - 'last_name', + 'image', + 'email', + 'email_confirmed', + 'username', ] + def validate_email(self, value): + """Validate email value""" + if value == self.instance.email: + raise serializers.ValidationError() + if not self.instance.email_confirmed: + raise serializers.ValidationError() + return value + + def update(self, instance, validated_data): + """ + Override update method + """ + if 'email' in validated_data: + validated_data['email_confirmed'] = False + instance = super().update(instance, validated_data) + # Send verification link on user email for change email address + if settings.USE_CELERY: + tasks.confirm_new_email_address.delay(instance.id) + else: + tasks.confirm_new_email_address(instance.id) + return instance + + +class ChangePasswordSerializer(serializers.ModelSerializer): + """Serializer for model User.""" + + password = serializers.CharField(write_only=True) + + class Meta: + """Meta class""" + model = models.User + fields = ('password', ) + + def validate(self, attrs): + """Override validate method""" + password = attrs.get('password') + try: + # Compare new password with the old ones + if self.instance.check_password(raw_password=password): + raise utils_exceptions.PasswordsAreEqual() + # Validate password + password_validators.validate_password(password=password) + except serializers.ValidationError as e: + raise serializers.ValidationError(str(e)) + else: + return attrs + + def update(self, instance, validated_data): + """Override update method""" + # Update user password from instance + instance.set_password(validated_data.get('password')) + instance.save() + + # Expire tokens + instance.expire_access_tokens() + instance.expire_refresh_tokens() + return instance + + +class ChangeEmailSerializer(serializers.ModelSerializer): + """Change user email serializer""" + + class Meta: + """Meta class""" + model = models.User + fields = ( + 'email', + ) + + def validate_email(self, value): + """Validate email value""" + if value == self.instance.email: + raise serializers.ValidationError() + return value + + def validate(self, attrs): + """Override validate method""" + email_confirmed = self.instance.email_confirmed + if not email_confirmed: + raise serializers.ValidationError() + return attrs + + def update(self, instance, validated_data): + """ + Override update method + """ + instance.email = validated_data.get('email') + instance.email_confirmed = False + instance.save() + # Send verification link on user email for change email address + if settings.USE_CELERY: + tasks.confirm_new_email_address.delay(instance.id) + else: + tasks.confirm_new_email_address(instance.id) + return instance + + +class ConfirmEmailSerializer(serializers.ModelSerializer): + """Confirm user email serializer""" + + class Meta: + """Meta class""" + model = models.User + fields = ( + 'email', + ) + + def validate(self, attrs): + """Override validate method""" + email_confirmed = self.instance.email_confirmed + if email_confirmed: + raise serializers.ValidationError() + return attrs + + def update(self, instance, validated_data): + """ + Override update method + """ + # Send verification link on user email for change email address + if settings.USE_CELERY: + tasks.confirm_new_email_address.delay(instance.id) + else: + tasks.confirm_new_email_address(instance.id) + return instance + # Firebase Cloud Messaging serializers class FCMDeviceSerializer(serializers.ModelSerializer): diff --git a/apps/account/serializers/web.py b/apps/account/serializers/web.py index 9d6c4c8c..cee66bfa 100644 --- a/apps/account/serializers/web.py +++ b/apps/account/serializers/web.py @@ -6,10 +6,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from account import models, tasks -from authorization.models import JWTRefreshToken from utils import exceptions as utils_exceptions -from utils.serializers import SourceSerializerMixin -from utils.tokens import GMRefreshToken class PasswordResetSerializer(serializers.ModelSerializer): @@ -94,144 +91,3 @@ class PasswordResetConfirmSerializer(serializers.ModelSerializer): # Overdue instance instance.overdue() return instance - - -class ChangePasswordSerializer(serializers.ModelSerializer): - """Serializer for model User.""" - - password = serializers.CharField(write_only=True) - - class Meta: - """Meta class""" - model = models.User - fields = ('password', ) - - def validate(self, attrs): - """Override validate method""" - password = attrs.get('password') - try: - # Compare new password with the old ones - if self.instance.check_password(raw_password=password): - raise utils_exceptions.PasswordsAreEqual() - # Validate password - password_validators.validate_password(password=password) - except serializers.ValidationError as e: - raise serializers.ValidationError(str(e)) - else: - return attrs - - def update(self, instance, validated_data): - """Override update method""" - # Update user password from instance - instance.set_password(validated_data.get('password')) - instance.save() - - # Expire tokens - instance.expire_access_tokens() - instance.expire_refresh_tokens() - return instance - - -class ChangeEmailSerializer(serializers.ModelSerializer): - """Change user email serializer""" - - class Meta: - """Meta class""" - model = models.User - fields = ( - 'email', - ) - - def validate_email(self, value): - """Validate email value""" - if value == self.instance.email: - raise serializers.ValidationError() - return value - - def validate(self, attrs): - """Override validate method""" - email_confirmed = self.instance.email_confirmed - if not email_confirmed: - raise serializers.ValidationError() - return attrs - - def update(self, instance, validated_data): - """ - Override update method - """ - instance.email = validated_data.get('email') - instance.email_confirmed = False - instance.save() - # Send verification link on user email for change email address - if settings.USE_CELERY: - tasks.confirm_new_email_address.delay(instance.id) - else: - tasks.confirm_new_email_address(instance.id) - return instance - - -class ConfirmEmailSerializer(serializers.ModelSerializer): - """Confirm user email serializer""" - - class Meta: - """Meta class""" - model = models.User - fields = ( - 'email', - ) - - def validate(self, attrs): - """Override validate method""" - email_confirmed = self.instance.email_confirmed - if email_confirmed: - raise serializers.ValidationError() - return attrs - - def update(self, instance, validated_data): - """ - Override update method - """ - # Send verification link on user email for change email address - if settings.USE_CELERY: - tasks.confirm_new_email_address.delay(instance.id) - else: - tasks.confirm_new_email_address(instance.id) - return instance - - -class RefreshTokenSerializer(SourceSerializerMixin): - """Serializer for refresh token view""" - refresh_token = serializers.CharField(read_only=True) - access_token = serializers.CharField(read_only=True) - - def get_request(self): - """Return request""" - return self.context.get('request') - - def validate(self, attrs): - """Override validate method""" - - cookie_refresh_token = self.get_request().COOKIES.get('refresh_token') - # Check if refresh_token in COOKIES - if not cookie_refresh_token: - raise utils_exceptions.NotValidRefreshTokenError() - - refresh_token = GMRefreshToken(cookie_refresh_token) - refresh_token_qs = JWTRefreshToken.objects.valid() \ - .by_jti(jti=refresh_token.payload.get('jti')) - # Check if the user has refresh token - if not refresh_token_qs.exists(): - raise utils_exceptions.NotValidRefreshTokenError() - - old_refresh_token = refresh_token_qs.first() - source = old_refresh_token.source - user = old_refresh_token.user - - # Expire existing tokens - old_refresh_token.expire() - old_refresh_token.access_token.expire() - - # Create new one for user - response = user.create_jwt_tokens(source=source) - - return response diff --git a/apps/account/urls/common.py b/apps/account/urls/common.py index 6a909588..ec380c2c 100644 --- a/apps/account/urls/common.py +++ b/apps/account/urls/common.py @@ -6,5 +6,12 @@ from account.views import common as views app_name = 'account' urlpatterns = [ - path('user/', views.UserView.as_view(), name='user-get-update'), + path('user/', views.UserRetrieveUpdateView.as_view(), + name='user-retrieve-update'), + path('change-password/', views.ChangePasswordView.as_view(), name='change-password'), + path('change-email/confirm///', views.ChangeEmailConfirmView.as_view(), + name='change-email-confirm'), + path('confirm-email/', views.ConfirmEmailView.as_view(), name='confirm-email'), + path('confirm-email///', views.ConfirmInactiveEmailView.as_view(), + name='inactive-email-confirm'), ] diff --git a/apps/account/urls/web.py b/apps/account/urls/web.py index 5c483d39..cc57f316 100644 --- a/apps/account/urls/web.py +++ b/apps/account/urls/web.py @@ -7,19 +7,11 @@ from account.views import web as views app_name = 'account' urlpatterns_api = [ - path('change-password/', views.ChangePasswordView.as_view(), name='change-password'), path('reset-password/', views.PasswordResetView.as_view(), name='password-reset'), path('form/reset-password///', views.FormPasswordResetConfirmView.as_view(), name='form-password-reset-confirm'), path('form/reset-password/success/', views.FormPasswordResetSuccessView.as_view(), name='form-password-reset-success'), - path('refresh-token/', views.RefreshTokenView.as_view(), name='refresh-token'), - path('change-email/', views.ChangeEmailView.as_view(), name='change-email'), - path('change-email/confirm///', views.ChangeEmailConfirmView.as_view(), - name='change-email-confirm'), - path('confirm-email/', views.ConfirmEmailView.as_view(), name='confirm-email'), - path('confirm-email///', views.ConfirmInactiveEmailView.as_view(), - name='inactive-email-confirm'), ] urlpatterns = urlpatterns_api + \ diff --git a/apps/account/views/common.py b/apps/account/views/common.py index 5e5f8734..cd17b87d 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -1,17 +1,23 @@ """Common account views""" +from django.utils.encoding import force_text +from django.utils.http import urlsafe_base64_decode from fcm_django.models import FCMDevice -from rest_framework import generics, status +from rest_framework import generics from rest_framework import permissions -from utils.permissions import IsAuthenticatedAndTokenIsValid +from rest_framework import status from rest_framework.response import Response from account import models from account.serializers import common as serializers +from utils import exceptions as utils_exceptions +from utils.models import GMTokenGenerator +from utils.views import (JWTUpdateAPIView, + JWTGenericViewMixin) # User views -class UserView(generics.RetrieveUpdateAPIView): - """### User update view.""" +class UserRetrieveUpdateView(generics.RetrieveUpdateAPIView): + """User update view.""" serializer_class = serializers.UserSerializer queryset = models.User.objects.active() @@ -19,6 +25,86 @@ class UserView(generics.RetrieveUpdateAPIView): return self.request.user +class ChangePasswordView(JWTUpdateAPIView): + """Change password view""" + serializer_class = serializers.ChangePasswordSerializer + queryset = models.User.objects.active() + + def patch(self, request, *args, **kwargs): + """Implement PUT method""" + serializer = self.get_serializer(instance=self.request.user, + data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(status=status.HTTP_200_OK) + + +class ConfirmEmailView(JWTGenericViewMixin): + """Confirm email view.""" + serializer_class = serializers.ConfirmEmailSerializer + queryset = models.User.objects.all() + + def patch(self, request, *args, **kwargs): + """Implement POST-method""" + # Get user instance + instance = self.request.user + + serializer = self.get_serializer(data=request.data, instance=instance) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(status=status.HTTP_200_OK) + + +class ChangeEmailConfirmView(JWTGenericViewMixin): + """View for confirm changing email""" + + permission_classes = (permissions.AllowAny,) + + def get(self, request, *args, **kwargs): + """Implement GET-method""" + uidb64 = kwargs.get('uidb64') + token = kwargs.get('token') + uid = force_text(urlsafe_base64_decode(uidb64)) + user_qs = models.User.objects.filter(pk=uid) + if user_qs.exists(): + user = user_qs.first() + if not GMTokenGenerator(GMTokenGenerator.CHANGE_EMAIL).check_token( + user, token): + raise utils_exceptions.NotValidTokenError() + # Approve email status + user.confirm_email() + # Expire user tokens + user.expire_access_tokens() + user.expire_refresh_tokens() + + return Response(status=status.HTTP_200_OK) + else: + raise utils_exceptions.UserNotFoundError() + + +class ConfirmInactiveEmailView(generics.GenericAPIView): + """View for confirm inactive email""" + + permission_classes = (permissions.AllowAny,) + + def get(self, request, *args, **kwargs): + """Implement GET-method""" + uidb64 = kwargs.get('uidb64') + token = kwargs.get('token') + uid = force_text(urlsafe_base64_decode(uidb64)) + user_qs = models.User.objects.filter(pk=uid) + if user_qs.exists(): + user = user_qs.first() + if not GMTokenGenerator(GMTokenGenerator.CHANGE_EMAIL).check_token( + user, token): + raise utils_exceptions.NotValidTokenError() + # Approve email status + user.confirm_email() + return Response(status=status.HTTP_200_OK) + else: + raise utils_exceptions.UserNotFoundError() + + # Firebase Cloud Messaging class FCMDeviceViewSet(generics.GenericAPIView): """FCMDevice registration view. diff --git a/apps/account/views/web.py b/apps/account/views/web.py index 39dd11fc..af147ba6 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -12,11 +12,9 @@ from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import never_cache from django.views.decorators.debug import sensitive_post_parameters from django.views.generic.edit import FormView -from rest_framework import generics from rest_framework import permissions from rest_framework import status from rest_framework import views -from rest_framework.permissions import AllowAny from rest_framework.response import Response from account import models @@ -25,7 +23,6 @@ from account.serializers import web as serializers from utils import exceptions as utils_exceptions from utils.models import GMTokenGenerator from utils.views import (JWTCreateAPIView, - JWTUpdateAPIView, JWTGenericViewMixin) @@ -76,108 +73,6 @@ class PasswordResetConfirmView(JWTGenericViewMixin): return Response(status=status.HTTP_200_OK) -class ChangePasswordView(JWTUpdateAPIView): - """Change password view""" - serializer_class = serializers.ChangePasswordSerializer - queryset = models.User.objects.active() - - def patch(self, request, *args, **kwargs): - """Implement PUT method""" - serializer = self.get_serializer(instance=self.request.user, - data=request.data) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(status=status.HTTP_200_OK) - - -class ChangeEmailView(JWTGenericViewMixin): - """Change user email view.""" - serializer_class = serializers.ChangeEmailSerializer - queryset = models.User.objects.all() - - def patch(self, request, *args, **kwargs): - """Implement POST-method""" - # Get user instance - instance = self.request.user - - serializer = self.get_serializer(data=request.data, instance=instance) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(status=status.HTTP_200_OK) - - -class ConfirmEmailView(ChangeEmailView): - """Confirm email view.""" - serializer_class = serializers.ConfirmEmailSerializer - - -class ChangeEmailConfirmView(JWTGenericViewMixin): - """View for confirm changing email""" - - permission_classes = (permissions.AllowAny,) - - def get(self, request, *args, **kwargs): - """Implement GET-method""" - uidb64 = kwargs.get('uidb64') - token = kwargs.get('token') - uid = force_text(urlsafe_base64_decode(uidb64)) - user_qs = models.User.objects.filter(pk=uid) - if user_qs.exists(): - user = user_qs.first() - if not GMTokenGenerator(GMTokenGenerator.CHANGE_EMAIL).check_token( - user, token): - raise utils_exceptions.NotValidTokenError() - # Approve email status - user.confirm_email() - # Expire user tokens - user.expire_access_tokens() - user.expire_refresh_tokens() - - return Response(status=status.HTTP_200_OK) - else: - raise utils_exceptions.UserNotFoundError() - - -class ConfirmInactiveEmailView(generics.GenericAPIView): - """View for confirm inactive email""" - - permission_classes = (permissions.AllowAny,) - - def get(self, request, *args, **kwargs): - """Implement GET-method""" - uidb64 = kwargs.get('uidb64') - token = kwargs.get('token') - uid = force_text(urlsafe_base64_decode(uidb64)) - user_qs = models.User.objects.filter(pk=uid) - if user_qs.exists(): - user = user_qs.first() - if not GMTokenGenerator(GMTokenGenerator.CHANGE_EMAIL).check_token( - user, token): - raise utils_exceptions.NotValidTokenError() - # Approve email status - user.confirm_email() - return Response(status=status.HTTP_200_OK) - else: - raise utils_exceptions.UserNotFoundError() - - -class RefreshTokenView(JWTGenericViewMixin): - """Refresh access_token""" - permission_classes = (AllowAny, ) - serializer_class = serializers.RefreshTokenSerializer - - def post(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - response = Response(serializer.data, status=status.HTTP_201_CREATED) - access_token = serializer.data.get('access_token') - refresh_token = serializer.data.get('refresh_token') - return self._put_cookies_in_response( - cookies=self._put_data_in_cookies(access_token=access_token, - refresh_token=refresh_token), - response=response) - - # Form view class PasswordContextMixin: extra_context = None diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index 5db0e5ed..551b31c9 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -8,9 +8,11 @@ from rest_framework import validators as rest_validators from account import models as account_models from authorization import tasks +from authorization.models import JWTRefreshToken from utils import exceptions as utils_exceptions from utils import methods as utils_methods from utils.serializers import SourceSerializerMixin +from utils.tokens import GMRefreshToken # Serializers @@ -123,6 +125,44 @@ class LogoutSerializer(SourceSerializerMixin): """Serializer for Logout endpoint.""" +class RefreshTokenSerializer(SourceSerializerMixin): + """Serializer for refresh token view""" + refresh_token = serializers.CharField(read_only=True) + access_token = serializers.CharField(read_only=True) + + def get_request(self): + """Return request""" + return self.context.get('request') + + def validate(self, attrs): + """Override validate method""" + + cookie_refresh_token = self.get_request().COOKIES.get('refresh_token') + # Check if refresh_token in COOKIES + if not cookie_refresh_token: + raise utils_exceptions.NotValidRefreshTokenError() + + refresh_token = GMRefreshToken(cookie_refresh_token) + refresh_token_qs = JWTRefreshToken.objects.valid() \ + .by_jti(jti=refresh_token.payload.get('jti')) + # Check if the user has refresh token + if not refresh_token_qs.exists(): + raise utils_exceptions.NotValidRefreshTokenError() + + old_refresh_token = refresh_token_qs.first() + source = old_refresh_token.source + user = old_refresh_token.user + + # Expire existing tokens + old_refresh_token.expire() + old_refresh_token.access_token.expire() + + # Create new one for user + response = user.create_jwt_tokens(source=source) + + return response + + # OAuth class OAuth2Serialzier(SourceSerializerMixin): """Serializer OAuth2 authorization""" diff --git a/apps/authorization/urls/common.py b/apps/authorization/urls/common.py index dd0fb54f..616f9d99 100644 --- a/apps/authorization/urls/common.py +++ b/apps/authorization/urls/common.py @@ -32,7 +32,8 @@ urlpatterns_jwt = [ path('signup/confirm///', views.VerifyEmailConfirmView.as_view(), name='signup-confirm'), path('login/', views.LoginByUsernameOrEmailView.as_view(), name='login'), - path('logout/', views.LogoutView.as_view(), name="logout") + path('logout/', views.LogoutView.as_view(), name="logout"), + path('refresh-token/', views.RefreshTokenView.as_view(), name='refresh-token'), ] diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index 97c1e4b8..bd6c8a26 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -226,3 +226,23 @@ class LogoutView(JWTGenericViewMixin): access_token_obj.refresh_token.expire() return Response(status=status.HTTP_204_NO_CONTENT) + + +# Refresh token +class RefreshTokenView(JWTGenericViewMixin): + """Refresh access_token""" + permission_classes = (permissions.AllowAny, ) + serializer_class = serializers.RefreshTokenSerializer + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + response = Response(serializer.data, status=status.HTTP_201_CREATED) + access_token = serializer.data.get('access_token') + refresh_token = serializer.data.get('refresh_token') + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies(access_token=access_token, + refresh_token=refresh_token), + response=response) + + From 411207868f44c1e861b7bea493df8d0314216bbe Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 5 Sep 2019 17:24:27 +0300 Subject: [PATCH 07/51] added field newsletter to UserSerializer --- apps/account/serializers/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index 4a942c52..019d23e8 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -19,6 +19,7 @@ class UserSerializer(serializers.ModelSerializer): image = serializers.ImageField(required=False) email = serializers.EmailField(required=False) username = serializers.CharField(required=False) + newsletter = serializers.BooleanField(required=False) class Meta: model = models.User @@ -27,6 +28,7 @@ class UserSerializer(serializers.ModelSerializer): 'email', 'email_confirmed', 'username', + 'newsletter', ] def validate_email(self, value): From 742d700d3cf97ce203ed4e10db9e7061a747d68a Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 6 Sep 2019 11:06:02 +0300 Subject: [PATCH 08/51] refactored comments --- apps/comment/serializers/common.py | 39 +------------------- apps/comment/urls/common.py | 9 +---- apps/comment/views/common.py | 33 ----------------- apps/establishment/serializers.py | 59 ++++++++++++++++++++++++++++-- apps/establishment/urls/common.py | 8 +++- apps/establishment/views.py | 38 +++++++++++++++++++ 6 files changed, 102 insertions(+), 84 deletions(-) diff --git a/apps/comment/serializers/common.py b/apps/comment/serializers/common.py index 9c7e91d8..3598be4c 100644 --- a/apps/comment/serializers/common.py +++ b/apps/comment/serializers/common.py @@ -2,20 +2,12 @@ from rest_framework import serializers from comment import models -from establishment.models import Establishment -class CommentBaseMixin(serializers.Serializer): - """Comment base serializer mixin""" - # RESPONSE +class CommentSerializer(serializers.ModelSerializer): + """Comment serializer""" nickname = serializers.CharField(read_only=True, source='user.username') - profile_pic = serializers.ImageField(read_only=True, - source='user.image') - - -class CommentSerializer(CommentBaseMixin, serializers.ModelSerializer): - """Comment serializer""" is_mine = serializers.BooleanField(read_only=True) profile_pic = serializers.SerializerMethodField() @@ -36,30 +28,3 @@ class CommentSerializer(CommentBaseMixin, serializers.ModelSerializer): def get_profile_pic(self, obj): """Get profile picture URL""" return obj.user.get_full_image_url(request=self.context.get('request')) - - -class EstablishmentCommentCreateSerializer(CommentSerializer): - """Create comment serializer""" - mark = serializers.IntegerField() - establishment_id = serializers.PrimaryKeyRelatedField(queryset=Establishment.objects.all(), - source='content_object', - write_only=True) - - class Meta: - """Serializer for model Comment""" - model = models.Comment - fields = [ - 'created', - 'text', - 'mark', - 'nickname', - 'profile_pic', - 'establishment_id', - ] - - def create(self, validated_data): - """Override create method""" - validated_data.update({ - 'user': self.context.get('request').user - }) - return super().create(validated_data) diff --git a/apps/comment/urls/common.py b/apps/comment/urls/common.py index 7791f4e4..07f8b122 100644 --- a/apps/comment/urls/common.py +++ b/apps/comment/urls/common.py @@ -1,12 +1,5 @@ """Comment urlpaths.""" -from django.urls import path - -from comment.views import common as views app_name = 'comment' -urlpatterns = [ - path('', views.CommentListView.as_view(), name='comment-list'), - path('create/', views.CommentCreateView.as_view(), name='comment-create'), - path('/', views.CommentRUD.as_view(), name='comment-rud'), -] +urlpatterns = [] diff --git a/apps/comment/views/common.py b/apps/comment/views/common.py index f492f4cb..19130d92 100644 --- a/apps/comment/views/common.py +++ b/apps/comment/views/common.py @@ -1,34 +1 @@ """Views for app comment.""" -from rest_framework import generics -from rest_framework.permissions import AllowAny - -from comment.models import Comment -from comment.serializers import common as serializers - - -class CommentViewMixin: - """Mixin for Comment views""" - queryset = Comment.objects.order_by('-created') - serializer_class = serializers.CommentSerializer - - -class CommentListView(CommentViewMixin, generics.ListAPIView): - """View for retrieving list of comments.""" - permission_classes = (AllowAny, ) - - def get_queryset(self): - """Override get_queryset method.""" - return self.queryset.annotate_is_mine_status(user=self.request.user) - - -class CommentCreateView(CommentViewMixin, generics.CreateAPIView): - """View for create new comment.""" - serializer_class = serializers.EstablishmentCommentCreateSerializer - - -class CommentRUD(CommentViewMixin, generics.RetrieveUpdateDestroyAPIView): - """View for retrieve/update/destroy view.""" - - def get_queryset(self): - """Override get_queryset method.""" - return self.queryset.by_user(self.request.user) diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 5883ad42..d37fca44 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -1,7 +1,8 @@ """Establishment serializers.""" from rest_framework import serializers -from comment.serializers.common import CommentSerializer +from comment import models as comment_models +from comment.serializers import common as comment_serializers from establishment import models from location.serializers import AddressSerializer from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer @@ -202,11 +203,61 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): def get_comments(self, obj): """Serializer method for comment field""" request = self.context.get('request') - return CommentSerializer(obj.comments.annotate_is_mine_status(user=request.user), - context={'request': self.context.get('request')}, - many=True).data + return comment_serializers.CommentSerializer(obj.comments.annotate_is_mine_status(user=request.user), + context={'request': self.context.get('request')}, + many=True).data def get_review(self, obj): """Serializer method for getting last published review""" return ReviewSerializer(obj.reviews.by_status(status=review_models.Review.READY) .order_by('-published_at').first()).data + + +class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer): + """Create comment serializer""" + mark = serializers.IntegerField() + + class Meta: + """Serializer for model Comment""" + model = comment_models.Comment + fields = [ + 'id', + 'created', + 'text', + 'mark', + 'nickname', + 'profile_pic', + ] + + def validate(self, attrs): + """Override validate method""" + # Check establishment object + establishment_id = self.context.get('request').parser_context.get('kwargs').get('pk') + establishment_qs = models.Establishment.objects.filter(id=establishment_id) + if not establishment_qs.exists(): + return serializers.ValidationError() + attrs['establishment'] = establishment_qs.first() + return attrs + + def create(self, validated_data, *args, **kwargs): + """Override create method""" + validated_data.update({ + 'user': self.context.get('request').user, + 'content_object': validated_data.pop('establishment') + }) + return super().create(validated_data) + + +class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer): + """Retrieve/Update/Destroy comment serializer.""" + class Meta: + """Meta class.""" + model = comment_models.Comment + fields = [ + 'id', + 'created', + 'text', + 'mark', + 'nickname', + 'profile_pic', + ] diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 605a703f..c296cd9d 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -1,7 +1,7 @@ """Establishment url patterns.""" -from django.urls import include, path -from establishment import views +from django.urls import path +from establishment import views app_name = 'establishment' @@ -9,4 +9,8 @@ app_name = 'establishment' urlpatterns = [ path('', views.EstablishmentListView.as_view(), name='list'), path('/', views.EstablishmentRetrieveView.as_view(), name='detail'), + path('/comment/', views.EstablishmentCommentCreateView.as_view(), + name='create-comment'), + path('/comment//', views.EstablishmentCommentRUDView.as_view(), + name='rud-comment'), ] \ No newline at end of file diff --git a/apps/establishment/views.py b/apps/establishment/views.py index 36c56fdd..b28ea094 100644 --- a/apps/establishment/views.py +++ b/apps/establishment/views.py @@ -1,6 +1,9 @@ """Establishment app views.""" + +from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions +from comment import models as comment_models from establishment import filters from establishment import models, serializers from utils.views import JWTGenericViewMixin @@ -39,3 +42,38 @@ class EstablishmentTypeListView(JWTGenericViewMixin, generics.ListAPIView): serializer_class = serializers.EstablishmentTypeSerializer queryset = models.EstablishmentType.objects.all() + +class EstablishmentCommentCreateView(generics.CreateAPIView): + """View for create new comment.""" + serializer_class = serializers.EstablishmentCommentCreateSerializer + queryset = comment_models.Comment.objects.order_by('-created') + + +class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView): + """View for retrieve/update/destroy establishment comment.""" + serializer_class = serializers.EstablishmentCommentRUDSerializer + queryset = models.Establishment.objects.all() + + def get_object(self): + """ + Returns the object the view is displaying. + """ + queryset = self.filter_queryset(self.get_queryset()) + lookup_url_kwargs = ('pk', 'comment_id') + + assert lookup_url_kwargs not in self.kwargs.keys(), ( + 'Expected view %s to be called with a URL keyword argument ' + 'named "%s". Fix your URL conf, or set the `.lookup_field` ' + 'attribute on the view correctly.' % + (self.__class__.__name__, lookup_url_kwargs) + ) + + establishment_obj = get_object_or_404(queryset, + pk=self.kwargs['pk']) + comment_obj = get_object_or_404(establishment_obj.comments.by_user(self.request.user), + pk=self.kwargs['comment_id']) + + # May raise a permission denied + self.check_object_permissions(self.request, comment_obj) + + return comment_obj From 20921719d314b073664b31509ed20cbd0b0182c4 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 6 Sep 2019 11:45:30 +0300 Subject: [PATCH 09/51] refactored account --- apps/account/serializers/common.py | 4 ---- apps/account/urls/common.py | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index 019d23e8..1d368c6e 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -83,10 +83,6 @@ class ChangePasswordSerializer(serializers.ModelSerializer): # Update user password from instance instance.set_password(validated_data.get('password')) instance.save() - - # Expire tokens - instance.expire_access_tokens() - instance.expire_refresh_tokens() return instance diff --git a/apps/account/urls/common.py b/apps/account/urls/common.py index ec380c2c..6416173a 100644 --- a/apps/account/urls/common.py +++ b/apps/account/urls/common.py @@ -6,8 +6,7 @@ from account.views import common as views app_name = 'account' urlpatterns = [ - path('user/', views.UserRetrieveUpdateView.as_view(), - name='user-retrieve-update'), + path('user/', views.UserRetrieveUpdateView.as_view(), name='user-retrieve-update'), path('change-password/', views.ChangePasswordView.as_view(), name='change-password'), path('change-email/confirm///', views.ChangeEmailConfirmView.as_view(), name='change-email-confirm'), From 54498913949d71a0a271abf029a00145214280b1 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 6 Sep 2019 11:55:56 +0300 Subject: [PATCH 10/51] added validation to UserSerializer --- apps/account/models.py | 8 ++++++-- apps/account/serializers/common.py | 6 ++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/account/models.py b/apps/account/models.py index 2f411466..01d75565 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -45,15 +45,19 @@ class UserQuerySet(models.QuerySet): return self.filter(is_active=switcher) def by_oauth2_access_token(self, token): - """Find user by access token""" + """Find users by access token""" return self.filter(oauth2_provider_accesstoken__token=token, oauth2_provider_accesstoken__expires__gt=timezone.now()) def by_oauth2_refresh_token(self, token): - """Find user by access token""" + """Find users by access token""" return self.filter(oauth2_provider_refreshtoken__token=token, oauth2_provider_refreshtoken__expires__gt=timezone.now()) + def by_username(self, username: str): + """Filter users by username.""" + return self.filter(username=username) + class User(ImageMixin, AbstractUser): """Base user model.""" diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index 1d368c6e..7163c237 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -39,6 +39,12 @@ class UserSerializer(serializers.ModelSerializer): raise serializers.ValidationError() return value + def validate_username(self, value): + """Validate username""" + if models.User.objects.by_username(username=value).exists(): + raise serializers.ValidationError() + return value + def update(self, instance, validated_data): """ Override update method From e1d6455e815c0e4c0e7074c79ed739f1136d575a Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 6 Sep 2019 13:47:37 +0300 Subject: [PATCH 11/51] added field cropped_image to User model --- .../migrations/0005_user_cropped_image.py | 20 +++++++++++++++++++ apps/account/models.py | 6 +++++- apps/account/serializers/common.py | 10 +++++++++- apps/utils/exceptions.py | 8 ++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 apps/account/migrations/0005_user_cropped_image.py diff --git a/apps/account/migrations/0005_user_cropped_image.py b/apps/account/migrations/0005_user_cropped_image.py new file mode 100644 index 00000000..0991d548 --- /dev/null +++ b/apps/account/migrations/0005_user_cropped_image.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.4 on 2019-09-06 10:45 + +from django.db import migrations +import easy_thumbnails.fields +import utils.methods + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0004_user_email_confirmed'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='cropped_image', + field=easy_thumbnails.fields.ThumbnailerImageField(blank=True, default=None, null=True, upload_to=utils.methods.image_path, verbose_name='Crop image'), + ), + ] diff --git a/apps/account/models.py b/apps/account/models.py index 01d75565..ee4d5d9d 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -1,5 +1,4 @@ """Account models""" - from django.conf import settings from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager from django.contrib.auth.tokens import default_token_generator as password_token_generator @@ -10,9 +9,11 @@ from django.utils import timezone from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode from django.utils.translation import ugettext_lazy as _ +from easy_thumbnails.fields import ThumbnailerImageField from rest_framework.authtoken.models import Token from authorization.models import Application +from utils.methods import image_path from utils.models import GMTokenGenerator from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin from utils.tokens import GMRefreshToken @@ -61,6 +62,9 @@ class UserQuerySet(models.QuerySet): class User(ImageMixin, AbstractUser): """Base user model.""" + cropped_image = ThumbnailerImageField(upload_to=image_path, + blank=True, null=True, default=None, + verbose_name=_('Crop image')) email = models.EmailField(_('email address'), blank=True, null=True, default=None) email_confirmed = models.BooleanField(_('email status'), default=False) diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index 7163c237..d4227877 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -13,10 +13,11 @@ from utils import exceptions as utils_exceptions class UserSerializer(serializers.ModelSerializer): """User serializer.""" # RESPONSE - email_confirmed = serializers.BooleanField() + email_confirmed = serializers.BooleanField(read_only=True) # REQUEST image = serializers.ImageField(required=False) + cropped_image = serializers.ImageField(required=False) email = serializers.EmailField(required=False) username = serializers.CharField(required=False) newsletter = serializers.BooleanField(required=False) @@ -24,6 +25,7 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = models.User fields = [ + 'cropped_image', 'image', 'email', 'email_confirmed', @@ -45,6 +47,12 @@ class UserSerializer(serializers.ModelSerializer): raise serializers.ValidationError() return value + def validate(self, attrs): + if ('cropped_image' in attrs or 'image' in attrs) and \ + ('cropped_image' not in attrs or 'image' not in attrs): + raise utils_exceptions.UserUpdateUploadImageError() + return attrs + def update(self, instance, validated_data): """ Override update method diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index a3db74ba..fcb40b45 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -120,6 +120,14 @@ class EmailConfirmedError(exceptions.APIException): default_detail = _('Email address is already confirmed') +class UserUpdateUploadImageError(exceptions.APIException): + """ + The exception should be raised when user tries upload an image without crop in request + """ + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _('Image invalid input.') + + class WrongAuthCredentials(AuthErrorMixin): """ The exception should be raised when credentials is not valid for this user From 1848bafd641c4f9aaa8dc61da14ef2895ee5fb28 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 6 Sep 2019 17:55:17 +0300 Subject: [PATCH 12/51] added new model favorites --- apps/establishment/serializers.py | 48 ++++++++++++++++++++++- apps/establishment/urls/common.py | 2 + apps/establishment/views.py | 34 +++++++++++++++- apps/favorites/__init__.py | 0 apps/favorites/admin.py | 11 ++++++ apps/favorites/apps.py | 7 ++++ apps/favorites/migrations/0001_initial.py | 34 ++++++++++++++++ apps/favorites/migrations/__init__.py | 0 apps/favorites/models.py | 44 +++++++++++++++++++++ apps/favorites/serializers.py | 1 + apps/favorites/tests.py | 1 + apps/utils/methods.py | 9 +++++ project/settings/base.py | 1 + 13 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 apps/favorites/__init__.py create mode 100644 apps/favorites/admin.py create mode 100644 apps/favorites/apps.py create mode 100644 apps/favorites/migrations/0001_initial.py create mode 100644 apps/favorites/migrations/__init__.py create mode 100644 apps/favorites/models.py create mode 100644 apps/favorites/serializers.py create mode 100644 apps/favorites/tests.py diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index d37fca44..230f2d2e 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -4,6 +4,7 @@ from rest_framework import serializers from comment import models as comment_models from comment.serializers import common as comment_serializers from establishment import models +from favorites.models import Favorites from location.serializers import AddressSerializer from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer from review import models as review_models @@ -175,6 +176,8 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): best_price_menu = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True) best_price_carte = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True) + in_favorites = serializers.SerializerMethodField() + class Meta: """Meta class.""" @@ -197,7 +200,7 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): 'employees', 'menu', 'best_price_menu', - 'best_price_carte' + 'best_price_carte', ) def get_comments(self, obj): @@ -261,3 +264,46 @@ class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer): 'nickname', 'profile_pic', ] + + +class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer): + """Create comment serializer""" + + class Meta: + """Serializer for model Comment""" + model = Favorites + fields = [ + 'id', + 'created', + ] + + def get_user(self): + """Get user from request""" + return self.context.get('request').user + + def validate(self, attrs): + """Override validate method""" + # Check establishment object + establishment_id = self.context.get('request').parser_context.get('kwargs').get('pk') + establishment_qs = models.Establishment.objects.filter(id=establishment_id) + + # Check establishment obj by pk from lookup_kwarg + if not establishment_qs.exists(): + return serializers.ValidationError() + + # Check existence in favorites + if self.get_user().favorites.by_content_type(app_label='establishment', + model='establishment', + object_id=establishment_id).exists(): + raise serializers.ValidationError() + + attrs['establishment'] = establishment_qs.first() + return attrs + + def create(self, validated_data, *args, **kwargs): + """Override create method""" + validated_data.update({ + 'user': self.get_user(), + 'content_object': validated_data.pop('establishment') + }) + return super().create(validated_data) diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index c296cd9d..96eef061 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -13,4 +13,6 @@ urlpatterns = [ name='create-comment'), path('/comment//', views.EstablishmentCommentRUDView.as_view(), name='rud-comment'), + path('/favorites/', views.EstablishmentFavoritesCreateView.as_view()), + path('/favorites//', views.EstablishmentFavoritesDestroyView.as_view()), ] \ No newline at end of file diff --git a/apps/establishment/views.py b/apps/establishment/views.py index b28ea094..dd7b003e 100644 --- a/apps/establishment/views.py +++ b/apps/establishment/views.py @@ -6,6 +6,7 @@ from rest_framework import generics, permissions from comment import models as comment_models from establishment import filters from establishment import models, serializers +from favorites import models as favorites_models from utils.views import JWTGenericViewMixin @@ -46,7 +47,7 @@ class EstablishmentTypeListView(JWTGenericViewMixin, generics.ListAPIView): class EstablishmentCommentCreateView(generics.CreateAPIView): """View for create new comment.""" serializer_class = serializers.EstablishmentCommentCreateSerializer - queryset = comment_models.Comment.objects.order_by('-created') + queryset = comment_models.Comment.objects.all() class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView): @@ -77,3 +78,34 @@ class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView): self.check_object_permissions(self.request, comment_obj) return comment_obj + + +class EstablishmentFavoritesCreateView(generics.CreateAPIView): + """View for adding establishment to favorites.""" + serializer_class = serializers.EstablishmentFavoritesCreateSerializer + queryset = favorites_models.Favorites.objects.all() + + +class EstablishmentFavoritesDestroyView(generics.DestroyAPIView): + """View for destroy establishment from favorites.""" + + def get_object(self): + """ + Returns the object the view is displaying. + """ + lookup_url_kwargs = ('pk', 'favorites_id') + assert lookup_url_kwargs not in self.kwargs.keys(), ( + 'Expected view %s to be called with a URL keyword argument ' + 'named "%s". Fix your URL conf, or set the `.lookup_field` ' + 'attribute on the view correctly.' % + (self.__class__.__name__, lookup_url_kwargs) + ) + + obj = get_object_or_404( + self.request.user.favorites.by_content_type(app_label='establishment', + model='establishment', + object_id=self.kwargs['pk']) + .filter(id=self.kwargs['favorites_id'])) + # May raise a permission denied + self.check_object_permissions(self.request, obj) + return obj diff --git a/apps/favorites/__init__.py b/apps/favorites/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/favorites/admin.py b/apps/favorites/admin.py new file mode 100644 index 00000000..1d9ff571 --- /dev/null +++ b/apps/favorites/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from favorites import models + + +@admin.register(models.Favorites) +class FavoritesModelAdmin(admin.ModelAdmin): + """Admin model for model Favorites""" + list_display = ('id', 'user', ) + list_filter = ('user', ) + diff --git a/apps/favorites/apps.py b/apps/favorites/apps.py new file mode 100644 index 00000000..01444052 --- /dev/null +++ b/apps/favorites/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class FavoritesConfig(AppConfig): + name = 'favorites' + verbose_name = _('Favorites') diff --git a/apps/favorites/migrations/0001_initial.py b/apps/favorites/migrations/0001_initial.py new file mode 100644 index 00000000..5d8bd341 --- /dev/null +++ b/apps/favorites/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.4 on 2019-09-06 12:36 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Favorites', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'Favorites', + 'verbose_name_plural': 'Favorites', + }, + ), + ] diff --git a/apps/favorites/migrations/__init__.py b/apps/favorites/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/favorites/models.py b/apps/favorites/models.py new file mode 100644 index 00000000..d2cc1db4 --- /dev/null +++ b/apps/favorites/models.py @@ -0,0 +1,44 @@ +from django.contrib.contenttypes import fields as generic +from django.contrib.contenttypes.models import ContentType +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from utils.methods import get_contenttype +from utils.models import ProjectBaseMixin + + +class FavoritesQuerySet(models.QuerySet): + """QuerySet for model Favorites""" + + def by_content_type(self, app_label, model, object_id): + """Filter QuerySet by ContentType.""" + return self.filter(content_type=get_contenttype(app_label=app_label, + model=model), + object_id=object_id) + + def by_user(self, user): + """Filter by user""" + return self.filter(user=user) + + +class Favorites(ProjectBaseMixin): + """Favorites model.""" + + user = models.ForeignKey('account.User', + on_delete=models.CASCADE, + related_name='favorites', + verbose_name=_('User')) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + + objects = FavoritesQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('Favorites') + verbose_name_plural = _('Favorites') + + def __str__(self): + """String representation.""" + return f'{self.id}' diff --git a/apps/favorites/serializers.py b/apps/favorites/serializers.py new file mode 100644 index 00000000..a759c6cd --- /dev/null +++ b/apps/favorites/serializers.py @@ -0,0 +1 @@ +"""Serializers for app favorites.""" diff --git a/apps/favorites/tests.py b/apps/favorites/tests.py new file mode 100644 index 00000000..a39b155a --- /dev/null +++ b/apps/favorites/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/apps/utils/methods.py b/apps/utils/methods.py index a34fcc07..e62138ce 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -4,6 +4,7 @@ import re import string from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.http.request import HttpRequest from django.utils.timezone import datetime from rest_framework.request import Request @@ -78,3 +79,11 @@ def generate_string_code(size=64, chars=string.ascii_lowercase + string.ascii_uppercase + string.digits): """Generate string code.""" return ''.join([random.SystemRandom().choice(chars) for _ in range(size)]) + + +def get_contenttype(app_label: str, model: str): + """Get ContentType instance by app_label and model""" + if app_label and model: + qs = ContentType.objects.filter(app_label=app_label, model=model) + if qs.exists(): + return qs.first() diff --git a/project/settings/base.py b/project/settings/base.py index 94827649..fceaa632 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -68,6 +68,7 @@ PROJECT_APPS = [ 'timetable.apps.TimetableConfig', 'review.apps.ReviewConfig', 'comment.apps.CommentConfig', + 'favorites.apps.FavoritesConfig', ] EXTERNAL_APPS = [ From e6144d8f33d99738eb4b6ee20f78cd01833e55c5 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 9 Sep 2019 09:25:38 +0300 Subject: [PATCH 13/51] small fix --- apps/establishment/serializers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 230f2d2e..37d0e9f2 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -176,8 +176,6 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): best_price_menu = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True) best_price_carte = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True) - in_favorites = serializers.SerializerMethodField() - class Meta: """Meta class.""" From 0f6786fb135cabeded10e1e55cf8ba8107c2dd33 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 9 Sep 2019 10:25:34 +0300 Subject: [PATCH 14/51] favorites refactoring --- apps/establishment/serializers.py | 3 ++- apps/establishment/urls/common.py | 4 ++-- apps/establishment/views.py | 10 ++++------ apps/utils/exceptions.py | 9 +++++++++ 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 37d0e9f2..ed3d1212 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -9,6 +9,7 @@ from location.serializers import AddressSerializer from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer from review import models as review_models from timetable.models import Timetable +from utils import exceptions as utils_exceptions class ContactPhonesSerializer(serializers.ModelSerializer): @@ -293,7 +294,7 @@ class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer): if self.get_user().favorites.by_content_type(app_label='establishment', model='establishment', object_id=establishment_id).exists(): - raise serializers.ValidationError() + raise utils_exceptions.FavoritesError() attrs['establishment'] = establishment_qs.first() return attrs diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 96eef061..12745c51 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -13,6 +13,6 @@ urlpatterns = [ name='create-comment'), path('/comment//', views.EstablishmentCommentRUDView.as_view(), name='rud-comment'), - path('/favorites/', views.EstablishmentFavoritesCreateView.as_view()), - path('/favorites//', views.EstablishmentFavoritesDestroyView.as_view()), + path('/favorites/add/', views.EstablishmentFavoritesCreateView.as_view()), + path('/favorites/delete/', views.EstablishmentFavoritesDestroyView.as_view()), ] \ No newline at end of file diff --git a/apps/establishment/views.py b/apps/establishment/views.py index dd7b003e..6324f17c 100644 --- a/apps/establishment/views.py +++ b/apps/establishment/views.py @@ -6,7 +6,6 @@ from rest_framework import generics, permissions from comment import models as comment_models from establishment import filters from establishment import models, serializers -from favorites import models as favorites_models from utils.views import JWTGenericViewMixin @@ -83,7 +82,6 @@ class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentFavoritesCreateView(generics.CreateAPIView): """View for adding establishment to favorites.""" serializer_class = serializers.EstablishmentFavoritesCreateSerializer - queryset = favorites_models.Favorites.objects.all() class EstablishmentFavoritesDestroyView(generics.DestroyAPIView): @@ -93,7 +91,7 @@ class EstablishmentFavoritesDestroyView(generics.DestroyAPIView): """ Returns the object the view is displaying. """ - lookup_url_kwargs = ('pk', 'favorites_id') + lookup_url_kwargs = ('pk',) assert lookup_url_kwargs not in self.kwargs.keys(), ( 'Expected view %s to be called with a URL keyword argument ' 'named "%s". Fix your URL conf, or set the `.lookup_field` ' @@ -102,10 +100,10 @@ class EstablishmentFavoritesDestroyView(generics.DestroyAPIView): ) obj = get_object_or_404( - self.request.user.favorites.by_content_type(app_label='establishment', + self.request.user.favorites.by_user(user=self.request.user) + .by_content_type(app_label='establishment', model='establishment', - object_id=self.kwargs['pk']) - .filter(id=self.kwargs['favorites_id'])) + object_id=self.kwargs['pk'])) # May raise a permission denied self.check_object_permissions(self.request, obj) return obj diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index fcb40b45..b2730b97 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -133,3 +133,12 @@ class WrongAuthCredentials(AuthErrorMixin): The exception should be raised when credentials is not valid for this user """ default_detail = _('Wrong authorization credentials') + + +class FavoritesError(exceptions.APIException): + """ + The exception should be thrown when you item that user + want add to favorites already exists. + """ + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _('Item is already in favorites.') From 7577ad330c3424974d8a356cd8fb0ab58e1ae7e4 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 9 Sep 2019 10:47:38 +0300 Subject: [PATCH 15/51] union favorites create/destroy in one endpoint --- apps/establishment/urls/common.py | 3 +-- apps/establishment/views.py | 8 ++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 12745c51..a73a3f3e 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -13,6 +13,5 @@ urlpatterns = [ name='create-comment'), path('/comment//', views.EstablishmentCommentRUDView.as_view(), name='rud-comment'), - path('/favorites/add/', views.EstablishmentFavoritesCreateView.as_view()), - path('/favorites/delete/', views.EstablishmentFavoritesDestroyView.as_view()), + path('/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view()), ] \ No newline at end of file diff --git a/apps/establishment/views.py b/apps/establishment/views.py index 6324f17c..077f8502 100644 --- a/apps/establishment/views.py +++ b/apps/establishment/views.py @@ -79,14 +79,10 @@ class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView): return comment_obj -class EstablishmentFavoritesCreateView(generics.CreateAPIView): - """View for adding establishment to favorites.""" +class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPIView): + """View for create/destroy establishment from favorites.""" serializer_class = serializers.EstablishmentFavoritesCreateSerializer - -class EstablishmentFavoritesDestroyView(generics.DestroyAPIView): - """View for destroy establishment from favorites.""" - def get_object(self): """ Returns the object the view is displaying. From 2f5240adfa0b98600e514f2a6cb28cad4549ddc8 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 9 Sep 2019 11:36:17 +0300 Subject: [PATCH 16/51] =?UTF-8?q?GM-67:=20=D0=92=D0=BD=D0=B5=D0=B4=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D1=87?= =?UTF-8?q?=D1=83=20=D1=84=D0=BB=D0=B0=D0=B3=D0=B0=20=D0=BD=D0=B0=D1=85?= =?UTF-8?q?=D0=BE=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B7=D0=B0=D0=B2?= =?UTF-8?q?=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=BD=D0=BD=D0=BE=D0=BC=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/establishment/serializers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index ed3d1212..bc83da09 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -214,6 +214,16 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): return ReviewSerializer(obj.reviews.by_status(status=review_models.Review.READY) .order_by('-published_at').first()).data + def get_in_favorites(self, obj): + """Get in_favorites status flag""" + user = self.context.get('request').user + if user.is_authenticated: + return obj.id in user.favorites.by_content_type(app_label='establishment', + model='establishment')\ + .values_list('object_id', flat=True) + else: + return False + class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer): """Create comment serializer""" From 273f74f91104a1889da73c4c8c57bb87618295ed Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 9 Sep 2019 11:37:57 +0300 Subject: [PATCH 17/51] added flag in_favorites in establishment --- apps/establishment/models.py | 14 ++++++++++++++ apps/establishment/serializers.py | 8 ++++++-- apps/establishment/views.py | 7 ++++--- apps/favorites/models.py | 9 ++++++--- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 7ab4bcfe..efe665ef 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -88,6 +88,20 @@ class EstablishmentQuerySet(models.QuerySet): 'position'), to_attr='actual_establishment_employees')) + def annotate_in_favorites(self, user): + """Annotate flag in_favorites""" + favorite_establishments = [] + if user.is_authenticated: + favorite_establishments = user.favorites.by_content_type(app_label='establishment', + model='establishment')\ + .values_list('object_id', flat=True) + return self.annotate(in_favorites=models.Case( + models.When( + id__in=favorite_establishments, + then=True), + default=False, + output_field=models.BooleanField(default=False))) + class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin): """Establishment model.""" diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index bc83da09..5b333493 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -134,6 +134,7 @@ class EstablishmentListSerializer(serializers.ModelSerializer): address = AddressSerializer() tags = MetaDataContentSerializer(many=True) preview_image = serializers.SerializerMethodField() + in_favorites = serializers.BooleanField() class Meta: """Meta class.""" @@ -150,6 +151,7 @@ class EstablishmentListSerializer(serializers.ModelSerializer): 'preview_image', 'address', 'tags', + 'in_favorites', ) def get_preview_image(self, obj): @@ -177,6 +179,8 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): best_price_menu = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True) best_price_carte = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True) + in_favorites = serializers.SerializerMethodField() + class Meta: """Meta class.""" @@ -302,8 +306,8 @@ class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer): # Check existence in favorites if self.get_user().favorites.by_content_type(app_label='establishment', - model='establishment', - object_id=establishment_id).exists(): + model='establishment')\ + .by_object_id(object_id=establishment_id).exists(): raise utils_exceptions.FavoritesError() attrs['establishment'] = establishment_qs.first() diff --git a/apps/establishment/views.py b/apps/establishment/views.py index 077f8502..f2a8571b 100644 --- a/apps/establishment/views.py +++ b/apps/establishment/views.py @@ -27,7 +27,8 @@ class EstablishmentListView(EstablishmentMixin, JWTGenericViewMixin, generics.Li def get_queryset(self): """Overrided method 'get_queryset'.""" qs = super(EstablishmentListView, self).get_queryset() - return qs.by_country_code(code=self.request.country_code) + return qs.by_country_code(code=self.request.country_code)\ + .annotate_in_favorites(user=self.request.user) class EstablishmentRetrieveView(EstablishmentMixin, JWTGenericViewMixin, generics.RetrieveAPIView): @@ -98,8 +99,8 @@ class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.D obj = get_object_or_404( self.request.user.favorites.by_user(user=self.request.user) .by_content_type(app_label='establishment', - model='establishment', - object_id=self.kwargs['pk'])) + model='establishment') + .by_object_id(object_id=self.kwargs['pk'])) # May raise a permission denied self.check_object_permissions(self.request, obj) return obj diff --git a/apps/favorites/models.py b/apps/favorites/models.py index d2cc1db4..6393701f 100644 --- a/apps/favorites/models.py +++ b/apps/favorites/models.py @@ -10,11 +10,14 @@ from utils.models import ProjectBaseMixin class FavoritesQuerySet(models.QuerySet): """QuerySet for model Favorites""" - def by_content_type(self, app_label, model, object_id): + def by_object_id(self, object_id: int): + """Filter by object_id""" + return self.filter(object_id=object_id) + + def by_content_type(self, app_label, model): """Filter QuerySet by ContentType.""" return self.filter(content_type=get_contenttype(app_label=app_label, - model=model), - object_id=object_id) + model=model)) def by_user(self, user): """Filter by user""" From 808178d56ce25b64e1dce1d0da36ae68da593e8e Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 9 Sep 2019 13:12:48 +0300 Subject: [PATCH 18/51] =?UTF-8?q?GM-70:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BF=D0=B0=D0=B3=D0=B8=D0=BD=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D0=B5=D0=B2=20=D0=B2=20=D0=BA=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=BE=D1=87=D0=BA=D0=B5=20=D1=80=D0=B5=D1=81=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/comment/models.py | 3 ++- apps/establishment/serializers.py | 9 --------- apps/establishment/urls/common.py | 8 +++++--- apps/establishment/views.py | 13 +++++++++++++ apps/favorites/models.py | 14 ++------------ apps/utils/querysets.py | 17 +++++++++++++++++ 6 files changed, 39 insertions(+), 25 deletions(-) create mode 100644 apps/utils/querysets.py diff --git a/apps/comment/models.py b/apps/comment/models.py index 470c46d9..fe781d4d 100644 --- a/apps/comment/models.py +++ b/apps/comment/models.py @@ -5,9 +5,10 @@ from django.utils.translation import gettext_lazy as _ from account.models import User from utils.models import ProjectBaseMixin +from utils.querysets import ContentTypeQuerySetMixin -class CommentQuerySet(models.QuerySet): +class CommentQuerySet(ContentTypeQuerySetMixin): """QuerySets for Comment model.""" def by_user(self, user: User): diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 5b333493..46d46439 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -170,7 +170,6 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): phones = ContactPhonesSerializer(read_only=True, many=True, ) emails = ContactEmailsSerializer(read_only=True, many=True, ) review = serializers.SerializerMethodField() - comments = serializers.SerializerMethodField() employees = EstablishmentEmployeeSerializer(source='actual_establishment_employees', many=True) menu = MenuSerializers(source='menu_set', many=True, read_only=True) @@ -199,20 +198,12 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): 'phones', 'emails', 'review', - 'comments', 'employees', 'menu', 'best_price_menu', 'best_price_carte', ) - def get_comments(self, obj): - """Serializer method for comment field""" - request = self.context.get('request') - return comment_serializers.CommentSerializer(obj.comments.annotate_is_mine_status(user=request.user), - context={'request': self.context.get('request')}, - many=True).data - def get_review(self, obj): """Serializer method for getting last published review""" return ReviewSerializer(obj.reviews.by_status(status=review_models.Review.READY) diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index a73a3f3e..0f2958eb 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -9,9 +9,11 @@ app_name = 'establishment' urlpatterns = [ path('', views.EstablishmentListView.as_view(), name='list'), path('/', views.EstablishmentRetrieveView.as_view(), name='detail'), - path('/comment/', views.EstablishmentCommentCreateView.as_view(), + path('/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'), + path('/comments/create/', views.EstablishmentCommentCreateView.as_view(), name='create-comment'), - path('/comment//', views.EstablishmentCommentRUDView.as_view(), + path('/comments//', views.EstablishmentCommentRUDView.as_view(), name='rud-comment'), - path('/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view()), + path('/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), + name='add-favorites'), ] \ No newline at end of file diff --git a/apps/establishment/views.py b/apps/establishment/views.py index f2a8571b..528d2377 100644 --- a/apps/establishment/views.py +++ b/apps/establishment/views.py @@ -50,6 +50,19 @@ class EstablishmentCommentCreateView(generics.CreateAPIView): queryset = comment_models.Comment.objects.all() +class EstablishmentCommentListView(generics.ListAPIView): + """View for return list of establishment comments.""" + permission_classes = (permissions.AllowAny,) + serializer_class = serializers.EstablishmentCommentCreateSerializer + + def get_queryset(self): + """Override get_queryset method""" + return comment_models.Comment.objects.by_content_type(app_label='establishment', + model='establishment')\ + .by_object_id(object_id=self.kwargs.get('pk'))\ + .order_by('-created') + + class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView): """View for retrieve/update/destroy establishment comment.""" serializer_class = serializers.EstablishmentCommentRUDSerializer diff --git a/apps/favorites/models.py b/apps/favorites/models.py index 6393701f..a30097a5 100644 --- a/apps/favorites/models.py +++ b/apps/favorites/models.py @@ -2,23 +2,13 @@ from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.translation import gettext_lazy as _ - -from utils.methods import get_contenttype +from utils.querysets import ContentTypeQuerySetMixin from utils.models import ProjectBaseMixin -class FavoritesQuerySet(models.QuerySet): +class FavoritesQuerySet(ContentTypeQuerySetMixin): """QuerySet for model Favorites""" - def by_object_id(self, object_id: int): - """Filter by object_id""" - return self.filter(object_id=object_id) - - def by_content_type(self, app_label, model): - """Filter QuerySet by ContentType.""" - return self.filter(content_type=get_contenttype(app_label=app_label, - model=model)) - def by_user(self, user): """Filter by user""" return self.filter(user=user) diff --git a/apps/utils/querysets.py b/apps/utils/querysets.py new file mode 100644 index 00000000..f25507f7 --- /dev/null +++ b/apps/utils/querysets.py @@ -0,0 +1,17 @@ +"""Utils QuerySet Mixins""" +from django.db import models + +from utils.methods import get_contenttype + + +class ContentTypeQuerySetMixin(models.QuerySet): + """QuerySet for ContentType""" + + def by_object_id(self, object_id: int): + """Filter by object_id""" + return self.filter(object_id=object_id) + + def by_content_type(self, app_label: str = 'favorites', model: str = 'favorites'): + """Filter QuerySet by ContentType.""" + return self.filter(content_type=get_contenttype(app_label=app_label, + model=model)) From efe1539dad1029425d45b18203e4563f3c3cc207 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 9 Sep 2019 13:14:43 +0300 Subject: [PATCH 19/51] Refactored ImageSerializer --- apps/gallery/serializers.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index 9efe2911..a7e3f0e1 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -10,8 +10,8 @@ class ImageSerializer(serializers.ModelSerializer): write_only=True) # RESPONSE - url = serializers.SerializerMethodField() - + url = serializers.ImageField(source='image', + read_only=True) class Meta: """Meta class""" @@ -22,6 +22,3 @@ class ImageSerializer(serializers.ModelSerializer): 'url' ) - def get_url(self, obj): - """Get absolute URL path""" - return obj.get_full_image_url(request=self.context.get('request')) From 64b101516d6de957499565f26554eb2dd7e869a3 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 9 Sep 2019 15:45:51 +0300 Subject: [PATCH 20/51] =?UTF-8?q?GM-81:=20=D0=94=D0=BE=D0=B0=D1=80=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=D1=82=D1=8C=20=D0=BC=D0=B5=D1=85=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BF=D1=80=D0=B8=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D0=BE=D0=BC=20email=20=D0=B8=20=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B8=20email?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/account/models.py | 7 ++++++ apps/account/serializers/common.py | 16 ++++++++++---- apps/account/urls/common.py | 2 -- apps/account/views/common.py | 27 ------------------------ apps/authorization/serializers/common.py | 3 +-- 5 files changed, 20 insertions(+), 35 deletions(-) diff --git a/apps/account/models.py b/apps/account/models.py index ee4d5d9d..cf883acd 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -181,6 +181,13 @@ class User(ImageMixin, AbstractUser): 'domain_uri': settings.DOMAIN_URI, 'site_name': settings.SITE_NAME}) + @property + def fullname(self): + fullname = [] + if self.first_name: fullname.append(self.first_name) + if self.last_name: fullname.append(self.last_name) + return ' '.join(fullname) + class ResetPasswordTokenQuerySet(models.QuerySet): """Reset password token query set""" diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index d4227877..9de2c3d0 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -14,31 +14,39 @@ class UserSerializer(serializers.ModelSerializer): """User serializer.""" # RESPONSE email_confirmed = serializers.BooleanField(read_only=True) + fullname = serializers.SerializerMethodField() # REQUEST + username = serializers.CharField(required=False) + first_name = serializers.CharField(required=False, write_only=True) + last_name = serializers.CharField(required=False, write_only=True) image = serializers.ImageField(required=False) cropped_image = serializers.ImageField(required=False) email = serializers.EmailField(required=False) - username = serializers.CharField(required=False) newsletter = serializers.BooleanField(required=False) class Meta: model = models.User fields = [ + 'username', + 'first_name', + 'last_name', + 'fullname', 'cropped_image', 'image', 'email', 'email_confirmed', - 'username', 'newsletter', ] + def get_fullname(self, obj): + """Get user full name""" + return obj.fullname + def validate_email(self, value): """Validate email value""" if value == self.instance.email: raise serializers.ValidationError() - if not self.instance.email_confirmed: - raise serializers.ValidationError() return value def validate_username(self, value): diff --git a/apps/account/urls/common.py b/apps/account/urls/common.py index 6416173a..0e8ae835 100644 --- a/apps/account/urls/common.py +++ b/apps/account/urls/common.py @@ -11,6 +11,4 @@ urlpatterns = [ path('change-email/confirm///', views.ChangeEmailConfirmView.as_view(), name='change-email-confirm'), path('confirm-email/', views.ConfirmEmailView.as_view(), name='confirm-email'), - path('confirm-email///', views.ConfirmInactiveEmailView.as_view(), - name='inactive-email-confirm'), ] diff --git a/apps/account/views/common.py b/apps/account/views/common.py index cd17b87d..2e182bd9 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -60,33 +60,6 @@ class ChangeEmailConfirmView(JWTGenericViewMixin): permission_classes = (permissions.AllowAny,) - def get(self, request, *args, **kwargs): - """Implement GET-method""" - uidb64 = kwargs.get('uidb64') - token = kwargs.get('token') - uid = force_text(urlsafe_base64_decode(uidb64)) - user_qs = models.User.objects.filter(pk=uid) - if user_qs.exists(): - user = user_qs.first() - if not GMTokenGenerator(GMTokenGenerator.CHANGE_EMAIL).check_token( - user, token): - raise utils_exceptions.NotValidTokenError() - # Approve email status - user.confirm_email() - # Expire user tokens - user.expire_access_tokens() - user.expire_refresh_tokens() - - return Response(status=status.HTTP_200_OK) - else: - raise utils_exceptions.UserNotFoundError() - - -class ConfirmInactiveEmailView(generics.GenericAPIView): - """View for confirm inactive email""" - - permission_classes = (permissions.AllowAny,) - def get(self, request, *args, **kwargs): """Implement GET-method""" uidb64 = kwargs.get('uidb64') diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index 551b31c9..e5c615e0 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -100,8 +100,7 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin, username_or_email = attrs.pop('username_or_email') password = attrs.pop('password') user_qs = account_models.User.objects.filter(Q(username=username_or_email) | - (Q(email=username_or_email) & - Q(email_confirmed=True))) + (Q(email=username_or_email))) if not user_qs.exists(): raise utils_exceptions.UserNotFoundError() else: From 89830ffdac6e5bf854eac07e1c26891bf705d6ee Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 9 Sep 2019 17:12:03 +0300 Subject: [PATCH 21/51] small fix --- apps/account/models.py | 7 ------- apps/account/serializers/common.py | 6 +----- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/apps/account/models.py b/apps/account/models.py index cf883acd..ee4d5d9d 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -181,13 +181,6 @@ class User(ImageMixin, AbstractUser): 'domain_uri': settings.DOMAIN_URI, 'site_name': settings.SITE_NAME}) - @property - def fullname(self): - fullname = [] - if self.first_name: fullname.append(self.first_name) - if self.last_name: fullname.append(self.last_name) - return ' '.join(fullname) - class ResetPasswordTokenQuerySet(models.QuerySet): """Reset password token query set""" diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index 9de2c3d0..58481960 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -14,7 +14,7 @@ class UserSerializer(serializers.ModelSerializer): """User serializer.""" # RESPONSE email_confirmed = serializers.BooleanField(read_only=True) - fullname = serializers.SerializerMethodField() + fullname = serializers.CharField(source='get_full_name') # REQUEST username = serializers.CharField(required=False) @@ -39,10 +39,6 @@ class UserSerializer(serializers.ModelSerializer): 'newsletter', ] - def get_fullname(self, obj): - """Get user full name""" - return obj.fullname - def validate_email(self, value): """Validate email value""" if value == self.instance.email: From 40605ff12344a17727dc1f2d2ac1dc2750701598 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 9 Sep 2019 18:00:17 +0300 Subject: [PATCH 22/51] change flag is_active after authorization --- apps/account/models.py | 7 ++----- apps/authorization/views/common.py | 2 -- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/account/models.py b/apps/account/models.py index ee4d5d9d..b0ae79d6 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -24,15 +24,12 @@ class UserManager(BaseUserManager): use_in_migrations = False - def make(self, username: str, email: str, password: str, - newsletter: bool, is_active: bool = False) -> object: + def make(self, username: str, email: str, password: str, newsletter: bool) -> object: """Register new user""" obj = self.model( username=username, email=email, - newsletter=newsletter, - is_active=is_active - ) + newsletter=newsletter) obj.set_password(password) obj.save() return obj diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index bd6c8a26..c77dd63e 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -182,8 +182,6 @@ class VerifyEmailConfirmView(JWTGenericViewMixin): raise utils_exceptions.NotValidTokenError() # Approve email status user.confirm_email() - # Set user status as active - user.approve() return Response(status=status.HTTP_200_OK) else: raise utils_exceptions.UserNotFoundError() From 96ecdc69ca021f96eeffe855d2d2a4da772d3420 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 09:45:48 +0300 Subject: [PATCH 23/51] fixed UserSerializer --- apps/account/serializers/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index 58481960..2f88d426 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -14,7 +14,7 @@ class UserSerializer(serializers.ModelSerializer): """User serializer.""" # RESPONSE email_confirmed = serializers.BooleanField(read_only=True) - fullname = serializers.CharField(source='get_full_name') + fullname = serializers.CharField(source='get_full_name', read_only=True) # REQUEST username = serializers.CharField(required=False) From 0647c46292de990081f62e7c6a67ba560942aa9e Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 12:00:32 +0300 Subject: [PATCH 24/51] =?UTF-8?q?GM-68:=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BC=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D0=B4=20=D0=B2=D0=BE=D0=B7=D0=B2=D1=80=D0=B0=D1=89=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B8?= =?UTF-8?q?=D0=B7=D0=B1=D1=80=D0=B0=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/establishment/serializers.py | 29 ++++++++++++++++++++--------- apps/favorites/serializers.py | 16 ++++++++++++++++ apps/favorites/urls.py | 12 ++++++++++++ apps/favorites/views.py | 25 +++++++++++++++++++++++++ project/urls/web.py | 1 + 5 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 apps/favorites/urls.py create mode 100644 apps/favorites/views.py diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 46d46439..c297ce54 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -125,22 +125,20 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer): fields = ('id', 'name', 'position_translated', 'awards') -class EstablishmentListSerializer(serializers.ModelSerializer): - """Serializer for Establishment model.""" - +class EstablishmentBaseSerializer(serializers.ModelSerializer): + """Base serializer for Establishment model.""" name_translated = serializers.CharField(allow_null=True) type = EstablishmentTypeSerializer(source='establishment_type') subtypes = EstablishmentSubTypeSerializer(many=True) address = AddressSerializer() tags = MetaDataContentSerializer(many=True) preview_image = serializers.SerializerMethodField() - in_favorites = serializers.BooleanField() class Meta: """Meta class.""" model = models.Establishment - fields = ( + fields = [ 'id', 'name_translated', 'price_level', @@ -151,8 +149,7 @@ class EstablishmentListSerializer(serializers.ModelSerializer): 'preview_image', 'address', 'tags', - 'in_favorites', - ) + ] def get_preview_image(self, obj): """Get preview image""" @@ -160,6 +157,20 @@ class EstablishmentListSerializer(serializers.ModelSerializer): thumbnail_key='establishment_preview') +class EstablishmentListSerializer(EstablishmentBaseSerializer): + """Serializer for Establishment model.""" + # Annotated fields + in_favorites = serializers.BooleanField(allow_null=True) + + class Meta: + """Meta class.""" + + model = models.Establishment + fields = EstablishmentBaseSerializer.Meta.fields + [ + 'in_favorites', + ] + + class EstablishmentDetailSerializer(EstablishmentListSerializer): """Serializer for Establishment model.""" description_translated = serializers.CharField(allow_null=True) @@ -184,7 +195,7 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): """Meta class.""" model = models.Establishment - fields = EstablishmentListSerializer.Meta.fields + ( + fields = EstablishmentListSerializer.Meta.fields + [ 'description_translated', 'price_level', 'image', @@ -202,7 +213,7 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): 'menu', 'best_price_menu', 'best_price_carte', - ) + ] def get_review(self, obj): """Serializer method for getting last published review""" diff --git a/apps/favorites/serializers.py b/apps/favorites/serializers.py index a759c6cd..d4485c54 100644 --- a/apps/favorites/serializers.py +++ b/apps/favorites/serializers.py @@ -1 +1,17 @@ """Serializers for app favorites.""" +from .models import Favorites +from rest_framework import serializers +from establishment.serializers import EstablishmentBaseSerializer + + +class FavoritesEstablishmentListSerializer(serializers.ModelSerializer): + """Serializer for model Favorites""" + detail = EstablishmentBaseSerializer(source='content_object') + + class Meta: + """Meta class.""" + model = Favorites + fields = ( + 'id', + 'detail', + ) diff --git a/apps/favorites/urls.py b/apps/favorites/urls.py new file mode 100644 index 00000000..4b8e8088 --- /dev/null +++ b/apps/favorites/urls.py @@ -0,0 +1,12 @@ +"""Favorites urlpaths.""" +from django.urls import path +from . import views + + +app_name = 'favorites' + +urlpatterns = [ + path('establishments/', views.FavoritesEstablishmentListView.as_view(), + name='establishment-list'), + path('remove//', views.FavoritesDestroyView.as_view(), name='delete-favorites'), +] diff --git a/apps/favorites/views.py b/apps/favorites/views.py new file mode 100644 index 00000000..c35edcf5 --- /dev/null +++ b/apps/favorites/views.py @@ -0,0 +1,25 @@ +"""Views for app favorites.""" +from rest_framework import generics +from .serializers import FavoritesEstablishmentListSerializer +from .models import Favorites + + +class FavoritesBaseView(generics.GenericAPIView): + """Base view for Favorites.""" + def get_queryset(self): + """Override get_queryset method.""" + return Favorites.objects.by_user(self.request.user) + + +class FavoritesEstablishmentListView(FavoritesBaseView, generics.ListAPIView): + """List views for favorites""" + serializer_class = FavoritesEstablishmentListSerializer + + def get_queryset(self): + """Override get_queryset method""" + return super().get_queryset().by_content_type(app_label='establishment', + model='establishment') + + +class FavoritesDestroyView(FavoritesBaseView, generics.DestroyAPIView): + """Destroy view for favorites""" diff --git a/project/urls/web.py b/project/urls/web.py index b7f67722..803f7243 100644 --- a/project/urls/web.py +++ b/project/urls/web.py @@ -29,4 +29,5 @@ urlpatterns = [ path('main/', include('main.urls')), path('translation/', include('translation.urls')), path('comments/', include('comment.urls.web')), + path('favorites/', include('favorites.urls')), ] From a4d7430379cd10d1d7ac9b2c51b7d2660b2172fe Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 12:19:21 +0300 Subject: [PATCH 25/51] small refactoring favorites --- apps/favorites/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/favorites/urls.py b/apps/favorites/urls.py index 4b8e8088..74748e94 100644 --- a/apps/favorites/urls.py +++ b/apps/favorites/urls.py @@ -1,12 +1,12 @@ """Favorites urlpaths.""" from django.urls import path -from . import views +from . import views app_name = 'favorites' urlpatterns = [ path('establishments/', views.FavoritesEstablishmentListView.as_view(), name='establishment-list'), - path('remove//', views.FavoritesDestroyView.as_view(), name='delete-favorites'), + path('/', views.FavoritesDestroyView.as_view(), name='remove-from-favorites'), ] From 500e470b39da3d91c72cfb9be86fd7b38e498bbc Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 15:01:10 +0300 Subject: [PATCH 26/51] Added CORSMiddleware --- apps/utils/middleware.py | 10 ++++++++++ project/settings/base.py | 7 +------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/utils/middleware.py b/apps/utils/middleware.py index 3cc57319..7203127c 100644 --- a/apps/utils/middleware.py +++ b/apps/utils/middleware.py @@ -32,3 +32,13 @@ def parse_cookies(get_response): return response return middleware + +class CORSMiddleware: + """Added parameter {Access-Control-Allow-Origin: *} to response""" + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + response["Access-Control-Allow-Origin"] = '*' + return response diff --git a/project/settings/base.py b/project/settings/base.py index fceaa632..4e8a7acc 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -79,7 +79,6 @@ EXTERNAL_APPS = [ 'rest_framework', 'rest_framework.authtoken', 'easy_select2', - 'corsheaders', 'oauth2_provider', 'social_django', 'rest_framework_social_oauth2', @@ -98,13 +97,13 @@ MIDDLEWARE = [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', 'oauth2_provider.middleware.OAuth2TokenMiddleware', - 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'utils.middleware.parse_cookies', + 'utils.middleware.CORSMiddleware', ] ROOT_URLCONF = 'project.urls' @@ -333,10 +332,6 @@ THUMBNAIL_ALIASES = { # Password reset RESETTING_TOKEN_EXPIRATION = 24 # hours -# CORS Config -CORS_ORIGIN_ALLOW_ALL = True -CORS_ALLOW_CREDENTIALS = True - GEOIP_PATH = os.path.join(PROJECT_ROOT, 'geoip_db') From c04d7b1bd3eb852e18d39006f94b878d8fdc6200 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 15:01:45 +0300 Subject: [PATCH 27/51] remove unused dependencies --- requirements/base.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 1c70e7c2..20773192 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -24,8 +24,5 @@ django-rest-framework-social-oauth2==1.1.0 django-extensions==2.2.1 -# CORS -django-cors-headers==3.0.2 - # JWT djangorestframework-simplejwt==4.3.0 \ No newline at end of file From cf1c10d6ecf62d6971e10417b815ef52f4ad7b97 Mon Sep 17 00:00:00 2001 From: "a.feteleu" Date: Tue, 10 Sep 2019 12:19:00 +0000 Subject: [PATCH 28/51] Revert "small refactoring favorites" This reverts commit a4d7430379cd10d1d7ac9b2c51b7d2660b2172fe --- apps/favorites/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/favorites/urls.py b/apps/favorites/urls.py index 74748e94..4b8e8088 100644 --- a/apps/favorites/urls.py +++ b/apps/favorites/urls.py @@ -1,12 +1,12 @@ """Favorites urlpaths.""" from django.urls import path - from . import views + app_name = 'favorites' urlpatterns = [ path('establishments/', views.FavoritesEstablishmentListView.as_view(), name='establishment-list'), - path('/', views.FavoritesDestroyView.as_view(), name='remove-from-favorites'), + path('remove//', views.FavoritesDestroyView.as_view(), name='delete-favorites'), ] From 7e468c539abcfb62b91496f148db69356196e053 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 15:35:24 +0300 Subject: [PATCH 29/51] change project settings --- apps/favorites/urls.py | 4 ++-- project/settings/base.py | 10 +++++++++- requirements/base.txt | 3 +++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/favorites/urls.py b/apps/favorites/urls.py index 4b8e8088..80498e65 100644 --- a/apps/favorites/urls.py +++ b/apps/favorites/urls.py @@ -1,12 +1,12 @@ """Favorites urlpaths.""" from django.urls import path -from . import views +from . import views app_name = 'favorites' urlpatterns = [ path('establishments/', views.FavoritesEstablishmentListView.as_view(), name='establishment-list'), - path('remove//', views.FavoritesDestroyView.as_view(), name='delete-favorites'), + path('remove//', views.FavoritesDestroyView.as_view(), name='remove-from-favorites'), ] diff --git a/project/settings/base.py b/project/settings/base.py index 4e8a7acc..995391c2 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -86,6 +86,7 @@ EXTERNAL_APPS = [ 'rest_framework_simplejwt.token_blacklist', 'solo', 'phonenumber_field', + 'corsheaders', ] @@ -93,6 +94,7 @@ INSTALLED_APPS = CONTRIB_APPS + PROJECT_APPS + EXTERNAL_APPS MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', @@ -103,7 +105,7 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'utils.middleware.parse_cookies', - 'utils.middleware.CORSMiddleware', + # 'utils.middleware.CORSMiddleware', ] ROOT_URLCONF = 'project.urls' @@ -376,6 +378,12 @@ CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html' # COOKIES COOKIES_MAX_AGE = 86400 # 24 hours +SESSION_COOKIE_SAMESITE = None + + +# CORS +CORS_ORIGIN_ALLOW_ALL = True +CORS_ALLOW_CREDENTIALS = True # UPLOAD FILES diff --git a/requirements/base.txt b/requirements/base.txt index 20773192..1c70e7c2 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -24,5 +24,8 @@ django-rest-framework-social-oauth2==1.1.0 django-extensions==2.2.1 +# CORS +django-cors-headers==3.0.2 + # JWT djangorestframework-simplejwt==4.3.0 \ No newline at end of file From 8e506bd5e1240089fd111738717bb54489af085d Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 17:05:41 +0300 Subject: [PATCH 30/51] added cookie settings for local and development --- project/settings/development.py | 6 +++++- project/settings/local.py | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/project/settings/development.py b/project/settings/development.py index 7ecc6aa2..2bf4d952 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -1,5 +1,4 @@ """Development settings.""" -from .base import * ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126'] @@ -11,3 +10,8 @@ SCHEMA_URI = 'http' DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm.id-east.ru' + + +# COOKIES +CSRF_COOKIE_DOMAIN = '.id-east.ru' +SESSION_COOKIE_DOMAIN = '.id-east.ru' diff --git a/project/settings/local.py b/project/settings/local.py index 1c73a857..6edf7124 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -12,6 +12,12 @@ DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'testserver.com:8000' DOMAIN_URI = '0.0.0.0:8000' + +# COOKIES +CSRF_COOKIE_DOMAIN = '0.0.0.0:8000' +SESSION_COOKIE_DOMAIN = '0.0.0.0:8000' + + # CELERY BROKER_URL = 'amqp://rabbitmq:5672' CELERY_RESULT_BACKEND = BROKER_URL From b5707c6b26cf387e7e16fa91767b3424cf54fdbf Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 17:12:56 +0300 Subject: [PATCH 31/51] Revert "added cookie settings for local and development" This reverts commit 8e506bd5 --- project/settings/development.py | 5 ----- project/settings/local.py | 6 ------ 2 files changed, 11 deletions(-) diff --git a/project/settings/development.py b/project/settings/development.py index 2bf4d952..99bfcc01 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -10,8 +10,3 @@ SCHEMA_URI = 'http' DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm.id-east.ru' - - -# COOKIES -CSRF_COOKIE_DOMAIN = '.id-east.ru' -SESSION_COOKIE_DOMAIN = '.id-east.ru' diff --git a/project/settings/local.py b/project/settings/local.py index 6edf7124..1c73a857 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -12,12 +12,6 @@ DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'testserver.com:8000' DOMAIN_URI = '0.0.0.0:8000' - -# COOKIES -CSRF_COOKIE_DOMAIN = '0.0.0.0:8000' -SESSION_COOKIE_DOMAIN = '0.0.0.0:8000' - - # CELERY BROKER_URL = 'amqp://rabbitmq:5672' CELERY_RESULT_BACKEND = BROKER_URL From 2d102f0005e40479261246bd9ba6c25310d148fb Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 17:16:03 +0300 Subject: [PATCH 32/51] added cookie settings for local and development --- project/settings/development.py | 5 +++++ project/settings/local.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/project/settings/development.py b/project/settings/development.py index 99bfcc01..f37a40b4 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -1,4 +1,5 @@ """Development settings.""" +from .base import * ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126'] @@ -10,3 +11,7 @@ SCHEMA_URI = 'http' DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm.id-east.ru' + +# COOKIES +CSRF_COOKIE_DOMAIN = '.id-east.ru' +SESSION_COOKIE_DOMAIN = '.id-east.ru' diff --git a/project/settings/local.py b/project/settings/local.py index 1c73a857..b40e8bef 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -17,6 +17,10 @@ BROKER_URL = 'amqp://rabbitmq:5672' CELERY_RESULT_BACKEND = BROKER_URL CELERY_BROKER_URL = BROKER_URL +# COOKIES +CSRF_COOKIE_DOMAIN = '0.0.0.0:8000' +SESSION_COOKIE_DOMAIN = '0.0.0.0:8000' + # LOGGING LOGGING = { From 530ce3852780ec667096de6a014133e19469f0f0 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 17:53:50 +0300 Subject: [PATCH 33/51] fixed account detail --- apps/account/models.py | 4 ---- apps/account/serializers/common.py | 4 +++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/account/models.py b/apps/account/models.py index b0ae79d6..508a52fd 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -52,10 +52,6 @@ class UserQuerySet(models.QuerySet): return self.filter(oauth2_provider_refreshtoken__token=token, oauth2_provider_refreshtoken__expires__gt=timezone.now()) - def by_username(self, username: str): - """Filter users by username.""" - return self.filter(username=username) - class User(ImageMixin, AbstractUser): """Base user model.""" diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index 2f88d426..deb0217e 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -43,11 +43,13 @@ class UserSerializer(serializers.ModelSerializer): """Validate email value""" if value == self.instance.email: raise serializers.ValidationError() + if models.User.objects.filter(email=value).exists(): + raise serializers.ValidationError() return value def validate_username(self, value): """Validate username""" - if models.User.objects.by_username(username=value).exists(): + if models.User.objects.filter(username=value).exists(): raise serializers.ValidationError() return value From 434df203da2f88ca1b69e02b84c173d27d7fe1ed Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 10 Sep 2019 18:03:01 +0300 Subject: [PATCH 34/51] fixed account detail --- apps/account/serializers/common.py | 20 +++++++++++++------- apps/authorization/serializers/common.py | 15 +++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index deb0217e..b6de5bb0 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -4,9 +4,11 @@ from django.contrib.auth import password_validation as password_validators from fcm_django.models import FCMDevice from rest_framework import exceptions from rest_framework import serializers +from rest_framework import validators as rest_validators from account import models, tasks from utils import exceptions as utils_exceptions +from utils import methods as utils_methods # User serializers @@ -17,12 +19,17 @@ class UserSerializer(serializers.ModelSerializer): fullname = serializers.CharField(source='get_full_name', read_only=True) # REQUEST - username = serializers.CharField(required=False) + username = serializers.CharField( + validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),), + write_only=True, + required=False) first_name = serializers.CharField(required=False, write_only=True) last_name = serializers.CharField(required=False, write_only=True) image = serializers.ImageField(required=False) cropped_image = serializers.ImageField(required=False) - email = serializers.EmailField(required=False) + email = serializers.EmailField( + validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),), + required=False) newsletter = serializers.BooleanField(required=False) class Meta: @@ -43,14 +50,13 @@ class UserSerializer(serializers.ModelSerializer): """Validate email value""" if value == self.instance.email: raise serializers.ValidationError() - if models.User.objects.filter(email=value).exists(): - raise serializers.ValidationError() return value def validate_username(self, value): - """Validate username""" - if models.User.objects.filter(username=value).exists(): - raise serializers.ValidationError() + """Custom username validation""" + valid = utils_methods.username_validator(username=value) + if not valid: + raise utils_exceptions.NotValidUsernameError() return value def validate(self, attrs): diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index e5c615e0..5817e191 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -21,8 +21,7 @@ class SignupSerializer(serializers.ModelSerializer): # REQUEST username = serializers.CharField( validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),), - write_only=True - ) + write_only=True) password = serializers.CharField(write_only=True) email = serializers.EmailField( validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),), @@ -38,21 +37,21 @@ class SignupSerializer(serializers.ModelSerializer): 'newsletter' ) - def validate_username(self, data): + def validate_username(self, value): """Custom username validation""" - valid = utils_methods.username_validator(username=data) + valid = utils_methods.username_validator(username=value) if not valid: raise utils_exceptions.NotValidUsernameError() - return data + return value - def validate_password(self, data): + def validate_password(self, value): """Custom password validation""" try: - password_validators.validate_password(password=data) + password_validators.validate_password(password=value) except serializers.ValidationError as e: raise serializers.ValidationError(str(e)) else: - return data + return value def create(self, validated_data): """Override create method""" From cf884ed906ccef57dc0484c98923b5fe41e345a3 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 11 Sep 2019 12:07:30 +0300 Subject: [PATCH 35/51] fixed UserSerializer, PasswordResetSerializer --- apps/account/serializers/common.py | 1 - apps/account/serializers/web.py | 11 +++--- apps/account/views/web.py | 13 +++++-- apps/authorization/serializers/common.py | 2 +- apps/utils/views.py | 46 ++++++++++++++++-------- 5 files changed, 49 insertions(+), 24 deletions(-) diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index b6de5bb0..bd28cb88 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -21,7 +21,6 @@ class UserSerializer(serializers.ModelSerializer): # REQUEST username = serializers.CharField( validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),), - write_only=True, required=False) first_name = serializers.CharField(required=False, write_only=True) last_name = serializers.CharField(required=False, write_only=True) diff --git a/apps/account/serializers/web.py b/apps/account/serializers/web.py index cee66bfa..60a68820 100644 --- a/apps/account/serializers/web.py +++ b/apps/account/serializers/web.py @@ -7,6 +7,7 @@ from rest_framework import serializers from account import models, tasks from utils import exceptions as utils_exceptions +from utils.methods import username_validator class PasswordResetSerializer(serializers.ModelSerializer): @@ -28,14 +29,15 @@ class PasswordResetSerializer(serializers.ModelSerializer): if user.is_anonymous: username_or_email = attrs.get('username_or_email') if not username_or_email: - raise serializers.ValidationError(_('Username or Email not requested')) + raise serializers.ValidationError(_('Username or Email not in request body.')) # Check user in DB + username_or_email = (username_or_email.lower() + if username_validator(username_or_email) is False + else username_or_email) user_qs = models.User.objects.filter(Q(email=username_or_email) | Q(username=username_or_email)) if user_qs.exists(): attrs['user'] = user_qs.first() - else: - raise utils_exceptions.UserNotFoundError() else: attrs['user'] = user return attrs @@ -48,8 +50,7 @@ class PasswordResetSerializer(serializers.ModelSerializer): obj = models.ResetPasswordToken.objects.create( user=user, ip_address=ip_address, - source=models.ResetPasswordToken.WEB - ) + source=models.ResetPasswordToken.WEB) if settings.USE_CELERY: tasks.send_reset_password_email.delay(obj.id) else: diff --git a/apps/account/views/web.py b/apps/account/views/web.py index af147ba6..4f10ccc2 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -22,16 +22,23 @@ from account.forms import SetPasswordForm from account.serializers import web as serializers from utils import exceptions as utils_exceptions from utils.models import GMTokenGenerator -from utils.views import (JWTCreateAPIView, - JWTGenericViewMixin) +from utils.views import JWTGenericViewMixin -class PasswordResetView(JWTCreateAPIView): +class PasswordResetView(JWTGenericViewMixin): """View for resetting user password""" permission_classes = (permissions.AllowAny, ) serializer_class = serializers.PasswordResetSerializer queryset = models.ResetPasswordToken.objects.valid() + def post(self, request, *args, **kwargs): + """Override create method""" + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + if serializer.validated_data.get('user'): + serializer.save() + return Response(status=status.HTTP_200_OK) + class PasswordResetConfirmView(JWTGenericViewMixin): """View for confirmation new password""" diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index 5817e191..f5e24fc9 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -58,7 +58,7 @@ class SignupSerializer(serializers.ModelSerializer): obj = account_models.User.objects.make( username=validated_data.get('username'), password=validated_data.get('password'), - email=validated_data.get('email'), + email=validated_data.get('email').lower(), newsletter=validated_data.get('newsletter')) # Send verification link on user email if settings.USE_CELERY: diff --git a/apps/utils/views.py b/apps/utils/views.py index d01d30cd..d129fb73 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -16,6 +16,13 @@ class JWTGenericViewMixin(generics.GenericAPIView): REFRESH_TOKEN_HTTP_ONLY = False REFRESH_TOKEN_SECURE = False + + LOCALE_HTTP_ONLY = False + LOCALE_SECURE = False + + COUNTRY_CODE_HTTP_ONLY = False + COUNTRY_CODE_SECURE = False + COOKIE = namedtuple('COOKIE', ['key', 'value', 'http_only', 'secure', 'max_age']) def _put_data_in_cookies(self, @@ -26,21 +33,32 @@ class JWTGenericViewMixin(generics.GenericAPIView): cookies it is list that contain namedtuples cookies would contain key, value and secure parameters. """ - COOKIES = list() + COOKIES = [] - # Write to cookie access and refresh token with secure flag - if access_token and refresh_token: - _access_token = self.COOKIE(key='access_token', - value=access_token, - http_only=self.ACCESS_TOKEN_HTTP_ONLY, - secure=self.ACCESS_TOKEN_SECURE, - max_age=settings.COOKIES_MAX_AGE if permanent else None) - _refresh_token = self.COOKIE(key='refresh_token', - value=refresh_token, - http_only=self.REFRESH_TOKEN_HTTP_ONLY, - secure=self.REFRESH_TOKEN_SECURE, - max_age=settings.COOKIES_MAX_AGE if permanent else None) - COOKIES.extend((_access_token, _refresh_token)) + if hasattr(self.request, 'locale'): + COOKIES.append(self.COOKIE(key='locale', + value=self.request.locale, + http_only=self.ACCESS_TOKEN_HTTP_ONLY, + secure=self.LOCALE_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if hasattr(self.request, 'country_code'): + COOKIES.append(self.COOKIE(key='country_code', + value=self.request.country_code, + http_only=self.COUNTRY_CODE_HTTP_ONLY, + secure=self.COUNTRY_CODE_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if access_token: + COOKIES.append(self.COOKIE(key='access_token', + value=access_token, + http_only=self.ACCESS_TOKEN_HTTP_ONLY, + secure=self.ACCESS_TOKEN_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) + if refresh_token: + COOKIES.append(self.COOKIE(key='refresh_token', + value=refresh_token, + http_only=self.REFRESH_TOKEN_HTTP_ONLY, + secure=self.REFRESH_TOKEN_SECURE, + max_age=settings.COOKIES_MAX_AGE if permanent else None)) return COOKIES def _put_cookies_in_response(self, cookies: list, response: Response): From 2622395006871b24aa5136fb0672917eeb4b56ef Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 11 Sep 2019 17:06:01 +0300 Subject: [PATCH 36/51] add local docker conf override to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 4aaea0c1..a32ff3df 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ logs/ /datadir/ /_files/ /geoip_db/ + +# dev +./docker-compose.override.yml \ No newline at end of file From f8c795cd031e1923f1c619c9e97a8976b843403e Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 11 Sep 2019 18:53:02 +0300 Subject: [PATCH 37/51] added redirect for ConfirmEmail and ResetPassword --- apps/account/models.py | 16 +++++----- apps/account/serializers/web.py | 28 ++++++++++------- apps/account/tasks.py | 4 +-- apps/account/views/common.py | 26 ++++++++++++++++ apps/authorization/models.py | 15 +++++----- apps/authorization/serializers/common.py | 30 +++++++++++-------- apps/authorization/tasks.py | 4 +-- apps/authorization/views/common.py | 10 ++++++- apps/utils/tokens.py | 11 ++++--- project/settings/stage.py | 17 +++++++++++ .../account/password_reset_email.html | 2 +- .../authorization/confirm_email.html | 5 ++-- 12 files changed, 114 insertions(+), 54 deletions(-) create mode 100644 project/settings/stage.py diff --git a/apps/account/models.py b/apps/account/models.py index 508a52fd..30255802 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -94,9 +94,9 @@ class User(ImageMixin, AbstractUser): self.is_active = switcher self.save() - def create_jwt_tokens(self, source: int): + def create_jwt_tokens(self, source: int = None): """Create JWT tokens for user""" - token = GMRefreshToken.for_user_by_source(self, source) + token = GMRefreshToken.for_user(self, source) return { 'access_token': str(token.access_token), 'refresh_token': str(token), @@ -154,15 +154,15 @@ class User(ImageMixin, AbstractUser): """Get base64 value for user by primary key identifier""" return urlsafe_base64_encode(force_bytes(self.pk)) - @property - def confirm_email_template(self): + def confirm_email_template(self, country_code): """Get confirm email template""" return render_to_string( template_name=settings.CONFIRM_EMAIL_TEMPLATE, context={'token': self.confirm_email_token, 'uidb64': self.get_user_uidb64, 'domain_uri': settings.DOMAIN_URI, - 'site_name': settings.SITE_NAME}) + 'site_name': settings.SITE_NAME, + 'country_code': country_code}) @property def change_email_template(self): @@ -245,15 +245,15 @@ class ResetPasswordToken(PlatformMixin, ProjectBaseMixin): """Generates a pseudo random code""" return password_token_generator.make_token(self.user) - @property - def reset_password_template(self): + def reset_password_template(self, country_code): """Get reset password template""" return render_to_string( template_name=settings.RESETTING_TOKEN_TEMPLATE, context={'token': self.key, 'uidb64': self.user.get_user_uidb64, 'domain_uri': settings.DOMAIN_URI, - 'site_name': settings.SITE_NAME}) + 'site_name': settings.SITE_NAME, + 'country_code': country_code}) @staticmethod def token_is_valid(user, token): diff --git a/apps/account/serializers/web.py b/apps/account/serializers/web.py index 60a68820..cdf616dc 100644 --- a/apps/account/serializers/web.py +++ b/apps/account/serializers/web.py @@ -22,21 +22,27 @@ class PasswordResetSerializer(serializers.ModelSerializer): 'username_or_email', ) + @property + def request(self): + """Get request from context""" + return self.context.get('request') + def validate(self, attrs): """Override validate method""" - user = self.context.get('request').user + user = self.request.user if user.is_anonymous: username_or_email = attrs.get('username_or_email') if not username_or_email: raise serializers.ValidationError(_('Username or Email not in request body.')) # Check user in DB - username_or_email = (username_or_email.lower() - if username_validator(username_or_email) is False - else username_or_email) - user_qs = models.User.objects.filter(Q(email=username_or_email) | - Q(username=username_or_email)) - if user_qs.exists(): + filters = {} + if username_validator(username_or_email): + filters.update({'username': username_or_email}) + else: + filters.update({'email': username_or_email.lower()}) + user_qs = models.User.objects.filter(**filters) + if user_qs.exists() and filters: attrs['user'] = user_qs.first() else: attrs['user'] = user @@ -45,16 +51,18 @@ class PasswordResetSerializer(serializers.ModelSerializer): def create(self, validated_data, *args, **kwargs): """Override create method""" user = validated_data.pop('user') - ip_address = self.context.get('request').META.get('REMOTE_ADDR') + ip_address = self.request.META.get('REMOTE_ADDR') obj = models.ResetPasswordToken.objects.create( user=user, ip_address=ip_address, source=models.ResetPasswordToken.WEB) if settings.USE_CELERY: - tasks.send_reset_password_email.delay(obj.id) + tasks.send_reset_password_email.delay(request_id=obj.id, + country_code=self.request.country_code) else: - tasks.send_reset_password_email(obj.id) + tasks.send_reset_password_email(request_id=obj.id, + country_code=self.request.country_code) return obj diff --git a/apps/account/tasks.py b/apps/account/tasks.py index 0367a59e..362daddf 100644 --- a/apps/account/tasks.py +++ b/apps/account/tasks.py @@ -11,13 +11,13 @@ logger = logging.getLogger(__name__) @shared_task -def send_reset_password_email(request_id): +def send_reset_password_email(request_id, country_code): """Send email to user for reset password.""" try: obj = models.ResetPasswordToken.objects.get(id=request_id) user = obj.user user.send_email(subject=_('Password resetting'), - message=obj.reset_password_template) + message=obj.reset_password_template(country_code)) except: logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n' f'DETAIL: Exception occurred for ResetPasswordToken instance: ' diff --git a/apps/account/views/common.py b/apps/account/views/common.py index 2e182bd9..360a1e5a 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -6,6 +6,7 @@ from rest_framework import generics from rest_framework import permissions from rest_framework import status from rest_framework.response import Response +from django.shortcuts import get_object_or_404 from account import models from account.serializers import common as serializers @@ -29,6 +30,31 @@ class ChangePasswordView(JWTUpdateAPIView): """Change password view""" serializer_class = serializers.ChangePasswordSerializer queryset = models.User.objects.active() + permission_classes = (permissions.AllowAny, ) + + def get_object(self): + """Overridden get_object method.""" + if not self.request.user.is_authenticated(): + queryset = self.filter_queryset(self.get_queryset()) + uidb64 = self.kwargs.get('uidb64') + + user_id = force_text(urlsafe_base64_decode(uidb64)) + token = self.kwargs.get('token') + + filter_kwargs = {'key': token, 'user_id': user_id} + password_reset_obj = get_object_or_404(models.ResetPasswordToken.objects.valid(), + **filter_kwargs) + if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token( + user=password_reset_obj.user, token=token): + raise utils_exceptions.NotValidAccessTokenError() + # todo: Add is_valid check status + obj = password_reset_obj.user + else: + obj = self.request.user + + # May raise a permission denied + self.check_object_permissions(self.request, obj) + return obj def patch(self, request, *args, **kwargs): """Implement PUT method""" diff --git a/apps/authorization/models.py b/apps/authorization/models.py index 69416420..c295329c 100644 --- a/apps/authorization/models.py +++ b/apps/authorization/models.py @@ -38,8 +38,8 @@ class Application(PlatformMixin, AbstractApplication): class JWTAccessTokenManager(models.Manager): """Manager for AccessToken model.""" - def add_to_db(self, user, access_token: AccessToken, - refresh_token: RefreshToken): + + def make(self, user, access_token: AccessToken, refresh_token: RefreshToken): """Create generated tokens to DB""" refresh_token_qs = JWTRefreshToken.objects.filter(user=user, jti=refresh_token.payload.get('jti')) @@ -106,18 +106,17 @@ class JWTAccessToken(ProjectBaseMixin): class JWTRefreshTokenManager(models.Manager): """Manager for model RefreshToken.""" - - def add_to_db(self, user, token: RefreshToken, source: int): - """Added generated refresh token to db""" + def make(self, user, token: RefreshToken, source: int): + """Make method""" jti = token[settings.SIMPLE_JWT.get('JTI_CLAIM')] exp = token['exp'] obj = self.model( user=user, jti=jti, - source=source, created_at=token.current_time, - expires_at=utils.datetime_from_epoch(exp), - ) + expires_at=utils.datetime_from_epoch(exp)) + if source: + obj.source = source obj.save() return obj diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index f5e24fc9..4d29887c 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -19,13 +19,9 @@ from utils.tokens import GMRefreshToken class SignupSerializer(serializers.ModelSerializer): """Signup serializer serializer mixin""" # REQUEST - username = serializers.CharField( - validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),), - write_only=True) + username = serializers.CharField(write_only=True) password = serializers.CharField(write_only=True) - email = serializers.EmailField( - validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),), - write_only=True) + email = serializers.EmailField(write_only=True) newsletter = serializers.BooleanField(write_only=True) class Meta: @@ -42,6 +38,14 @@ class SignupSerializer(serializers.ModelSerializer): valid = utils_methods.username_validator(username=value) if not valid: raise utils_exceptions.NotValidUsernameError() + if account_models.User.objects.filter(username__icontains=value).exists(): + raise serializers.ValidationError() + return value + + def validate_email(self, value): + """Validate email""" + if account_models.User.objects.filter(email__icontains=value).exists(): + raise serializers.ValidationError() return value def validate_password(self, value): @@ -62,9 +66,13 @@ class SignupSerializer(serializers.ModelSerializer): newsletter=validated_data.get('newsletter')) # Send verification link on user email if settings.USE_CELERY: - tasks.send_confirm_email.delay(obj.id) + tasks.send_confirm_email.delay( + user_id=obj.id, + country_code=self.context.get('request').country_code) else: - tasks.send_confirm_email(obj.id) + tasks.send_confirm_email( + user_id=obj.id, + country_code=self.context.get('request').country_code) return obj @@ -128,14 +136,10 @@ class RefreshTokenSerializer(SourceSerializerMixin): refresh_token = serializers.CharField(read_only=True) access_token = serializers.CharField(read_only=True) - def get_request(self): - """Return request""" - return self.context.get('request') - def validate(self, attrs): """Override validate method""" - cookie_refresh_token = self.get_request().COOKIES.get('refresh_token') + cookie_refresh_token = self.context.get('request').COOKIES.get('refresh_token') # Check if refresh_token in COOKIES if not cookie_refresh_token: raise utils_exceptions.NotValidRefreshTokenError() diff --git a/apps/authorization/tasks.py b/apps/authorization/tasks.py index a2ae4bb3..9947c2a3 100644 --- a/apps/authorization/tasks.py +++ b/apps/authorization/tasks.py @@ -10,12 +10,12 @@ logger = logging.getLogger(__name__) @shared_task -def send_confirm_email(user_id): +def send_confirm_email(user_id, country_code): """Send verification email to user.""" try: obj = account_models.User.objects.get(id=user_id) obj.send_email(subject=_('Email confirmation'), - message=obj.confirm_email_template) + message=obj.confirm_email_template(country_code)) except: logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n' f'DETAIL: Exception occurred for user: {user_id}') diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index c77dd63e..827ff108 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -182,7 +182,15 @@ class VerifyEmailConfirmView(JWTGenericViewMixin): raise utils_exceptions.NotValidTokenError() # Approve email status user.confirm_email() - return Response(status=status.HTTP_200_OK) + response = Response(status=status.HTTP_200_OK) + + # Create tokens + tokens = user.create_jwt_tokens() + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies( + access_token=tokens.get('access_token'), + refresh_token=tokens.get('refresh_token')), + response=response) else: raise utils_exceptions.UserNotFoundError() diff --git a/apps/utils/tokens.py b/apps/utils/tokens.py index e686b42b..a236bfa3 100644 --- a/apps/utils/tokens.py +++ b/apps/utils/tokens.py @@ -33,12 +33,11 @@ class GMBlacklistMixin(BlacklistMixin): """ @classmethod - def for_user_by_source(cls, user, source: int): + def for_user(cls, user, source: int = None): """Create a refresh token.""" token = super().for_user(user) token['user'] = user.get_user_info() - # Create a record in DB - JWTRefreshToken.objects.add_to_db(user=user, token=token, source=source) + JWTRefreshToken.objects.make(user=user, token=token, source=source) return token @@ -70,7 +69,7 @@ class GMRefreshToken(GMBlacklistMixin, GMToken, RefreshToken): # Create a record in DB user = User.objects.get(id=self.payload.get('user_id')) - JWTAccessToken.objects.add_to_db(user=user, - access_token=access_token, - refresh_token=self) + JWTAccessToken.objects.make(user=user, + access_token=access_token, + refresh_token=self) return access_token diff --git a/project/settings/stage.py b/project/settings/stage.py new file mode 100644 index 00000000..998abaa6 --- /dev/null +++ b/project/settings/stage.py @@ -0,0 +1,17 @@ +"""Stage settings.""" +from .base import * + +ALLOWED_HOSTS = ['gm-stage.id-east.ru', '95.213.204.126'] + +SEND_SMS = False +SMS_CODE_SHOW = True +USE_CELERY = False + +SCHEMA_URI = 'https' +DEFAULT_SUBDOMAIN = 'www' +SITE_DOMAIN_URI = 'id-east.ru' +DOMAIN_URI = 'gm-stage.id-east.ru' + +# COOKIES +CSRF_COOKIE_DOMAIN = '.id-east.ru' +SESSION_COOKIE_DOMAIN = '.id-east.ru' diff --git a/project/templates/account/password_reset_email.html b/project/templates/account/password_reset_email.html index 1a395cee..b743d71c 100644 --- a/project/templates/account/password_reset_email.html +++ b/project/templates/account/password_reset_email.html @@ -3,7 +3,7 @@ {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} -http://{{ domain_uri }}{% url 'web:account:form-password-reset-confirm' uidb64=uidb64 token=token %} +Reset link. {% endblock %} {% trans "Thanks for using our site!" %} diff --git a/project/templates/authorization/confirm_email.html b/project/templates/authorization/confirm_email.html index 7fa06aa5..9056525d 100644 --- a/project/templates/authorization/confirm_email.html +++ b/project/templates/authorization/confirm_email.html @@ -2,9 +2,8 @@ {% blocktrans %}You're receiving this email because you trying to register new account at {{ site_name }}.{% endblocktrans %} {% trans "Please confirm your email address to complete the registration:" %} -{% block signup_confirm %} -http://{{ domain_uri }}{% url 'auth:signup-confirm' uidb64=uidb64 token=token %} -{% endblock %} + +Confirmation link. {% trans "Thanks for using our site!" %} From 06c7d790bb4cf6529405b7909158793ebaa1ddd3 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 11 Sep 2019 18:53:02 +0300 Subject: [PATCH 38/51] added redirect for ConfirmEmail and ResetPassword --- apps/account/models.py | 16 +++++----- apps/account/serializers/web.py | 28 ++++++++++------- apps/account/tasks.py | 4 +-- apps/account/views/common.py | 26 ++++++++++++++++ apps/authorization/models.py | 15 +++++----- apps/authorization/serializers/common.py | 30 +++++++++++-------- apps/authorization/tasks.py | 4 +-- apps/authorization/views/common.py | 10 ++++++- apps/utils/tokens.py | 11 ++++--- project/settings/stage.py | 17 +++++++++++ .../account/password_reset_email.html | 2 +- .../authorization/confirm_email.html | 5 ++-- 12 files changed, 114 insertions(+), 54 deletions(-) create mode 100644 project/settings/stage.py diff --git a/apps/account/models.py b/apps/account/models.py index 508a52fd..30255802 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -94,9 +94,9 @@ class User(ImageMixin, AbstractUser): self.is_active = switcher self.save() - def create_jwt_tokens(self, source: int): + def create_jwt_tokens(self, source: int = None): """Create JWT tokens for user""" - token = GMRefreshToken.for_user_by_source(self, source) + token = GMRefreshToken.for_user(self, source) return { 'access_token': str(token.access_token), 'refresh_token': str(token), @@ -154,15 +154,15 @@ class User(ImageMixin, AbstractUser): """Get base64 value for user by primary key identifier""" return urlsafe_base64_encode(force_bytes(self.pk)) - @property - def confirm_email_template(self): + def confirm_email_template(self, country_code): """Get confirm email template""" return render_to_string( template_name=settings.CONFIRM_EMAIL_TEMPLATE, context={'token': self.confirm_email_token, 'uidb64': self.get_user_uidb64, 'domain_uri': settings.DOMAIN_URI, - 'site_name': settings.SITE_NAME}) + 'site_name': settings.SITE_NAME, + 'country_code': country_code}) @property def change_email_template(self): @@ -245,15 +245,15 @@ class ResetPasswordToken(PlatformMixin, ProjectBaseMixin): """Generates a pseudo random code""" return password_token_generator.make_token(self.user) - @property - def reset_password_template(self): + def reset_password_template(self, country_code): """Get reset password template""" return render_to_string( template_name=settings.RESETTING_TOKEN_TEMPLATE, context={'token': self.key, 'uidb64': self.user.get_user_uidb64, 'domain_uri': settings.DOMAIN_URI, - 'site_name': settings.SITE_NAME}) + 'site_name': settings.SITE_NAME, + 'country_code': country_code}) @staticmethod def token_is_valid(user, token): diff --git a/apps/account/serializers/web.py b/apps/account/serializers/web.py index 60a68820..cdf616dc 100644 --- a/apps/account/serializers/web.py +++ b/apps/account/serializers/web.py @@ -22,21 +22,27 @@ class PasswordResetSerializer(serializers.ModelSerializer): 'username_or_email', ) + @property + def request(self): + """Get request from context""" + return self.context.get('request') + def validate(self, attrs): """Override validate method""" - user = self.context.get('request').user + user = self.request.user if user.is_anonymous: username_or_email = attrs.get('username_or_email') if not username_or_email: raise serializers.ValidationError(_('Username or Email not in request body.')) # Check user in DB - username_or_email = (username_or_email.lower() - if username_validator(username_or_email) is False - else username_or_email) - user_qs = models.User.objects.filter(Q(email=username_or_email) | - Q(username=username_or_email)) - if user_qs.exists(): + filters = {} + if username_validator(username_or_email): + filters.update({'username': username_or_email}) + else: + filters.update({'email': username_or_email.lower()}) + user_qs = models.User.objects.filter(**filters) + if user_qs.exists() and filters: attrs['user'] = user_qs.first() else: attrs['user'] = user @@ -45,16 +51,18 @@ class PasswordResetSerializer(serializers.ModelSerializer): def create(self, validated_data, *args, **kwargs): """Override create method""" user = validated_data.pop('user') - ip_address = self.context.get('request').META.get('REMOTE_ADDR') + ip_address = self.request.META.get('REMOTE_ADDR') obj = models.ResetPasswordToken.objects.create( user=user, ip_address=ip_address, source=models.ResetPasswordToken.WEB) if settings.USE_CELERY: - tasks.send_reset_password_email.delay(obj.id) + tasks.send_reset_password_email.delay(request_id=obj.id, + country_code=self.request.country_code) else: - tasks.send_reset_password_email(obj.id) + tasks.send_reset_password_email(request_id=obj.id, + country_code=self.request.country_code) return obj diff --git a/apps/account/tasks.py b/apps/account/tasks.py index 0367a59e..362daddf 100644 --- a/apps/account/tasks.py +++ b/apps/account/tasks.py @@ -11,13 +11,13 @@ logger = logging.getLogger(__name__) @shared_task -def send_reset_password_email(request_id): +def send_reset_password_email(request_id, country_code): """Send email to user for reset password.""" try: obj = models.ResetPasswordToken.objects.get(id=request_id) user = obj.user user.send_email(subject=_('Password resetting'), - message=obj.reset_password_template) + message=obj.reset_password_template(country_code)) except: logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n' f'DETAIL: Exception occurred for ResetPasswordToken instance: ' diff --git a/apps/account/views/common.py b/apps/account/views/common.py index 2e182bd9..360a1e5a 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -6,6 +6,7 @@ from rest_framework import generics from rest_framework import permissions from rest_framework import status from rest_framework.response import Response +from django.shortcuts import get_object_or_404 from account import models from account.serializers import common as serializers @@ -29,6 +30,31 @@ class ChangePasswordView(JWTUpdateAPIView): """Change password view""" serializer_class = serializers.ChangePasswordSerializer queryset = models.User.objects.active() + permission_classes = (permissions.AllowAny, ) + + def get_object(self): + """Overridden get_object method.""" + if not self.request.user.is_authenticated(): + queryset = self.filter_queryset(self.get_queryset()) + uidb64 = self.kwargs.get('uidb64') + + user_id = force_text(urlsafe_base64_decode(uidb64)) + token = self.kwargs.get('token') + + filter_kwargs = {'key': token, 'user_id': user_id} + password_reset_obj = get_object_or_404(models.ResetPasswordToken.objects.valid(), + **filter_kwargs) + if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token( + user=password_reset_obj.user, token=token): + raise utils_exceptions.NotValidAccessTokenError() + # todo: Add is_valid check status + obj = password_reset_obj.user + else: + obj = self.request.user + + # May raise a permission denied + self.check_object_permissions(self.request, obj) + return obj def patch(self, request, *args, **kwargs): """Implement PUT method""" diff --git a/apps/authorization/models.py b/apps/authorization/models.py index 69416420..c295329c 100644 --- a/apps/authorization/models.py +++ b/apps/authorization/models.py @@ -38,8 +38,8 @@ class Application(PlatformMixin, AbstractApplication): class JWTAccessTokenManager(models.Manager): """Manager for AccessToken model.""" - def add_to_db(self, user, access_token: AccessToken, - refresh_token: RefreshToken): + + def make(self, user, access_token: AccessToken, refresh_token: RefreshToken): """Create generated tokens to DB""" refresh_token_qs = JWTRefreshToken.objects.filter(user=user, jti=refresh_token.payload.get('jti')) @@ -106,18 +106,17 @@ class JWTAccessToken(ProjectBaseMixin): class JWTRefreshTokenManager(models.Manager): """Manager for model RefreshToken.""" - - def add_to_db(self, user, token: RefreshToken, source: int): - """Added generated refresh token to db""" + def make(self, user, token: RefreshToken, source: int): + """Make method""" jti = token[settings.SIMPLE_JWT.get('JTI_CLAIM')] exp = token['exp'] obj = self.model( user=user, jti=jti, - source=source, created_at=token.current_time, - expires_at=utils.datetime_from_epoch(exp), - ) + expires_at=utils.datetime_from_epoch(exp)) + if source: + obj.source = source obj.save() return obj diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index f5e24fc9..4d29887c 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -19,13 +19,9 @@ from utils.tokens import GMRefreshToken class SignupSerializer(serializers.ModelSerializer): """Signup serializer serializer mixin""" # REQUEST - username = serializers.CharField( - validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),), - write_only=True) + username = serializers.CharField(write_only=True) password = serializers.CharField(write_only=True) - email = serializers.EmailField( - validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),), - write_only=True) + email = serializers.EmailField(write_only=True) newsletter = serializers.BooleanField(write_only=True) class Meta: @@ -42,6 +38,14 @@ class SignupSerializer(serializers.ModelSerializer): valid = utils_methods.username_validator(username=value) if not valid: raise utils_exceptions.NotValidUsernameError() + if account_models.User.objects.filter(username__icontains=value).exists(): + raise serializers.ValidationError() + return value + + def validate_email(self, value): + """Validate email""" + if account_models.User.objects.filter(email__icontains=value).exists(): + raise serializers.ValidationError() return value def validate_password(self, value): @@ -62,9 +66,13 @@ class SignupSerializer(serializers.ModelSerializer): newsletter=validated_data.get('newsletter')) # Send verification link on user email if settings.USE_CELERY: - tasks.send_confirm_email.delay(obj.id) + tasks.send_confirm_email.delay( + user_id=obj.id, + country_code=self.context.get('request').country_code) else: - tasks.send_confirm_email(obj.id) + tasks.send_confirm_email( + user_id=obj.id, + country_code=self.context.get('request').country_code) return obj @@ -128,14 +136,10 @@ class RefreshTokenSerializer(SourceSerializerMixin): refresh_token = serializers.CharField(read_only=True) access_token = serializers.CharField(read_only=True) - def get_request(self): - """Return request""" - return self.context.get('request') - def validate(self, attrs): """Override validate method""" - cookie_refresh_token = self.get_request().COOKIES.get('refresh_token') + cookie_refresh_token = self.context.get('request').COOKIES.get('refresh_token') # Check if refresh_token in COOKIES if not cookie_refresh_token: raise utils_exceptions.NotValidRefreshTokenError() diff --git a/apps/authorization/tasks.py b/apps/authorization/tasks.py index a2ae4bb3..9947c2a3 100644 --- a/apps/authorization/tasks.py +++ b/apps/authorization/tasks.py @@ -10,12 +10,12 @@ logger = logging.getLogger(__name__) @shared_task -def send_confirm_email(user_id): +def send_confirm_email(user_id, country_code): """Send verification email to user.""" try: obj = account_models.User.objects.get(id=user_id) obj.send_email(subject=_('Email confirmation'), - message=obj.confirm_email_template) + message=obj.confirm_email_template(country_code)) except: logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n' f'DETAIL: Exception occurred for user: {user_id}') diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index c77dd63e..827ff108 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -182,7 +182,15 @@ class VerifyEmailConfirmView(JWTGenericViewMixin): raise utils_exceptions.NotValidTokenError() # Approve email status user.confirm_email() - return Response(status=status.HTTP_200_OK) + response = Response(status=status.HTTP_200_OK) + + # Create tokens + tokens = user.create_jwt_tokens() + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies( + access_token=tokens.get('access_token'), + refresh_token=tokens.get('refresh_token')), + response=response) else: raise utils_exceptions.UserNotFoundError() diff --git a/apps/utils/tokens.py b/apps/utils/tokens.py index e686b42b..a236bfa3 100644 --- a/apps/utils/tokens.py +++ b/apps/utils/tokens.py @@ -33,12 +33,11 @@ class GMBlacklistMixin(BlacklistMixin): """ @classmethod - def for_user_by_source(cls, user, source: int): + def for_user(cls, user, source: int = None): """Create a refresh token.""" token = super().for_user(user) token['user'] = user.get_user_info() - # Create a record in DB - JWTRefreshToken.objects.add_to_db(user=user, token=token, source=source) + JWTRefreshToken.objects.make(user=user, token=token, source=source) return token @@ -70,7 +69,7 @@ class GMRefreshToken(GMBlacklistMixin, GMToken, RefreshToken): # Create a record in DB user = User.objects.get(id=self.payload.get('user_id')) - JWTAccessToken.objects.add_to_db(user=user, - access_token=access_token, - refresh_token=self) + JWTAccessToken.objects.make(user=user, + access_token=access_token, + refresh_token=self) return access_token diff --git a/project/settings/stage.py b/project/settings/stage.py new file mode 100644 index 00000000..998abaa6 --- /dev/null +++ b/project/settings/stage.py @@ -0,0 +1,17 @@ +"""Stage settings.""" +from .base import * + +ALLOWED_HOSTS = ['gm-stage.id-east.ru', '95.213.204.126'] + +SEND_SMS = False +SMS_CODE_SHOW = True +USE_CELERY = False + +SCHEMA_URI = 'https' +DEFAULT_SUBDOMAIN = 'www' +SITE_DOMAIN_URI = 'id-east.ru' +DOMAIN_URI = 'gm-stage.id-east.ru' + +# COOKIES +CSRF_COOKIE_DOMAIN = '.id-east.ru' +SESSION_COOKIE_DOMAIN = '.id-east.ru' diff --git a/project/templates/account/password_reset_email.html b/project/templates/account/password_reset_email.html index 1a395cee..b743d71c 100644 --- a/project/templates/account/password_reset_email.html +++ b/project/templates/account/password_reset_email.html @@ -3,7 +3,7 @@ {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} -http://{{ domain_uri }}{% url 'web:account:form-password-reset-confirm' uidb64=uidb64 token=token %} +Reset link. {% endblock %} {% trans "Thanks for using our site!" %} diff --git a/project/templates/authorization/confirm_email.html b/project/templates/authorization/confirm_email.html index 7fa06aa5..9056525d 100644 --- a/project/templates/authorization/confirm_email.html +++ b/project/templates/authorization/confirm_email.html @@ -2,9 +2,8 @@ {% blocktrans %}You're receiving this email because you trying to register new account at {{ site_name }}.{% endblocktrans %} {% trans "Please confirm your email address to complete the registration:" %} -{% block signup_confirm %} -http://{{ domain_uri }}{% url 'auth:signup-confirm' uidb64=uidb64 token=token %} -{% endblock %} + +Confirmation link. {% trans "Thanks for using our site!" %} From 2c41e532c8e81f0cf57a9493530ca934b69687bf Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 12 Sep 2019 09:33:09 +0300 Subject: [PATCH 39/51] fixed ChangePasswordView --- apps/account/views/common.py | 9 +++++---- apps/utils/exceptions.py | 4 ++-- project/templates/account/password_reset_email.html | 5 +---- project/templates/authorization/confirm_email.html | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/account/views/common.py b/apps/account/views/common.py index 360a1e5a..66ad3fa0 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -34,8 +34,7 @@ class ChangePasswordView(JWTUpdateAPIView): def get_object(self): """Overridden get_object method.""" - if not self.request.user.is_authenticated(): - queryset = self.filter_queryset(self.get_queryset()) + if not self.request.user.is_authenticated: uidb64 = self.kwargs.get('uidb64') user_id = force_text(urlsafe_base64_decode(uidb64)) @@ -47,7 +46,8 @@ class ChangePasswordView(JWTUpdateAPIView): if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token( user=password_reset_obj.user, token=token): raise utils_exceptions.NotValidAccessTokenError() - # todo: Add is_valid check status + if not password_reset_obj.user.is_active: + raise utils_exceptions.UserNotFoundError() obj = password_reset_obj.user else: obj = self.request.user @@ -58,7 +58,8 @@ class ChangePasswordView(JWTUpdateAPIView): def patch(self, request, *args, **kwargs): """Implement PUT method""" - serializer = self.get_serializer(instance=self.request.user, + instance = self.get_object() + serializer = self.get_serializer(instance=instance, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index b2730b97..5251c4e4 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -137,8 +137,8 @@ class WrongAuthCredentials(AuthErrorMixin): class FavoritesError(exceptions.APIException): """ - The exception should be thrown when you item that user - want add to favorites already exists. + The exception should be thrown when item that user + want to add to favorites is already exists. """ status_code = status.HTTP_400_BAD_REQUEST default_detail = _('Item is already in favorites.') diff --git a/project/templates/account/password_reset_email.html b/project/templates/account/password_reset_email.html index b743d71c..c32469f7 100644 --- a/project/templates/account/password_reset_email.html +++ b/project/templates/account/password_reset_email.html @@ -2,10 +2,7 @@ {% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} {% trans "Please go to the following page and choose a new password:" %} -{% block reset_link %} -Reset link. -{% endblock %} - +https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/ {% trans "Thanks for using our site!" %} {% blocktrans %}The {{ site_name }} team{% endblocktrans %} diff --git a/project/templates/authorization/confirm_email.html b/project/templates/authorization/confirm_email.html index 9056525d..f3bbd50e 100644 --- a/project/templates/authorization/confirm_email.html +++ b/project/templates/authorization/confirm_email.html @@ -3,7 +3,7 @@ {% trans "Please confirm your email address to complete the registration:" %} -Confirmation link. +https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/ {% trans "Thanks for using our site!" %} From ec2e87fe1a099e0c48186c09e84bbfe5e61b8ac1 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 12 Sep 2019 10:20:11 +0300 Subject: [PATCH 40/51] fixed settings for COOKIES, related to unable to login in admin page (CSRF ERROR) --- project/settings/base.py | 3 +-- project/settings/development.py | 4 ---- project/settings/local.py | 4 ---- project/settings/stage.py | 4 ---- 4 files changed, 1 insertion(+), 14 deletions(-) diff --git a/project/settings/base.py b/project/settings/base.py index 995391c2..b7047f98 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -94,18 +94,17 @@ INSTALLED_APPS = CONTRIB_APPS + PROJECT_APPS + EXTERNAL_APPS MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', 'oauth2_provider.middleware.OAuth2TokenMiddleware', + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'utils.middleware.parse_cookies', - # 'utils.middleware.CORSMiddleware', ] ROOT_URLCONF = 'project.urls' diff --git a/project/settings/development.py b/project/settings/development.py index f37a40b4..7ecc6aa2 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -11,7 +11,3 @@ SCHEMA_URI = 'http' DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm.id-east.ru' - -# COOKIES -CSRF_COOKIE_DOMAIN = '.id-east.ru' -SESSION_COOKIE_DOMAIN = '.id-east.ru' diff --git a/project/settings/local.py b/project/settings/local.py index b40e8bef..1c73a857 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -17,10 +17,6 @@ BROKER_URL = 'amqp://rabbitmq:5672' CELERY_RESULT_BACKEND = BROKER_URL CELERY_BROKER_URL = BROKER_URL -# COOKIES -CSRF_COOKIE_DOMAIN = '0.0.0.0:8000' -SESSION_COOKIE_DOMAIN = '0.0.0.0:8000' - # LOGGING LOGGING = { diff --git a/project/settings/stage.py b/project/settings/stage.py index 998abaa6..bbb2f245 100644 --- a/project/settings/stage.py +++ b/project/settings/stage.py @@ -11,7 +11,3 @@ SCHEMA_URI = 'https' DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm-stage.id-east.ru' - -# COOKIES -CSRF_COOKIE_DOMAIN = '.id-east.ru' -SESSION_COOKIE_DOMAIN = '.id-east.ru' From 5943839edacf569a147f1064fbe4f396240aeafb Mon Sep 17 00:00:00 2001 From: Semyon Date: Thu, 12 Sep 2019 14:24:48 +0300 Subject: [PATCH 41/51] Added view for nearest establishments --- apps/establishment/models.py | 14 ++++++++++++++ apps/establishment/views.py | 25 ++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index efe665ef..a0c9c44c 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -102,6 +102,20 @@ class EstablishmentQuerySet(models.QuerySet): default=False, output_field=models.BooleanField(default=False))) + def by_distance_from_point(self, center, radius, unit='m'): + """ + Returns nearest establishments + + :param center: point from which to find nearby establishments + :param radius: the maximum distance within the radius of which to look for establishments + :return: all establishments within the specified radius of specified point + :param unit: length unit e.g. m, km. Default is 'm'. + """ + + from django.contrib.gis.measure import Distance + kwargs = {unit: radius} + return self.filter(address__coordinates__distance_lte=(center, Distance(**kwargs))) + class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin): """Establishment model.""" diff --git a/apps/establishment/views.py b/apps/establishment/views.py index 528d2377..7745af2d 100644 --- a/apps/establishment/views.py +++ b/apps/establishment/views.py @@ -111,9 +111,28 @@ class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.D obj = get_object_or_404( self.request.user.favorites.by_user(user=self.request.user) - .by_content_type(app_label='establishment', - model='establishment') - .by_object_id(object_id=self.kwargs['pk'])) + .by_content_type(app_label='establishment', + model='establishment') + .by_object_id(object_id=self.kwargs['pk'])) # May raise a permission denied self.check_object_permissions(self.request, obj) return obj + + +class EstablishmentNearestRetrieveView(EstablishmentMixin, JWTGenericViewMixin, generics.ListAPIView): + """Resource for getting list of nearest establishments.""" + serializer_class = serializers.EstablishmentListSerializer + filter_class = filters.EstablishmentFilter + + def get_queryset(self): + """Overrided method 'get_queryset'.""" + from django.contrib.gis.geos import Point + + center = Point(float(self.request.query_params["lat"]), float(self.request.query_params["lon"])) + radius = float(self.request.query_params["radius"]) + unit = self.request.query_params.get("unit", None) + by_distance_from_point_kwargs = {"center": center, "radius": radius, "unit": unit} + return super(EstablishmentNearestRetrieveView, self).get_queryset() \ + .by_distance_from_point(**{k: v for k, v in by_distance_from_point_kwargs.items() if v is not None}) \ + .by_country_code(code=self.request.country_code) \ + .annotate_in_favorites(user=self.request.user) From bdc2c327f6fe4d3fe4d0ae827e4d063be076892b Mon Sep 17 00:00:00 2001 From: Semyon Date: Thu, 12 Sep 2019 14:25:03 +0300 Subject: [PATCH 42/51] Registered path for nearest establishments --- apps/establishment/urls/common.py | 5 ++--- apps/establishment/urls/mobile.py | 11 +++++++++++ project/urls/mobile.py | 3 ++- 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 apps/establishment/urls/mobile.py diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 0f2958eb..ff6af5c8 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -5,7 +5,6 @@ from establishment import views app_name = 'establishment' - urlpatterns = [ path('', views.EstablishmentListView.as_view(), name='list'), path('/', views.EstablishmentRetrieveView.as_view(), name='detail'), @@ -15,5 +14,5 @@ urlpatterns = [ path('/comments//', views.EstablishmentCommentRUDView.as_view(), name='rud-comment'), path('/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), - name='add-favorites'), -] \ No newline at end of file + name='add-favorites') +] diff --git a/apps/establishment/urls/mobile.py b/apps/establishment/urls/mobile.py new file mode 100644 index 00000000..2803be18 --- /dev/null +++ b/apps/establishment/urls/mobile.py @@ -0,0 +1,11 @@ +"""Establishment url patterns.""" +from django.urls import path + +from establishment import views +from establishment.urls.common import urlpatterns as common_urlpatterns + +urlpatterns = [ + path('geo/', views.EstablishmentNearestRetrieveView.as_view(), name='nearest-establishments-list') +] + +urlpatterns.extend(common_urlpatterns) diff --git a/project/urls/mobile.py b/project/urls/mobile.py index c007e260..0bcbd31c 100644 --- a/project/urls/mobile.py +++ b/project/urls/mobile.py @@ -3,10 +3,11 @@ from django.urls import path, include app_name = 'mobile' urlpatterns = [ + path('establishments/', include('establishment.urls.mobile')), # path('account/', include('account.urls.web')), # path('advertisement/', include('advertisement.urls.web')), # path('collection/', include('collection.urls.web')), # path('establishments/', include('establishment.urls.web')), # path('news/', include('news.urls.web')), # path('partner/', include('partner.urls.web')), -] \ No newline at end of file +] From 075298f6ec31ffacfc19ade212c58e41433f4055 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 12 Sep 2019 14:44:49 +0300 Subject: [PATCH 43/51] refactored apps account, authorization, news, utils --- apps/account/admin.py | 9 - .../0006_delete_resetpasswordtoken.py | 16 ++ apps/account/models.py | 129 +++----------- apps/account/serializers/common.py | 33 ++-- apps/account/serializers/web.py | 79 +++------ apps/account/tasks.py | 27 ++- apps/account/urls/common.py | 4 +- apps/account/urls/web.py | 6 +- apps/account/views/common.py | 50 ++---- apps/account/views/web.py | 159 +++--------------- apps/authorization/urls/common.py | 2 +- apps/authorization/views/common.py | 9 +- apps/news/views/common.py | 5 +- apps/utils/exceptions.py | 18 +- apps/utils/views.py | 101 +---------- project/settings/base.py | 1 - project/templates/account/change_email.html | 5 +- .../account/password_reset_confirm.html | 31 ---- .../account/password_reset_email.html | 2 + 19 files changed, 165 insertions(+), 521 deletions(-) create mode 100644 apps/account/migrations/0006_delete_resetpasswordtoken.py delete mode 100644 project/templates/account/password_reset_confirm.html diff --git a/apps/account/admin.py b/apps/account/admin.py index 938be965..dc88c34b 100644 --- a/apps/account/admin.py +++ b/apps/account/admin.py @@ -46,12 +46,3 @@ class UserAdmin(BaseUserAdmin): return obj.get_short_name() short_name.short_description = _('Name') - - -@admin.register(models.ResetPasswordToken) -class ResetPasswordToken(admin.ModelAdmin): - """Model admin for ResetPasswordToken""" - list_display = ('id', 'user', 'expiry_datetime') - list_filter = ('expiry_datetime', 'user') - search_fields = ('user', ) - readonly_fields = ('user', 'key', ) diff --git a/apps/account/migrations/0006_delete_resetpasswordtoken.py b/apps/account/migrations/0006_delete_resetpasswordtoken.py new file mode 100644 index 00000000..6b34bdb2 --- /dev/null +++ b/apps/account/migrations/0006_delete_resetpasswordtoken.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2.4 on 2019-09-12 11:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0005_user_cropped_image'), + ] + + operations = [ + migrations.DeleteModel( + name='ResetPasswordToken', + ), + ] diff --git a/apps/account/models.py b/apps/account/models.py index 30255802..ca5a4b96 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -147,120 +147,43 @@ class User(ImageMixin, AbstractUser): @property def reset_password_token(self): """Make a token for finish signup.""" - return GMTokenGenerator(purpose=GMTokenGenerator.RESET_PASSWORD).make_token(self) + return password_token_generator.make_token(self) @property def get_user_uidb64(self): """Get base64 value for user by primary key identifier""" return urlsafe_base64_encode(force_bytes(self.pk)) - def confirm_email_template(self, country_code): - """Get confirm email template""" - return render_to_string( - template_name=settings.CONFIRM_EMAIL_TEMPLATE, - context={'token': self.confirm_email_token, - 'uidb64': self.get_user_uidb64, - 'domain_uri': settings.DOMAIN_URI, - 'site_name': settings.SITE_NAME, - 'country_code': country_code}) - @property - def change_email_template(self): - """Get change email template""" - return render_to_string( - template_name=settings.CHANGE_EMAIL_TEMPLATE, - context={'token': self.change_email_token, - 'uidb64': self.get_user_uidb64, - 'domain_uri': settings.DOMAIN_URI, - 'site_name': settings.SITE_NAME}) - - -class ResetPasswordTokenQuerySet(models.QuerySet): - """Reset password token query set""" - - def expired(self): - """Show only expired""" - return self.filter(expiry_datetime__lt=timezone.now()) - - def valid(self): - """Show only valid""" - return self.filter(expiry_datetime__gt=timezone.now()) - - def by_user(self, user): - """Show obj by user""" - return self.filter(user=user) - - -class ResetPasswordToken(PlatformMixin, ProjectBaseMixin): - """Reset password model""" - - user = models.ForeignKey(User, - related_name='password_reset_tokens', - on_delete=models.CASCADE, - verbose_name=_('The User which is associated to ' - 'this password reset token')) - # Key field, though it is not the primary key of the model - key = models.CharField(max_length=255, - verbose_name=_('Key')) - - ip_address = models.GenericIPAddressField(default='', - blank=True, null=True, - verbose_name=_('The IP address of this session')) - - expiry_datetime = models.DateTimeField(blank=True, null=True, - verbose_name=_('Expiration datetime')) - - objects = ResetPasswordTokenQuerySet.as_manager() - - class Meta: - verbose_name = _("Password Reset Token") - verbose_name_plural = _("Password Reset Tokens") - - def __str__(self): - return "Password reset token for user {user}".format(user=self.user) - - def save(self, *args, **kwargs): - """Override save method""" - if not self.expiry_datetime: - self.expiry_datetime = ( - timezone.now() + - timezone.timedelta(hours=self.get_resetting_token_expiration) - ) - if not self.key: - self.key = self.generate_token - return super(ResetPasswordToken, self).save(*args, **kwargs) - - @property - def get_resetting_token_expiration(self): - """Get resetting token expiration""" - return settings.RESETTING_TOKEN_EXPIRATION - - @property - def is_valid(self): - """Check if valid token or not""" - return timezone.now() > self.expiry_datetime - - @property - def generate_token(self): - """Generates a pseudo random code""" - return password_token_generator.make_token(self.user) + def base_template(self): + """Base email template""" + return {'domain_uri': settings.DOMAIN_URI, + 'uidb64': self.get_user_uidb64, + 'site_name': settings.SITE_NAME} def reset_password_template(self, country_code): """Get reset password template""" + context = {'token': self.reset_password_token, + 'country_code': country_code} + context.update(self.base_template) return render_to_string( template_name=settings.RESETTING_TOKEN_TEMPLATE, - context={'token': self.key, - 'uidb64': self.user.get_user_uidb64, - 'domain_uri': settings.DOMAIN_URI, - 'site_name': settings.SITE_NAME, - 'country_code': country_code}) + context=context) - @staticmethod - def token_is_valid(user, token): - """Check if token is valid""" - return password_token_generator.check_token(user, token) + def confirm_email_template(self, country_code): + """Get confirm email template""" + context = {'token': self.confirm_email_token, + 'country_code': country_code} + context.update(self.base_template) + return render_to_string( + template_name=settings.CONFIRM_EMAIL_TEMPLATE, + context=context) - def overdue(self): - """Overdue instance""" - self.expiry_datetime = timezone.now() - self.save() + def change_email_template(self, country_code): + """Get change email template""" + context = {'token': self.change_email_token, + 'country_code': country_code} + context.update(self.base_template) + return render_to_string( + template_name=settings.CHANGE_EMAIL_TEMPLATE, + context=context) diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index bd28cb88..e6599f96 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -48,7 +48,7 @@ class UserSerializer(serializers.ModelSerializer): def validate_email(self, value): """Validate email value""" if value == self.instance.email: - raise serializers.ValidationError() + raise serializers.ValidationError(detail='Equal email address.') return value def validate_username(self, value): @@ -58,24 +58,21 @@ class UserSerializer(serializers.ModelSerializer): raise utils_exceptions.NotValidUsernameError() return value - def validate(self, attrs): - if ('cropped_image' in attrs or 'image' in attrs) and \ - ('cropped_image' not in attrs or 'image' not in attrs): - raise utils_exceptions.UserUpdateUploadImageError() - return attrs - def update(self, instance, validated_data): - """ - Override update method - """ - if 'email' in validated_data: - validated_data['email_confirmed'] = False + """Override update method""" instance = super().update(instance, validated_data) - # Send verification link on user email for change email address - if settings.USE_CELERY: - tasks.confirm_new_email_address.delay(instance.id) - else: - tasks.confirm_new_email_address(instance.id) + if 'email' in validated_data: + instance.email_confirmed = False + instance.save() + # Send verification link on user email for change email address + if settings.USE_CELERY: + tasks.change_email_address.delay( + user_id=instance.id, + country_code=self.context.get('request').country_code) + else: + tasks.change_email_address( + user_id=instance.id, + country_code=self.context.get('request').country_code) return instance @@ -163,7 +160,7 @@ class ConfirmEmailSerializer(serializers.ModelSerializer): """Override validate method""" email_confirmed = self.instance.email_confirmed if email_confirmed: - raise serializers.ValidationError() + raise utils_exceptions.EmailConfirmedError() return attrs def update(self, instance, validated_data): diff --git a/apps/account/serializers/web.py b/apps/account/serializers/web.py index cdf616dc..d325cea6 100644 --- a/apps/account/serializers/web.py +++ b/apps/account/serializers/web.py @@ -1,27 +1,18 @@ """Serializers for account web""" -from django.conf import settings from django.contrib.auth import password_validation as password_validators -from django.db.models import Q from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from account import models, tasks +from account import models from utils import exceptions as utils_exceptions from utils.methods import username_validator -class PasswordResetSerializer(serializers.ModelSerializer): +class PasswordResetSerializer(serializers.Serializer): """Serializer from model PasswordReset""" username_or_email = serializers.CharField(required=False, write_only=True,) - class Meta: - """Meta class""" - model = models.ResetPasswordToken - fields = ( - 'username_or_email', - ) - @property def request(self): """Get request from context""" @@ -30,41 +21,29 @@ class PasswordResetSerializer(serializers.ModelSerializer): def validate(self, attrs): """Override validate method""" user = self.request.user + username_or_email = attrs.get('username_or_email') - if user.is_anonymous: - username_or_email = attrs.get('username_or_email') + if not user.is_authenticated: if not username_or_email: - raise serializers.ValidationError(_('Username or Email not in request body.')) - # Check user in DB + raise serializers.ValidationError(_('username or email not in request body.')) + filters = {} if username_validator(username_or_email): - filters.update({'username': username_or_email}) + filters.update({'username__icontains': username_or_email}) else: - filters.update({'email': username_or_email.lower()}) - user_qs = models.User.objects.filter(**filters) - if user_qs.exists() and filters: - attrs['user'] = user_qs.first() - else: - attrs['user'] = user + filters.update({'email__icontains': username_or_email}) + + if filters: + filters.update({'is_active': True}) + user_qs = models.User.objects.filter(**filters) + + if not user_qs.exists(): + raise utils_exceptions.UserNotFoundError() + user = user_qs.first() + + attrs['user'] = user return attrs - def create(self, validated_data, *args, **kwargs): - """Override create method""" - user = validated_data.pop('user') - ip_address = self.request.META.get('REMOTE_ADDR') - - obj = models.ResetPasswordToken.objects.create( - user=user, - ip_address=ip_address, - source=models.ResetPasswordToken.WEB) - if settings.USE_CELERY: - tasks.send_reset_password_email.delay(request_id=obj.id, - country_code=self.request.country_code) - else: - tasks.send_reset_password_email(request_id=obj.id, - country_code=self.request.country_code) - return obj - class PasswordResetConfirmSerializer(serializers.ModelSerializer): """Serializer for model User""" @@ -73,30 +52,24 @@ class PasswordResetConfirmSerializer(serializers.ModelSerializer): class Meta: """Meta class""" - model = models.ResetPasswordToken + model = models.User fields = ('password', ) - def validate(self, attrs): - """Override validate method""" - user = self.instance.user - password = attrs.get('password') + def validate_password(self, value): + """Password validation method.""" try: # Compare new password with the old ones - if user.check_password(raw_password=password): + if self.instance.check_password(raw_password=value): raise utils_exceptions.PasswordsAreEqual() # Validate password - password_validators.validate_password(password=password) + password_validators.validate_password(password=value) except serializers.ValidationError as e: raise serializers.ValidationError(str(e)) - else: - return attrs + return value def update(self, instance, validated_data): """Override update method""" # Update user password from instance - instance.user.set_password(validated_data.get('password')) - instance.user.save() - - # Overdue instance - instance.overdue() + instance.set_password(validated_data.get('password')) + instance.save() return instance diff --git a/apps/account/tasks.py b/apps/account/tasks.py index 362daddf..03a231b3 100644 --- a/apps/account/tasks.py +++ b/apps/account/tasks.py @@ -11,26 +11,37 @@ logger = logging.getLogger(__name__) @shared_task -def send_reset_password_email(request_id, country_code): +def send_reset_password_email(user_id, country_code): """Send email to user for reset password.""" try: - obj = models.ResetPasswordToken.objects.get(id=request_id) - user = obj.user + user = models.User.objects.get(id=user_id) user.send_email(subject=_('Password resetting'), - message=obj.reset_password_template(country_code)) + message=user.reset_password_template(country_code)) except: logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n' - f'DETAIL: Exception occurred for ResetPasswordToken instance: ' - f'{request_id}') + f'DETAIL: Exception occurred for reset password: ' + f'{user_id}') @shared_task -def confirm_new_email_address(user_id): +def confirm_new_email_address(user_id, country_code): """Send email to user new email.""" try: user = models.User.objects.get(id=user_id) user.send_email(subject=_('Validate new email address'), - message=user.change_email_template) + message=user.confirm_email_template(country_code)) except: logger.error(f'METHOD_NAME: {confirm_new_email_address.__name__}\n' f'DETAIL: Exception occurred for user: {user_id}') + + +@shared_task +def change_email_address(user_id, country_code): + """Send email to user new email.""" + try: + user = models.User.objects.get(id=user_id) + user.send_email(subject=_('Validate new email address'), + message=user.change_email_template(country_code)) + except: + logger.error(f'METHOD_NAME: {change_email_address.__name__}\n' + f'DETAIL: Exception occurred for user: {user_id}') diff --git a/apps/account/urls/common.py b/apps/account/urls/common.py index 0e8ae835..34583010 100644 --- a/apps/account/urls/common.py +++ b/apps/account/urls/common.py @@ -8,7 +8,5 @@ app_name = 'account' urlpatterns = [ path('user/', views.UserRetrieveUpdateView.as_view(), name='user-retrieve-update'), path('change-password/', views.ChangePasswordView.as_view(), name='change-password'), - path('change-email/confirm///', views.ChangeEmailConfirmView.as_view(), - name='change-email-confirm'), - path('confirm-email/', views.ConfirmEmailView.as_view(), name='confirm-email'), + path('email/confirm///', views.ConfirmEmailView.as_view(), name='confirm-email'), ] diff --git a/apps/account/urls/web.py b/apps/account/urls/web.py index cc57f316..e590e76d 100644 --- a/apps/account/urls/web.py +++ b/apps/account/urls/web.py @@ -8,10 +8,8 @@ app_name = 'account' urlpatterns_api = [ path('reset-password/', views.PasswordResetView.as_view(), name='password-reset'), - path('form/reset-password///', views.FormPasswordResetConfirmView.as_view(), - name='form-password-reset-confirm'), - path('form/reset-password/success/', views.FormPasswordResetSuccessView.as_view(), - name='form-password-reset-success'), + path('reset-password/confirm///', views.PasswordResetConfirmView.as_view(), + name='password-reset-confirm'), ] urlpatterns = urlpatterns_api + \ diff --git a/apps/account/views/common.py b/apps/account/views/common.py index 66ad3fa0..ab62343f 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -6,14 +6,12 @@ from rest_framework import generics from rest_framework import permissions from rest_framework import status from rest_framework.response import Response -from django.shortcuts import get_object_or_404 from account import models from account.serializers import common as serializers from utils import exceptions as utils_exceptions from utils.models import GMTokenGenerator -from utils.views import (JWTUpdateAPIView, - JWTGenericViewMixin) +from utils.views import JWTGenericViewMixin # User views @@ -26,53 +24,27 @@ class UserRetrieveUpdateView(generics.RetrieveUpdateAPIView): return self.request.user -class ChangePasswordView(JWTUpdateAPIView): +class ChangePasswordView(generics.GenericAPIView): """Change password view""" serializer_class = serializers.ChangePasswordSerializer queryset = models.User.objects.active() - permission_classes = (permissions.AllowAny, ) - - def get_object(self): - """Overridden get_object method.""" - if not self.request.user.is_authenticated: - uidb64 = self.kwargs.get('uidb64') - - user_id = force_text(urlsafe_base64_decode(uidb64)) - token = self.kwargs.get('token') - - filter_kwargs = {'key': token, 'user_id': user_id} - password_reset_obj = get_object_or_404(models.ResetPasswordToken.objects.valid(), - **filter_kwargs) - if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token( - user=password_reset_obj.user, token=token): - raise utils_exceptions.NotValidAccessTokenError() - if not password_reset_obj.user.is_active: - raise utils_exceptions.UserNotFoundError() - obj = password_reset_obj.user - else: - obj = self.request.user - - # May raise a permission denied - self.check_object_permissions(self.request, obj) - return obj def patch(self, request, *args, **kwargs): """Implement PUT method""" - instance = self.get_object() - serializer = self.get_serializer(instance=instance, + serializer = self.get_serializer(instance=self.request.user, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() return Response(status=status.HTTP_200_OK) -class ConfirmEmailView(JWTGenericViewMixin): +class SendConfirmationEmailView(JWTGenericViewMixin): """Confirm email view.""" serializer_class = serializers.ConfirmEmailSerializer queryset = models.User.objects.all() def patch(self, request, *args, **kwargs): - """Implement POST-method""" + """Implement PATCH-method""" # Get user instance instance = self.request.user @@ -82,7 +54,7 @@ class ConfirmEmailView(JWTGenericViewMixin): return Response(status=status.HTTP_200_OK) -class ChangeEmailConfirmView(JWTGenericViewMixin): +class ConfirmEmailView(JWTGenericViewMixin): """View for confirm changing email""" permission_classes = (permissions.AllowAny,) @@ -95,12 +67,18 @@ class ChangeEmailConfirmView(JWTGenericViewMixin): user_qs = models.User.objects.filter(pk=uid) if user_qs.exists(): user = user_qs.first() - if not GMTokenGenerator(GMTokenGenerator.CHANGE_EMAIL).check_token( + if not GMTokenGenerator(GMTokenGenerator.CONFIRM_EMAIL).check_token( user, token): raise utils_exceptions.NotValidTokenError() # Approve email status user.confirm_email() - return Response(status=status.HTTP_200_OK) + # Create tokens + tokens = user.create_jwt_tokens() + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies( + access_token=tokens.get('access_token'), + refresh_token=tokens.get('refresh_token')), + response=Response(status=status.HTTP_200_OK)) else: raise utils_exceptions.UserNotFoundError() diff --git a/apps/account/views/web.py b/apps/account/views/web.py index 4f10ccc2..e4596e9f 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -1,42 +1,35 @@ """Web account views""" from django.conf import settings -from django.contrib.auth.tokens import default_token_generator -from django.core.exceptions import ValidationError -from django.http import HttpResponseRedirect +from django.contrib.auth.tokens import default_token_generator as password_token_generator from django.shortcuts import get_object_or_404 -from django.urls import reverse_lazy -from django.utils.decorators import method_decorator from django.utils.encoding import force_text from django.utils.http import urlsafe_base64_decode -from django.utils.translation import gettext_lazy as _ -from django.views.decorators.cache import never_cache -from django.views.decorators.debug import sensitive_post_parameters -from django.views.generic.edit import FormView -from rest_framework import permissions -from rest_framework import status -from rest_framework import views +from rest_framework import permissions, status, generics from rest_framework.response import Response -from account import models -from account.forms import SetPasswordForm +from account import tasks, models from account.serializers import web as serializers from utils import exceptions as utils_exceptions -from utils.models import GMTokenGenerator from utils.views import JWTGenericViewMixin -class PasswordResetView(JWTGenericViewMixin): +class PasswordResetView(generics.GenericAPIView): """View for resetting user password""" permission_classes = (permissions.AllowAny, ) serializer_class = serializers.PasswordResetSerializer - queryset = models.ResetPasswordToken.objects.valid() def post(self, request, *args, **kwargs): """Override create method""" serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) if serializer.validated_data.get('user'): - serializer.save() + user = serializer.validated_data.pop('user') + if settings.USE_CELERY: + tasks.send_reset_password_email.delay(user_id=user.id, + country_code=self.request.country_code) + else: + tasks.send_reset_password_email(user_id=user.id, + country_code=self.request.country_code) return Response(status=status.HTTP_200_OK) @@ -44,10 +37,7 @@ class PasswordResetConfirmView(JWTGenericViewMixin): """View for confirmation new password""" serializer_class = serializers.PasswordResetConfirmSerializer permission_classes = (permissions.AllowAny,) - - def get_queryset(self): - """Override get_queryset method""" - return models.ResetPasswordToken.objects.valid() + queryset = models.User.objects.active() def get_object(self): """Override get_object method @@ -58,128 +48,27 @@ class PasswordResetConfirmView(JWTGenericViewMixin): user_id = force_text(urlsafe_base64_decode(uidb64)) token = self.kwargs.get('token') - filter_kwargs = {'key': token, 'user_id': user_id} - obj = get_object_or_404(queryset, **filter_kwargs) + obj = get_object_or_404(queryset, id=user_id) - if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token( - user=obj.user, token=token): - raise utils_exceptions.NotValidAccessTokenError() + if not password_token_generator.check_token(user=obj, token=token): + raise utils_exceptions.NotValidTokenError() # May raise a permission denied self.check_object_permissions(self.request, obj) return obj - def put(self, request, *args, **kwargs): - """Implement PUT method""" + def patch(self, request, *args, **kwargs): + """Implement PATCH method""" instance = self.get_object() serializer = self.get_serializer(instance=instance, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() - return Response(status=status.HTTP_200_OK) - - -# Form view -class PasswordContextMixin: - extra_context = None - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update({ - 'title': self.title, - **(self.extra_context or {}) - }) - return context - - -class FormPasswordResetSuccessView(views.APIView): - """View for successful reset password""" - - permission_classes = (permissions.AllowAny, ) - - def get(self, request, *args, **kwargs): - """Implement GET-method""" - return Response(status=status.HTTP_200_OK) - - -class FormPasswordResetConfirmView(PasswordContextMixin, FormView): - - INTERNAL_RESET_URL_TOKEN = 'set-password' - INTERNAL_RESET_SESSION_TOKEN = '_password_reset_token' - - form_class = SetPasswordForm - post_reset_login = False - post_reset_login_backend = None - success_url = reverse_lazy('web:account:form-password-reset-success') - template_name = settings.CONFIRMATION_PASSWORD_RESET_TEMPLATE - title = _('Enter new password') - token_generator = default_token_generator - - @method_decorator(sensitive_post_parameters()) - @method_decorator(never_cache) - def dispatch(self, *args, **kwargs): - assert 'uidb64' in kwargs and 'token' in kwargs - - self.validlink = False - self.user = self.get_user(kwargs['uidb64']) - - if self.user is not None: - token = kwargs['token'] - if token == self.INTERNAL_RESET_URL_TOKEN: - session_token = self.request.session.get(self.INTERNAL_RESET_SESSION_TOKEN) - if self.token_generator.check_token(self.user, session_token): - # If the token is valid, display the password reset form. - self.validlink = True - return super().dispatch(*args, **kwargs) - else: - if self.token_generator.check_token(self.user, token): - # Store the token in the session and redirect to the - # password reset form at a URL without the token. That - # avoids the possibility of leaking the token in the - # HTTP Referer header. - self.request.session[self.INTERNAL_RESET_SESSION_TOKEN] = token - redirect_url = self.request.path.replace(token, self.INTERNAL_RESET_URL_TOKEN) - return HttpResponseRedirect(redirect_url) - - # Display the "Password reset unsuccessful" page. - return self.render_to_response(self.get_context_data()) - - def get_user(self, uidb64): - try: - # urlsafe_base64_decode() decodes to bytestring - uid = urlsafe_base64_decode(uidb64).decode() - user = models.User.objects.get(pk=uid) - except (TypeError, ValueError, OverflowError, models.User.DoesNotExist, ValidationError): - user = None - return user - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['user'] = self.user - return kwargs - - def form_valid(self, form): - # Saving form - form.save() - user = form.user - - # Expire user tokens - user.expire_access_tokens() - user.expire_refresh_tokens() - - # Pop session token - del self.request.session[self.INTERNAL_RESET_SESSION_TOKEN] - return super().form_valid(form) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - if self.validlink: - context['validlink'] = True - else: - context.update({ - 'form': None, - 'title': _('Password reset unsuccessful'), - 'validlink': False, - }) - return context + # Create tokens + tokens = instance.create_jwt_tokens() + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies( + access_token=tokens.get('access_token'), + refresh_token=tokens.get('refresh_token')), + response=Response(status=status.HTTP_200_OK)) diff --git a/apps/authorization/urls/common.py b/apps/authorization/urls/common.py index 616f9d99..4e6e59e1 100644 --- a/apps/authorization/urls/common.py +++ b/apps/authorization/urls/common.py @@ -29,7 +29,7 @@ urlpatterns_oauth2 = [ urlpatterns_jwt = [ path('signup/', views.SignUpView.as_view(), name='signup'), - path('signup/confirm///', views.VerifyEmailConfirmView.as_view(), + path('signup/confirm///', views.ConfirmationEmailView.as_view(), name='signup-confirm'), path('login/', views.LoginByUsernameOrEmailView.as_view(), name='login'), path('logout/', views.LogoutView.as_view(), name="logout"), diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index 827ff108..98f79bd9 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -24,13 +24,12 @@ from authorization.serializers import common as serializers from utils import exceptions as utils_exceptions from utils.models import GMTokenGenerator from utils.permissions import IsAuthenticatedAndTokenIsValid -from utils.views import (JWTGenericViewMixin, - JWTCreateAPIView) +from utils.views import JWTGenericViewMixin # Mixins # JWTAuthView mixin -class JWTAuthViewMixin(JWTCreateAPIView): +class JWTAuthViewMixin(JWTGenericViewMixin): """Mixin for authentication views""" def post(self, request, *args, **kwargs): @@ -151,7 +150,7 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin): # JWT # Sign in via username and password -class SignUpView(JWTCreateAPIView): +class SignUpView(generics.GenericAPIView): """View for classic signup""" permission_classes = (permissions.AllowAny, ) serializer_class = serializers.SignupSerializer @@ -164,7 +163,7 @@ class SignUpView(JWTCreateAPIView): return Response(status=status.HTTP_201_CREATED) -class VerifyEmailConfirmView(JWTGenericViewMixin): +class ConfirmationEmailView(JWTGenericViewMixin): """View for confirmation email""" permission_classes = (permissions.AllowAny, ) diff --git a/apps/news/views/common.py b/apps/news/views/common.py index 2678289b..cf3c7e29 100644 --- a/apps/news/views/common.py +++ b/apps/news/views/common.py @@ -1,8 +1,9 @@ """News app common app.""" from rest_framework import generics, permissions + from news import filters, models from news.serializers import common as serializers -from utils.views import JWTGenericViewMixin, JWTListAPIView +from utils.views import JWTGenericViewMixin class NewsMixin: @@ -18,7 +19,7 @@ class NewsMixin: .order_by('-is_highlighted', '-created') -class NewsListView(NewsMixin, JWTListAPIView): +class NewsListView(NewsMixin, generics.ListAPIView): """News list view.""" filter_class = filters.NewsListFilterSet diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index 5251c4e4..df9c2c27 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -32,15 +32,6 @@ class UserNotFoundError(AuthErrorMixin, ProjectBaseException): default_detail = _('User not found') -class PasswordRequestResetExists(ProjectBaseException): - """ - The exception should be thrown when request for reset password - is already exists and valid - """ - status_code = status.HTTP_400_BAD_REQUEST - default_detail = _('Password request is already exists. Please wait.') - - class EmailSendingError(exceptions.APIException): """The exception should be thrown when unable to send an email""" status_code = status.HTTP_400_BAD_REQUEST @@ -142,3 +133,12 @@ class FavoritesError(exceptions.APIException): """ status_code = status.HTTP_400_BAD_REQUEST default_detail = _('Item is already in favorites.') + + +class PasswordResetRequestExistedError(exceptions.APIException): + """ + The exception should be thrown when password reset request + already exists and valid. + """ + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _('Password reset request is already exists and valid.') diff --git a/apps/utils/views.py b/apps/utils/views.py index d129fb73..9af89360 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -7,7 +7,7 @@ from rest_framework.response import Response # JWT -# Login base view mixin +# Login base view mixins class JWTGenericViewMixin(generics.GenericAPIView): """JWT view mixin""" @@ -95,102 +95,3 @@ class JWTGenericViewMixin(generics.GenericAPIView): http_only=self.REFRESH_TOKEN_HTTP_ONLY, secure=self.REFRESH_TOKEN_SECURE, max_age=_cookies.get('max_age'))] - - -class JWTListAPIView(JWTGenericViewMixin, generics.ListAPIView): - """ - Concrete view for creating a model instance. - """ - def get(self, request, *args, **kwargs): - queryset = self.filter_queryset(self.get_queryset()) - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - response = self.get_paginated_response(serializer.data) - else: - serializer = self.get_serializer(queryset, many=True) - response = Response(serializer.data) - access_token, refresh_token = self._get_tokens_from_cookies(request) - return self._put_cookies_in_response( - cookies=self._put_data_in_cookies(access_token=access_token.value, - refresh_token=refresh_token.value), - response=response) - - -class JWTCreateAPIView(JWTGenericViewMixin, generics.CreateAPIView): - """ - Concrete view for creating a model instance. - """ - def post(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - serializer.save() - response = Response(serializer.data, status=status.HTTP_201_CREATED) - access_token, refresh_token = self._get_tokens_from_cookies(request) - return self._put_cookies_in_response( - cookies=self._put_data_in_cookies(access_token=access_token.value, - refresh_token=refresh_token.value), - response=response) - - -class JWTRetrieveAPIView(JWTGenericViewMixin, generics.RetrieveAPIView): - """ - Concrete view for retrieving a model instance. - """ - def get(self, request, *args, **kwargs): - """Implement GET method""" - queryset = self.filter_queryset(self.get_queryset()) - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - response = self.get_paginated_response(serializer.data) - else: - serializer = self.get_serializer(queryset, many=True) - response = Response(serializer.data, status.HTTP_200_OK) - access_token, refresh_token = self._get_tokens_from_cookies(request) - return self._put_cookies_in_response( - cookies=self._put_data_in_cookies(access_token=access_token, - refresh_token=refresh_token), - response=response) - - -class JWTDestroyAPIView(JWTGenericViewMixin, generics.DestroyAPIView): - """ - Concrete view for deleting a model instance. - """ - def delete(self, request, *args, **kwargs): - instance = self.get_object() - instance.delete() - response = Response(status=status.HTTP_204_NO_CONTENT) - access_token, refresh_token = self._get_tokens_from_cookies(request) - return self._put_cookies_in_response( - cookies=self._put_data_in_cookies(access_token=access_token, - refresh_token=refresh_token), - response=response) - - -class JWTUpdateAPIView(JWTGenericViewMixin, generics.UpdateAPIView): - """ - Concrete view for updating a model instance. - """ - def put(self, request, *args, **kwargs): - partial = kwargs.pop('partial', False) - instance = self.get_object() - serializer = self.get_serializer(instance, data=request.data, partial=partial) - serializer.is_valid(raise_exception=True) - serializer.save() - if getattr(instance, '_prefetched_objects_cache', None): - # If 'prefetch_related' has been applied to a queryset, we need to - # forcibly invalidate the prefetch cache on the instance. - instance._prefetched_objects_cache = {} - response = Response(serializer.data) - access_token, refresh_token = self._get_tokens_from_cookies(request) - return self._put_cookies_in_response( - cookies=self._put_data_in_cookies(access_token=access_token, - refresh_token=refresh_token), - response=response) - - def patch(self, request, *args, **kwargs): - kwargs['partial'] = True - return self.put(request, *args, **kwargs) - diff --git a/project/settings/base.py b/project/settings/base.py index b7047f98..16ac6002 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -369,7 +369,6 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1 # TEMPLATES -CONFIRMATION_PASSWORD_RESET_TEMPLATE = 'account/password_reset_confirm.html' RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html' CHANGE_EMAIL_TEMPLATE = 'account/change_email.html' CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html' diff --git a/project/templates/account/change_email.html b/project/templates/account/change_email.html index ceec753f..40c1b227 100644 --- a/project/templates/account/change_email.html +++ b/project/templates/account/change_email.html @@ -2,9 +2,8 @@ {% blocktrans %}You're receiving this email because you want to change email address at {{ site_name }}.{% endblocktrans %} {% trans "Please go to the following page for confirmation new email address:" %} -{% block reset_link %} -http://{{ domain_uri }}{% url 'web:account:change-email-confirm' uidb64=uidb64 token=token %} -{% endblock %} + +https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/ {% trans "Thanks for using our site!" %} diff --git a/project/templates/account/password_reset_confirm.html b/project/templates/account/password_reset_confirm.html deleted file mode 100644 index 62cdb8eb..00000000 --- a/project/templates/account/password_reset_confirm.html +++ /dev/null @@ -1,31 +0,0 @@ -{% load i18n static %} - -{% block content %} - -{% if validlink %} - -

{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

- -
{% csrf_token %} -
-
- {{ form.new_password1.errors }} - - {{ form.new_password1 }} -
-
- {{ form.new_password2.errors }} - - {{ form.new_password2 }} -
- -
-
- -{% else %} - -

{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}

- -{% endif %} - -{% endblock %} \ No newline at end of file diff --git a/project/templates/account/password_reset_email.html b/project/templates/account/password_reset_email.html index c32469f7..ee84fd0b 100644 --- a/project/templates/account/password_reset_email.html +++ b/project/templates/account/password_reset_email.html @@ -2,7 +2,9 @@ {% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} {% trans "Please go to the following page and choose a new password:" %} + https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/ + {% trans "Thanks for using our site!" %} {% blocktrans %}The {{ site_name }} team{% endblocktrans %} From 1ee21edb8e0feb7b979d0e93eaac8040d69788a5 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 12 Sep 2019 16:18:42 +0300 Subject: [PATCH 44/51] added endpoint to retrieve establishment tags --- apps/authorization/serializers/common.py | 2 +- apps/establishment/serializers.py | 21 +++++++++++++++------ apps/establishment/urls/common.py | 1 + apps/establishment/views.py | 13 +++++++++++++ 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index f5e24fc9..a4c6f817 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -107,7 +107,7 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin, authentication = authenticate(username=user.get_username(), password=password) if not authentication: - raise utils_exceptions.WrongAuthCredentials() + raise utils_exceptions.UserNotFoundError() self.instance = user return attrs diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index c297ce54..3c1e3407 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -6,6 +6,7 @@ from comment.serializers import common as comment_serializers from establishment import models from favorites.models import Favorites from location.serializers import AddressSerializer +from main.models import MetaDataContent from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer from review import models as review_models from timetable.models import Timetable @@ -132,7 +133,7 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer): subtypes = EstablishmentSubTypeSerializer(many=True) address = AddressSerializer() tags = MetaDataContentSerializer(many=True) - preview_image = serializers.SerializerMethodField() + preview_image = serializers.ImageField(source='image', use_url=False) class Meta: """Meta class.""" @@ -151,11 +152,6 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer): 'tags', ] - def get_preview_image(self, obj): - """Get preview image""" - return obj.get_full_image_url(request=self.context.get('request'), - thumbnail_key='establishment_preview') - class EstablishmentListSerializer(EstablishmentBaseSerializer): """Serializer for Establishment model.""" @@ -322,3 +318,16 @@ class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer): 'content_object': validated_data.pop('establishment') }) return super().create(validated_data) + + +class EstablishmentTagListSerializer(serializers.ModelSerializer): + """List establishment tag serializer.""" + label_translated = serializers.CharField( + source='metadata.label_translated', read_only=True, allow_null=True) + + class Meta: + """Meta class.""" + model = MetaDataContent + fields = [ + 'label_translated', + ] diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 0f2958eb..007f7802 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -8,6 +8,7 @@ app_name = 'establishment' urlpatterns = [ path('', views.EstablishmentListView.as_view(), name='list'), + path('tags/', views.EstablishmentTagListView.as_view(), name='tags'), path('/', views.EstablishmentRetrieveView.as_view(), name='detail'), path('/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'), path('/comments/create/', views.EstablishmentCommentCreateView.as_view(), diff --git a/apps/establishment/views.py b/apps/establishment/views.py index 528d2377..379407bd 100644 --- a/apps/establishment/views.py +++ b/apps/establishment/views.py @@ -6,6 +6,7 @@ from rest_framework import generics, permissions from comment import models as comment_models from establishment import filters from establishment import models, serializers +from main.models import MetaDataContent from utils.views import JWTGenericViewMixin @@ -117,3 +118,15 @@ class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.D # May raise a permission denied self.check_object_permissions(self.request, obj) return obj + + +class EstablishmentTagListView(generics.ListAPIView): + """List view for establishment tags.""" + serializer_class = serializers.EstablishmentTagListSerializer + permission_classes = (permissions.AllowAny,) + pagination_class = None + + def get_queryset(self): + """Override get_queryset method""" + return MetaDataContent.objects.by_content_type(app_label='establishment', + model='establishment') From f45ac7e7fed84ae3b64313211dc35d8fa2aadaed Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 12 Sep 2019 17:33:55 +0300 Subject: [PATCH 45/51] fixed establishment detail --- apps/establishment/serializers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 3c1e3407..abb3585a 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -133,7 +133,7 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer): subtypes = EstablishmentSubTypeSerializer(many=True) address = AddressSerializer() tags = MetaDataContentSerializer(many=True) - preview_image = serializers.ImageField(source='image', use_url=False) + preview_image = serializers.ImageField(source='image') class Meta: """Meta class.""" @@ -152,6 +152,11 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer): 'tags', ] + def get_preview_image(self, obj): + """Get preview image""" + return obj.get_full_image_url(request=self.context.get('request'), + thumbnail_key='establishment_preview') + class EstablishmentListSerializer(EstablishmentBaseSerializer): """Serializer for Establishment model.""" From 1e650b9b1f33a3d93e05429651a8789fcefd5a4f Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 12 Sep 2019 18:10:52 +0300 Subject: [PATCH 46/51] replace ImageFields to URLFields in User model, refactored serializers that included this fields --- apps/account/admin.py | 18 +++++++++++++-- .../migrations/0007_auto_20190912_1323.py | 21 +++++++++++++++++ .../migrations/0008_auto_20190912_1325.py | 23 +++++++++++++++++++ apps/account/models.py | 20 +++++++++++----- apps/account/serializers/common.py | 8 +++---- apps/comment/serializers/common.py | 7 ++---- apps/gallery/views.py | 2 +- 7 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 apps/account/migrations/0007_auto_20190912_1323.py create mode 100644 apps/account/migrations/0008_auto_20190912_1325.py diff --git a/apps/account/admin.py b/apps/account/admin.py index dc88c34b..8429952f 100644 --- a/apps/account/admin.py +++ b/apps/account/admin.py @@ -15,11 +15,13 @@ class UserAdmin(BaseUserAdmin): list_filter = ('is_active', 'is_staff', 'is_superuser', 'email_confirmed', 'groups',) search_fields = ('email', 'first_name', 'last_name') - readonly_fields = ('last_login', 'date_joined',) + readonly_fields = ('last_login', 'date_joined', 'image_preview', 'cropped_image_preview') fieldsets = ( (None, {'fields': ('email', 'password',)}), (_('Personal info'), { - 'fields': ('username', 'first_name', 'last_name', 'image')}), + 'fields': ('username', 'first_name', 'last_name', + 'image_url', 'image_preview', + 'cropped_image_url', 'cropped_image_preview',)}), (_('Subscription'), { 'fields': ( 'newsletter', @@ -46,3 +48,15 @@ class UserAdmin(BaseUserAdmin): return obj.get_short_name() short_name.short_description = _('Name') + + def image_preview(self, obj): + """Get user image preview""" + return obj.image_tag + + image_preview.short_description = 'Image preview' + + def cropped_image_preview(self, obj): + """Get user cropped image preview""" + return obj.cropped_image_tag + + cropped_image_preview.short_description = 'Cropped image preview' diff --git a/apps/account/migrations/0007_auto_20190912_1323.py b/apps/account/migrations/0007_auto_20190912_1323.py new file mode 100644 index 00000000..3fe08c7b --- /dev/null +++ b/apps/account/migrations/0007_auto_20190912_1323.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.4 on 2019-09-12 13:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0006_delete_resetpasswordtoken'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='cropped_image', + ), + migrations.RemoveField( + model_name='user', + name='image', + ), + ] diff --git a/apps/account/migrations/0008_auto_20190912_1325.py b/apps/account/migrations/0008_auto_20190912_1325.py new file mode 100644 index 00000000..b0a09ad2 --- /dev/null +++ b/apps/account/migrations/0008_auto_20190912_1325.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.4 on 2019-09-12 13:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0007_auto_20190912_1323'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='cropped_image_url', + field=models.URLField(blank=True, default=None, null=True, verbose_name='Cropped image URL path'), + ), + migrations.AddField( + model_name='user', + name='image_url', + field=models.URLField(blank=True, default=None, null=True, verbose_name='Image URL path'), + ), + ] diff --git a/apps/account/models.py b/apps/account/models.py index ca5a4b96..81ade4fc 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -7,13 +7,12 @@ from django.db import models from django.template.loader import render_to_string from django.utils import timezone from django.utils.encoding import force_bytes +from django.utils.html import mark_safe from django.utils.http import urlsafe_base64_encode from django.utils.translation import ugettext_lazy as _ -from easy_thumbnails.fields import ThumbnailerImageField from rest_framework.authtoken.models import Token from authorization.models import Application -from utils.methods import image_path from utils.models import GMTokenGenerator from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin from utils.tokens import GMRefreshToken @@ -53,11 +52,12 @@ class UserQuerySet(models.QuerySet): oauth2_provider_refreshtoken__expires__gt=timezone.now()) -class User(ImageMixin, AbstractUser): +class User(AbstractUser): """Base user model.""" - cropped_image = ThumbnailerImageField(upload_to=image_path, - blank=True, null=True, default=None, - verbose_name=_('Crop image')) + image_url = models.URLField(verbose_name=_('Image URL path'), + blank=True, null=True, default=None) + cropped_image_url = models.URLField(verbose_name=_('Cropped image URL path'), + blank=True, null=True, default=None) email = models.EmailField(_('email address'), blank=True, null=True, default=None) email_confirmed = models.BooleanField(_('email status'), default=False) @@ -161,6 +161,14 @@ class User(ImageMixin, AbstractUser): 'uidb64': self.get_user_uidb64, 'site_name': settings.SITE_NAME} + @property + def image_tag(self): + return mark_safe(f'') + + @property + def cropped_image_tag(self): + return mark_safe(f'') + def reset_password_template(self, country_code): """Get reset password template""" context = {'token': self.reset_password_token, diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index e6599f96..0c72d036 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -24,8 +24,8 @@ class UserSerializer(serializers.ModelSerializer): required=False) first_name = serializers.CharField(required=False, write_only=True) last_name = serializers.CharField(required=False, write_only=True) - image = serializers.ImageField(required=False) - cropped_image = serializers.ImageField(required=False) + image_url = serializers.URLField(required=False) + cropped_image_url = serializers.URLField(required=False) email = serializers.EmailField( validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),), required=False) @@ -38,8 +38,8 @@ class UserSerializer(serializers.ModelSerializer): 'first_name', 'last_name', 'fullname', - 'cropped_image', - 'image', + 'cropped_image_url', + 'image_url', 'email', 'email_confirmed', 'newsletter', diff --git a/apps/comment/serializers/common.py b/apps/comment/serializers/common.py index 3598be4c..8175df7f 100644 --- a/apps/comment/serializers/common.py +++ b/apps/comment/serializers/common.py @@ -9,7 +9,8 @@ class CommentSerializer(serializers.ModelSerializer): nickname = serializers.CharField(read_only=True, source='user.username') is_mine = serializers.BooleanField(read_only=True) - profile_pic = serializers.SerializerMethodField() + profile_pic = serializers.URLField(read_only=True, + source='user.cropped_image_url') class Meta: """Serializer for model Comment""" @@ -24,7 +25,3 @@ class CommentSerializer(serializers.ModelSerializer): 'nickname', 'profile_pic' ] - - def get_profile_pic(self, obj): - """Get profile picture URL""" - return obj.user.get_full_image_url(request=self.context.get('request')) diff --git a/apps/gallery/views.py b/apps/gallery/views.py index a3b61727..109a01ef 100644 --- a/apps/gallery/views.py +++ b/apps/gallery/views.py @@ -6,7 +6,7 @@ from . import models, serializers class ImageUploadView(generics.CreateAPIView): """Upload image to gallery""" - permission_classes = (IsAuthenticatedAndTokenIsValid, ) model = models.Image queryset = models.Image.objects.all() serializer_class = serializers.ImageSerializer + permission_classes = (IsAuthenticatedAndTokenIsValid, ) From efac785248350306abb869a6e015f420349bee54 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Thu, 12 Sep 2019 18:24:21 +0300 Subject: [PATCH 47/51] add api for estab, social, plates, menu --- apps/establishment/filters.py | 2 +- .../0016_remove_establishment_name.py | 17 ++++++ .../migrations/0017_auto_20190911_1258.py | 55 +++++++++++++++++++ .../migrations/0018_socialnetwork.py | 27 +++++++++ apps/establishment/models.py | 44 ++++++++++----- apps/establishment/serializers/__init__.py | 3 + apps/establishment/serializers/back.py | 55 +++++++++++++++++++ .../{serializers.py => serializers/common.py} | 26 +++++++-- apps/establishment/serializers/web.py | 0 apps/establishment/urls/back.py | 19 +++++++ apps/establishment/views/__init__.py | 3 + apps/establishment/views/back.py | 50 +++++++++++++++++ apps/establishment/views/common.py | 15 +++++ apps/establishment/{views.py => views/web.py} | 13 +---- project/urls/back.py | 2 +- 15 files changed, 299 insertions(+), 32 deletions(-) create mode 100644 apps/establishment/migrations/0016_remove_establishment_name.py create mode 100644 apps/establishment/migrations/0017_auto_20190911_1258.py create mode 100644 apps/establishment/migrations/0018_socialnetwork.py create mode 100644 apps/establishment/serializers/__init__.py create mode 100644 apps/establishment/serializers/back.py rename apps/establishment/{serializers.py => serializers/common.py} (94%) create mode 100644 apps/establishment/serializers/web.py create mode 100644 apps/establishment/urls/back.py create mode 100644 apps/establishment/views/__init__.py create mode 100644 apps/establishment/views/back.py create mode 100644 apps/establishment/views/common.py rename apps/establishment/{views.py => views/web.py} (93%) diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index 46960d70..51b207dc 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -5,7 +5,7 @@ from establishment import models class EstablishmentFilter(filters.FilterSet): - """Establishment filterset.""" + """Establishment filter set.""" tag_id = filters.NumberFilter(field_name='tags__metadata__id',) award_id = filters.NumberFilter(field_name='awards__id',) diff --git a/apps/establishment/migrations/0016_remove_establishment_name.py b/apps/establishment/migrations/0016_remove_establishment_name.py new file mode 100644 index 00000000..863f3d13 --- /dev/null +++ b/apps/establishment/migrations/0016_remove_establishment_name.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.4 on 2019-09-11 12:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0015_delete_comment'), + ] + + operations = [ + migrations.RemoveField( + model_name='establishment', + name='name', + ), + ] diff --git a/apps/establishment/migrations/0017_auto_20190911_1258.py b/apps/establishment/migrations/0017_auto_20190911_1258.py new file mode 100644 index 00000000..c6aa8657 --- /dev/null +++ b/apps/establishment/migrations/0017_auto_20190911_1258.py @@ -0,0 +1,55 @@ +# Generated by Django 2.2.4 on 2019-09-11 12:58 + +from django.db import migrations, models +import django.db.models.deletion +import utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0016_remove_establishment_name'), + ] + + operations = [ + migrations.AddField( + model_name='establishment', + name='name', + field=models.CharField(default='', max_length=255, verbose_name='name'), + ), + migrations.AlterField( + model_name='establishment', + name='address', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='location.Address', verbose_name='address'), + ), + migrations.AlterField( + model_name='establishment', + name='description', + field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='description'), + ), + migrations.AlterField( + model_name='establishment', + name='establishment_subtypes', + field=models.ManyToManyField(related_name='subtype_establishment', to='establishment.EstablishmentSubType', verbose_name='subtype'), + ), + migrations.AlterField( + model_name='establishment', + name='establishment_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='establishment', to='establishment.EstablishmentType', verbose_name='type'), + ), + migrations.AlterField( + model_name='establishment', + name='price_level', + field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='price level'), + ), + migrations.AlterField( + model_name='establishment', + name='public_mark', + field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='public mark'), + ), + migrations.AlterField( + model_name='establishment', + name='toque_number', + field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='toque number'), + ), + ] diff --git a/apps/establishment/migrations/0018_socialnetwork.py b/apps/establishment/migrations/0018_socialnetwork.py new file mode 100644 index 00000000..8efdb1d2 --- /dev/null +++ b/apps/establishment/migrations/0018_socialnetwork.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.4 on 2019-09-12 13:04 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0017_auto_20190911_1258'), + ] + + operations = [ + migrations.CreateModel( + name='SocialNetwork', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='title')), + ('url', models.URLField(verbose_name='URL')), + ('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='socials', to='establishment.Establishment', verbose_name='establishment')), + ], + options={ + 'verbose_name': 'social network', + 'verbose_name_plural': 'social networks', + }, + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index efe665ef..a0e28d11 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -106,32 +106,29 @@ class EstablishmentQuerySet(models.QuerySet): class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin): """Establishment model.""" - STR_FIELD_NAME = 'name' - - name = TJSONField(blank=True, null=True, default=None, - verbose_name=_('Name'), help_text='{"en-GB":"some text"}') + name = models.CharField(_('name'), max_length=255, default='') description = TJSONField(blank=True, null=True, default=None, - verbose_name=_('Description'), + verbose_name=_('description'), help_text='{"en-GB":"some text"}') public_mark = models.PositiveIntegerField(blank=True, null=True, default=None, - verbose_name=_('Public mark'),) + verbose_name=_('public mark'),) toque_number = models.PositiveIntegerField(blank=True, null=True, default=None, - verbose_name=_('Toque number'),) + verbose_name=_('toque number'),) establishment_type = models.ForeignKey(EstablishmentType, related_name='establishment', on_delete=models.PROTECT, - verbose_name=_('Type')) + verbose_name=_('type')) establishment_subtypes = models.ManyToManyField(EstablishmentSubType, related_name='subtype_establishment', - verbose_name=_('Subtype')) + verbose_name=_('subtype')) address = models.ForeignKey(Address, blank=True, null=True, default=None, on_delete=models.PROTECT, - verbose_name=_('Address')) + verbose_name=_('address')) price_level = models.PositiveIntegerField(blank=True, null=True, default=None, - verbose_name=_('Price level')) + verbose_name=_('price level')) website = models.URLField(blank=True, null=True, default=None, verbose_name=_('Web site URL')) facebook = models.URLField(blank=True, null=True, default=None, @@ -155,6 +152,9 @@ class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin): verbose_name = _('Establishment') verbose_name_plural = _('Establishments') + def __str__(self): + return f'id:{self.id}-{self.name}' + # todo: recalculate toque_number def recalculate_toque_number(self): self.toque_number = 4 @@ -326,6 +326,7 @@ class ContactEmail(models.Model): class Plate(TranslatedFieldsMixin, models.Model): """Plate model.""" + STR_FIELD_NAME = 'name' name = TJSONField( blank=True, null=True, default=None, verbose_name=_('name'), @@ -346,12 +347,12 @@ class Plate(TranslatedFieldsMixin, models.Model): verbose_name = _('plate') verbose_name_plural = _('plates') - def __str__(self): - return f'plate_id:{self.id}' - class Menu(TranslatedFieldsMixin, BaseAttributes): """Menu model.""" + + STR_FIELD_NAME = 'category' + category = TJSONField( blank=True, null=True, default=None, verbose_name=_('category'), help_text='{"en-GB":"some text"}') @@ -362,3 +363,18 @@ class Menu(TranslatedFieldsMixin, BaseAttributes): class Meta: verbose_name = _('menu') verbose_name_plural = _('menu') + + +class SocialNetwork(models.Model): + establishment = models.ForeignKey( + 'Establishment', verbose_name=_('establishment'), + related_name='socials', on_delete=models.CASCADE) + title = models.CharField(_('title'), max_length=255) + url = models.URLField(_('URL')) + + class Meta: + verbose_name = _('social network') + verbose_name_plural = _('social networks') + + def __str__(self): + return self.title diff --git a/apps/establishment/serializers/__init__.py b/apps/establishment/serializers/__init__.py new file mode 100644 index 00000000..d21c60b9 --- /dev/null +++ b/apps/establishment/serializers/__init__.py @@ -0,0 +1,3 @@ +from establishment.serializers.common import * +from establishment.serializers.web import * +from establishment.serializers.back import * diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py new file mode 100644 index 00000000..89fb5f91 --- /dev/null +++ b/apps/establishment/serializers/back.py @@ -0,0 +1,55 @@ +from establishment.serializers import EstablishmentBaseSerializer, PlateSerializer +from rest_framework import serializers +from establishment import models +from main.models import Currency + +class EstablishmentListCreateSerializer(EstablishmentBaseSerializer): + """Establishment create serializer""" + + type_id = serializers.PrimaryKeyRelatedField( + source='establishment_type', + queryset=models.EstablishmentType.objects.all(), write_only=True + ) + + class Meta: + model = models.Establishment + fields = [ + 'id', + 'name', + 'website', + 'phone', + 'email', + 'price_level', + 'toque_number', + 'type_id', + 'type' + ] + + +class SocialNetworkSerializers(serializers.ModelSerializer): + """Social network serializers.""" + class Meta: + model = models.SocialNetwork + fields = [ + 'id', + 'establishment', + 'title', + 'url', + ] + + +class PlatesSerializers(PlateSerializer): + """Social network serializers.""" + name = serializers.JSONField() + currency_id = serializers.PrimaryKeyRelatedField( + source='currency', + queryset=Currency.objects.all(), write_only=True + ) + + class Meta: + model = models.Plate + fields = PlateSerializer.Meta.fields + [ + 'name', + 'currency_id', + 'menu' + ] \ No newline at end of file diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers/common.py similarity index 94% rename from apps/establishment/serializers.py rename to apps/establishment/serializers/common.py index c297ce54..b8d212f8 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers/common.py @@ -32,7 +32,7 @@ class ContactEmailsSerializer(serializers.ModelSerializer): class PlateSerializer(serializers.ModelSerializer): - name_translated = serializers.CharField(allow_null=True) + name_translated = serializers.CharField(allow_null=True, read_only=True) currency = CurrencySerializer(read_only=True) class Meta: @@ -47,14 +47,31 @@ class PlateSerializer(serializers.ModelSerializer): class MenuSerializers(serializers.ModelSerializer): plates = PlateSerializer(read_only=True, many=True, source='plate_set') + category = serializers.JSONField() category_translated = serializers.CharField(read_only=True) class Meta: model = models.Menu fields = [ 'id', + 'category', 'category_translated', - 'plates' + 'plates', + 'establishment' + ] + + +class MenuRUDSerializers(serializers.ModelSerializer): + plates = PlateSerializer(read_only=True, many=True, source='plate_set') + category = serializers.JSONField() + + class Meta: + model = models.Menu + fields = [ + 'id', + 'category', + 'plates', + 'establishment' ] @@ -127,8 +144,7 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer): class EstablishmentBaseSerializer(serializers.ModelSerializer): """Base serializer for Establishment model.""" - name_translated = serializers.CharField(allow_null=True) - type = EstablishmentTypeSerializer(source='establishment_type') + type = EstablishmentTypeSerializer(source='establishment_type', read_only=True) subtypes = EstablishmentSubTypeSerializer(many=True) address = AddressSerializer() tags = MetaDataContentSerializer(many=True) @@ -140,7 +156,7 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer): model = models.Establishment fields = [ 'id', - 'name_translated', + 'name', 'price_level', 'toque_number', 'public_mark', diff --git a/apps/establishment/serializers/web.py b/apps/establishment/serializers/web.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py new file mode 100644 index 00000000..efe9db8d --- /dev/null +++ b/apps/establishment/urls/back.py @@ -0,0 +1,19 @@ +"""Establishment url patterns for backoffice.""" + +from django.urls import path + +from establishment import views + +app_name = 'establishment' + + +urlpatterns = [ + path('', views.EstablishmentListCreateView.as_view(), name='list'), + path('/', views.EstablishmentRetrieveView.as_view(), name='detail'), + path('menus/', views.MenuListCreateView.as_view(), name='menu-list'), + path('menus//', views.MenuRUDView.as_view(), name='menu-rud'), + path('plates/', views.PlateListCreateView.as_view(), name='plates'), + path('plates//', views.PlateListCreateView.as_view(), name='plate-rud'), + path('socials/', views.SocialListCreateView.as_view(), name='socials'), + path('socials//', views.SocialRUDView.as_view(), name='social-rud'), +] \ No newline at end of file diff --git a/apps/establishment/views/__init__.py b/apps/establishment/views/__init__.py new file mode 100644 index 00000000..2c7b88bc --- /dev/null +++ b/apps/establishment/views/__init__.py @@ -0,0 +1,3 @@ +from establishment.views.common import * +from establishment.views.web import * +from establishment.views.back import * diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py new file mode 100644 index 00000000..45fd8745 --- /dev/null +++ b/apps/establishment/views/back.py @@ -0,0 +1,50 @@ +"""Establishment app views.""" + +from rest_framework import generics + +from establishment import models, serializers +from establishment.views.common import EstablishmentMixin + + +class EstablishmentListCreateView(EstablishmentMixin, generics.ListCreateAPIView): + """Establishment list/create view.""" + queryset = models.Establishment.objects.all() + serializer_class = serializers.EstablishmentListCreateSerializer + + +class MenuListCreateView(generics.ListCreateAPIView): + """Menu list create view.""" + serializer_class = serializers.MenuSerializers + queryset = models.Menu.objects.all() + + +class MenuRUDView(generics.RetrieveUpdateDestroyAPIView): + """Menu RUD view.""" + serializer_class = serializers.MenuRUDSerializers + queryset = models.Menu.objects.all() + + +class SocialListCreateView(generics.ListCreateAPIView): + """Social list create view.""" + serializer_class = serializers.SocialNetworkSerializers + queryset = models.SocialNetwork.objects.all() + pagination_class = None + + +class SocialRUDView(generics.RetrieveUpdateDestroyAPIView): + """Social RUD view.""" + serializer_class = serializers.SocialNetworkSerializers + queryset = models.SocialNetwork.objects.all() + + +class PlateListCreateView(generics.ListCreateAPIView): + """Plate list create view.""" + serializer_class = serializers.PlatesSerializers + queryset = models.Plate.objects.all() + pagination_class = None + + +class PlateRUDView(generics.RetrieveUpdateDestroyAPIView): + """Social RUD view.""" + serializer_class = serializers.PlatesSerializers + queryset = models.Plate.objects.all() \ No newline at end of file diff --git a/apps/establishment/views/common.py b/apps/establishment/views/common.py new file mode 100644 index 00000000..454ef1d0 --- /dev/null +++ b/apps/establishment/views/common.py @@ -0,0 +1,15 @@ +"""Establishment app views.""" + +from rest_framework import permissions + +from establishment import models + + +class EstablishmentMixin: + """Establishment mixin.""" + + permission_classes = (permissions.AllowAny,) + + def get_queryset(self): + """Overrided method 'get_queryset'.""" + return models.Establishment.objects.all().prefetch_actual_employees() \ No newline at end of file diff --git a/apps/establishment/views.py b/apps/establishment/views/web.py similarity index 93% rename from apps/establishment/views.py rename to apps/establishment/views/web.py index 528d2377..b1a8d8d4 100644 --- a/apps/establishment/views.py +++ b/apps/establishment/views/web.py @@ -7,16 +7,7 @@ from comment import models as comment_models from establishment import filters from establishment import models, serializers from utils.views import JWTGenericViewMixin - - -class EstablishmentMixin: - """Establishment mixin.""" - - permission_classes = (permissions.AllowAny,) - - def get_queryset(self): - """Overrided method 'get_queryset'.""" - return models.Establishment.objects.all().prefetch_actual_employees() +from establishment.views import EstablishmentMixin class EstablishmentListView(EstablishmentMixin, JWTGenericViewMixin, generics.ListAPIView): @@ -25,7 +16,7 @@ class EstablishmentListView(EstablishmentMixin, JWTGenericViewMixin, generics.Li filter_class = filters.EstablishmentFilter def get_queryset(self): - """Overrided method 'get_queryset'.""" + """Overridden method 'get_queryset'.""" qs = super(EstablishmentListView, self).get_queryset() return qs.by_country_code(code=self.request.country_code)\ .annotate_in_favorites(user=self.request.user) diff --git a/project/urls/back.py b/project/urls/back.py index af823cd2..6337f4cd 100644 --- a/project/urls/back.py +++ b/project/urls/back.py @@ -8,7 +8,7 @@ urlpatterns = [ # path('account/', include('account.urls.web')), # path('advertisement/', include('advertisement.urls.web')), # path('collection/', include('collection.urls.web')), - # path('establishments/', include('establishment.urls.web')), + path('establishments/', include('establishment.urls.back')), # path('news/', include('news.urls.web')), # path('partner/', include('partner.urls.web')), ] \ No newline at end of file From 15f02b72cab8a3aa61dea79d39e80fe30c8abdaa Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Fri, 13 Sep 2019 12:20:36 +0300 Subject: [PATCH 48/51] add email and phone api for establishments --- apps/establishment/serializers/back.py | 38 +++++++++++++++++++++--- apps/establishment/serializers/common.py | 11 +++++++ apps/establishment/urls/back.py | 4 +++ apps/establishment/views/back.py | 28 ++++++++++++++++- 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 89fb5f91..1c7d9d27 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -1,8 +1,12 @@ -from establishment.serializers import EstablishmentBaseSerializer, PlateSerializer from rest_framework import serializers + from establishment import models +from establishment.serializers import ( + EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer, + ContactPhonesSerializer, SocialNetworkRelatedSerializers) from main.models import Currency + class EstablishmentListCreateSerializer(EstablishmentBaseSerializer): """Establishment create serializer""" @@ -10,6 +14,9 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer): source='establishment_type', queryset=models.EstablishmentType.objects.all(), write_only=True ) + phones = ContactPhonesSerializer(read_only=True, many=True, ) + emails = ContactEmailsSerializer(read_only=True, many=True, ) + socials = SocialNetworkRelatedSerializers(read_only=True, many=True, ) class Meta: model = models.Establishment @@ -17,12 +24,13 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer): 'id', 'name', 'website', - 'phone', - 'email', + 'phones', + 'emails', 'price_level', 'toque_number', 'type_id', - 'type' + 'type', + 'socials' ] @@ -52,4 +60,26 @@ class PlatesSerializers(PlateSerializer): 'name', 'currency_id', 'menu' + ] + + +class ContactPhoneBackSerializers(PlateSerializer): + """Social network serializers.""" + class Meta: + model = models.ContactPhone + fields = [ + 'id', + 'establishment', + 'phone' + ] + + +class ContactEmailBackSerializers(PlateSerializer): + """Social network serializers.""" + class Meta: + model = models.ContactEmail + fields = [ + 'id', + 'establishment', + 'email' ] \ No newline at end of file diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index d6b21180..65331ad7 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -31,6 +31,17 @@ class ContactEmailsSerializer(serializers.ModelSerializer): ] +class SocialNetworkRelatedSerializers(serializers.ModelSerializer): + """Social network serializers.""" + class Meta: + model = models.SocialNetwork + fields = [ + 'id', + 'title', + 'url', + ] + + class PlateSerializer(serializers.ModelSerializer): name_translated = serializers.CharField(allow_null=True, read_only=True) diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index efe9db8d..bce07ee5 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -16,4 +16,8 @@ urlpatterns = [ path('plates//', views.PlateListCreateView.as_view(), name='plate-rud'), path('socials/', views.SocialListCreateView.as_view(), name='socials'), path('socials//', views.SocialRUDView.as_view(), name='social-rud'), + path('phones/', views.PhonesListCreateView.as_view(), name='phones'), + path('phones//', views.PhonesRUDView.as_view(), name='phones-rud'), + path('emails/', views.EmailListCreateView.as_view(), name='emails'), + path('emails//', views.EmailRUDView.as_view(), name='emails-rud'), ] \ No newline at end of file diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 45fd8745..14488993 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -47,4 +47,30 @@ class PlateListCreateView(generics.ListCreateAPIView): class PlateRUDView(generics.RetrieveUpdateDestroyAPIView): """Social RUD view.""" serializer_class = serializers.PlatesSerializers - queryset = models.Plate.objects.all() \ No newline at end of file + queryset = models.Plate.objects.all() + + +class PhonesListCreateView(generics.ListCreateAPIView): + """Plate list create view.""" + serializer_class = serializers.ContactPhoneBackSerializers + queryset = models.ContactPhone.objects.all() + pagination_class = None + + +class PhonesRUDView(generics.RetrieveUpdateDestroyAPIView): + """Social RUD view.""" + serializer_class = serializers.ContactPhoneBackSerializers + queryset = models.ContactPhone.objects.all() + + +class EmailListCreateView(generics.ListCreateAPIView): + """Plate list create view.""" + serializer_class = serializers.ContactEmailBackSerializers + queryset = models.ContactEmail.objects.all() + pagination_class = None + + +class EmailRUDView(generics.RetrieveUpdateDestroyAPIView): + """Social RUD view.""" + serializer_class = serializers.ContactEmailBackSerializers + queryset = models.ContactEmail.objects.all() \ No newline at end of file From 30875958ea6972b6334f1208857ddc154eab1c7f Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 13 Sep 2019 12:36:54 +0300 Subject: [PATCH 49/51] increase access token lifetime to 30 days and unregister unused models in admin page --- apps/authorization/admin.py | 16 ++++++++++++++++ project/settings/base.py | 6 ++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/authorization/admin.py b/apps/authorization/admin.py index 4d7fa8ea..bc76edfd 100644 --- a/apps/authorization/admin.py +++ b/apps/authorization/admin.py @@ -1,2 +1,18 @@ from django.contrib import admin +from oauth2_provider import models as oauth2_models +from rest_framework.authtoken.models import Token +from rest_framework_simplejwt.token_blacklist import models as jwt_models +from social_django import models as social_models + from authorization import models + + +# Unregister unused models +admin.site.unregister(jwt_models.OutstandingToken) +admin.site.unregister(jwt_models.BlacklistedToken) +admin.site.unregister(oauth2_models.AccessToken) +admin.site.unregister(oauth2_models.RefreshToken) +admin.site.unregister(oauth2_models.Grant) +admin.site.unregister(social_models.Association) +admin.site.unregister(social_models.Nonce) +admin.site.unregister(Token) diff --git a/project/settings/base.py b/project/settings/base.py index 16ac6002..b5c69ccf 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -90,7 +90,7 @@ EXTERNAL_APPS = [ ] -INSTALLED_APPS = CONTRIB_APPS + PROJECT_APPS + EXTERNAL_APPS +INSTALLED_APPS = CONTRIB_APPS + EXTERNAL_APPS + PROJECT_APPS MIDDLEWARE = [ @@ -338,7 +338,9 @@ GEOIP_PATH = os.path.join(PROJECT_ROOT, 'geoip_db') # JWT SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(hours=6), + # Increase access token lifetime b.c. front-end dev's cant send multiple + # requests to API in one HTTP request. + 'ACCESS_TOKEN_LIFETIME': timedelta(days=30), 'ACCESS_TOKEN_LIFETIME_SECONDS': 21600, # 6 hours in seconds 'REFRESH_TOKEN_LIFETIME': timedelta(days=30), 'REFRESH_TOKEN_LIFETIME_SECONDS': 2592000, # 30 days in seconds From 555aa9a4e1c38f0a4f6676694e5f59777859dfab Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 13 Sep 2019 18:08:36 +0300 Subject: [PATCH 50/51] =?UTF-8?q?GM-25:=20=D0=9F=D0=BE=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B3=D0=BB=D0=B0=D0=B2=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B7=D0=BE=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/main/admin.py | 3 ++ apps/main/migrations/0014_carousel.py | 27 ++++++++++++ apps/main/models.py | 60 ++++++++++++++++++++++++++- apps/main/serializers.py | 39 +++++++++++++++-- apps/main/urls.py | 1 + apps/main/views.py | 8 ++++ apps/utils/methods.py | 7 ++-- 7 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 apps/main/migrations/0014_carousel.py diff --git a/apps/main/admin.py b/apps/main/admin.py index 2d512868..bdbfe46e 100644 --- a/apps/main/admin.py +++ b/apps/main/admin.py @@ -46,3 +46,6 @@ class CurrencContentAdmin(admin.ModelAdmin): """CurrencContent admin""" +@admin.register(models.Carousel) +class CarouselAdmin(admin.ModelAdmin): + """Carousel admin.""" diff --git a/apps/main/migrations/0014_carousel.py b/apps/main/migrations/0014_carousel.py new file mode 100644 index 00000000..2e90e692 --- /dev/null +++ b/apps/main/migrations/0014_carousel.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.4 on 2019-09-13 11:33 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('main', '0013_auto_20190901_1032'), + ] + + operations = [ + migrations.CreateModel( + name='Carousel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + options={ + 'verbose_name': 'Carousel', + 'verbose_name_plural': 'Carousel', + }, + ), + ] diff --git a/apps/main/models.py b/apps/main/models.py index 3fdd5fed..82c155d7 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -7,7 +7,9 @@ from django.utils.translation import gettext_lazy as _ from django.contrib.contenttypes.models import ContentType from location.models import Country from main import methods -from utils.models import ProjectBaseMixin, TJSONField, TranslatedFieldsMixin +from utils.models import (ProjectBaseMixin, TJSONField, + TranslatedFieldsMixin, ImageMixin) +from utils.querysets import ContentTypeQuerySetMixin from configuration.models import TranslationSettings # @@ -253,6 +255,10 @@ class MetaData(TranslatedFieldsMixin, models.Model): return f'id:{self.id}-{label}' +class MetaDataContentQuerySet(ContentTypeQuerySetMixin): + """QuerySets for MetaDataContent model.""" + + class MetaDataContent(models.Model): """MetaDataContent model.""" content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) @@ -260,6 +266,8 @@ class MetaDataContent(models.Model): content_object = generic.GenericForeignKey('content_type', 'object_id') metadata = models.ForeignKey(MetaData, on_delete=models.CASCADE) + objects = MetaDataContentQuerySet.as_manager() + class Currency(models.Model): """Currency model.""" @@ -271,3 +279,53 @@ class Currency(models.Model): def __str__(self): return f'{self.name}' + + +class CarouselQuerySet(models.QuerySet): + """Carousel QuerySet.""" + + +class Carousel(models.Model): + """Carousel model.""" + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + + objects = CarouselQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('Carousel') + verbose_name_plural = _('Carousel') + + @property + def name(self): + if hasattr(self.content_object, 'name'): + return self.content_object.name + if hasattr(self.content_object, 'title'): + return self.content_object.title_translated + + @property + def awards(self): + if hasattr(self.content_object, 'awards'): + return self.content_object.awards + + @property + def toque_number(self): + if hasattr(self.content_object, 'toque_number'): + return self.content_object.toque_number + + @property + def public_mark(self): + if hasattr(self.content_object, 'public_mark'): + return self.content_object.public_mark + + @property + def image(self): + if not hasattr(self.content_object.image, 'url'): + return self.content_object.image.image.url + return self.content_object.image.url + + @property + def model_name(self): + return self.content_object.__class__.__name__ diff --git a/apps/main/serializers.py b/apps/main/serializers.py index d5f3cc3f..7f94f105 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -1,5 +1,6 @@ """Main app serializers.""" from rest_framework import serializers + from location.serializers import CountrySerializer from main import models @@ -82,8 +83,8 @@ class SiteSerializer(serializers.ModelSerializer): # ) -class AwardSerializer(serializers.ModelSerializer): - """Award serializer.""" +class AwardBaseSerializer(serializers.ModelSerializer): + """Award base serializer.""" title_translated = serializers.CharField(read_only=True, allow_null=True) @@ -92,11 +93,18 @@ class AwardSerializer(serializers.ModelSerializer): fields = [ 'id', 'title_translated', - 'award_type', 'vintage_year', ] +class AwardSerializer(AwardBaseSerializer): + """Award serializer.""" + + class Meta: + model = models.Award + fields = AwardBaseSerializer.Meta.fields + ['award_type', ] + + class MetaDataContentSerializer(serializers.ModelSerializer): id = serializers.IntegerField(source='metadata.id', read_only=True,) label_translated = serializers.CharField( @@ -117,4 +125,27 @@ class CurrencySerializer(serializers.ModelSerializer): fields = [ 'id', 'name' - ] \ No newline at end of file + ] + + +class CarouselListSerializer(serializers.ModelSerializer): + """Serializer for retrieving list of carousel items.""" + model_name = serializers.CharField() + name = serializers.CharField() + toque_number = serializers.CharField() + public_mark = serializers.CharField() + image = serializers.URLField() + awards = AwardBaseSerializer(many=True) + + class Meta: + """Meta class.""" + model = models.Carousel + fields = [ + 'id', + 'model_name', + 'name', + 'awards', + 'toque_number', + 'public_mark', + 'image', + ] diff --git a/apps/main/urls.py b/apps/main/urls.py index b848352f..a74c0b49 100644 --- a/apps/main/urls.py +++ b/apps/main/urls.py @@ -10,4 +10,5 @@ urlpatterns = [ path('site-settings//', views.SiteSettingsView.as_view(), name='site-settings'), path('awards/', views.AwardView.as_view(), name='awards_list'), path('awards//', views.AwardRetrieveView.as_view(), name='awards_retrieve'), + path('carousel/', views.CarouselListView.as_view(), name='carousel-list'), ] \ No newline at end of file diff --git a/apps/main/views.py b/apps/main/views.py index a4696f16..d7d1fa2c 100644 --- a/apps/main/views.py +++ b/apps/main/views.py @@ -84,3 +84,11 @@ class AwardRetrieveView(generics.RetrieveAPIView): serializer_class = serializers.AwardSerializer queryset = models.Award.objects.all() permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + +class CarouselListView(generics.ListAPIView): + """Return list of carousel items.""" + queryset = models.Carousel.objects.all() + serializer_class = serializers.CarouselListSerializer + permission_classes = (permissions.AllowAny, ) + pagination_class = None diff --git a/apps/utils/methods.py b/apps/utils/methods.py index e62138ce..acad6502 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -83,7 +83,6 @@ def generate_string_code(size=64, def get_contenttype(app_label: str, model: str): """Get ContentType instance by app_label and model""" - if app_label and model: - qs = ContentType.objects.filter(app_label=app_label, model=model) - if qs.exists(): - return qs.first() + qs = ContentType.objects.filter(app_label=app_label, model=model) + if qs.exists(): + return qs.first() From 306cb76e1c4966ecb118d1d7db456377ca61bab6 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 16 Sep 2019 09:35:20 +0300 Subject: [PATCH 51/51] GM-25: Added comments --- apps/main/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/main/models.py b/apps/main/models.py index 82c155d7..d2a3dbb0 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -300,6 +300,7 @@ class Carousel(models.Model): @property def name(self): + # Check if Generic obj has name or title if hasattr(self.content_object, 'name'): return self.content_object.name if hasattr(self.content_object, 'title'): @@ -322,7 +323,9 @@ class Carousel(models.Model): @property def image(self): + # Check if Generic obj has an image if not hasattr(self.content_object.image, 'url'): + # Check if Generic obj has a FK to gallery return self.content_object.image.image.url return self.content_object.image.url