From d5a14ef8c2e5daad644afdf0f6608d38016a0691 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 15 Aug 2019 11:59:10 +0300 Subject: [PATCH] version 0.0.11: added new model and CRUD endpoints to them; refactored auth app --- apps/account/models.py | 4 +- apps/authorization/admin.py | 6 +- apps/authorization/serializers/common.py | 36 ++-- apps/authorization/views/common.py | 208 ++++++++++---------- apps/location/serializers.py | 35 ++-- apps/location/urls.py | 40 ++-- apps/news/views/common.py | 3 +- apps/translation/__init__.py | 0 apps/translation/admin.py | 7 + apps/translation/apps.py | 7 + apps/translation/migrations/0001_initial.py | 26 +++ apps/translation/migrations/__init__.py | 0 apps/translation/models.py | 33 ++++ apps/translation/serializers.py | 14 ++ apps/translation/tests.py | 3 + apps/translation/urls.py | 18 ++ apps/translation/views.py | 40 ++++ apps/utils/exceptions.py | 14 ++ apps/utils/views.py | 98 +++++++++ project/settings/base.py | 1 + project/urls/__init__.py | 2 + 21 files changed, 438 insertions(+), 157 deletions(-) create mode 100644 apps/translation/__init__.py create mode 100644 apps/translation/admin.py create mode 100644 apps/translation/apps.py create mode 100644 apps/translation/migrations/0001_initial.py create mode 100644 apps/translation/migrations/__init__.py create mode 100644 apps/translation/models.py create mode 100644 apps/translation/serializers.py create mode 100644 apps/translation/tests.py create mode 100644 apps/translation/urls.py create mode 100644 apps/translation/views.py diff --git a/apps/account/models.py b/apps/account/models.py index df25e50a..c9342680 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -40,12 +40,12 @@ class UserQuerySet(models.QuerySet): """Filter only active users.""" return self.filter(is_active=switcher) - def by_access_token(self, token): + def by_oauth2_access_token(self, token): """Find user by access token""" return self.filter(oauth2_provider_accesstoken__token=token, oauth2_provider_accesstoken__expires__gt=timezone.now()) - def by_refresh_token(self, token): + def by_oauth2_refresh_token(self, token): """Find user by access token""" return self.filter(oauth2_provider_refreshtoken__token=token, oauth2_provider_refreshtoken__expires__gt=timezone.now()) diff --git a/apps/authorization/admin.py b/apps/authorization/admin.py index 8c38f3f3..eeb6f354 100644 --- a/apps/authorization/admin.py +++ b/apps/authorization/admin.py @@ -1,3 +1,7 @@ from django.contrib import admin +from authorization import models -# Register your models here. + +@admin.register(models.BlacklistedAccessToken) +class BlacklistedAccessTokenAdmin(admin.ModelAdmin): + """Admin for BlackListedAccessToken""" diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index 509e3abc..12a98bf7 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -24,7 +24,7 @@ class BaseAuthSerializerMixin(serializers.Serializer): source = serializers.ChoiceField(choices=Application.SOURCES) -class JWTBaseMixin(serializers.Serializer): +class JWTBaseSerializerMixin(serializers.Serializer): """ Mixin for JWT authentication. Uses in serializers when need give in response access and refresh token @@ -43,8 +43,8 @@ class JWTBaseMixin(serializers.Serializer): def to_representation(self, instance): """Override to_representation method""" token = self.get_token() - setattr(instance, 'refresh_token', str(token)) setattr(instance, 'access_token', str(token.access_token)) + setattr(instance, 'refresh_token', str(token)) return super().to_representation(instance) @@ -60,7 +60,7 @@ class ClassicAuthSerializerMixin(BaseAuthSerializerMixin): # Serializers -class SignupSerializer(JWTBaseMixin, serializers.ModelSerializer): +class SignupSerializer(JWTBaseSerializerMixin, serializers.ModelSerializer): """Signup serializer serializer mixin""" # REQUEST username = serializers.CharField( @@ -100,7 +100,7 @@ class SignupSerializer(JWTBaseMixin, serializers.ModelSerializer): return obj -class LoginByUsernameOrEmailSerializer(JWTBaseMixin, serializers.ModelSerializer): +class LoginByUsernameOrEmailSerializer(JWTBaseSerializerMixin, serializers.ModelSerializer): """Serializer for login user""" username_or_email = serializers.CharField(write_only=True) password = serializers.CharField(write_only=True) @@ -164,22 +164,28 @@ class LogoutSerializer(serializers.ModelSerializer): class Meta: model = BlacklistedAccessToken - fields = '__all__' - read_only_fields = [ - 'jti', 'token', 'user' - ] + fields = ( + 'user', + 'token', + 'jti' + ) + read_only_fields = ( + 'user', + 'token', + 'jti' + ) - def create(self, validated_data, *args, **kwargs): - """Override create method""" + def validate(self, attrs): + """Override validated data""" request = self.context.get('request') - token = request._request.headers.get('Authorization')\ + token = request._request.headers.get('Authorization') \ .split(' ')[::-1][0] access_token = tokens.AccessToken(token) # Prepare validated data - validated_data['user'] = request.user - validated_data['token'] = access_token.token - validated_data['jti'] = access_token.payload.get('jti') - return super().create(validated_data) + attrs['user'] = request.user + attrs['token'] = access_token.token + attrs['jti'] = access_token.payload.get('jti') + return attrs # OAuth diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index f6412b21..4043cefb 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -1,6 +1,5 @@ """Common views for application Account""" import json -from collections import namedtuple from braces.views import CsrfExemptMixin from django.conf import settings @@ -20,12 +19,36 @@ from account.models import User from authorization.models import Application from authorization.serializers import common as serializers from utils import exceptions as utils_exceptions - - -# JWT +from utils.views import JWTViewMixin # Mixins +# JWTAuthView mixin +class JWTAuthViewMixin(JWTViewMixin): + """Mixin for authentication views""" + + def post(self, request, *args, **kwargs): + """Implement POST method""" + _locale = request.COOKIES.get('locale') + try: + locale = self._check_locale(locale=_locale) + + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + response = Response(serializer.data, status=status.HTTP_200_OK) + + access_token = serializer.data.get('access_token') + refresh_token = serializer.data.get('refresh_token') + except utils_exceptions.LocaleNotExisted: + raise utils_exceptions.LocaleNotExisted(locale=_locale) + else: + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies(locale=locale, + access_token=access_token, + refresh_token=refresh_token), + response=response) + + # OAuth2 class BaseOAuth2ViewMixin(generics.GenericAPIView): """BaseMixin for classic auth views""" @@ -74,53 +97,8 @@ class OAuth2ViewMixin(CsrfExemptMixin, OAuthLibMixin, BaseOAuth2ViewMixin): raise utils_exceptions.ServiceError() -# JWT -# Login base view mixin -class JWTViewMixin(generics.GenericAPIView): - """JWT view mixin""" - def _handle_cookies(self, request, access_token, refresh_token): - """ - CHECK locale in cookies and PUT access and refresh tokens there. - cookies it is list that contain namedtuples - cookies would contain key, value and secure parameters. - """ - cookies = list() - COOKIE = namedtuple('COOKIE', ['key', 'value', 'secure']) - - if 'locale' in request.COOKIES: - # Write locale in cookie - _locale = COOKIE(key='locale', value=request.COOKIES.get('locale'), secure=False) - cookies.append(_locale) - - # Write to cookie access and refresh token with secure flag - _access_token = COOKIE(key='access_token', value=access_token, secure=True) - _refresh_token = COOKIE(key='refresh_token', value=refresh_token, secure=True) - cookies.extend([_access_token, _refresh_token]) - return cookies - - def _put_cookies_in_response(self, cookies: list, response: Response): - """Update COOKIES in response obj""" - for cookie in cookies: - response.set_cookie(key=cookie.key, - value=cookie.value, - secure=cookie.secure) - return response - - 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_200_OK) - access_token = serializer.data.get('access_token') - refresh_token = serializer.data.get('access_token') - return self._put_cookies_in_response( - cookies=self._handle_cookies(request, access_token, refresh_token), - response=response) - - -# Serializers # Sign in via Facebook -class OAuth2SignUpView(OAuth2ViewMixin, JWTViewMixin): +class OAuth2SignUpView(OAuth2ViewMixin, JWTAuthViewMixin): """ Implements an endpoint to convert a provider token to an access token @@ -135,96 +113,112 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTViewMixin): serializer_class = serializers.OAuth2Serialzier def get_jwt_token(self, user: User, - access_token: str, - refresh_token: str): + oauth2_access_token: str, + oauth2_refresh_token: str): """Get JWT token""" token = jwt_tokens.RefreshToken.for_user(user) # Adding additional information about user to payload token['user'] = user.get_user_info() # Adding OAuth2 tokens to payloads - token['oauth2_fb'] = {'access_token': access_token, - 'refresh_token': refresh_token} + token['oauth2_fb'] = {'access_token': oauth2_access_token, + 'refresh_token': oauth2_refresh_token} return token def post(self, request, *args, **kwargs): - # Preparing request data - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - request_data = self.prepare_request_data(serializer.validated_data) - request_data.update({ - 'grant_type': settings.OAUTH2_SOCIAL_AUTH_GRANT_TYPE, - 'backend': settings.OAUTH2_SOCIAL_AUTH_BACKEND_NAME - }) - # Use the rest framework `.data` to fake the post body of the django request. - request._request.POST = request._request.POST.copy() - for key, value in request_data.items(): - request._request.POST[key] = value + _locale = request.COOKIES.get('locale') + try: + locale = self._check_locale(locale=_locale) + # Preparing request data + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + request_data = self.prepare_request_data(serializer.validated_data) + request_data.update({ + 'grant_type': settings.OAUTH2_SOCIAL_AUTH_GRANT_TYPE, + 'backend': settings.OAUTH2_SOCIAL_AUTH_BACKEND_NAME + }) + # Use the rest framework `.data` to fake the post body of the django request. + request._request.POST = request._request.POST.copy() + for key, value in request_data.items(): + request._request.POST[key] = value - url, headers, body, oauth2_status = self.create_token_response(request._request) - body = json.loads(body) - # Get JWT token - if oauth2_status != status.HTTP_200_OK: - raise ValueError('status isn\'t 200') - user = User.objects.by_access_token(token=body.get('access_token'))\ - .first() - token = self.get_jwt_token(user=user, - access_token=body.get('access_token'), - refresh_token=body.get('refresh_token')) - refresh_token = str(token) - access_token = str(token.access_token) - response = Response(data={'refresh_token': refresh_token, - 'access_token': access_token}, - status=status.HTTP_200_OK) - return self._put_cookies_in_response( - cookies=self._handle_cookies(request, access_token, refresh_token), - response=response) + # OAuth2 authentication process + url, headers, body, oauth2_status = self.create_token_response(request._request) + body = json.loads(body) + + # Get JWT token + if oauth2_status != status.HTTP_200_OK: + raise ValueError('status isn\'t 200') + + # Get authenticated user + user = User.objects.by_oauth2_access_token(token=body.get('access_token'))\ + .first() + + # Create JWT token and put oauth2 token (access, refresh tokens) in payload + token = self.get_jwt_token(user=user, + oauth2_access_token=body.get('access_token'), + oauth2_refresh_token=body.get('refresh_token')) + + access_token = str(token.access_token) + refresh_token = str(token) + response = Response(data={'access_token': access_token, + 'refresh_token': refresh_token}, + status=status.HTTP_200_OK) + except utils_exceptions.LocaleNotExisted: + raise utils_exceptions.LocaleNotExisted(locale=_locale) + else: + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies(locale=locale, + access_token=access_token, + refresh_token=refresh_token), + response=response) # JWT # Sign in via username and password -class SignUpView(JWTViewMixin): +class SignUpView(JWTAuthViewMixin): """View for classic signup""" permission_classes = (permissions.AllowAny, ) serializer_class = serializers.SignupSerializer def post(self, request, *args, **kwargs): + """Implement POST-method""" + _locale = request.COOKIES.get('locale') + try: + locale = self._check_locale(locale=_locale) - 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 = serializer.data.get('access_token') - refresh_token = serializer.data.get('access_token') - return self._put_cookies_in_response( - cookies=self._handle_cookies(request, access_token, refresh_token), - response=response) + 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 = serializer.data.get('access_token') + refresh_token = serializer.data.get('refresh_token') + except utils_exceptions.LocaleNotExisted: + raise utils_exceptions.LocaleNotExisted(locale=_locale) + else: + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies( + locale=locale, + access_token=access_token, + refresh_token=refresh_token), + response=response) # Login by username|email + password -class LoginByUsernameOrEmailView(JWTViewMixin): +class LoginByUsernameOrEmailView(JWTAuthViewMixin): """Login by email and password""" permission_classes = (permissions.AllowAny,) serializer_class = serializers.LoginByUsernameOrEmailSerializer # Refresh access_token -class RefreshTokenView(JWTViewMixin): +class RefreshTokenView(JWTAuthViewMixin): """Refresh access_token""" permission_classes = (permissions.IsAuthenticated,) 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.validated_data, status=status.HTTP_200_OK) - access_token = serializer.data.get('access_token') - refresh_token = serializer.data.get('access_token') - return self._put_cookies_in_response( - cookies=self._handle_cookies(request, access_token, refresh_token), - response=response) - # Logout class LogoutView(generics.CreateAPIView): diff --git a/apps/location/serializers.py b/apps/location/serializers.py index 9dd1c9fe..2ffeed69 100644 --- a/apps/location/serializers.py +++ b/apps/location/serializers.py @@ -1,5 +1,6 @@ from rest_framework import serializers from location import models +from django.contrib.gis.geos import Point class CountrySerializer(serializers.ModelSerializer): @@ -48,8 +49,8 @@ class CitySerializer(serializers.ModelSerializer): class AddressSerializer(serializers.ModelSerializer): """Address serializer.""" city = CitySerializer() - longitude = serializers.DecimalField(max_digits=10, decimal_places=6) - latitude = serializers.DecimalField(max_digits=10, decimal_places=6) + geo_lon = serializers.FloatField(allow_null=True) + geo_lat = serializers.FloatField(allow_null=True) class Meta: model = models.Address @@ -59,14 +60,26 @@ class AddressSerializer(serializers.ModelSerializer): 'street_name_2', 'number', 'postal_code', - 'longitude', - 'latitude' + 'geo_lon', + 'geo_lat' ] - # def validate(self, attrs): - # geo_lat = attrs.pop('geo_lat') if 'geo_lat' in attrs else None - # geo_lon = attrs.pop('geo_lon') if 'geo_lon' in attrs else None - # if geo_lat and geo_lon: - # # Point(longitude, latitude) - # attrs['coordinates'] = Point(geo_lon, geo_lat) - # return attrs \ No newline at end of file + def validate(self, attrs): + # if geo_lat and geo_lon was sent + geo_lat = attrs.pop('geo_lat') if 'geo_lat' in attrs else None + geo_lon = attrs.pop('geo_lon') if 'geo_lon' in attrs else None + if geo_lat and geo_lon: + # Point(longitude, latitude) + attrs['location'] = Point(geo_lat, geo_lon) + return attrs + + def to_representation(self, instance): + """Override to_representation method""" + if instance.coordinates and isinstance(instance.coordinates, Point): + # Point(longitude, latitude) + setattr(instance, 'geo_lat', instance.coordinates.x) + setattr(instance, 'geo_lon', instance.coordinates.y) + else: + setattr(instance, 'geo_lat', float(0)) + setattr(instance, 'geo_lon', float(0)) + return super().to_representation(instance) diff --git a/apps/location/urls.py b/apps/location/urls.py index ce12d097..b8c71db3 100644 --- a/apps/location/urls.py +++ b/apps/location/urls.py @@ -6,27 +6,27 @@ from location import views app_name = 'location' urlpatterns = [ - path('country/list/', views.CountryListView.as_view(), name='country-list'), - path('country/create/', views.CountryCreateView.as_view(), name='country-create'), - path('country//detail/', views.CountryRetrieveView.as_view(), name='country-retrieve'), - path('country//delete/', views.CountryDestroyView.as_view(), name='country-destroy'), - path('country//update/', views.CountryUpdateView.as_view(), name='country-update'), + path('country/', views.CountryListView.as_view(), name='country_list'), + path('country/create/', views.CountryCreateView.as_view(), name='country_create'), + path('country//', views.CountryRetrieveView.as_view(), name='country_retrieve'), + path('country//delete/', views.CountryDestroyView.as_view(), name='country_destroy'), + path('country//update/', views.CountryUpdateView.as_view(), name='country_update'), - path('region/list/', views.RegionListView.as_view(), name='region-list'), - path('region/create/', views.RegionCreateView.as_view(), name='region-create'), - path('region//detail/', views.RegionRetrieveView.as_view(), name='region-retrieve'), - path('region//delete/', views.RegionDestroyView.as_view(), name='region-destroy'), - path('region//update/', views.RegionUpdateView.as_view(), name='region-update'), + path('region/', views.RegionListView.as_view(), name='region_list'), + path('region/create/', views.RegionCreateView.as_view(), name='region_create'), + path('region//', views.RegionRetrieveView.as_view(), name='region_retrieve'), + path('region//delete/', views.RegionDestroyView.as_view(), name='region_destroy'), + path('region//update/', views.RegionUpdateView.as_view(), name='region_update'), - path('city/list/', views.CityListView.as_view(), name='city-list'), - path('city/create/', views.CityCreateView.as_view(), name='city-create'), - path('city//detail/', views.CityRetrieveView.as_view(), name='city-retrieve'), - path('city//delete/', views.CityDestroyView.as_view(), name='city-destroy'), - path('city//update/', views.CityUpdateView.as_view(), name='city-update'), + path('city/', views.CityListView.as_view(), name='city_list'), + path('city/create/', views.CityCreateView.as_view(), name='city_create'), + path('city//', views.CityRetrieveView.as_view(), name='city_retrieve'), + path('city//delete/', views.CityDestroyView.as_view(), name='city_destroy'), + path('city//update/', views.CityUpdateView.as_view(), name='city_update'), - path('address/list/', views.AddressListView.as_view(), name='address-list'), - path('address/create/', views.AddressCreateView.as_view(), name='address-create'), - path('address//detail/', views.AddressRetrieveView.as_view(), name='address-retrieve'), - path('address//delete/', views.AddressDestroyView.as_view(), name='address-destroy'), - path('address//update/', views.AddressUpdateView.as_view(), name='address-update'), + path('address/', views.AddressListView.as_view(), name='address_list'), + path('address/create/', views.AddressCreateView.as_view(), name='address_create'), + path('address//', views.AddressRetrieveView.as_view(), name='address_retrieve'), + path('address//delete/', views.AddressDestroyView.as_view(), name='address_destroy'), + path('address//update/', views.AddressUpdateView.as_view(), name='address_update'), ] diff --git a/apps/news/views/common.py b/apps/news/views/common.py index f9cec2de..e7d149e0 100644 --- a/apps/news/views/common.py +++ b/apps/news/views/common.py @@ -1,9 +1,10 @@ from rest_framework import generics, permissions from news.models import News from news.serializers import common as serializers +from utils.views import JWTViewMixin -class NewsList(generics.ListAPIView): +class NewsList(JWTViewMixin, generics.ListAPIView): """News list view.""" queryset = News.objects.all() permission_classes = (permissions.AllowAny, ) diff --git a/apps/translation/__init__.py b/apps/translation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/translation/admin.py b/apps/translation/admin.py new file mode 100644 index 00000000..02b46ec5 --- /dev/null +++ b/apps/translation/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from translation.models import Language + + +@admin.register(Language) +class LanguageAdmin(admin.ModelAdmin): + """Language admin.""" diff --git a/apps/translation/apps.py b/apps/translation/apps.py new file mode 100644 index 00000000..bea8f580 --- /dev/null +++ b/apps/translation/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class TranslationConfig(AppConfig): + name = 'translation' + verbose_name = _('Translation') diff --git a/apps/translation/migrations/0001_initial.py b/apps/translation/migrations/0001_initial.py new file mode 100644 index 00000000..135acbfe --- /dev/null +++ b/apps/translation/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.4 on 2019-08-14 13:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Language', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='Language title')), + ('locale', models.CharField(max_length=10, verbose_name='Locale identifier')), + ], + options={ + 'verbose_name': 'Language', + 'verbose_name_plural': 'Languages', + }, + ), + ] diff --git a/apps/translation/migrations/__init__.py b/apps/translation/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/translation/models.py b/apps/translation/models.py new file mode 100644 index 00000000..6f54fdce --- /dev/null +++ b/apps/translation/models.py @@ -0,0 +1,33 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class LanguageQuerySet(models.QuerySet): + """QuerySet for model Language""" + + def by_locale(self, locale: str) -> models.QuerySet: + """Filter by locale""" + return self.filter(locale=locale) + + def by_title(self, title: str) -> models.QuerySet: + """Filter by title""" + return self.filter(title=title) + + +class Language(models.Model): + """Language model.""" + + title = models.CharField(max_length=255, + verbose_name=_('Language title')) + locale = models.CharField(max_length=10, + verbose_name=_('Locale identifier')) + + objects = LanguageQuerySet.as_manager() + + class Meta: + verbose_name = _('Language') + verbose_name_plural = _('Languages') + + def __str__(self): + """String method""" + return f'{self.title} ({self.locale})' diff --git a/apps/translation/serializers.py b/apps/translation/serializers.py new file mode 100644 index 00000000..de703681 --- /dev/null +++ b/apps/translation/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers +from translation import models + + +class LanguageSerializer(serializers.ModelSerializer): + """Serializer for model Language""" + + class Meta: + model = models.Language + fields = [ + 'id', + 'title', + 'locale' + ] diff --git a/apps/translation/tests.py b/apps/translation/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/apps/translation/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/translation/urls.py b/apps/translation/urls.py new file mode 100644 index 00000000..4a1d5d46 --- /dev/null +++ b/apps/translation/urls.py @@ -0,0 +1,18 @@ +from django.urls import path + +from translation import views + +app_name = 'translation' + +urlpatterns = [ + path('language/', views.LanguageListView.as_view(), + name='language_list'), + path('language/create/', views.LanguageCreateView.as_view(), + name='language_create'), + path('language//update/', views.LanguageUpdateView.as_view(), + name='language_update'), + path('language//destroy/', views.LanguageDestroyView.as_view(), + name='language_destroy'), + path('language//', views.LanguageRetrieveView.as_view(), + name='language_retrieve'), +] diff --git a/apps/translation/views.py b/apps/translation/views.py new file mode 100644 index 00000000..cce82e1c --- /dev/null +++ b/apps/translation/views.py @@ -0,0 +1,40 @@ +from rest_framework import generics +from translation import models +from translation import serializers +from rest_framework import permissions +from utils.views import JWTViewMixin + + +# Mixins +class LanguageViewMixin(generics.GenericAPIView): + """Mixin for Language views""" + model = models.Language + queryset = models.Language.objects.all() + + +# Views +class LanguageListView(LanguageViewMixin, JWTViewMixin, generics.ListAPIView): + """List view for model Language""" + permission_classes = (permissions.AllowAny, ) + serializer_class = serializers.LanguageSerializer + + +class LanguageCreateView(LanguageViewMixin, generics.CreateAPIView): + """Create view for model Language""" + permission_classes = (permissions.AllowAny, ) + serializer_class = serializers.LanguageSerializer + + +class LanguageRetrieveView(LanguageViewMixin, generics.RetrieveAPIView): + """Retrieve view for model Language""" + serializer_class = serializers.LanguageSerializer + + +class LanguageUpdateView(LanguageViewMixin, generics.UpdateAPIView): + """Update view for model Language""" + serializer_class = serializers.LanguageSerializer + + +class LanguageDestroyView(LanguageViewMixin, generics.DestroyAPIView): + """Destroy view for model Language""" + serializer_class = serializers.LanguageSerializer diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index cb883e03..4e8b56fa 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -47,3 +47,17 @@ class EmailSendingError(exceptions.APIException): 'detail': self.default_detail % recipient, } super().__init__() + + +class LocaleNotExisted(exceptions.APIException): + """The exception should be thrown when passed locale isn't in model Language + """ + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _('Locale not found in database (%s)') + + def __init__(self, locale: str = None): + if locale: + self.default_detail = { + 'detail': self.default_detail % locale + } + super().__init__() diff --git a/apps/utils/views.py b/apps/utils/views.py index aad1c580..d2602c79 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -1,3 +1,101 @@ from rest_framework import generics +from collections import namedtuple +from translation import models as translation_models +from utils import exceptions +from rest_framework.response import Response +# JWT +# Login base view mixin +class JWTViewMixin(generics.GenericAPIView): + """JWT view mixin""" + + ACCESS_TOKEN_HTTP = True + ACCESS_TOKEN_SECURE = False + + REFRESH_TOKEN_HTTP = True + REFRESH_TOKEN_SECURE = False + COOKIE = namedtuple('COOKIE', ['key', 'value', 'http', 'secure']) + + def _check_locale(self, locale: str): + + locale_qs = translation_models.Language.objects.by_locale(locale=locale) + if not locale_qs.exists(): + raise exceptions.LocaleNotExisted() + return locale + + def _put_data_in_cookies(self, + locale: str, + access_token: str, + refresh_token: str): + """ + CHECK locale in cookies and PUT access and refresh tokens there. + cookies it is list that contain namedtuples + cookies would contain key, value and secure parameters. + """ + COOKIES = list() + + # Create locale namedtuple + locale = self.COOKIE(key='locale', + value=locale, + http=True, + secure=False) + COOKIES.append(locale) + + # Write to cookie access and refresh token with secure flag + _access_token = self.COOKIE(key='access_token', + value=access_token, + http=self.ACCESS_TOKEN_HTTP, + secure=self.ACCESS_TOKEN_SECURE) + _refresh_token = self.COOKIE(key='refresh_token', + value=refresh_token, + http=self.REFRESH_TOKEN_HTTP, + secure=self.REFRESH_TOKEN_SECURE) + COOKIES.extend((_access_token, _refresh_token)) + return COOKIES + + def _put_cookies_in_response(self, cookies: list, response: Response): + """Update COOKIES in response from namedtuple""" + for cookie in cookies: + response.set_cookie(key=cookie.key, + value=cookie.value, + secure=cookie.secure) + return response + + def _get_tokens_from_cookies(self, request, cookies: dict = None): + """Get user tokens from cookies and put in namedtuple""" + _cookies = request.COOKIES or cookies + return [self.COOKIE(key='access_token', + value=_cookies.get('access_token'), + http=self.ACCESS_TOKEN_HTTP, + secure=self.ACCESS_TOKEN_SECURE), + self.COOKIE(key='refresh_token', + value=_cookies.get('refresh_token'), + http=self.REFRESH_TOKEN_HTTP, + secure=self.REFRESH_TOKEN_SECURE)] + + def get(self, request, *args, **kwargs): + """Implement GET method""" + _locale = request.COOKIES.get('locale') + try: + locale = self._check_locale(locale=_locale) + + 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) + + except exceptions.LocaleNotExisted: + raise exceptions.LocaleNotExisted(locale=_locale) + else: + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies(locale=locale, + access_token=access_token, + refresh_token=refresh_token), + response=response) diff --git a/project/settings/base.py b/project/settings/base.py index 02267e5c..fce72038 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -56,6 +56,7 @@ PROJECT_APPS = [ 'authorization.apps.AuthorizationConfig', 'location.apps.LocationConfig', 'news.apps.NewsConfig', + 'translation.apps.TranslationConfig', ] diff --git a/project/urls/__init__.py b/project/urls/__init__.py index dae5ef5d..37c25b50 100644 --- a/project/urls/__init__.py +++ b/project/urls/__init__.py @@ -24,6 +24,7 @@ from rest_framework import permissions # URL platform patterns from project.urls import web as web_urlpatterns from location import urls as location_urls +from translation import urls as translation_urls schema_view = get_schema_view( @@ -60,6 +61,7 @@ urlpatterns = [ path('admin/', admin.site.urls), path('api/web/', include(web_urlpatterns)), path('api/location/', include(location_urls.urlpatterns)), + path('api/translation/', include(translation_urls.urlpatterns)), ] urlpatterns = urlpatterns + \