feat: Add comprehensive Django user app with tests using model-bakery

- Implemented user authentication with JWT tokens
- Added user and profile models with OneToOne relationship
- Created service layer for business logic separation
- Implemented DRF serializers and views
- Added comprehensive test suite with model-bakery factories
- Fixed ipdb/pdbpp dependency conflicts with custom test runner
- Configured development and production environments
- Added deployment configurations for Apache, systemd, and Docker
This commit is contained in:
2026-01-19 14:12:33 +01:00
commit cbfbd8652d
51 changed files with 4183 additions and 0 deletions

312
src/apps/user/views.py Normal file
View File

@@ -0,0 +1,312 @@
from django.contrib.auth import authenticate
from django.contrib.auth.hashers import check_password
from rest_framework import status, generics, permissions
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from .models import User
from .services import UserService, ProfileService
from .serializers import (
UserRegistrationSerializer,
UserSerializer,
LoginSerializer,
TokenSerializer,
PasswordChangeSerializer,
UserUpdateSerializer,
ProfileUpdateSerializer
)
class RegisterView(APIView):
"""Регистрация нового пользователя"""
permission_classes = [AllowAny]
@swagger_auto_schema(
request_body=UserRegistrationSerializer,
responses={201: UserSerializer}
)
def post(self, request):
serializer = UserRegistrationSerializer(data=request.data)
if serializer.is_valid():
# Убираем password_confirm из данных для создания пользователя
user_data = serializer.validated_data.copy()
user_data.pop('password_confirm', None)
user = UserService.create_user(**user_data)
user_serializer = UserSerializer(user)
tokens = UserService.get_tokens_for_user(user)
return Response({
'user': user_serializer.data,
'tokens': tokens
}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class LoginView(APIView):
"""Вход пользователя"""
permission_classes = [AllowAny]
@swagger_auto_schema(
request_body=LoginSerializer,
responses={200: TokenSerializer}
)
def post(self, request):
serializer = LoginSerializer(data=request.data)
if serializer.is_valid():
email = serializer.validated_data['email']
password = serializer.validated_data['password']
user = authenticate(email=email, password=password)
if user:
tokens = UserService.get_tokens_for_user(user)
return Response(tokens, status=status.HTTP_200_OK)
else:
return Response(
{'error': 'Неверные учетные данные'},
status=status.HTTP_401_UNAUTHORIZED
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class LogoutView(APIView):
"""Выход пользователя"""
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(
'Authorization',
openapi.IN_HEADER,
description="Bearer <token>",
type=openapi.TYPE_STRING,
required=True
)
],
responses={200: 'Успешный выход'}
)
def post(self, request):
try:
refresh_token = request.data.get('refresh')
if refresh_token:
token = RefreshToken(refresh_token)
token.blacklist()
return Response({'message': 'Успешный выход'}, status=status.HTTP_200_OK)
except Exception:
return Response({'error': 'Неверный токен'}, status=status.HTTP_400_BAD_REQUEST)
class CurrentUserView(APIView):
"""Получение данных текущего пользователя"""
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(
'Authorization',
openapi.IN_HEADER,
description="Bearer <token>",
type=openapi.TYPE_STRING,
required=True
)
],
responses={200: UserSerializer}
)
def get(self, request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
class UserUpdateView(APIView):
"""Обновление данных пользователя"""
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
request_body=UserUpdateSerializer,
manual_parameters=[
openapi.Parameter(
'Authorization',
openapi.IN_HEADER,
description="Bearer <token>",
type=openapi.TYPE_STRING,
required=True
)
],
responses={200: UserSerializer}
)
def patch(self, request):
serializer = UserUpdateSerializer(request.user, data=request.data, partial=True)
if serializer.is_valid():
user = UserService.update_user(
request.user.id,
**serializer.validated_data
)
if user:
user_serializer = UserSerializer(user)
return Response(user_serializer.data)
return Response(
{'error': 'Пользователь не найден'},
status=status.HTTP_404_NOT_FOUND
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class ProfileDetailView(generics.RetrieveUpdateAPIView):
"""Получение и обновление профиля пользователя"""
permission_classes = [IsAuthenticated]
serializer_class = ProfileUpdateSerializer
def get_object(self):
profile = ProfileService.get_profile_by_user_id(self.request.user.id)
if not profile:
# Если профиль не существует, создаем его
from .models import Profile
profile = Profile.objects.create(user=self.request.user)
return profile
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(
'Authorization',
openapi.IN_HEADER,
description="Bearer <token>",
type=openapi.TYPE_STRING,
required=True
)
]
)
def get(self, request, *args, **kwargs):
profile = self.get_object()
serializer = self.get_serializer(profile)
return Response(serializer.data)
@swagger_auto_schema(
request_body=ProfileUpdateSerializer,
manual_parameters=[
openapi.Parameter(
'Authorization',
openapi.IN_HEADER,
description="Bearer <token>",
type=openapi.TYPE_STRING,
required=True
)
]
)
def patch(self, request, *args, **kwargs):
profile = self.get_object()
serializer = self.get_serializer(profile, data=request.data, partial=True)
if serializer.is_valid():
updated_profile = ProfileService.update_profile(
request.user.id,
**serializer.validated_data
)
if updated_profile:
return Response(ProfileUpdateSerializer(updated_profile).data)
return Response(
{'error': 'Профиль не найден'},
status=status.HTTP_404_NOT_FOUND
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class PasswordChangeView(APIView):
"""Смена пароля"""
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
request_body=PasswordChangeSerializer,
manual_parameters=[
openapi.Parameter(
'Authorization',
openapi.IN_HEADER,
description="Bearer <token>",
type=openapi.TYPE_STRING,
required=True
)
],
responses={200: 'Пароль успешно изменен'}
)
def post(self, request):
serializer = PasswordChangeSerializer(data=request.data)
if serializer.is_valid():
user = request.user
old_password = serializer.validated_data['old_password']
if check_password(old_password, user.password):
new_password = serializer.validated_data['new_password']
user.set_password(new_password)
user.save()
return Response(
{'message': 'Пароль успешно изменен'},
status=status.HTTP_200_OK
)
else:
return Response(
{'error': 'Неверный старый пароль'},
status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def user_profile_detail(request):
"""Получение полных данных профиля пользователя"""
profile_data = ProfileService.get_full_profile_data(request.user.id)
if profile_data:
return Response(profile_data)
return Response(
{'error': 'Профиль не найден'},
status=status.HTTP_404_NOT_FOUND
)
class TokenRefreshView(APIView):
"""Обновление access токена через refresh токен"""
permission_classes = [AllowAny]
@swagger_auto_schema(
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'refresh': openapi.Schema(type=openapi.TYPE_STRING, description='Refresh token')
},
required=['refresh']
),
responses={200: TokenSerializer}
)
def post(self, request):
refresh_token = request.data.get('refresh')
if not refresh_token:
return Response(
{'error': 'Refresh token обязателен'},
status=status.HTTP_400_BAD_REQUEST
)
try:
refresh = RefreshToken(refresh_token)
return Response({
'access': str(refresh.access_token),
'refresh': str(refresh)
})
except Exception:
return Response(
{'error': 'Неверный refresh token'},
status=status.HTTP_401_UNAUTHORIZED
)