Coverage for api\views\user_data_views.py: 100.0%
77 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-30 20:58 +0200
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-30 20:58 +0200
1"""
2Endpoints de los datos que pertenecen a un usuario: notificaciones, dispositivos, preferencias, valoraciones emitidas y vehiculos.
3Todos heredan de OwnedQuerysetMixin, asi que la proteccion viene de serie,
4cada uno solo ve y toca lo suyo sin tener que acordarse de comprobarlo en cada metodo
5"""
6import logging
8from rest_framework import status, viewsets
9from django.core.exceptions import ValidationError as DjangoValidationError
10from django.http import Http404
11from rest_framework.exceptions import APIException
12from rest_framework.permissions import IsAuthenticated
13from rest_framework.response import Response
15from api.errors import ErrorCodes
16from api.ownership import OwnedCreateMixin, OwnedQuerysetMixin
17from api.serializers.user_serializer import ( DeviceSerializer, DriverRatingsSerializer, NotificationsSerializer,
18 PreferencesSerializer, VehicleSerializer)
19from api.soft_delete import SoftDeleteQuerysetMixin
20from users.models import Device, DriverRatings, Notifications, Preferences, Vehicles
21from users.services.exceptions import (VehicleHasActiveTravelsError, VehicleSeatsInsufficientError)
22from users.services.user_service import UserService
24# Logger para almacenar los logs
25logger = logging.getLogger(__name__)
28# Base comun de los datos propios de un usuario
29class OwnedDataViewSet(OwnedCreateMixin, OwnedQuerysetMixin, SoftDeleteQuerysetMixin, viewsets.ModelViewSet):
30 permission_classes = [IsAuthenticated]
32 # El metodo put no se usa, por lo que no se publica
33 http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
36# Endpoints para gestionar las notificaciones
37class NotificationsViewSet(OwnedDataViewSet):
38 queryset = Notifications.objects.all()
39 serializer_class = NotificationsSerializer
41# Endpoints para gestionar las preferencias de los usuarios
42class PreferencesViewSet(OwnedDataViewSet):
43 queryset = Preferences.objects.all()
44 serializer_class = PreferencesSerializer
46# Endpoints para gestionar las valoraciones de los conductores
47class DriverRatingsViewSet(OwnedDataViewSet):
48 queryset = DriverRatings.objects.all()
49 serializer_class = DriverRatingsSerializer
51# Endpoints para gestionar los dispositivos registrados para notificaciones push
52class DeviceViewSet(OwnedDataViewSet):
53 queryset = Device.objects.all()
54 serializer_class = DeviceSerializer
56 # Si el dispositivo ya existe, se actualiza, si no, se crea
57 def create(self, request, *args, **kwargs):
58 # coge el token fcm
59 fcm_token = request.data.get('fcm_token')
60 if not fcm_token: # si no hay token, devuelve un error
61 return Response(
62 {"error": "fcm_token is required", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD},
63 status=status.HTTP_400_BAD_REQUEST
64 )
66 device = UserService.register_device(
67 request.user, fcm_token, request.data.get('platform', '')
68 )
69 serializer = self.get_serializer(device)
70 return Response(serializer.data, status=status.HTTP_200_OK)
73# Endpoints para gestionar los vehiculos de los usuarios
74class VehiclesViewSet(OwnedDataViewSet):
75 queryset = Vehicles.objects.all()
76 serializer_class = VehicleSerializer
78 # Se sobreescribe el metodo partial_update para poder hacer las comprobaciones de asientos necesarias al actualizar un vehiculo
79 def partial_update(self, request, *args, **kwargs):
80 vehicle = self.get_object()
81 new_seats = request.data.get('seats')
83 # El numero de asientos es obligatorio para actualizar el vehiculo
84 if new_seats is None:
85 logger.error("Seats field is required for updating vehicle")
86 return Response({"error": "Numero de asientos invalido", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST)
88 try:
89 new_seats = int(new_seats)
90 except ValueError:
91 return Response({"error": "Numero de asientos invalido", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST)
93 try:
94 UserService.check_new_seats(request.user, vehicle, new_seats)
95 except VehicleSeatsInsufficientError:
96 return Response({"error": "El numero de asientos no puede ser menor que 2", "error_code": ErrorCodes.VEHICLE_SEATS_INSUFFICIENT}, status=status.HTTP_400_BAD_REQUEST)
97 except VehicleHasActiveTravelsError:
98 return Response({"error": {"code": ErrorCodes.VEHICLE_SEATS_INSUFFICIENT, "message":
99 "Tiene algun viaje activo asociado a este vehículo con más asientos que los nuevos, modifique el viaje antes de actualizar el vehículo."}}, status=status.HTTP_400_BAD_REQUEST)
100 except (Http404, APIException, DjangoValidationError):
101 raise
102 except Exception as e:
103 logger.error(f"Error occurred while checking associated travels for seats: {str(e)}")
104 return Response({"error": "Error occurred while checking associated travels for seats", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
106 logger.info(f"Updating vehicle ID: {vehicle.id_vehicle} with new seats: {new_seats}")
107 return super().partial_update(request, *args, **kwargs)
110 # Se sobreescribe el metodo destroy para hacer las comprobaciones necesarias antes de borrar el vehiculo
111 def destroy(self, request, *args, **kwargs):
112 """
113 Endpoint DELETE /vehicles/{id_vehicle}/
114 Elimina el vehiculo indicado, siempre que no tenga un viaje asociado a él
115 """
116 vehicle = self.get_object()
118 # Solo el dueño del vehiculo puede eliminarlo
119 if vehicle.id_user != request.user:
120 return Response({
121 "status": "error",
122 "message": "No tienes permiso para borrar este vehiculo.",
123 "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS
124 }, status=status.HTTP_403_FORBIDDEN)
126 # Solo se puede eliminar si no tiene viajes activos asociados a él
127 try:
128 UserService.check_vehicle_is_free(request.user, vehicle)
129 except VehicleHasActiveTravelsError:
130 return Response({
131 "error": {
132 "code": ErrorCodes.VEHICLE_ASSOCIATED_TO_TRAVEL,
133 "message": "El vehículo tiene viajes asociados, sustituyalo en ellos antes de eliminarlo."
134 }
135 }, status=status.HTTP_400_BAD_REQUEST)
136 except (Http404, APIException, DjangoValidationError):
137 raise
138 except Exception as e:
139 logger.error(f"Error occurred while checking associated travels: {str(e)}")
140 return Response({"error": "Error occurred while checking associated travels", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
142 return super().destroy(request, *args, **kwargs)