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

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 

7 

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 

14 

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 

23 

24# Logger para almacenar los logs 

25logger = logging.getLogger(__name__) 

26 

27 

28# Base comun de los datos propios de un usuario 

29class OwnedDataViewSet(OwnedCreateMixin, OwnedQuerysetMixin, SoftDeleteQuerysetMixin, viewsets.ModelViewSet): 

30 permission_classes = [IsAuthenticated] 

31 

32 # El metodo put no se usa, por lo que no se publica 

33 http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options'] 

34 

35 

36# Endpoints para gestionar las notificaciones 

37class NotificationsViewSet(OwnedDataViewSet): 

38 queryset = Notifications.objects.all() 

39 serializer_class = NotificationsSerializer 

40 

41# Endpoints para gestionar las preferencias de los usuarios 

42class PreferencesViewSet(OwnedDataViewSet): 

43 queryset = Preferences.objects.all() 

44 serializer_class = PreferencesSerializer 

45 

46# Endpoints para gestionar las valoraciones de los conductores 

47class DriverRatingsViewSet(OwnedDataViewSet): 

48 queryset = DriverRatings.objects.all() 

49 serializer_class = DriverRatingsSerializer 

50 

51# Endpoints para gestionar los dispositivos registrados para notificaciones push 

52class DeviceViewSet(OwnedDataViewSet): 

53 queryset = Device.objects.all() 

54 serializer_class = DeviceSerializer 

55 

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 ) 

65 

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) 

71 

72 

73# Endpoints para gestionar los vehiculos de los usuarios 

74class VehiclesViewSet(OwnedDataViewSet): 

75 queryset = Vehicles.objects.all() 

76 serializer_class = VehicleSerializer 

77 

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') 

82 

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) 

87 

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) 

92 

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) 

105 

106 logger.info(f"Updating vehicle ID: {vehicle.id_vehicle} with new seats: {new_seats}") 

107 return super().partial_update(request, *args, **kwargs) 

108 

109 

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() 

117 

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) 

125 

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) 

141 

142 return super().destroy(request, *args, **kwargs)