Coverage for api\views\user_views.py: 93.8%

370 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-30 20:58 +0200

1import logging 

2from django.core.exceptions import ValidationError as DjangoValidationError 

3from django.http import Http404 

4from drf_spectacular.utils import extend_schema 

5from rest_framework import status, viewsets 

6from rest_framework.decorators import action 

7from rest_framework.parsers import FormParser, MultiPartParser 

8from rest_framework.exceptions import APIException, MethodNotAllowed 

9from rest_framework.permissions import IsAuthenticated 

10from rest_framework.response import Response 

11from api.errors import ErrorCodes 

12from api.serializers.travel_serializer import RequestTravelsSerializer, TravelSerializer 

13from api.serializers.user_serializer import (NotificationsSerializer, PreferencesSerializer, 

14 ProfilePicSerializer, UserSerializer, VehicleSerializer) 

15from api.ownership import OwnedQuerysetMixin 

16from api.soft_delete import SoftDeleteQuerysetMixin 

17from users.models import Users 

18from users.services.auth_service import EMAIL_DOMAIN_ERROR_MESSAGE, AuthService 

19from users.services.exceptions import (EmailAlreadyRegisteredError,EmailDeliveryError,EmailDomainNotAllowedError, 

20 IncorrectPasswordError,InvalidTokenError,NoUpcomingTravelsError,RequestStatusMissingError, 

21 RequestTravelNotValidatedError,TokenExpiredError,UserNotFoundError) 

22from users.services.user_service import UserService 

23 

24# Logger para almacenar los logs 

25logger = logging.getLogger(__name__) 

26 

27 

28# Endpoints de la cuenta del propio usuario. 

29class UsersViewSet(OwnedQuerysetMixin, SoftDeleteQuerysetMixin, viewsets.ModelViewSet): 

30 queryset = Users.objects.all() 

31 serializer_class = UserSerializer 

32 permission_classes = [IsAuthenticated] 

33 # EL dueño del registro es el propio registro 

34 owner_field = 'id' 

35 

36 lookup_field = 'id' 

37 lookup_url_kwarg = 'id' 

38 lookup_value_regex = '[0-9a-f-]{36}' 

39 

40 # No se utiliza put, por lo que no se pone 

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

42 

43 # Sin ambito por defecto 

44 throttle_scope = None 

45 

46 @extend_schema(exclude=True) 

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

48 """ 

49 POST /api/v1/users/ no existe. El alta es con POST /api/v1/register/. 

50 Este endpoint lo genera automaticamente Django, por lo que se cierra 

51 """ 

52 raise MethodNotAllowed('POST') 

53 

54 # Se sobreescriben los metodos list y retrieve para añadir logs y manejo de errores 

55 def list(self, request, *args, **kwargs): 

56 logger.info("Accessed UsersViewSet list endpoint") 

57 try: 

58 response = super().list(request, *args, **kwargs) 

59 logger.info("Successfully retrieved user list") 

60 return response 

61 except (Http404, APIException, DjangoValidationError): 

62 raise 

63 except Exception as e: 

64 logger.error(f"Error in UsersViewSet list endpoint: {str(e)}") 

