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

1import logging 

2 

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 

11 

12from django.core.paginator import Paginator 

13from django.db import connection, transaction 

14from django.db.models import Q 

15 

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 

20 

21from channels.layers import get_channel_layer 

22from asgiref.sync import async_to_sync 

23from chats.models import Chat 

24 

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) 

36 

37# Logger para ir almacenando los logs 

38logger = logging.getLogger(__name__) 

39 

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)}") 

58 

59# Endpoints de los viajes 

60class TravelViewSet(SoftDeleteQuerysetMixin, viewsets.ModelViewSet): 

61 queryset = Travel.objects.all() 

62 serializer_class = TravelSerializer 

63 permission_classes = [IsAuthenticated] 

64 

65 # PUT no se utiliza, por lo que no se publica 

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

67 

68 # Sin ambito por defecto, solo se limita la accion que declara el suyo 

69 throttle_scope = None 

70 

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

74 

75 logger.info(f"Request to update travel {travel.id_travel} by user {request.user.username}") 

76 

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) 

84 

85 started_state = TravelStates.objects.get(code='started') 

86 

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) 

93 

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

95 

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

103 

104 logger.info(f"Request to delete travel {travel.id_travel} by user {request.user.username}") 

105 

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) 

113 

114 try: 

115 with transaction.atomic(): 

116 # El servicio avisa a los solicitantes y borra sus solicitudes 

117 TravelService.delete_travel(travel) 

118 

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) 

130 

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 

144 

145 # Paginacion 

146 page_number = int(data.get('page', 1)) 

147 page_size = 10 

148 

149 try: 

150 # Todo el filtrado y la ordenacion los construye el servicio 

151 travels = TravelService.search_travels(self.get_queryset(), request.user, data) 

152 

153 # Se paginan los resultados 

154 paginator = Paginator(travels, page_size) 

155 page_obj = paginator.get_page(page_number) 

156 

157 # Serializamos solo los viajes de esta página 

158 serializer = self.get_serializer(page_obj, many=True) 

159 

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

165 

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) 

171 

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 

187 

188 logger.info(f"User {user.username} is requesting travel {travel_ids}") 

189 

190 try: 

191 requests_created = TravelService.request_seat(user, travel_ids) 

192 

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) 

198 

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) 

218 

219 

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

234 

235 logger.info(f"Request to change pickup points for travel {travel.id_travel} by user {request.user.username}") 

236 

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) 

244 

245 pickup_points = request.data.get('pickup_points', []) # Lista de puntos de recogida 

246 

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) 

253 

254 try: 

255 TravelService.change_pickup_points(travel, pickup_points) 

256 

257 return Response({ 

258 "status": "ok", 

259 "message": "Los puntos de recogida se han actualizado correctamente." 

260 }, status=status.HTTP_200_OK) 

261 

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) 

277 

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

293 

294 logger.info(f"Request to edit travel {travel.id_travel} by user {request.user.username}") 

295 

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) 

303 

304 try: 

305 TravelService.edit_travel(travel, request.data) 

306 

307 return Response({ 

308 "status": "ok", 

309 "message": "La modificación se ha realizado correctamente." 

310 }, status=status.HTTP_200_OK) 

311 

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) 

375 

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

390 

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 

394 

395 is_finished = travel.state.code == 'fnd' 

396 

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

402 

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 ] 

423 

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 ) 

435 

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] 

439 

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 } 

470 

471 return Response(data, status=status.HTTP_200_OK) 

472 

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) 

481 

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

496 

497 logger.info(f"Requesting to remove passenger from travel {travel.id_travel}") 

498 

499 user_id = request.data.get('user_id') 

500 passenger_username = request.data.get('passenger') 

501 user = travel.creation_user 

502 

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) 

511 

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) 

520 

521 try: 

522 travel, passenger = TravelService.remove_passenger(travel, passenger_username) 

523 

524 # Se le notifica que el chat del viaje ha sido eliminado para ese pasajero 

525 _notify_chat_removed(travel, passenger) 

526 

527 return Response({ 

528 "status": "ok", 

529 "message": "Pasajero eliminado del viaje. Asiento liberado." 

530 }, status=status.HTTP_200_OK) 

531 

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) 

550 

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

566 

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) 

573 

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) 

581 

582 try: 

583 request_travel = TravelService.validate_passenger(travel, code) 

584 

585 return Response({ 

586 "status": "ok", 

587 "message": f"Pasajero {request_travel.user.username} validado correctamente." 

588 }, status=status.HTTP_200_OK) 

589 

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) 

617 

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

631 

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) 

639 

640 try: 

641 unvalidated_count = TravelService.finish_travel(travel) 

642 

643 return Response({ 

644 "status": "ok", 

645 "message": f"Viaje finalizado. {unvalidated_count} pasajeros no validados." 

646 }, status=status.HTTP_200_OK) 

647 

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) 

657 

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

671 

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) 

679 

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) 

692 

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

700 

701 # Como no se utilizan put ni post, no se añaden 

702 http_method_names = ['get', 'patch', 'delete', 'head', 'options'] 

703 

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) 

719 

720 # El servicio libera el asiento y avisa al conductor 

721 TravelService.cancel_request(instance) 

722 

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) 

737 

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

742 

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) 

750 

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) 

754 

755 return Response({ 

756 "status": "ok", 

757 "message": f"Solicitud marcada como {new_status_code}" 

758 }, status=status.HTTP_200_OK) 

759 

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) 

791 

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' 

800 

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) 

808 

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 ) 

815 

816 # Se sobreescribe el POST para permitir la creacion de varios de golpe 

817 def create(self, request, *args, **kwargs): 

818 

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) 

821 

822 serializer = self.get_serializer(data=request.data, many=is_many) 

823 

824 # Se valida primero 

825 serializer.is_valid(raise_exception=True) 

826 

827 try: 

828 self._check_travels(serializer, is_many) 

829 except NotTravelOwnerError: 

830 return self._not_owner_response() 

831 

832 self.perform_create(serializer) 

833 

834 headers = self.get_success_headers(serializer.data) 

835 return Response( 

836 serializer.data, 

837 status=status.HTTP_201_CREATED, 

838 headers=headers 

839 ) 

840 

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

845 

846 serializer = self.get_serializer(instance, data=request.data, partial=partial) 

847 serializer.is_valid(raise_exception=True) 

848 

849 try: 

850 self._check_travels(serializer) 

851 except NotTravelOwnerError: 

852 return self._not_owner_response() 

853 

854 self.perform_update(serializer) 

855 return Response(serializer.data) 

856 

857 

858# Endpoint para gestionar los puntos de recogida de los viajes 

859class PickUpPointsViewSet(TravelChildViewSet): 

860 queryset = PickUpPoints.objects.all() 

861 serializer_class = PickUpPointSerializer 

862 

863# Endpoint para gestionar los usuarios denegados en los viajes 

864class UsersDeniedViewSet(TravelChildViewSet): 

865 queryset = UsersDenied.objects.all() 

866 serializer_class = UsersDeniedSerializer