408 lines
16 KiB
Python
408 lines
16 KiB
Python
from rest_framework import status
|
||
from rest_framework.viewsets import ViewSet
|
||
from rest_framework.permissions import IsAuthenticated
|
||
from rest_framework.decorators import action
|
||
from rest_framework.response import Response
|
||
|
||
from django.shortcuts import get_object_or_404
|
||
from django.core.validators import validate_email
|
||
from django.core.exceptions import ValidationError
|
||
from django.db import models
|
||
from django.db.models import Q
|
||
from datetime import timedelta
|
||
from rest_framework.exceptions import PermissionDenied
|
||
from django.utils.timezone import now as timezone_now
|
||
|
||
|
||
from .serializers import RouteSerializer, CreateRouteSerializer, CitySerializer, CountrySerializer, PlanChangeSerializer, PricingSerializer, LeadSerializer, LeadResponseSerializer
|
||
from api.auth.serializers import UserResponseSerializer
|
||
from api.models import UserProfile
|
||
from routes.models import Route, City, Country, Leads
|
||
from sitemanagement.models import Pricing, RoutePromotionLog, Transactions
|
||
|
||
from api.utils.decorators import handle_exceptions
|
||
from api.utils.emailSender import send_email
|
||
from api.utils.permissionChecker import check_monthly_limit
|
||
|
||
|
||
class UserDataView(ViewSet):
|
||
"""Эндпоинт для наполнения стора фронта данными"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def initial(self, request, *args, **kwargs):
|
||
try:
|
||
super().initial(request, *args, **kwargs)
|
||
except Exception as e:
|
||
print(f"Authentication error: {e}")
|
||
raise
|
||
|
||
@action(detail=False, methods=['get'])
|
||
@handle_exceptions
|
||
def user_data(self, request):
|
||
user = request.user
|
||
|
||
try:
|
||
user_data = UserResponseSerializer(user).data
|
||
return Response(user_data, status=status.HTTP_200_OK)
|
||
except UserProfile.DoesNotExist:
|
||
return Response(
|
||
{"error": "User profile not found"},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
class AccountActionsView(ViewSet):
|
||
"""Действия в аккаунте пользователя:
|
||
- PATCH данных в account/main
|
||
- POST новых заявок"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@action(detail=False, methods=['patch'])
|
||
@handle_exceptions
|
||
|
||
def change_data_main_tab(self, request):
|
||
"""Обновление данных на главной странице аккаунта"""
|
||
|
||
user = request.user
|
||
user_profile = get_object_or_404(UserProfile, user=user)
|
||
|
||
# обновляем данные пользователя
|
||
if 'firstName' in request.data:
|
||
user.first_name = request.data['firstName']
|
||
if 'lastName' in request.data:
|
||
user.last_name = request.data['lastName']
|
||
if 'email' in request.data:
|
||
email = request.data['email']
|
||
validate_email(email) # handle_exceptions обработает ValidationError
|
||
user.email = email
|
||
user.username = email
|
||
|
||
# обновляем номер телефона
|
||
if 'phone_number' in request.data:
|
||
phone = request.data['phone_number']
|
||
if phone:
|
||
if len(phone) < 13: # +375XXXXXXXXX
|
||
raise ValidationError("Номер телефона слишком короткий")
|
||
|
||
if len(phone) > 18:
|
||
raise ValidationError("Номер телефона слишком длинный")
|
||
|
||
# проверка на уникальность
|
||
if UserProfile.objects.filter(phone_number=phone).exclude(user=user).exists():
|
||
raise ValidationError("Этот номер телефона уже используется")
|
||
|
||
user_profile.phone_number = phone
|
||
|
||
# сохраняем изменения
|
||
user.save()
|
||
user_profile.save()
|
||
|
||
return Response({
|
||
"message": "Данные успешно обновлены",
|
||
"user": UserResponseSerializer(user).data
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
@action(detail=False, methods=['get'])
|
||
@handle_exceptions
|
||
def user_routes(self, request):
|
||
"""Получаем список заявок юзера"""
|
||
|
||
user = request.user
|
||
routes = Route.objects.filter(owner=user)
|
||
return Response(RouteSerializer(routes, many=True).data, status=status.HTTP_200_OK)
|
||
|
||
@action(detail=False, methods=['post'])
|
||
@handle_exceptions
|
||
def create_route(self, request):
|
||
"""Создаем новую заявку"""
|
||
|
||
serializer = CreateRouteSerializer(data=request.data, context={'request': request})
|
||
if not serializer.is_valid():
|
||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||
route = serializer.save()
|
||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||
|
||
|
||
class CityView(ViewSet):
|
||
"""Получаем список городов из базы для автокомплита"""
|
||
|
||
@action(detail=False, methods=['get'])
|
||
@handle_exceptions
|
||
def get_cities(self, request):
|
||
# получаем параметр country_id из query params
|
||
country_id = request.query_params.get('country_id')
|
||
|
||
# базовый QuerySet
|
||
cities = City.objects.all()
|
||
|
||
# фильтруем города по стране, если указан country_id
|
||
if country_id:
|
||
cities = cities.filter(country_id=country_id)
|
||
|
||
# поиск по названию города
|
||
search = request.query_params.get('search')
|
||
if search:
|
||
cities = cities.filter(
|
||
Q(name__icontains=search) | # поиск по английскому названию
|
||
Q(russian_name__icontains=search) # поиск по русскому названию
|
||
)
|
||
|
||
# ограничиваем количество результатов и сортируем по имени
|
||
cities = cities.order_by('name')[:100]
|
||
|
||
return Response(CitySerializer(cities, many=True).data, status=status.HTTP_200_OK)
|
||
|
||
class CountryView(ViewSet):
|
||
"""Получаем список стран из базы для автокомплита"""
|
||
|
||
@action(detail=False, methods=['get'])
|
||
@handle_exceptions
|
||
def get_countries(self, request):
|
||
# базовый QuerySet
|
||
countries = Country.objects.all()
|
||
|
||
# поиск по названию страны
|
||
search = request.query_params.get('search')
|
||
if search:
|
||
countries = countries.filter(
|
||
models.Q(international_name__icontains=search) |
|
||
models.Q(official_name__icontains=search)
|
||
)
|
||
|
||
# сортируем по международному названию
|
||
countries = countries.order_by('international_name')
|
||
|
||
return Response(CountrySerializer(countries, many=True).data, status=status.HTTP_200_OK)
|
||
|
||
class ChangeUserMembership(ViewSet):
|
||
"""Меняем тарифный план пользователя"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@action(detail=False, methods=['patch'])
|
||
@handle_exceptions
|
||
def change_plan(self, request):
|
||
"""Меняем пользователю тарифный план"""
|
||
user = request.user
|
||
user_profile = get_object_or_404(UserProfile, user=user)
|
||
|
||
# преобразуем plan в account_type
|
||
if 'plan' in request.data and 'account_type' not in request.data:
|
||
request.data['account_type'] = request.data['plan']
|
||
|
||
if 'account_type' not in request.data:
|
||
return Response({
|
||
"error": "Не указан тарифный план",
|
||
"details": "Необходимо указать plan или account_type"
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
serializer = PlanChangeSerializer(user_profile, data=request.data)
|
||
|
||
try:
|
||
if not serializer.is_valid():
|
||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
account_type = request.data['account_type']
|
||
# получаем тарифный план
|
||
new_plan = get_object_or_404(Pricing, plan=account_type)
|
||
|
||
# создаем транзакцию
|
||
transaction = Transactions.objects.create(
|
||
user=user,
|
||
plan=new_plan,
|
||
amount=new_plan.price,
|
||
status='success'
|
||
)
|
||
|
||
# если транзакция успешно создана, меняем тариф
|
||
if transaction:
|
||
serializer.save()
|
||
return Response({
|
||
"message": "Тариф успешно изменен",
|
||
"account_type": new_plan.plan
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
except Exception as e:
|
||
return Response({
|
||
"error": "Ошибка при изменении тарифного плана",
|
||
"details": str(e)
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
class GetMembershipData(ViewSet):
|
||
"""Получаем все тарифные планы"""
|
||
|
||
@action(detail=False, methods=['get'])
|
||
@handle_exceptions
|
||
def get_pricing_data(self, request):
|
||
"""Получаем данные по тарифам"""
|
||
pricing_data = Pricing.objects.all().order_by('price')
|
||
serializer = PricingSerializer(pricing_data, many=True)
|
||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||
|
||
class LeadViewSet(ViewSet):
|
||
"""ViewSet для работы с заявками на перевозку"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@action(detail=False, methods=['post'])
|
||
@handle_exceptions
|
||
def send_lead(self, request):
|
||
"""
|
||
Создание новой заявки на перевозку
|
||
"""
|
||
|
||
# добавляем текущего пользователя в данные
|
||
data = request.data.copy()
|
||
data['moving_user'] = request.user.id
|
||
|
||
# проверяем существование и доступность маршрута
|
||
try:
|
||
route = Route.objects.get(id=data.get('route'))
|
||
if route.owner.id == request.user.id:
|
||
print("Error: User trying to respond to their own route")
|
||
return Response(
|
||
{"error": "Вы не можете откликнуться на собственную заявку"},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
except Route.DoesNotExist:
|
||
print("Route not found for ID:", data.get('route'))
|
||
return Response(
|
||
{"error": "Указанный маршрут не найден"},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
serializer = LeadSerializer(data=data)
|
||
is_valid = serializer.is_valid()
|
||
|
||
if is_valid:
|
||
try:
|
||
lead: Leads = serializer.save()
|
||
|
||
# собираем ответ с данными о заявке для фронта
|
||
response_data = {
|
||
"status": "success",
|
||
"message": "Заявка успешно создана",
|
||
"data": {
|
||
"id": lead.id,
|
||
"route_id": lead.route.id,
|
||
"moving_price": lead.moving_price,
|
||
"moving_date": lead.moving_date,
|
||
}
|
||
}
|
||
|
||
# отправляем емаил владельцу маршрута
|
||
try:
|
||
route_owner_email = route.owner.email
|
||
email_subject = f"Новая заявка на перевозку #{lead.id}"
|
||
email_content = f"""
|
||
<h2>Получена новая заявка на перевозку</h2>
|
||
<p>Детали заявки:</p>
|
||
<ul>
|
||
<li>Номер заявки: {lead.id}</li>
|
||
<li>Маршрут: {route.id}</li>
|
||
<li>Предложенная цена: {lead.moving_price}</li>
|
||
<li>Дата перевозки: {lead.moving_date}</li>
|
||
</ul>
|
||
"""
|
||
|
||
email_result = send_email(
|
||
to=[route_owner_email],
|
||
subject=email_subject,
|
||
html_content=email_content
|
||
)
|
||
|
||
if "Ошибка" in email_result:
|
||
print(f"Warning: Failed to send email notification: {email_result}")
|
||
except Exception as email_error:
|
||
print(f"Warning: Error while sending email notification: {str(email_error)}")
|
||
|
||
return Response(response_data, status=status.HTTP_201_CREATED)
|
||
except Exception as e:
|
||
return Response(
|
||
{
|
||
"status": "error",
|
||
"message": "Ошибка при сохранении заявки",
|
||
"error": str(e)
|
||
},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
else:
|
||
return Response(
|
||
{
|
||
"status": "error",
|
||
"message": "Ошибка валидации данных",
|
||
"errors": serializer.errors
|
||
},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
@action(detail=False, methods=['get'])
|
||
@handle_exceptions
|
||
def get_leads(self, request):
|
||
"""Получаем список заявок на перевозку"""
|
||
leads = Leads.objects.select_related(
|
||
'route',
|
||
'route__from_city',
|
||
'route__to_city',
|
||
'route__from_city__country',
|
||
'route__to_city__country',
|
||
'route__owner'
|
||
).filter(moving_user=request.user).order_by('-created_at')
|
||
|
||
return Response(
|
||
LeadResponseSerializer(leads, many=True).data,
|
||
status=status.HTTP_200_OK
|
||
)
|
||
|
||
class PremiumMembershipActionsView(ViewSet):
|
||
"""Выделение и поднятие объявления"""
|
||
|
||
@action(detail=False, methods=['patch'])
|
||
@handle_exceptions
|
||
def highlight_route(self, request):
|
||
"""Выделяем объявление"""
|
||
route_id = request.data.get('route_id')
|
||
route = get_object_or_404(Route, id=route_id)
|
||
|
||
if not check_monthly_limit(request.user, route, 'highlight'):
|
||
raise PermissionDenied("Превышен лимит выделений за месяц")
|
||
|
||
# подсвечиваем объявление на 24 часа
|
||
now = timezone_now()
|
||
route.highlight_end_DT = now + timedelta(days=1)
|
||
route.is_highlighted = True
|
||
route.save()
|
||
|
||
# логируем действие
|
||
RoutePromotionLog.objects.create(
|
||
user=request.user,
|
||
route=route,
|
||
action_type='highlight'
|
||
)
|
||
|
||
return Response({
|
||
"message": "Объявление выделено",
|
||
"is_highlighted": route.is_highlighted
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
@action(detail=False, methods=['patch'])
|
||
@handle_exceptions
|
||
def upper_route(self, request):
|
||
"""Поднимаем объявление"""
|
||
route_id = request.data.get('route_id')
|
||
route = get_object_or_404(Route, id=route_id)
|
||
|
||
if not check_monthly_limit(request.user, route, 'rising'):
|
||
raise PermissionDenied("Превышен лимит поднятий за месяц")
|
||
|
||
route.rising_DT = timezone_now()
|
||
route.save()
|
||
|
||
# логируем действие
|
||
RoutePromotionLog.objects.create(
|
||
user=request.user,
|
||
route=route,
|
||
action_type='rising'
|
||
)
|
||
|
||
return Response({"message": "Объявление поднято"}, status=status.HTTP_200_OK) |