version 0.0.11: added new model and CRUD endpoints to them; refactored auth app
This commit is contained in:
parent
033674c5b2
commit
d5a14ef8c2
|
|
@ -40,12 +40,12 @@ class UserQuerySet(models.QuerySet):
|
||||||
"""Filter only active users."""
|
"""Filter only active users."""
|
||||||
return self.filter(is_active=switcher)
|
return self.filter(is_active=switcher)
|
||||||
|
|
||||||
def by_access_token(self, token):
|
def by_oauth2_access_token(self, token):
|
||||||
"""Find user by access token"""
|
"""Find user by access token"""
|
||||||
return self.filter(oauth2_provider_accesstoken__token=token,
|
return self.filter(oauth2_provider_accesstoken__token=token,
|
||||||
oauth2_provider_accesstoken__expires__gt=timezone.now())
|
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"""
|
"""Find user by access token"""
|
||||||
return self.filter(oauth2_provider_refreshtoken__token=token,
|
return self.filter(oauth2_provider_refreshtoken__token=token,
|
||||||
oauth2_provider_refreshtoken__expires__gt=timezone.now())
|
oauth2_provider_refreshtoken__expires__gt=timezone.now())
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from authorization import models
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
|
@admin.register(models.BlacklistedAccessToken)
|
||||||
|
class BlacklistedAccessTokenAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin for BlackListedAccessToken"""
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class BaseAuthSerializerMixin(serializers.Serializer):
|
||||||
source = serializers.ChoiceField(choices=Application.SOURCES)
|
source = serializers.ChoiceField(choices=Application.SOURCES)
|
||||||
|
|
||||||
|
|
||||||
class JWTBaseMixin(serializers.Serializer):
|
class JWTBaseSerializerMixin(serializers.Serializer):
|
||||||
"""
|
"""
|
||||||
Mixin for JWT authentication.
|
Mixin for JWT authentication.
|
||||||
Uses in serializers when need give in response access and refresh token
|
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):
|
def to_representation(self, instance):
|
||||||
"""Override to_representation method"""
|
"""Override to_representation method"""
|
||||||
token = self.get_token()
|
token = self.get_token()
|
||||||
setattr(instance, 'refresh_token', str(token))
|
|
||||||
setattr(instance, 'access_token', str(token.access_token))
|
setattr(instance, 'access_token', str(token.access_token))
|
||||||
|
setattr(instance, 'refresh_token', str(token))
|
||||||
return super().to_representation(instance)
|
return super().to_representation(instance)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -60,7 +60,7 @@ class ClassicAuthSerializerMixin(BaseAuthSerializerMixin):
|
||||||
|
|
||||||
|
|
||||||
# Serializers
|
# Serializers
|
||||||
class SignupSerializer(JWTBaseMixin, serializers.ModelSerializer):
|
class SignupSerializer(JWTBaseSerializerMixin, serializers.ModelSerializer):
|
||||||
"""Signup serializer serializer mixin"""
|
"""Signup serializer serializer mixin"""
|
||||||
# REQUEST
|
# REQUEST
|
||||||
username = serializers.CharField(
|
username = serializers.CharField(
|
||||||
|
|
@ -100,7 +100,7 @@ class SignupSerializer(JWTBaseMixin, serializers.ModelSerializer):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class LoginByUsernameOrEmailSerializer(JWTBaseMixin, serializers.ModelSerializer):
|
class LoginByUsernameOrEmailSerializer(JWTBaseSerializerMixin, serializers.ModelSerializer):
|
||||||
"""Serializer for login user"""
|
"""Serializer for login user"""
|
||||||
username_or_email = serializers.CharField(write_only=True)
|
username_or_email = serializers.CharField(write_only=True)
|
||||||
password = serializers.CharField(write_only=True)
|
password = serializers.CharField(write_only=True)
|
||||||
|
|
@ -164,22 +164,28 @@ class LogoutSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BlacklistedAccessToken
|
model = BlacklistedAccessToken
|
||||||
fields = '__all__'
|
fields = (
|
||||||
read_only_fields = [
|
'user',
|
||||||
'jti', 'token', 'user'
|
'token',
|
||||||
]
|
'jti'
|
||||||
|
)
|
||||||
|
read_only_fields = (
|
||||||
|
'user',
|
||||||
|
'token',
|
||||||
|
'jti'
|
||||||
|
)
|
||||||
|
|
||||||
def create(self, validated_data, *args, **kwargs):
|
def validate(self, attrs):
|
||||||
"""Override create method"""
|
"""Override validated data"""
|
||||||
request = self.context.get('request')
|
request = self.context.get('request')
|
||||||
token = request._request.headers.get('Authorization')\
|
token = request._request.headers.get('Authorization') \
|
||||||
.split(' ')[::-1][0]
|
.split(' ')[::-1][0]
|
||||||
access_token = tokens.AccessToken(token)
|
access_token = tokens.AccessToken(token)
|
||||||
# Prepare validated data
|
# Prepare validated data
|
||||||
validated_data['user'] = request.user
|
attrs['user'] = request.user
|
||||||
validated_data['token'] = access_token.token
|
attrs['token'] = access_token.token
|
||||||
validated_data['jti'] = access_token.payload.get('jti')
|
attrs['jti'] = access_token.payload.get('jti')
|
||||||
return super().create(validated_data)
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
# OAuth
|
# OAuth
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"""Common views for application Account"""
|
"""Common views for application Account"""
|
||||||
import json
|
import json
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
from braces.views import CsrfExemptMixin
|
from braces.views import CsrfExemptMixin
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -20,12 +19,36 @@ from account.models import User
|
||||||
from authorization.models import Application
|
from authorization.models import Application
|
||||||
from authorization.serializers import common as serializers
|
from authorization.serializers import common as serializers
|
||||||
from utils import exceptions as utils_exceptions
|
from utils import exceptions as utils_exceptions
|
||||||
|
from utils.views import JWTViewMixin
|
||||||
|
|
||||||
# JWT
|
|
||||||
|
|
||||||
|
|
||||||
# Mixins
|
# 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
|
# OAuth2
|
||||||
class BaseOAuth2ViewMixin(generics.GenericAPIView):
|
class BaseOAuth2ViewMixin(generics.GenericAPIView):
|
||||||
"""BaseMixin for classic auth views"""
|
"""BaseMixin for classic auth views"""
|
||||||
|
|
@ -74,53 +97,8 @@ class OAuth2ViewMixin(CsrfExemptMixin, OAuthLibMixin, BaseOAuth2ViewMixin):
|
||||||
raise utils_exceptions.ServiceError()
|
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
|
# Sign in via Facebook
|
||||||
class OAuth2SignUpView(OAuth2ViewMixin, JWTViewMixin):
|
class OAuth2SignUpView(OAuth2ViewMixin, JWTAuthViewMixin):
|
||||||
"""
|
"""
|
||||||
Implements an endpoint to convert a provider token to an access token
|
Implements an endpoint to convert a provider token to an access token
|
||||||
|
|
||||||
|
|
@ -135,96 +113,112 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTViewMixin):
|
||||||
serializer_class = serializers.OAuth2Serialzier
|
serializer_class = serializers.OAuth2Serialzier
|
||||||
|
|
||||||
def get_jwt_token(self, user: User,
|
def get_jwt_token(self, user: User,
|
||||||
access_token: str,
|
oauth2_access_token: str,
|
||||||
refresh_token: str):
|
oauth2_refresh_token: str):
|
||||||
"""Get JWT token"""
|
"""Get JWT token"""
|
||||||
token = jwt_tokens.RefreshToken.for_user(user)
|
token = jwt_tokens.RefreshToken.for_user(user)
|
||||||
# Adding additional information about user to payload
|
# Adding additional information about user to payload
|
||||||
token['user'] = user.get_user_info()
|
token['user'] = user.get_user_info()
|
||||||
# Adding OAuth2 tokens to payloads
|
# Adding OAuth2 tokens to payloads
|
||||||
token['oauth2_fb'] = {'access_token': access_token,
|
token['oauth2_fb'] = {'access_token': oauth2_access_token,
|
||||||
'refresh_token': refresh_token}
|
'refresh_token': oauth2_refresh_token}
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
# Preparing request data
|
_locale = request.COOKIES.get('locale')
|
||||||
serializer = self.get_serializer(data=request.data)
|
try:
|
||||||
serializer.is_valid(raise_exception=True)
|
locale = self._check_locale(locale=_locale)
|
||||||
request_data = self.prepare_request_data(serializer.validated_data)
|
# Preparing request data
|
||||||
request_data.update({
|
serializer = self.get_serializer(data=request.data)
|
||||||
'grant_type': settings.OAUTH2_SOCIAL_AUTH_GRANT_TYPE,
|
serializer.is_valid(raise_exception=True)
|
||||||
'backend': settings.OAUTH2_SOCIAL_AUTH_BACKEND_NAME
|
request_data = self.prepare_request_data(serializer.validated_data)
|
||||||
})
|
request_data.update({
|
||||||
# Use the rest framework `.data` to fake the post body of the django request.
|
'grant_type': settings.OAUTH2_SOCIAL_AUTH_GRANT_TYPE,
|
||||||
request._request.POST = request._request.POST.copy()
|
'backend': settings.OAUTH2_SOCIAL_AUTH_BACKEND_NAME
|
||||||
for key, value in request_data.items():
|
})
|
||||||
request._request.POST[key] = value
|
# 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)
|
# OAuth2 authentication process
|
||||||
body = json.loads(body)
|
url, headers, body, oauth2_status = self.create_token_response(request._request)
|
||||||
# Get JWT token
|
body = json.loads(body)
|
||||||
if oauth2_status != status.HTTP_200_OK:
|
|
||||||
raise ValueError('status isn\'t 200')
|
# Get JWT token
|
||||||
user = User.objects.by_access_token(token=body.get('access_token'))\
|
if oauth2_status != status.HTTP_200_OK:
|
||||||
.first()
|
raise ValueError('status isn\'t 200')
|
||||||
token = self.get_jwt_token(user=user,
|
|
||||||
access_token=body.get('access_token'),
|
# Get authenticated user
|
||||||
refresh_token=body.get('refresh_token'))
|
user = User.objects.by_oauth2_access_token(token=body.get('access_token'))\
|
||||||
refresh_token = str(token)
|
.first()
|
||||||
access_token = str(token.access_token)
|
|
||||||
response = Response(data={'refresh_token': refresh_token,
|
# Create JWT token and put oauth2 token (access, refresh tokens) in payload
|
||||||
'access_token': access_token},
|
token = self.get_jwt_token(user=user,
|
||||||
status=status.HTTP_200_OK)
|
oauth2_access_token=body.get('access_token'),
|
||||||
return self._put_cookies_in_response(
|
oauth2_refresh_token=body.get('refresh_token'))
|
||||||
cookies=self._handle_cookies(request, access_token, refresh_token),
|
|
||||||
response=response)
|
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
|
# JWT
|
||||||
# Sign in via username and password
|
# Sign in via username and password
|
||||||
class SignUpView(JWTViewMixin):
|
class SignUpView(JWTAuthViewMixin):
|
||||||
"""View for classic signup"""
|
"""View for classic signup"""
|
||||||
permission_classes = (permissions.AllowAny, )
|
permission_classes = (permissions.AllowAny, )
|
||||||
serializer_class = serializers.SignupSerializer
|
serializer_class = serializers.SignupSerializer
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
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 = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
response = Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
||||||
access_token = serializer.data.get('access_token')
|
response = Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
refresh_token = serializer.data.get('access_token')
|
|
||||||
return self._put_cookies_in_response(
|
access_token = serializer.data.get('access_token')
|
||||||
cookies=self._handle_cookies(request, access_token, refresh_token),
|
refresh_token = serializer.data.get('refresh_token')
|
||||||
response=response)
|
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
|
# Login by username|email + password
|
||||||
class LoginByUsernameOrEmailView(JWTViewMixin):
|
class LoginByUsernameOrEmailView(JWTAuthViewMixin):
|
||||||
"""Login by email and password"""
|
"""Login by email and password"""
|
||||||
permission_classes = (permissions.AllowAny,)
|
permission_classes = (permissions.AllowAny,)
|
||||||
serializer_class = serializers.LoginByUsernameOrEmailSerializer
|
serializer_class = serializers.LoginByUsernameOrEmailSerializer
|
||||||
|
|
||||||
|
|
||||||
# Refresh access_token
|
# Refresh access_token
|
||||||
class RefreshTokenView(JWTViewMixin):
|
class RefreshTokenView(JWTAuthViewMixin):
|
||||||
"""Refresh access_token"""
|
"""Refresh access_token"""
|
||||||
permission_classes = (permissions.IsAuthenticated,)
|
permission_classes = (permissions.IsAuthenticated,)
|
||||||
serializer_class = serializers.RefreshTokenSerializer
|
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
|
# Logout
|
||||||
class LogoutView(generics.CreateAPIView):
|
class LogoutView(generics.CreateAPIView):
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from location import models
|
from location import models
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
|
||||||
|
|
||||||
class CountrySerializer(serializers.ModelSerializer):
|
class CountrySerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -48,8 +49,8 @@ class CitySerializer(serializers.ModelSerializer):
|
||||||
class AddressSerializer(serializers.ModelSerializer):
|
class AddressSerializer(serializers.ModelSerializer):
|
||||||
"""Address serializer."""
|
"""Address serializer."""
|
||||||
city = CitySerializer()
|
city = CitySerializer()
|
||||||
longitude = serializers.DecimalField(max_digits=10, decimal_places=6)
|
geo_lon = serializers.FloatField(allow_null=True)
|
||||||
latitude = serializers.DecimalField(max_digits=10, decimal_places=6)
|
geo_lat = serializers.FloatField(allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Address
|
model = models.Address
|
||||||
|
|
@ -59,14 +60,26 @@ class AddressSerializer(serializers.ModelSerializer):
|
||||||
'street_name_2',
|
'street_name_2',
|
||||||
'number',
|
'number',
|
||||||
'postal_code',
|
'postal_code',
|
||||||
'longitude',
|
'geo_lon',
|
||||||
'latitude'
|
'geo_lat'
|
||||||
]
|
]
|
||||||
|
|
||||||
# def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
# geo_lat = attrs.pop('geo_lat') if 'geo_lat' in attrs else None
|
# if geo_lat and geo_lon was sent
|
||||||
# geo_lon = attrs.pop('geo_lon') if 'geo_lon' in attrs else None
|
geo_lat = attrs.pop('geo_lat') if 'geo_lat' in attrs else None
|
||||||
# if geo_lat and geo_lon:
|
geo_lon = attrs.pop('geo_lon') if 'geo_lon' in attrs else None
|
||||||
# # Point(longitude, latitude)
|
if geo_lat and geo_lon:
|
||||||
# attrs['coordinates'] = Point(geo_lon, geo_lat)
|
# Point(longitude, latitude)
|
||||||
# return attrs
|
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)
|
||||||
|
|
|
||||||
|
|
@ -6,27 +6,27 @@ from location import views
|
||||||
app_name = 'location'
|
app_name = 'location'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('country/list/', views.CountryListView.as_view(), name='country-list'),
|
path('country/', views.CountryListView.as_view(), name='country_list'),
|
||||||
path('country/create/', views.CountryCreateView.as_view(), name='country-create'),
|
path('country/create/', views.CountryCreateView.as_view(), name='country_create'),
|
||||||
path('country/<int:pk>/detail/', views.CountryRetrieveView.as_view(), name='country-retrieve'),
|
path('country/<int:pk>/', views.CountryRetrieveView.as_view(), name='country_retrieve'),
|
||||||
path('country/<int:pk>/delete/', views.CountryDestroyView.as_view(), name='country-destroy'),
|
path('country/<int:pk>/delete/', views.CountryDestroyView.as_view(), name='country_destroy'),
|
||||||
path('country/<int:pk>/update/', views.CountryUpdateView.as_view(), name='country-update'),
|
path('country/<int:pk>/update/', views.CountryUpdateView.as_view(), name='country_update'),
|
||||||
|
|
||||||
path('region/list/', views.RegionListView.as_view(), name='region-list'),
|
path('region/', views.RegionListView.as_view(), name='region_list'),
|
||||||
path('region/create/', views.RegionCreateView.as_view(), name='region-create'),
|
path('region/create/', views.RegionCreateView.as_view(), name='region_create'),
|
||||||
path('region/<int:pk>/detail/', views.RegionRetrieveView.as_view(), name='region-retrieve'),
|
path('region/<int:pk>/', views.RegionRetrieveView.as_view(), name='region_retrieve'),
|
||||||
path('region/<int:pk>/delete/', views.RegionDestroyView.as_view(), name='region-destroy'),
|
path('region/<int:pk>/delete/', views.RegionDestroyView.as_view(), name='region_destroy'),
|
||||||
path('region/<int:pk>/update/', views.RegionUpdateView.as_view(), name='region-update'),
|
path('region/<int:pk>/update/', views.RegionUpdateView.as_view(), name='region_update'),
|
||||||
|
|
||||||
path('city/list/', views.CityListView.as_view(), name='city-list'),
|
path('city/', views.CityListView.as_view(), name='city_list'),
|
||||||
path('city/create/', views.CityCreateView.as_view(), name='city-create'),
|
path('city/create/', views.CityCreateView.as_view(), name='city_create'),
|
||||||
path('city/<int:pk>/detail/', views.CityRetrieveView.as_view(), name='city-retrieve'),
|
path('city/<int:pk>/', views.CityRetrieveView.as_view(), name='city_retrieve'),
|
||||||
path('city/<int:pk>/delete/', views.CityDestroyView.as_view(), name='city-destroy'),
|
path('city/<int:pk>/delete/', views.CityDestroyView.as_view(), name='city_destroy'),
|
||||||
path('city/<int:pk>/update/', views.CityUpdateView.as_view(), name='city-update'),
|
path('city/<int:pk>/update/', views.CityUpdateView.as_view(), name='city_update'),
|
||||||
|
|
||||||
path('address/list/', views.AddressListView.as_view(), name='address-list'),
|
path('address/', views.AddressListView.as_view(), name='address_list'),
|
||||||
path('address/create/', views.AddressCreateView.as_view(), name='address-create'),
|
path('address/create/', views.AddressCreateView.as_view(), name='address_create'),
|
||||||
path('address/<int:pk>/detail/', views.AddressRetrieveView.as_view(), name='address-retrieve'),
|
path('address/<int:pk>/', views.AddressRetrieveView.as_view(), name='address_retrieve'),
|
||||||
path('address/<int:pk>/delete/', views.AddressDestroyView.as_view(), name='address-destroy'),
|
path('address/<int:pk>/delete/', views.AddressDestroyView.as_view(), name='address_destroy'),
|
||||||
path('address/<int:pk>/update/', views.AddressUpdateView.as_view(), name='address-update'),
|
path('address/<int:pk>/update/', views.AddressUpdateView.as_view(), name='address_update'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions
|
||||||
from news.models import News
|
from news.models import News
|
||||||
from news.serializers import common as serializers
|
from news.serializers import common as serializers
|
||||||
|
from utils.views import JWTViewMixin
|
||||||
|
|
||||||
|
|
||||||
class NewsList(generics.ListAPIView):
|
class NewsList(JWTViewMixin, generics.ListAPIView):
|
||||||
"""News list view."""
|
"""News list view."""
|
||||||
queryset = News.objects.all()
|
queryset = News.objects.all()
|
||||||
permission_classes = (permissions.AllowAny, )
|
permission_classes = (permissions.AllowAny, )
|
||||||
|
|
|
||||||
0
apps/translation/__init__.py
Normal file
0
apps/translation/__init__.py
Normal file
7
apps/translation/admin.py
Normal file
7
apps/translation/admin.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from translation.models import Language
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Language)
|
||||||
|
class LanguageAdmin(admin.ModelAdmin):
|
||||||
|
"""Language admin."""
|
||||||
7
apps/translation/apps.py
Normal file
7
apps/translation/apps.py
Normal file
|
|
@ -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')
|
||||||
26
apps/translation/migrations/0001_initial.py
Normal file
26
apps/translation/migrations/0001_initial.py
Normal file
|
|
@ -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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/translation/migrations/__init__.py
Normal file
0
apps/translation/migrations/__init__.py
Normal file
33
apps/translation/models.py
Normal file
33
apps/translation/models.py
Normal file
|
|
@ -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})'
|
||||||
14
apps/translation/serializers.py
Normal file
14
apps/translation/serializers.py
Normal file
|
|
@ -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'
|
||||||
|
]
|
||||||
3
apps/translation/tests.py
Normal file
3
apps/translation/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
18
apps/translation/urls.py
Normal file
18
apps/translation/urls.py
Normal file
|
|
@ -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/<int:pk>/update/', views.LanguageUpdateView.as_view(),
|
||||||
|
name='language_update'),
|
||||||
|
path('language/<int:pk>/destroy/', views.LanguageDestroyView.as_view(),
|
||||||
|
name='language_destroy'),
|
||||||
|
path('language/<int:pk>/', views.LanguageRetrieveView.as_view(),
|
||||||
|
name='language_retrieve'),
|
||||||
|
]
|
||||||
40
apps/translation/views.py
Normal file
40
apps/translation/views.py
Normal file
|
|
@ -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
|
||||||
|
|
@ -47,3 +47,17 @@ class EmailSendingError(exceptions.APIException):
|
||||||
'detail': self.default_detail % recipient,
|
'detail': self.default_detail % recipient,
|
||||||
}
|
}
|
||||||
super().__init__()
|
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__()
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,101 @@
|
||||||
from rest_framework import generics
|
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)
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ PROJECT_APPS = [
|
||||||
'authorization.apps.AuthorizationConfig',
|
'authorization.apps.AuthorizationConfig',
|
||||||
'location.apps.LocationConfig',
|
'location.apps.LocationConfig',
|
||||||
'news.apps.NewsConfig',
|
'news.apps.NewsConfig',
|
||||||
|
'translation.apps.TranslationConfig',
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ from rest_framework import permissions
|
||||||
# URL platform patterns
|
# URL platform patterns
|
||||||
from project.urls import web as web_urlpatterns
|
from project.urls import web as web_urlpatterns
|
||||||
from location import urls as location_urls
|
from location import urls as location_urls
|
||||||
|
from translation import urls as translation_urls
|
||||||
|
|
||||||
|
|
||||||
schema_view = get_schema_view(
|
schema_view = get_schema_view(
|
||||||
|
|
@ -60,6 +61,7 @@ urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('api/web/', include(web_urlpatterns)),
|
path('api/web/', include(web_urlpatterns)),
|
||||||
path('api/location/', include(location_urls.urlpatterns)),
|
path('api/location/', include(location_urls.urlpatterns)),
|
||||||
|
path('api/translation/', include(translation_urls.urlpatterns)),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = urlpatterns + \
|
urlpatterns = urlpatterns + \
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user