feat: implement CI/CD pipeline with Gitea Actions

- Add Gitea Actions workflow with 4 stages: lint, test, build, push
- Configure ruff linting and formatting checks
- Set up Django tests with PostgreSQL and Redis services
- Implement Docker image building for web and celery services
- Add requirements.txt and requirements-dev.txt generation
- Fix ipdb compatibility issues in test runner
- Update ruff configuration for Django compatibility
- Add comprehensive CI/CD documentation
This commit is contained in:
2026-01-19 14:24:48 +01:00
parent cbfbd8652d
commit 06b30fca02
26 changed files with 1769 additions and 726 deletions

View File

@@ -1,127 +1,124 @@
from django.contrib.auth import authenticate
from django.contrib.auth.hashers import check_password
from rest_framework import status, generics, permissions
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import generics, status
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 (
LoginSerializer,
PasswordChangeSerializer,
ProfileUpdateSerializer,
TokenSerializer,
UserRegistrationSerializer,
UserSerializer,
LoginSerializer,
TokenSerializer,
PasswordChangeSerializer,
UserUpdateSerializer,
ProfileUpdateSerializer
)
from .services import ProfileService, UserService
class RegisterView(APIView):
"""Регистрация нового пользователя"""
permission_classes = [AllowAny]
@swagger_auto_schema(
request_body=UserRegistrationSerializer,
responses={201: UserSerializer}
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_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(
{"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}
)
@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']
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
{"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',
"Authorization",
openapi.IN_HEADER,
description="Bearer <token>",
type=openapi.TYPE_STRING,
required=True
required=True,
)
],
responses={200: 'Успешный выход'}
responses={200: "Успешный выход"},
)
def post(self, request):
try:
refresh_token = request.data.get('refresh')
refresh_token = request.data.get("refresh")
if refresh_token:
token = RefreshToken(refresh_token)
token.blacklist()
return Response({'message': 'Успешный выход'}, status=status.HTTP_200_OK)
return Response({"message": "Успешный выход"}, status=status.HTTP_200_OK)
except Exception:
return Response({'error': 'Неверный токен'}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{"error": "Неверный токен"}, status=status.HTTP_400_BAD_REQUEST
)
class CurrentUserView(APIView):
"""Получение данных текущего пользователя"""
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(
'Authorization',
"Authorization",
openapi.IN_HEADER,
description="Bearer <token>",
type=openapi.TYPE_STRING,
required=True
required=True,
)
],
responses={200: UserSerializer}
responses={200: UserSerializer},
)
def get(self, request):
serializer = UserSerializer(request.user)
@@ -130,42 +127,38 @@ class CurrentUserView(APIView):
class UserUpdateView(APIView):
"""Обновление данных пользователя"""
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
request_body=UserUpdateSerializer,
manual_parameters=[
openapi.Parameter(
'Authorization',
"Authorization",
openapi.IN_HEADER,
description="Bearer <token>",
type=openapi.TYPE_STRING,
required=True
required=True,
)
],
responses={200: UserSerializer}
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
)
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
{"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
@@ -174,17 +167,18 @@ class ProfileDetailView(generics.RetrieveUpdateAPIView):
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',
"Authorization",
openapi.IN_HEADER,
description="Bearer <token>",
type=openapi.TYPE_STRING,
required=True
required=True,
)
]
)
@@ -197,116 +191,110 @@ class ProfileDetailView(generics.RetrieveUpdateAPIView):
request_body=ProfileUpdateSerializer,
manual_parameters=[
openapi.Parameter(
'Authorization',
"Authorization",
openapi.IN_HEADER,
description="Bearer <token>",
type=openapi.TYPE_STRING,
required=True
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
request.user.id, **serializer.validated_data
)
if updated_profile:
return Response(ProfileUpdateSerializer(updated_profile).data)
return Response(
{'error': 'Профиль не найден'},
status=status.HTTP_404_NOT_FOUND
{"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',
"Authorization",
openapi.IN_HEADER,
description="Bearer <token>",
type=openapi.TYPE_STRING,
required=True
required=True,
)
],
responses={200: 'Пароль успешно изменен'}
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']
old_password = serializer.validated_data["old_password"]
if check_password(old_password, user.password):
new_password = serializer.validated_data['new_password']
new_password = serializer.validated_data["new_password"]
user.set_password(new_password)
user.save()
return Response(
{'message': 'Пароль успешно изменен'},
status=status.HTTP_200_OK
{"message": "Пароль успешно изменен"}, status=status.HTTP_200_OK
)
else:
return Response(
{'error': 'Неверный старый пароль'},
status=status.HTTP_400_BAD_REQUEST
{"error": "Неверный старый пароль"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET'])
@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
)
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')
"refresh": openapi.Schema(
type=openapi.TYPE_STRING, description="Refresh token"
)
},
required=['refresh']
required=["refresh"],
),
responses={200: TokenSerializer}
responses={200: TokenSerializer},
)
def post(self, request):
refresh_token = request.data.get('refresh')
refresh_token = request.data.get("refresh")
if not refresh_token:
return Response(
{'error': 'Refresh token обязателен'},
status=status.HTTP_400_BAD_REQUEST
{"error": "Refresh token обязателен"},
status=status.HTTP_400_BAD_REQUEST,
)
try:
refresh = RefreshToken(refresh_token)
return Response({
'access': str(refresh.access_token),
'refresh': str(refresh)
})
return Response(
{"access": str(refresh.access_token), "refresh": str(refresh)}
)
except Exception:
return Response(
{'error': 'Неверный refresh token'},
status=status.HTTP_401_UNAUTHORIZED
)
{"error": "Неверный refresh token"}, status=status.HTTP_401_UNAUTHORIZED
)