65 return Response({"error": "An error occurred while retrieving the user list", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 

66 

67 def retrieve(self, request, *args, **kwargs): 

68 logger.info(f"Accessed UsersViewSet retrieve endpoint for user ID: {kwargs.get('id')}") 

69 try: 

70 response = super().retrieve(request, *args, **kwargs) 

71 logger.info(f"Successfully retrieved user ID: {kwargs.get('id')}") 

72 return response 

73 except Http404: 

74 raise 

75 except Exception as e: 

76 logger.error(f"Error in UsersViewSet retrieve endpoint for user ID {kwargs.get('id')}: {str(e)}") 

77 return Response({"error": "An error occurred while retrieving the user", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 

78 

79 @action( 

80 detail=True, 

81 methods=['patch'], 

82 url_path='upload-profile_picture', 

83 parser_classes=[MultiPartParser, FormParser], 

84 permission_classes=[IsAuthenticated] 

85 ) 

86 def upload_profile_pic(self, request, *args, **kwargs): 

87 """ 

88 Endpoint PATCH /users/{id}/upload-profile_picture/ 

89 Permite subir la imagen de perfil de un usuario 

90 Recibe una imagen 

91 Devuelve un mensaje de error o exito y el nombre del archivo subido 

92 """ 

93 user = self.get_object() 

94 logger.info(f"Accessed upload_profile_pic endpoint for user ID: {user.id}") 

95 

96 # Es necesario que se añada el campo de profile picture 

97 if 'profile_picture' not in request.data: 

98 logger.warning("profile_picture field is missing in the request") 

99 return Response( 

100 {"error": "profile_picture field is required", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, 

101 status=status.HTTP_400_BAD_REQUEST 

102 ) 

103 

104 try: 

105 # Se carga y guarda la imagen de perfil 

106 profile_picture = request.data['profile_picture'] 

107 serializer = ProfilePicSerializer(data={"profile_picture": profile_picture}) 

108 serializer.is_valid(raise_exception=True) 

109 

110 name = UserService.update_profile_picture(user, profile_picture) 

111 return Response({"message": "Profile picture updated successfully", "name": name}, status=status.HTTP_200_OK) 

112 except (Http404, APIException, DjangoValidationError): 

113 raise 

114 except Exception as e: 

115 logger.error(f"Error in upload_profile_pic endpoint for user ID {user.id}: {str(e)}") 

116 return Response({"error": "An error occurred while uploading the profile picture", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 

117 

118 @action( 

119 detail=True, 

120 methods=['delete'], 

121 url_path='delete-profile_picture', 

122 parser_classes=[MultiPartParser, FormParser], 

123 permission_classes=[IsAuthenticated] 

124 ) 

125 def delete_profile_pic(self, request, *args, **kwargs): 

126 """ 

127 Endpoint DELETE /users/{id}/delete-profile_picture/ 

128 Permite borrar la imagen de perfil de un usuario 

129 Devuelve un mensaje de error o exito 

130 """ 

131 user = self.get_object() 

132 

133 # Si no habia imagen de perfil, se debe devolver un mensaje de exito, ya que esta lo que queria, eliminar la imagen 

134 if not UserService.delete_profile_picture(user): 

135 return Response({"message": "No profile picture to delete"}, status=status.HTTP_200_OK) 

136 

137 return Response({ 

138 "message": "Profile picture deleted successfully", 

139 }, status=status.HTTP_200_OK) 

140 

141 @action( 

142 detail=True, 

143 methods=['get'], 

144 url_path='vehicles', 

145 permission_classes=[IsAuthenticated] 

146 ) 

147 def get_vehicles(self, request, *args, **kwargs): 

148 """ 

149 Endpoint GET /users/{id}/vehicles/ 

150 Devuelve todos los vehículos activos del usuario paginados 

151 """ 

152 user = self.get_object() 

153 logger.info(f"Accessed get_vehicles endpoint for user ID: {user.id}") 

154 

155 try: 

156 return self.paginated_response( 

157 request, UserService.list_vehicles(user), VehicleSerializer, 

158 f"Returned vehicles for user ID: {user.id}" 

159 ) 

160 except (Http404, APIException, DjangoValidationError): 

161 raise 

162 except Exception as e: 

163 logger.error(f"Error in get_vehicles: {str(e)}") 

164 return Response({"error": "Error retrieving vehicles", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500) 

165 

166 @action( 

167 detail=True, 

168 methods=['get'], 

169 url_path='notifications', 

170 permission_classes=[IsAuthenticated] 

171 ) 

172 def get_notifications(self, request, *args, **kwargs): 

173 """ 

174 Endpoint GET /users/{id}/notifications/?ordering=desc|asc 

175 Devuelve todas las notificaciones no borradas del usuario paginadas, ordenadas por fecha 

176 """ 

177 user = self.get_object() 

178 logger.info(f"Accessed get_notifications endpoint for user ID: {user.id}") 

179 

180 try: 

181 ordering = request.query_params.get('ordering', 'desc') 

182 return self.paginated_response( 

183 request, UserService.list_notifications(user, ordering), NotificationsSerializer, 

184 f"Returned notifications for user ID: {user.id}" 

185 ) 

186 except (Http404, APIException, DjangoValidationError): 

187 raise 

188 except Exception as e: 

189 logger.error(f"Error in get_notifications: {str(e)}") 

190 return Response({"error": "Error retrieving notifications", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500) 

191 

192 @action( 

193 detail=True, 

194 methods=['get'], 

195 url_path='notifications/unread-count', 

196 permission_classes=[IsAuthenticated] 

197 ) 

198 def get_unread_notifications_count(self, request, *args, **kwargs): 

199 """ 

200 Endpoint GET /users/{id}/notifications/unread-count/ 

201 Devuelve el numero de notificaciones no leidas del usuario 

202 """ 

203 user = self.get_object() 

204 logger.info(f"Accessed get_unread_notifications_count endpoint for user ID: {user.id}") 

205 

206 try: 

207 return Response({"count": UserService.count_unread_notifications(user)}) 

208 except (Http404, APIException, DjangoValidationError): 

209 raise 

210 except Exception as e: 

211 logger.error(f"Error in get_unread_notifications_count: {str(e)}") 

212 return Response({"error": "Error retrieving unread notifications count", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500) 

213 

214 @action( 

215 detail=True, 

216 methods=['get'], 

217 url_path='driverratings', 

218 permission_classes=[IsAuthenticated] 

219 ) 

220 def get_driverratings(self, request, *args, **kwargs): 

221 """ 

222 Endpoint GET /users/{id}/driverratings/ 

223 Devuelve la valoración media de cada criterio para un conductor, junto con el número total de valoraciones recibidas 

224 """ 

225 user = self.get_object() 

226 logger.info(f"Accessed get_driverratings endpoint for user ID: {user.id}") 

227 

228 try: 

229 return Response(UserService.get_driver_ratings(user), status=200) 

230 except (Http404, APIException, DjangoValidationError): 

231 raise 

232 except Exception as e: 

233 logger.error(f"Error in get_driverratings: {str(e)}") 

234 return Response({"error": "Error retrieving driverratings", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500) 

235 

236 @action( 

237 detail=True, 

238 methods=['get'], 

239 url_path='preferences', 

240 permission_classes=[IsAuthenticated] 

241 ) 

242 def get_preferences(self, request, *args, **kwargs): 

243 """ 

244 Endpoint GET /users/{id}/preferences/ 

245 Devuelve todas las preferencias no borradas del usuario paginadas 

246 """ 

247 user = self.get_object() 

248 logger.info(f"Accessed get_preferences endpoint for user ID: {user.id}") 

249 

250 try: 

251 return self.paginated_response( 

252 request, UserService.list_preferences(user), PreferencesSerializer, 

253 f"Returned preferences for user ID: {user.id}" 

254 ) 

255 except (Http404, APIException, DjangoValidationError): 

256 raise 

257 except Exception as e: 

258 logger.error(f"Error in get_preferences: {str(e)}") 

259 return Response({"error": "Error retrieving preferences", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500) 

260 

261 @action( 

262 detail=True, 

263 methods=['patch'], 

264 url_path='update-preferences', 

265 permission_classes=[IsAuthenticated] 

266 ) 

267 def update_preferences(self, request, *args, **kwargs): 

268 """ 

269 Endpoint PATCH /users/{id}/update-preferences/ 

270 Se encarga de actualizar las preferencias del usuario 

271 Recibe una lista de nombres de preferencias: {"preferences": ["MUSIC", "SMOKING"...]} 

272 Devuelve un mensaje de exito o error 

273 """ 

274 user = self.get_object() 

275 # Se sacan los codigos de las preferencias para buscarlas en bbdd 

276 pref_codes = request.data.get('preferences', []) 

277 

278 logger.info(f"Accessed update_preferences endpoint for user ID: {user.id} with preferences: {pref_codes}") 

279 try: 

280 UserService.update_preferences(user, pref_codes) 

281 return Response({"message": "Preferencias actualizadas"}, status=200) 

282 

283 except (Http404, APIException, DjangoValidationError): 

284 raise 

285 except Exception as e: 

286 logger.error(f"Error: {str(e)}") 

287 return Response({"error": "Error al actualizar preferencias", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500) 

288 

289 @action( 

290 detail=True, 

291 methods=['delete'], 

292 url_path='clear-preferences', 

293 permission_classes=[IsAuthenticated] 

294 ) 

295 def clear_preferences(self, request, *args, **kwargs): 

296 """ 

297 Endpoint DELETE /users/{id}/clear-preferences/ 

298 Elimina todas las preferencias asociadas al usuario 

299 Devuelve un mensaje de exito o error 

300 """ 

301 user = self.get_object() 

302 logger.info(f"Accessed clear_preferences endpoint for user ID: {user.id}") 

303 UserService.clear_preferences(user) 

304 return Response({"message": "All preferences cleared"}, status=status.HTTP_200_OK) 

305 

306 @action( 

307 detail=True, 

308 methods=['get'], 

309 url_path='travel', 

310 permission_classes=[IsAuthenticated] 

311 ) 

312 def get_travels(self, request, *args, **kwargs): 

313 """ 

314 Endpoint GET /users/{id}/travel/?type=pending|past 

315 Devuelve los viajes creados por el usuario filtrando pasados o futuros segun el parametro type 

316 """ 

317 user = self.get_object() 

318 

319 # Si no se especifica, se devuelven todos 

320 travel_type = request.query_params.get('type', 'all') 

321 

322 logger.info(f"Accessed get_travels endpoint for user ID: {user.id} with param: {travel_type}") 

323 

324 try: 

325 qs = UserService.list_created_travels(user, travel_type) 

326 logger.info(f"Successfully retrieved travels for user ID: {user.id} with type: {travel_type}") 

327 return self.paginated_response( 

328 request, qs, TravelSerializer, 

329 f"Returned {travel_type} travels for user ID: {user.id}" 

330 ) 

331 

332 except (Http404, APIException, DjangoValidationError): 

333 raise 

334 except Exception as e: 

335 logger.error(f"Error in get_travels: {str(e)}") 

336 return Response({"error": "Error retrieving travels", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500) 

337 

338 @action( 

339 detail=True, 

340 methods=['get'], 

341 url_path='my-requests', 

342 permission_classes=[IsAuthenticated] 

343 ) 

344 def my_requests(self, request, *args, **kwargs): 

345 """ 

346 Endpoint GET /users/{id}/my-requests/?type=active|past|pending 

347 Devuelve los RequestTravels solicitados por el usuario, filtrando por el estado de la solicitud, futuro, pasado o pendiente de aceptar 

348 """ 

349 user = self.get_object() 

350 request_type = request.query_params.get('type', 'all') 

351 

352 logger.info(f"Accessed my_requests for user ID: {user.id} with type: {request_type}") 

353 

354 try: 

355 qs = UserService.list_own_requests(user, request_type) 

356 logger.info(f"Successfully retrieved {request_type} requests for user ID: {user.id}") 

357 

358 return self.paginated_response( 

359 request, qs, RequestTravelsSerializer, 

360 f"Returned {request_type} requests for user ID: {user.id}" 

361 ) 

362 

363 except (Http404, APIException, DjangoValidationError): 

364 raise 

365 except Exception as e: 

366 logger.error(f"Error in my_requests: {str(e)}") 

367 return Response({"error": "Error retrieving your requests", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500) 

368 

369 @action( 

370 detail=True, 

371 methods=['get'], 

372 url_path='received-requests', 

373 permission_classes=[IsAuthenticated] 

374 ) 

375 def received_requests(self, request, *args, **kwargs): 

376 """ 

377 Endpoint GET /users/{id}/received-requests/ 

378 Devuelve los RequestTravels pendientes de aprobar que ha recibido el usuario 

379 """ 

380 user = self.get_object() 

381 

382 logger.info(f"Accessed received_requests for user ID: {user.id}") 

383 

384 try: 

385 return self.paginated_response( 

386 request, UserService.list_received_requests(user), RequestTravelsSerializer, 

387 f"Returned pending received requests for user ID: {user.id}" 

388 ) 

389 

390 except (Http404, APIException, DjangoValidationError): 

391 raise 

392 except Exception as e: 

393 logger.error(f"Error in received_requests: {str(e)}") 

394 return Response({"error": "Error retrieving received requests", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500) 

395 

396 # Funcion para paginar las respuestas de los endpoints 

397 def paginated_response(self, request, queryset, serializer_class, log_msg): 

398 context = self.get_serializer_context() 

399 

400 page = self.paginate_queryset(queryset) 

401 if page is not None: 401 ↛ 406line 401 didn't jump to line 406 because the condition on line 401 was always true

402 serializer = serializer_class(page, many=True, context=context) 

403 logger.info(log_msg + f" (page {request.query_params.get('page', 1)})") 

404 return self.get_paginated_response(serializer.data) 

405 

406 serializer = serializer_class(queryset, many=True, context=context) 

407 return Response(serializer.data) 

408 

409 @action( 

410 detail=False, 

411 methods=['post'], 

412 url_path='verification_email', 

413 permission_classes=[], 

414 throttle_scope='verification_email' 

415 ) 

416 def get_verification_email(self, request, *args, **kwargs): 

417 """ 

418 Endpoint POST /users/verification_email/ 

419 Envia un mensaje de confirmacion al email del usuario especificado 

420 Si se envia además el email, envia el correo de verificacion a ese email, 

421 pero solo si quien llama esta autenticado y es el dueño de la cuenta 

422 El nombre de usuario es obligatorio 

423 """ 

424 

425 username = request.data.get("username") 

426 email = request.data.get("email") 

427 

428 logger.info(f"Accessed get_verification_email endpoint for username: {username}") 

429 

430 # El nombre de usuario es obligatorio 

431 if not username: 431 ↛ 432line 431 didn't jump to line 432 because the condition on line 431 was never true

432 return Response({"error": "username query param is required", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST) 

433 

434 try: 

435 # El usuario debe existir 

436 user = UserService.find_by_username_or_email(username) 

437 except UserNotFoundError: 

438 return Response({"error": "User not found", "error_code": ErrorCodes.USER_DONT_EXIST}, status=status.HTTP_404_NOT_FOUND) 

439 

440 # El destino del codigo no lo elige quien llama. 

441 # Se admite una direccion distinta a la de la cuenta unicamente cuando el que la pide esta autenticado 

442 # y es el dueño de esa cuenta, que es el flujo de cambio de correo desde el perfil 

443 if email and not (request.user.is_authenticated and request.user == user): 

444 logger.warning(f"Rejected verification email for username {username}: the caller is not the owner of the account") 

445 return Response({"error": "Authentication as the owner is required to choose the destination address", "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS}, status=status.HTTP_403_FORBIDDEN) 

446 

447 if email: 

448 try: 

449 AuthService.assert_email_domain_is_allowed(email) 

450 except EmailDomainNotAllowedError: 

451 return Response({"error": EMAIL_DOMAIN_ERROR_MESSAGE, "error_code": ErrorCodes.EMAIL_DOMAIN_NOT_ALLOWED}, status=status.HTTP_400_BAD_REQUEST) 

452 

453 try: 

454 AuthService.send_verification_code(user, to_email=email) 

455 return Response({"status": "OK"}, status=status.HTTP_200_OK) 

456 

457 except EmailDeliveryError: 

458 return Response({"error": "Error sending verification email", "error_code": ErrorCodes.EMAIL_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 

459 except (Http404, APIException, DjangoValidationError): 

460 raise 

461 except Exception as e: 

462 logger.error(f"Error in get_verification_email: {str(e)}") 

463 return Response({"error": "Error sending verification email", "error_code": ErrorCodes.EMAIL_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 

464 

465 @action( 

466 detail=False, 

467 methods=['post'], 

468 url_path='verify_code', 

469 permission_classes=[], 

470 throttle_scope='verify_code' 

471 ) 

472 def post_verify_code(self, request, *args, **kwargs): 

473 """ 

474 Endpoint POST /users/verify_code/ 

475 Verifica el token del usuario 

476 Si se pasa el email, se cambia el correo del usuario a ese (si el token es correcto) 

477 Si se pasa la contraseña, se cambia la contraseña del usuario a esa (si el token es correcto) 

478 Si se pasa el parametro validate con valor true, se marca el usuario como verificado (si el token es correcto) 

479 El nombre de usuario y token son obligatorios 

480 """ 

481 username = request.data.get("username") 

482 token = request.data.get("token") 

483 email = request.data.get("email") 

484 password = request.data.get("password") 

485 validate = request.data.get("validate", False) 

486 

487 logger.info(f"Accessed post_verify_code endpoint for username: {username}") 

488 

489 # El nombre de usuario y el token son obligatorios 

490 if not username or not token: 

491 return Response({"error": "username and token query param are required", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST) 

492 

493 try: # Si el usuario no existe, se devuelve un error 

494 user = UserService.find_by_username_or_email(username) 

495 except UserNotFoundError: 

496 return Response({"error": "User not found", "error_code": ErrorCodes.USER_DONT_EXIST}, status=status.HTTP_404_NOT_FOUND) 

497 

498 # Cambiar el correo de la cuenta solo puede hacerlo su dueño autenticado 

499 if email and not (request.user.is_authenticated and request.user == user): 

500 logger.warning(f"Rejected email change for username {username}: the caller is not the owner of the account") 

501 return Response({"error": "Authentication as the owner is required to change the email", "error_code": ErrorCodes.INSUFICIENT_CREDENTIALS}, status=status.HTTP_403_FORBIDDEN) 

502 was_unverified = not user.is_verify 

503 

504 try: 

505 user = AuthService.consume_verification_code( 

506 user, token, email=email, password=password, validate=validate 

507 ) 

508 

509 if validate and was_unverified and not email and not password: 

510 logger.info(f"Registration completed, issuing session for user ID: {user.id}") 

511 return Response( 

512 {"status": "OK", **AuthService.build_auth_payload(user)}, 

513 status=status.HTTP_200_OK 

514 ) 

515 

516 return Response({"status": "OK"}, status=status.HTTP_200_OK) 

517 

518 except TokenExpiredError: 

519 return Response({"error": "Token expired", "error_code": ErrorCodes.TOKEN_EXPIRED}, status=status.HTTP_400_BAD_REQUEST) 

520 except InvalidTokenError: 

521 return Response({"error": "Invalid token", "error_code": ErrorCodes.INCORRECT_TOKEN}, status=status.HTTP_400_BAD_REQUEST) 

522 except EmailDomainNotAllowedError: 

523 return Response({"error": EMAIL_DOMAIN_ERROR_MESSAGE, "error_code": ErrorCodes.EMAIL_DOMAIN_NOT_ALLOWED}, status=status.HTTP_400_BAD_REQUEST) 

524 except EmailAlreadyRegisteredError: 

525 # El correo nuevo ya lo tiene otra cuenta activa (ver §2.10) 

526 return Response({"error": "Email already in use", "error_code": ErrorCodes.EMAIL_ALREADY_EXISTS}, status=status.HTTP_409_CONFLICT) 

527 except (Http404, APIException, DjangoValidationError): 

528 raise 

529 except Exception as e: 

530 logger.error(f"Error in post_verify_code: {str(e)}") 

531 return Response({"error": "Error changing user data", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 

532 

533 @action( 

534 detail=False, 

535 methods=['get'], 

536 url_path='exist_email', 

537 permission_classes=[IsAuthenticated], 

538 throttle_scope='user_lookup' 

539 ) 

540 def get_exist_email(self, request, *args, **kwargs): 

541 """ 

542 Endpoint GET /users/exist_email/ 

543 Verifica si un email ya esta en uso 

544 Devuelve OK si el email no esta en uso o KO en caso contrario 

545 """ 

546 

547 email = request.query_params.get("email") 

548 

549 logger.info(f"Accessed get_exist_email endpoint for email: {email}") 

550 if not email: 

551 return Response({"error": "email query param is required", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST) 

552 

553 if AuthService.email_is_taken(email): 

554 logger.info(f"The email {email} is in use") 

555 return Response({"status": "KO"}, status=status.HTTP_200_OK) 

556 

557 logger.info(f"The email {email} is not in use") 

558 return Response({"status": "OK"}, status=status.HTTP_200_OK) 

559 

560 

561 @action( 

562 detail=False, 

563 methods=['get'], 

564 url_path='verify_user', 

565 permission_classes=[], 

566 throttle_scope='user_lookup' 

567 ) 

568 def get_verify_user(self, request, *args, **kwargs): 

569 """ 

570 Endpoint GET /users/verify_user/ 

571 Verifica si un usuario (nombre de usuario o email) ya esta en uso 

572 Devuelve OK ambos estan libres o los campos en uso en caso contrario 

573 """ 

574 

575 email = request.query_params.get("email") 

576 username = request.query_params.get("username") 

577 

578 logger.info(f"Accessed get_verify_user endpoint for email: {email}, username: {username}") 

579 if not email or not username: 579 ↛ 580line 579 didn't jump to line 580 because the condition on line 579 was never true

580 return Response({"error": "Both email and username query params are required", "status": "KO", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST) 

581 

582 email_taken = AuthService.email_is_taken(email) 

583 user_taken = AuthService.username_is_taken(username) 

584 

585 if email_taken or user_taken: 

586 logger.info(f"Email taken: {email_taken} or username taken: {user_taken}") 

587 return Response({ 

588 "status": "KO", 

589 "email_taken": email_taken, 

590 "username_taken": user_taken 

591 }, status=status.HTTP_200_OK) 

592 

593 logger.info(f"Both {email} and {username} are available") 

594 return Response({"status": "OK"}, status=status.HTTP_200_OK) 

595 

596 

597 @action( 

598 detail=True, 

599 methods=['post'], 

600 url_path='rate_a_driver', 

601 permission_classes=[IsAuthenticated] 

602 ) 

603 def post_rate_a_driver(self, request, *args, **kwargs): 

604 """ 

605 Endpoint POST /users/{id}/rate_a_driver/ 

606 Permite valorar a un conductor tras haber realizado un viaje con el 

607 """ 

608 results = request.data.get('results') 

609 request_travel = request.data.get('request_travel') 

610 

611 if not results or not request_travel: 

612 logger.error("results and request_travel fields are required in the request body") 

613 return Response({"error": "results and request_travel fields are required", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST) 

614 

615 user = self.get_object() 

616 

617 try: 

618 UserService.rate_driver(user, request_travel, results) 

619 return Response({"status": "OK"}, status=status.HTTP_200_OK) 

620 

621 except RequestTravelNotValidatedError: 

622 return Response({"error": "The specified request_travel is not found or not in the correct status", "error_code": ErrorCodes.TRAVEL_DONT_EXIST}, status=status.HTTP_400_BAD_REQUEST) 

623 except RequestStatusMissingError: 

624 return Response({"error": "Unvalidated status not found", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 

625 except (Http404, APIException, DjangoValidationError): 

626 raise 

627 except Exception as e: 

628 logger.error(f"Error saving driver ratings: {str(e)}") 

629 return Response({"error": "Error saving driver ratings", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 

630 

631 

632 @action( 

633 detail=True, 

634 methods=['post'], 

635 url_path='logout', 

636 permission_classes=[IsAuthenticated] 

637 ) 

638 def post_logout(self, request, *args, **kwargs): 

639 """ 

640 Endpoint POST /users/{id}/logout/ 

641 Cierra la sesion invalidando todos los tokens emitidos hasta ahora 

642 Cierra todas las sesiones de la cuenta, no solo la del dispositivo que llama 

643 """ 

644 user = self.get_object() 

645 logger.info(f"Accessed logout endpoint for user ID: {user.id}") 

646 

647 AuthService.revoke_all_sessions(user) 

648 

649 return Response({"status": "OK"}, status=status.HTTP_200_OK) 

650 

651 @action( 

652 detail=True, 

653 methods=['post'], 

654 url_path='change_password', 

655 permission_classes=[IsAuthenticated] 

656 ) 

657 def post_change_password(self, request, *args, **kwargs): 

658 """ 

659 Endpoint POST /users/{id}/change_password/ 

660 Permite cambiar la contraseña de un usuario 

661 Recibe la contraseña actual y la nueva contraseña 

662 """ 

663 current_password = request.data.get('current_password') 

664 new_password = request.data.get('new_password') 

665 

666 if not current_password or not new_password: 

667 logger.error("current_password and new_password fields are required") 

668 return Response({"error": "current_password and new_password fields are required", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST) 

669 

670 user = self.get_object() 

671 

672 try: 

673 UserService.change_password(user, current_password, new_password) 

674 

675 # Cambiar la contraseña revoca TODAS las sesiones de la cuenta 

676 return Response( 

677 {"status": "OK", **AuthService.build_auth_payload(user)}, 

678 status=status.HTTP_200_OK, 

679 ) 

680 

681 except IncorrectPasswordError: 

682 return Response({"error": "Incorrect current password", "error_code": ErrorCodes.INVALID_CREDENTIALS}, status=status.HTTP_400_BAD_REQUEST) 

683 

684 @action( 

685 detail=True, 

686 methods=['post'], 

687 url_path='two_factor/challenge', 

688 permission_classes=[IsAuthenticated], 

689 throttle_scope='two_factor' 

690 ) 

691 def post_two_factor_challenge(self, request, *args, **kwargs): 

692 """ 

693 Endpoint POST /users/{id}/two_factor/challenge/ 

694 Arranca el cambio del segundo factor y responde con lo que hay que 

695 presentar para confirmarlo: la contraseña actual, o un codigo que se 

696 acaba de enviar al correo de la cuenta si esta solo entra por Google 

697 Devuelve {"method": "password"} o {"method": "email"} 

698 """ 

699 user = self.get_object() 

700 logger.info(f"Accessed two_factor challenge endpoint for user ID: {user.id}") 

701 

702 try: 

703 return Response({"method": AuthService.start_second_factor_change(user)}, status=status.HTTP_200_OK) 

704 

705 except EmailDeliveryError: 

706 return Response({"error": "Error sending verification email", "error_code": ErrorCodes.EMAIL_ERROR}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 

707 

708 @action( 

709 detail=True, 

710 methods=['post'], 

711 url_path='two_factor', 

712 permission_classes=[IsAuthenticated], 

713 throttle_scope='two_factor' 

714 ) 

715 def post_two_factor(self, request, *args, **kwargs): 

716 """ 

717 Endpoint POST /users/{id}/two_factor/ 

718 Activa o desactiva la verificacion en dos pasos 

719 Recibe enabled y la credencial que pidio el challenge: password o code 

720 Devuelve el estado en el que ha quedado el ajuste 

721 """ 

722 enabled = request.data.get('enabled') 

723 

724 if not isinstance(enabled, bool): 

725 logger.error("enabled field is required and must be a boolean") 

726 return Response({"error": "enabled field is required and must be a boolean", "error_code": ErrorCodes.MISSING_REQUIRED_FIELD}, status=status.HTTP_400_BAD_REQUEST) 

727 

728 user = self.get_object() 

729 logger.info(f"Accessed two_factor endpoint for user ID: {user.id} with enabled: {enabled}") 

730 

731 try: 

732 user = AuthService.set_second_factor( 

733 user, enabled, 

734 password=request.data.get('password'), 

735 code=request.data.get('code'), 

736 ) 

737 return Response( 

738 { 

739 "status": "OK", 

740 "has_2FA": user.has_2FA, 

741 **AuthService.build_auth_payload(user), 

742 }, 

743 status=status.HTTP_200_OK, 

744 ) 

745 

746 except IncorrectPasswordError: 

747 return Response({"error": "Incorrect current password", "error_code": ErrorCodes.INVALID_CREDENTIALS}, status=status.HTTP_400_BAD_REQUEST) 

748 except TokenExpiredError: 

749 return Response({"error": "Token expired", "error_code": ErrorCodes.TOKEN_EXPIRED}, status=status.HTTP_400_BAD_REQUEST) 

750 except InvalidTokenError: 

751 return Response({"error": "Invalid token", "error_code": ErrorCodes.INCORRECT_TOKEN}, status=status.HTTP_400_BAD_REQUEST) 

752 

753 @action( 

754 detail=True, 

755 methods=['get'], 

756 url_path='next_travel', 

757 permission_classes=[IsAuthenticated] 

758 ) 

759 def get_next_travel(self, request, *args, **kwargs): 

760 """ 

761 Endpoint GET /users/{id}/next_travel/ 

762 Devuelve los siguientes viajes activos del usuario, si no tiene devuelve un 404 

763 """ 

764 user = self.get_object() 

765 

766 logger.info(f"Accessed get_next_travel endpoint for user ID: {user.id}") 

767 

768 try: 

769 return Response({"results": UserService.get_next_travels(user)}, status=status.HTTP_200_OK) 

770 

771 except NoUpcomingTravelsError: 

772 return Response({"error": "No upcoming travels found", "error_code": ErrorCodes.TRAVEL_DONT_EXIST}, status=status.HTTP_404_NOT_FOUND) 

773 except (Http404, APIException, DjangoValidationError): 

774 raise 

775 except Exception as e: 

776 logger.error(f"Error in get_next_travel: {str(e)}") 

777 return Response({"error": "Error retrieving next travel", "error_code": ErrorCodes.INTERNAL_SERVER_ERROR}, status=500)