Coverage for api\views\travel_views.py: 97.1%
342 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
1import logging
3from rest_framework import viewsets
4from rest_framework import status
5from rest_framework.decorators import action
6from django.core.exceptions import ValidationError as DjangoValidationError
7from django.http import Http404
8from rest_framework.exceptions import APIException
9from rest_framework.permissions import IsAuthenticated
10from rest_framework.response import Response
12from django.core.paginator import Paginator
13from django.db import connection, transaction
14from django.db.models import Q
16from travels.models import Travel, TravelStates, RequestTravels, PickUpPoints, UsersDenied
17from api.serializers.travel_serializer import TravelSerializer, \
18 RequestTravelsSerializer, PickUpPointSerializer, UsersDeniedSerializer
19from api.serializers.user_serializer import PreferencesSerializer
21from channels.layers import get_channel_layer
22from asgiref.sync import async_to_sync
23from chats.models import Chat
25from api.ownership import OwnedQuerysetMixin
26from api.soft_delete import SoftDeleteQuerysetMixin
27from api.errors import ErrorCodes
28from travels.services.travel_service import TravelService
29from travels.services.exceptions import (
30 AlreadyRequestedError, DateChangeWithOccupiedSeatsError, EndDateRequiredError, InvalidPeriodicIntervalError,
31 InvalidRequestStatusError, InvalidStatusTransitionError, InvalidValidationCodeError, StatusNotRequestableError,
32 NotTravelOwnerError, PassengerAlreadyValidatedError, PassengerNotAcceptedError, PassengerNotFoundError,
33 PassengerNotInTravelError, PeriodicDeniedUsersError, PeriodicSeatsReductionError, PickUpPointNotFoundError,
34 RequestOwnTravelError, SeatsBelowOccupiedError, SeatsFullError, SeriesVehicleSeatsInsufficientError,
35 TravelIsNotPunctualError, VehicleNotFoundError, VehicleSeatsInsufficientError)
37# Logger para ir almacenando los logs
38logger = logging.getLogger(__name__)
40# Cierra la conexion WebSocket del chat de un usuario al que se acaba de expulsar del viaje
41def _notify_chat_removed(travel, user):
42 logger.info(f"Notifying user {user.username} about removal from chat of travel {travel.id_travel}")
43 try:
44 channel_layer = get_channel_layer() # Se obtiene el canal para enviar los mensajes del websocket
45 if channel_layer is None: # Si no existe, no se hace nada 45 ↛ 46line 45 didn't jump to line 46 because the condition on line 45 was never true
46 return
47 chat = Chat.get_or_create_for_travel(travel)
48 async_to_sync(channel_layer.group_send)( # Se envia un mensaje indicando que el usuario ha sido expulsado del chat
49 f"chat_{chat.id}_user_{user.id}",
50 {
51 'type': 'chat.removed',
52 'message': 'El creador del viaje te ha eliminado, ya no tienes acceso a este chat.',
53 },
54 )
55 logger.info(f"Successfully notified user {user.username} about removal from chat of travel {travel.id_travel}")
56 except Exception as e:
57 logger.error(f"Error occurred while notifying user {user.username} about removal from chat of travel {travel.id_travel}: {str(e)}")
59# Endpoints de los viajes
60class TravelViewSet(SoftDeleteQuerysetMixin, viewsets.ModelViewSet):
61 queryset = Travel.objects.all()
62 serializer_class = TravelSerializer
63 permission_classes = [IsAuthenticated]
65 # PUT no se utiliza, por lo que no se publica
66 http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
68 # Sin ambito por defecto, solo se limita la accion que declara el suyo
69 throttle_scope = None
71 # Se sobreescribe el metodo partial_update para evitar que se puedan comenzar dos viajes a la vez
72 def partial_update(self, request, *args, **kwargs):
73 travel = self.get_object()
75 logger.info(f"Request to update travel {travel.id_travel} by user {request.user.username}")
77 # Solo el creador del viaje puede editarlo
78 if travel.creation_user != request.user:
79 return Response({
80 "status": "error",
81 "message": "No tienes permiso para actualizar este viaje.",
82 "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS
83 }, status=status.HTTP_403_FORBIDDEN)
85 started_state = TravelStates.objects.get(code='started')
87 if Travel.objects.filter(creation_user=request.user, is_deleted=False, state=started_state).exclude(pk=travel.pk).count() >= 1:
88 return Response({
89 "status": "error",
90 "message": "No puedes iniciar un nuevo viaje mientras tengas otro en curso.",
91 "error_code": ErrorCodes.TRAVEL_ALREADY_STARTED
92 }, status=status.HTTP_400_BAD_REQUEST)
94 return super().partial_update(request, *args, **kwargs)
96 # Se sobreescribe el metodo destroy para eliminar el viaje, las solicitudes asociadas y notificar a los usuarios afectados
97 def destroy(self, request, *args, **kwargs):
98 """
99 Endpoint DELETE /travels/{id_travel}/
100 Elimina el viaje indicado, y las solicitudes asociadas a este
101 """
102 travel = self.get_object()
104 logger.info(f"Request to delete travel {travel.id_travel} by user {request.user.username}")
106 # Solo el creador del viaje puede eliminarlo
107 if travel.creation_user != request.user:
108 return Response({
109 "status": "error",
110 "message": "No tienes permiso para eliminar este viaje.",
111 "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS
112 }, status=status.HTTP_403_FORBIDDEN)
114 try:
115 with transaction.atomic():
116 # El servicio avisa a los solicitantes y borra sus solicitudes
117 TravelService.delete_travel(travel)
119 logger.info(f"Soft-deleting travel {travel.id_travel}.")
120 return super().destroy(request, *args, **kwargs)
121 except (Http404, APIException, DjangoValidationError):
122 raise
123 except Exception as e:
124 logger.error(f"Error deleting travel {travel.id_travel}: {str(e)}")
125 return Response({
126 "status": "error",
127 "message": "Error interno del servidor, vuelve a intentarlo más tarde.",
128 "error_code": ErrorCodes.INTERNAL_SERVER_ERROR
129 }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
131 @action(
132 detail=False,
133 methods=['post'],
134 url_path='search-travels',
135 permission_classes=[IsAuthenticated]
136 )
137 def search_travels(self, request, *args, **kwargs):
138 """
139 Endpoint POST /travel/search-travels/
140 Realiza la busqueda de los viajes segun los filtros, las respuestas estan paginadas
141 Devuelve un diccionario con los datos de los viajes y la url de la siguiente pagina (si existe)
142 """
143 data = request.data
145 # Paginacion
146 page_number = int(data.get('page', 1))
147 page_size = 10
149 try:
150 # Todo el filtrado y la ordenacion los construye el servicio
151 travels = TravelService.search_travels(self.get_queryset(), request.user, data)
153 # Se paginan los resultados
154 paginator = Paginator(travels, page_size)
155 page_obj = paginator.get_page(page_number)
157 # Serializamos solo los viajes de esta página
158 serializer = self.get_serializer(page_obj, many=True)
160 return Response({
161 'status': 'ok',
162 'data': list(serializer.data),
163 'has_next': page_obj.has_next() if hasattr(page_obj, 'has_next') else False
164 })
166 except (Http404, APIException, DjangoValidationError):
167 raise
168 except Exception as e:
169 logger.error(f"Error in search_travels: {str(e)}")
170 return Response({'status': 'error', 'message': str(e), 'error_code': ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)
172 @action(
173 detail=False,
174 methods=['post'],
175 url_path='request-travel',
176 permission_classes=[IsAuthenticated]
177 )
178 def request_travel(self, request):
179 """
180 Endpoint POST /travel/request-travel/
181 Solicita una plaza en uno o varios viajes.
182 Recibe una lista de ids de los viajes que se quiere solicitar.
183 Devuelve un menssaje de exito o error
184 """
185 travel_ids = request.data.get('travel_ids', []) # La lista de viajes, deben ser los UUID de los viajes
186 user = request.user
188 logger.info(f"User {user.username} is requesting travel {travel_ids}")
190 try:
191 requests_created = TravelService.request_seat(user, travel_ids)
193 return Response({
194 "status": "ok",
195 "requests_created": requests_created,
196 "message": f"Se han creado {requests_created} solicitudes."
197 }, status=status.HTTP_200_OK)
199 except RequestOwnTravelError:
200 return Response({
201 "status": "error",
202 "message": "No puedes solicitar una plaza en tu propio viaje.",
203 "error_code": ErrorCodes.REQUEST_OWN_TRAVEL
204 }, status=status.HTTP_400_BAD_REQUEST)
205 except AlreadyRequestedError:
206 return Response({
207 "status": "error",
208 "message": "Ya has solicitado una plaza en alguno de uno estos viajes." if len(travel_ids) > 1 else "Ya has solicitado una plaza en este viaje.",
209 "error_code": ErrorCodes.ALREADY_REQUESTED
210 }, status=status.HTTP_400_BAD_REQUEST)
211 except Travel.DoesNotExist: # Si no existe el viaje, se devuelve un error
212 return Response({"status": "error", "message": "Viaje no encontrado", "error_code": ErrorCodes.TRAVEL_DONT_EXIST}, status=status.HTTP_404_NOT_FOUND)
213 except (Http404, APIException, DjangoValidationError):
214 raise
215 except Exception as e:
216 logger.error(f"Error in request_travel: {str(e)}")
217 return Response({"status": "error", "message": "Error interno del servidor, vuelve a intentarlo más tarde", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
220 @action(
221 detail=True,
222 methods=['patch'],
223 url_path='change_pickup_points',
224 permission_classes=[IsAuthenticated]
225 )
226 def change_pickup_points(self, request, *args, **kwargs):
227 """
228 Endpoint POST /travel/{id}/change_pickup_points/
229 Cambia los puntos de recogida del viaje
230 Recibe una lista de los puntos intermedios con su direccion, coordenadas, fecha y el orden en el viaje
231 Devuelve un mensaje de exito o error
232 """
233 travel = self.get_object()
235 logger.info(f"Request to change pickup points for travel {travel.id_travel} by user {request.user.username}")
237 # Solo el creador del viaje puede modificar
238 if travel.creation_user != request.user:
239 return Response({
240 "status": "error",
241 "message": "No tienes permiso para modificar este viaje.",
242 "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS
243 }, status=status.HTTP_403_FORBIDDEN)
245 pickup_points = request.data.get('pickup_points', []) # Lista de puntos de recogida
247 if not pickup_points:
248 return Response({
249 "status": "error",
250 "message": "Se requiere una lista de puntos de recogida.",
251 "error_code": ErrorCodes.MISSING_REQUIRED_FIELD
252 }, status=status.HTTP_400_BAD_REQUEST)
254 try:
255 TravelService.change_pickup_points(travel, pickup_points)
257 return Response({
258 "status": "ok",
259 "message": "Los puntos de recogida se han actualizado correctamente."
260 }, status=status.HTTP_200_OK)
262 except TravelIsNotPunctualError:
263 return Response({
264 "status": "error",
265 "message": "No se pueden modificar los puntos de recogida en un viaje periodico.",
266 "error_code": ErrorCodes.TRAVEL_IS_NOT_PUNCTUAL
267 }, status=status.HTTP_400_BAD_REQUEST)
268 except (Http404, APIException, DjangoValidationError):
269 raise
270 except Exception as e:
271 logger.error(f"Error changing pickup points for travel {travel.id_travel}: {str(e)}")
272 return Response({
273 "status": "error",
274 "message": "Error en el servidor, vuelve a intentarlo más tarde.",
275 "error_code": ErrorCodes.INTERNAL_SERVER_ERROR
276 }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
278 @action(
279 detail=True,
280 methods=['patch'],
281 url_path='edit',
282 permission_classes=[IsAuthenticated]
283 )
284 def edit(self, request, *args, **kwargs):
285 """
286 Endpoint POST /travel/{id}/edit/
287 Modifica los datos del viaje
288 Para cambiar de periódico a puntual, se requiere un parámetro 'delete_date' con la fecha a partir de la cual eliminar los viajes periódicos futuros.
289 Para cambiar de puntual a periódico, se requieren los parámetros 'interval_days' (días entre viajes) y 'end_date' (fecha final de periodicidad).
290 Devuelve un mensaje de exito o error
291 """
292 travel = self.get_object()
294 logger.info(f"Request to edit travel {travel.id_travel} by user {request.user.username}")
296 # Solo el creador del viaje puede modificar
297 if travel.creation_user != request.user:
298 return Response({
299 "status": "error",
300 "message": "No tienes permiso para modificar este viaje.",
301 "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS
302 }, status=status.HTTP_403_FORBIDDEN)
304 try:
305 TravelService.edit_travel(travel, request.data)
307 return Response({
308 "status": "ok",
309 "message": "La modificación se ha realizado correctamente."
310 }, status=status.HTTP_200_OK)
312 except PeriodicSeatsReductionError:
313 return Response({
314 "status": "error",
315 "message": "No se puede reducir el número de asientos en un viaje periódico.",
316 "error_code": ErrorCodes.TRAVEL_IS_NOT_PUNCTUAL
317 }, status=status.HTTP_400_BAD_REQUEST)
318 except PeriodicDeniedUsersError:
319 return Response({
320 "status": "error",
321 "message": "No se puede cambiar los usuarios denegados en un viaje periódico.",
322 "error_code": ErrorCodes.TRAVEL_IS_NOT_PUNCTUAL
323 }, status=status.HTTP_400_BAD_REQUEST)
324 except DateChangeWithOccupiedSeatsError:
325 return Response({
326 "status": "error",
327 "message": "No se puede cambiar la fecha del viaje porque ya hay plazas ocupadas.",
328 "error_code": ErrorCodes.SEATS_BELOW_OCCUPIED
329 }, status=status.HTTP_400_BAD_REQUEST)
330 except SeatsBelowOccupiedError as e:
331 return Response({
332 "status": "error",
333 "message": f"No se puede reducir el número de asientos a {e.seats} porque ya hay {e.occupied_seats} plazas ocupadas.",
334 "error_code": ErrorCodes.SEATS_BELOW_OCCUPIED
335 }, status=status.HTTP_400_BAD_REQUEST)
336 except VehicleNotFoundError:
337 return Response({
338 "status": "error",
339 "message": "Vehículo no encontrado.",
340 "error_code": ErrorCodes.VEHICLE_DONT_EXIST
341 }, status=status.HTTP_404_NOT_FOUND)
342 except SeriesVehicleSeatsInsufficientError:
343 return Response({
344 "status": "error",
345 "message": "No se puede cambiar a este vehículo en toda la serie periódica porque un viaje futuro tiene más asientos que el vehículo seleccionado.",
346 "error_code": ErrorCodes.VEHICLE_SEATS_INSUFFICIENT
347 }, status=status.HTTP_400_BAD_REQUEST)
348 except VehicleSeatsInsufficientError:
349 return Response({
350 "status": "error",
351 "message": "No se puede cambiar a este vehículo porque tiene menos asientos que el viaje actual.",
352 "error_code": ErrorCodes.VEHICLE_SEATS_INSUFFICIENT
353 }, status=status.HTTP_400_BAD_REQUEST)
354 except EndDateRequiredError:
355 return Response({
356 "status": "error",
357 "message": "Se requiere fecha de finalización de periodicidad para cambiar a periódico.",
358 "error_code": ErrorCodes.TO_DATE_REQUIRED
359 }, status=status.HTTP_400_BAD_REQUEST)
360 except InvalidPeriodicIntervalError:
361 return Response({
362 "status": "error",
363 "message": "El intervalo de periodicidad es requerido y debe ser un número entre 1 y 31.",
364 "error_code": ErrorCodes.INVALID_PERIODIC_INTERVAL
365 }, status=status.HTTP_400_BAD_REQUEST)
366 except (Http404, APIException, DjangoValidationError):
367 raise
368 except Exception as e:
369 logger.error(f"Error changing the type of travel: {str(e)}")
370 return Response({
371 "status": "error",
372 "message": "Error en el servidor, vuelve a intentarlo más tarde.",
373 "error_code": ErrorCodes.INTERNAL_SERVER_ERROR
374 }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
376 @action(
377 detail=True,
378 methods=['get'],
379 url_path='get_travel_details',
380 permission_classes=[IsAuthenticated]
381 )
382 def get_travel_details(self, request, *args, **kwargs):
383 """
384 Endpoint GET /travel/{id}/get_travel_details/
385 Obtiene los detalles de un viaje especificado por su id
386 Devuelve un mapa con los datos del viaje
387 """
388 try:
389 travel = Travel.objects.get(pk=self.kwargs['pk'])
391 logger.info(f"Requesting details for travel {travel.id_travel}")
392 want_travels = request.query_params.get("future_travels", "false").lower() == "true"
393 user = travel.creation_user
395 is_finished = travel.state.code == 'fnd'
397 try:
398 # Las valoraciones se obtienen mediante un stored procedure
399 with connection.cursor() as cursor:
400 cursor.execute("SELECT code, avg_score, num_ratings FROM public.getdriverratings(%s)", [str(user.id)])
401 results = cursor.fetchall()
403 next_travels = []
404 requested_travel_ids = set()
405 # Se sacan los puntos de recogida del viaje, ordenados por el orden establecido en el viaje
406 points = PickUpPoints.objects.filter(id_travel=travel, is_deleted=False).order_by('order_in_travel')
407 pickup_serializer = PickUpPointSerializer(points, many=True)
408 preferences = PreferencesSerializer(user.preferences.all(), many=True)
409 # Solo se deben sacar las solicitudes acepatadas (no borradas) o realizadas
410 travel_requests = RequestTravels.objects.filter(
411 Q(status__code='accepted', is_deleted=False) |
412 Q(status__code__in=['validated', 'unvalidated']),
413 id_travel=travel
414 ).select_related('user')
415 # Se saca tambien el nombre de usuario de los pasajeros que han sido aceptados
416 passengers_data = [
417 {
418 "username": req.user.username,
419 "profile_picture": req.user.profile_picture.name.split('/')[-1] if req.user.profile_picture else None
420 }
421 for req in travel_requests
422 ]
424 if want_travels: # Si se desea consultar tambien los proximos 5 viajes
425 id_origin = travel.id_origin_travel if travel.id_origin_travel else travel.id_travel
426 next_travels = Travel.objects.filter(id_origin_travel=id_origin, travel_date__gt=travel.travel_date, is_deleted=False).order_by('travel_date')[:5]
427 requested_travel_ids = set(
428 RequestTravels.objects.filter(
429 id_travel__in=next_travels,
430 user=request.user,
431 status__code__in=['accepted', 'pending', 'validated', 'unvalidated'],
432 is_deleted=False
433 ).values_list('id_travel', flat=True)
434 )
436 # Se muestra que tipos de usuarios denegados tiene el viaje asociados
437 users_denied = UsersDenied.objects.filter(id_travel=travel, is_deleted=False).select_related('user_type')
438 denied_user_types = [ud.user_type.code for ud in users_denied]
440 data = { # Se construye el mapa con los datos de los viajes
441 "ratings":{
442 "results": {
443 row[0]: float(row[1]) if row[1] is not None else None
444 for row in results
445 } if results else {},
446 "count": float(results[0][2]) if results and results[0][2] is not None else None,
447 },
448 "pickup_points": pickup_serializer.data,
449 "preferences": [pref["pref_type"] for pref in preferences.data],
450 "driver": {
451 "username": user.username,
452 "profile_picture": user.profile_picture.name.split('/')[-1] if user.profile_picture else None,
453 },
454 "passengers": passengers_data,
455 "is_requested" : RequestTravels.objects.filter(id_travel=travel.id_travel, user=request.user, status__code__in=['accepted', 'pending', 'validated', 'unvalidated'], is_deleted=False).exists(),
456 "denied_roles": denied_user_types,
457 "origin_lat": travel.origin_point.y if travel.origin_point else None,
458 "origin_lng": travel.origin_point.x if travel.origin_point else None,
459 "destination_lat": travel.destination_point.y if travel.destination_point else None,
460 "destination_lng": travel.destination_point.x if travel.destination_point else None,
461 "next_travels": {
462 str(t.id_travel): {
463 "id" : str(t.id_travel),
464 "travel_date": t.travel_date,
465 "remaining_seats": t.remaining_seats,
466 "is_requested": t.id_travel in requested_travel_ids,
467 } for t in next_travels
468 } if next_travels else {}
469 }
471 return Response(data, status=status.HTTP_200_OK)
473 except (Http404, APIException, DjangoValidationError):
474 raise
475 except Exception as e:
476 logger.error(f"Error getting travel details: {str(e)}")
477 return Response({"error": "Error retrieving travel details", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)
478 except Exception as e:
479 logger.error(f"Error in get_travel_details: {str(e)}")
480 return Response({"error": "Error retrieving travel details", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)
482 @action(
483 detail=True,
484 methods=['post'],
485 url_path='remove_passenger',
486 permission_classes=[IsAuthenticated]
487 )
488 def remove_passenger(self, request, *args, **kwargs):
489 """
490 Endpoint POST /travel/{id}/remove_passenger/
491 Elimina un pasajero de un viaje especificado por su nombre de usuario
492 Recibe el nombre de usuario del pasajero a eliminar y el id del usuario que hace la petición (debe ser el creador del viaje)
493 Devuelve un mensaje de exito o error
494 """
495 travel = self.get_object()
497 logger.info(f"Requesting to remove passenger from travel {travel.id_travel}")
499 user_id = request.data.get('user_id')
500 passenger_username = request.data.get('passenger')
501 user = travel.creation_user
503 # Los datos del id y el nombre de usuario son necesarios
504 if not user_id or not passenger_username: 504 ↛ 505line 504 didn't jump to line 505 because the condition on line 504 was never true
505 logger.error("Missing user_id or passenger username in request data")
506 return Response({
507 "status": "error",
508 "message": "Faltan parámetros necesarios.",
509 "error_code": ErrorCodes.MISSING_REQUIRED_FIELD
510 }, status=status.HTTP_400_BAD_REQUEST)
512 # Solo el creador del viaje puede eliminar pasajeros.
513 if travel.creation_user != request.user or str(user.id) != str(user_id):
514 logger.warning(f"User {request.user.username} tried to remove a passenger from travel {travel.id_travel} without being the creator")
515 return Response({
516 "status": "error",
517 "message": "No tienes permiso para eliminar pasajeros de este viaje.",
518 "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS
519 }, status=status.HTTP_403_FORBIDDEN)
521 try:
522 travel, passenger = TravelService.remove_passenger(travel, passenger_username)
524 # Se le notifica que el chat del viaje ha sido eliminado para ese pasajero
525 _notify_chat_removed(travel, passenger)
527 return Response({
528 "status": "ok",
529 "message": "Pasajero eliminado del viaje. Asiento liberado."
530 }, status=status.HTTP_200_OK)
532 except PassengerNotFoundError:
533 return Response({
534 "status": "error",
535 "message": "Pasajero no encontrado.",
536 "error_code": ErrorCodes.USER_DONT_EXIST
537 }, status=status.HTTP_400_BAD_REQUEST)
538 except PassengerNotInTravelError as e:
539 logger.warning(f"Passenger not in travel: {str(e)}")
540 return Response({
541 "status": "error",
542 "message": "El pasajero no tiene una solicitud aceptada en este viaje.",
543 "error_code": ErrorCodes.NOT_FOUND
544 }, status=status.HTTP_404_NOT_FOUND)
545 except (Http404, APIException, DjangoValidationError):
546 raise
547 except Exception as e:
548 logger.error(f"Error removing passenger from travel: {str(e)}")
549 return Response({"error": "Error removing passenger from travel", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)
551 @action(
552 detail=True,
553 methods=['post'],
554 url_path='validate_passenger',
555 permission_classes=[IsAuthenticated],
556 throttle_scope='validation_code'
557 )
558 def validate_passenger(self, request, *args, **kwargs):
559 """
560 Endpoint POST /travel/{id}/validate_passenger/
561 Valida un pasajero del viaje mediante su codigo de validacion.
562 Recibe el codigo del pasajero y actualiza su estado a 'validated'.
563 """
564 travel = self.get_object()
565 code = request.data.get('code', '').strip()
567 if not code:
568 return Response({
569 "status": "error",
570 "message": "Se requiere un código de validación.",
571 "error_code": ErrorCodes.MISSING_REQUIRED_FIELD
572 }, status=status.HTTP_400_BAD_REQUEST)
574 # Solo el creador del viaje puede validar pasajeros
575 if travel.creation_user != request.user:
576 return Response({
577 "status": "error",
578 "message": "No tienes permiso para validar pasajeros en este viaje.",
579 "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS
580 }, status=status.HTTP_403_FORBIDDEN)
582 try:
583 request_travel = TravelService.validate_passenger(travel, code)
585 return Response({
586 "status": "ok",
587 "message": f"Pasajero {request_travel.user.username} validado correctamente."
588 }, status=status.HTTP_200_OK)
590 except PassengerAlreadyValidatedError as e:
591 return Response({
592 "status": "error",
593 "message": f"Pasajero {e.username} ya ha sido validado.",
594 "error_code": ErrorCodes.PASSENGER_ALREADY_VALIDATED
595 }, status=status.HTTP_400_BAD_REQUEST)
596 except PassengerNotAcceptedError as e:
597 return Response({
598 "status": "error",
599 "message": f"Pasajero {e.username} no está en estado aceptado y no puede ser validado.",
600 "error_code": ErrorCodes.PASSENGER_NOT_ACCEPTED
601 }, status=status.HTTP_400_BAD_REQUEST)
602 except InvalidValidationCodeError:
603 return Response({
604 "status": "error",
605 "message": "Código de validación no válido o pasajero no encontrado.",
606 "error_code": ErrorCodes.INVALID_VALIDATION_CODE
607 }, status=status.HTTP_404_NOT_FOUND)
608 except (Http404, APIException, DjangoValidationError):
609 raise
610 except Exception as e:
611 logger.error(f"Error validating passenger: {str(e)}")
612 return Response({
613 "status": "error",
614 "message": "Error en el servidor, vuelve a intentarlo más tarde.",
615 "error_code": ErrorCodes.INTERNAL_SERVER_ERROR
616 }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
618 @action(
619 detail=True,
620 methods=['post'],
621 url_path='finish_travel',
622 permission_classes=[IsAuthenticated]
623 )
624 def finish_travel(self, request, *args, **kwargs):
625 """
626 Endpoint POST /travel/{id}/finish_travel/
627 Finaliza el viaje: cambia el estado a 'fnd' y marca como 'unvalidated'
628 a los pasajeros que no hayan sido validados durante el viaje.
629 """
630 travel = self.get_object()
632 # Solo el creador del viaje puede finalizarlo
633 if travel.creation_user != request.user:
634 return Response({
635 "status": "error",
636 "message": "No tienes permiso para finalizar este viaje.",
637 "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS
638 }, status=status.HTTP_403_FORBIDDEN)
640 try:
641 unvalidated_count = TravelService.finish_travel(travel)
643 return Response({
644 "status": "ok",
645 "message": f"Viaje finalizado. {unvalidated_count} pasajeros no validados."
646 }, status=status.HTTP_200_OK)
648 except (Http404, APIException, DjangoValidationError):
649 raise
650 except Exception as e:
651 logger.error(f"Error finishing travel: {str(e)}")
652 return Response({
653 "status": "error",
654 "message": "Error en el servidor, vuelve a intentarlo más tarde.",
655 "error_code": ErrorCodes.INTERNAL_SERVER_ERROR
656 }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
658 @action(
659 detail=True,
660 methods=['post'],
661 url_path='reached_pickup_point',
662 permission_classes=[IsAuthenticated]
663 )
664 def reached_pickup_point(self, request, *args, **kwargs):
665 """
666 Endpoint POST /travel/{id}/reached_pickup_point/
667 Marca un punto de recogida como pasado por el.
668 """
669 travel = self.get_object()
670 point_id = request.data.get('id_point')
672 # Solo el creador del viaje puede marcar los puntos de recogida
673 if travel.creation_user != request.user:
674 return Response({
675 "status": "error",
676 "message": "No tienes permiso para marcar puntos de recogida en este viaje.",
677 "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS
678 }, status=status.HTTP_403_FORBIDDEN)
680 try:
681 TravelService.reach_pickup_point(travel, point_id)
682 return Response({
683 "status": "ok",
684 "message": "Punto de recogida marcado como alcanzado."
685 }, status=status.HTTP_200_OK)
686 except PickUpPointNotFoundError:
687 return Response({
688 "status": "error",
689 "message": "Punto de recogida no encontrado.",
690 "error_code": ErrorCodes.PICKUP_POINT_NOT_FOUND
691 }, status=status.HTTP_400_BAD_REQUEST)
693# Endpoints para gestionar las solicitudes de los viajes
694class RequestTravelsViewSet(OwnedQuerysetMixin, SoftDeleteQuerysetMixin, viewsets.ModelViewSet):
695 queryset = RequestTravels.objects.all()
696 serializer_class = RequestTravelsSerializer
697 permission_classes = [IsAuthenticated]
698 # Representa al dueño del viaje y al que ha solicitado la plaza (que ya ha sido aprobada)
699 owner_field = ('user', 'id_travel__creation_user')
701 # Como no se utilizan put ni post, no se añaden
702 http_method_names = ['get', 'patch', 'delete', 'head', 'options']
704 def destroy(self, request, *args, **kwargs):
705 """
706 Endpoint DELETE /requesttravels/{id}/
707 Elimina una solicitud de viaje especificada por su id
708 """
709 try:
710 with transaction.atomic():
711 instance = self.get_object()
712 # Solo el usuario que hizo la solicitud o el creador del viaje puede eliminarla
713 if instance.user != request.user and instance.id_travel.creation_user != request.user: 713 ↛ 714line 713 didn't jump to line 714 because the condition on line 713 was never true
714 return Response({
715 "status": "error",
716 "message": "No tienes permiso para eliminar esta solicitud.",
717 "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS
718 }, status=status.HTTP_403_FORBIDDEN)
720 # El servicio libera el asiento y avisa al conductor
721 TravelService.cancel_request(instance)
723 self.perform_destroy(instance)
724 return Response({
725 "status": "ok",
726 "message": "Solicitud eliminada correctamente."
727 }, status=status.HTTP_200_OK)
728 except (Http404, APIException, DjangoValidationError):
729 raise
730 except Exception as e:
731 logger.error(f"Error deleting request travel: {str(e)}")
732 return Response({
733 "status": "error",
734 "message": "Error en el servidor, vuelve a intentarlo más tarde.",
735 "error_code": ErrorCodes.INTERNAL_SERVER_ERROR
736 }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
738 # Se sobreescribe el metodo PATCH para que se pueda gestionar bien la aprobacion y rechazo de las solicitudes
739 def partial_update(self, request, *args, **kwargs):
740 instance = self.get_object()
741 new_status_code = request.data.get('status')
743 if new_status_code and request.user != instance.id_travel.creation_user:
744 logger.warning(f"User {request.user.username} tried to change status of request {instance.id} without proper credentials.")
745 return Response({
746 "status": "error",
747 "message": "No tienes permiso para cambiar el estado de esta solicitud.",
748 "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS
749 }, status=status.HTTP_403_FORBIDDEN)
751 if new_status_code: # Si se ha cambiado el estado de la solicitud
752 try:
753 TravelService.change_request_status(instance, new_status_code)
755 return Response({
756 "status": "ok",
757 "message": f"Solicitud marcada como {new_status_code}"
758 }, status=status.HTTP_200_OK)
760 except SeatsFullError:
761 return Response({
762 "status": "error",
763 "message": "El viaje ya está completo. No puedes aceptar más pasajeros.",
764 "error_code": ErrorCodes.TRAVEL_IS_FULL
765 }, status=status.HTTP_400_BAD_REQUEST)
766 except InvalidRequestStatusError:
767 return Response({"status": "error", "message": "Código de estado inválido.", "error_code": ErrorCodes.INVALID_REQUEST_STATUS}, status=status.HTTP_400_BAD_REQUEST)
768 except StatusNotRequestableError as e:
769 # El cambio a ese estado debe hacerse por el metodo correcto
770 message = f"El estado -{e.status_code}- no se puede asignar desde aquí."
771 if e.gateway:
772 message += f" Se consigue {e.gateway}."
773 return Response({
774 "status": "error",
775 "message": message,
776 "error_code": ErrorCodes.STATUS_NOT_REQUESTABLE
777 }, status=status.HTTP_400_BAD_REQUEST)
778 except InvalidStatusTransitionError as e:
779 # El paso al esatdo solicitado es imposible de hacer
780 return Response({
781 "status": "error",
782 "message": f"No se puede pasar de -{e.current_status}- a -{e.new_status}-.",
783 "error_code": ErrorCodes.INVALID_STATUS_TRANSITION
784 }, status=status.HTTP_400_BAD_REQUEST)
785 except (Http404, APIException, DjangoValidationError):
786 raise
787 except Exception as e:
788 logger.error(f"Error: {str(e)}")
789 return Response({"status": "error", "message": str(e), "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
790 return super().partial_update(request, *args, **kwargs)
792# Base comun de los endpoints de lo que depende de un viaje
793class TravelChildViewSet(OwnedQuerysetMixin, SoftDeleteQuerysetMixin, viewsets.ModelViewSet):
794 """
795 Base de los ViewSets de lo que depende de un viaje: puntos de recogida y tipos
796 de usuario denegados. Solo el creador del viaje puede tocarlos.
797 """
798 permission_classes = [IsAuthenticated]
799 owner_field = 'id_travel__creation_user'
801 # Respuesta comun cuando se intenta tocar un viaje ajeno
802 def _not_owner_response(self):
803 return Response({
804 "status": "error",
805 "message": "No tienes permiso para modificar los datos de este viaje.",
806 "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS
807 }, status=status.HTTP_403_FORBIDDEN)
809 # Comprueba la propiedad de los viajes referenciados en el cuerpo ya validado
810 def _check_travels(self, serializer, is_many=False):
811 rows = serializer.validated_data if is_many else [serializer.validated_data]
812 TravelService.assert_travels_belong_to(
813 self.request.user, [row.get('id_travel') for row in rows]
814 )
816 # Se sobreescribe el POST para permitir la creacion de varios de golpe
817 def create(self, request, *args, **kwargs):
819 # Se comprueba si se ha pasado una lista o un solo objeto para crear varios o solo uno
820 is_many = isinstance(request.data, list)
822 serializer = self.get_serializer(data=request.data, many=is_many)
824 # Se valida primero
825 serializer.is_valid(raise_exception=True)
827 try:
828 self._check_travels(serializer, is_many)
829 except NotTravelOwnerError:
830 return self._not_owner_response()
832 self.perform_create(serializer)
834 headers = self.get_success_headers(serializer.data)
835 return Response(
836 serializer.data,
837 status=status.HTTP_201_CREATED,
838 headers=headers
839 )
841 # Se sobreescribe el update para que no se pueda mover una fila propia a un viaje ajeno
842 def update(self, request, *args, **kwargs):
843 partial = kwargs.pop('partial', False)
844 instance = self.get_object()
846 serializer = self.get_serializer(instance, data=request.data, partial=partial)
847 serializer.is_valid(raise_exception=True)
849 try:
850 self._check_travels(serializer)
851 except NotTravelOwnerError:
852 return self._not_owner_response()
854 self.perform_update(serializer)
855 return Response(serializer.data)
858# Endpoint para gestionar los puntos de recogida de los viajes
859class PickUpPointsViewSet(TravelChildViewSet):
860 queryset = PickUpPoints.objects.all()
861 serializer_class = PickUpPointSerializer
863# Endpoint para gestionar los usuarios denegados en los viajes
864class UsersDeniedViewSet(TravelChildViewSet):
865 queryset = UsersDenied.objects.all()
866 serializer_class = UsersDeniedSerializer