Coverage for api\views\chat_views.py: 87.0%
88 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, status
4from rest_framework.decorators import action
5from rest_framework.permissions import IsAuthenticated
6from rest_framework.response import Response
8from travels.models import Travel
9from chats.models import user_is_member
10from chats.services.chat_service import ChatService
11from chats.services.exceptions import DriverCannotLeaveError
12from api.serializers.chat_serializer import ChatMessageSerializer
13from api.errors import ErrorCodes
15logger = logging.getLogger(__name__)
17# Endpoints de los chats
18class ChatViewSet(viewsets.ViewSet):
19 permission_classes = [IsAuthenticated]
21 def list(self, request):
22 """
23 Endpoint GET /chats/
24 Se le pasa como parametro opcional archived=true para obtener solo los chats archivados.
25 Devuelve los chats de los viajes en los que participa el usuario
26 """
27 user = request.user
28 logger.info(f"User {user.username} requested chat list")
29 # Se mira si quiere los archivados o no
30 want_archived = str(request.query_params.get('archived', 'false')).lower() == 'true'
32 results = ChatService.list_chats(user, want_archived)
34 logger.info(f"User {user.username} retrieved {len(results)} chats (archived={want_archived})")
36 return Response({'results': results}, status=status.HTTP_200_OK)
38 @action(
39 detail=True,
40 methods=['get'],
41 url_path='messages'
42 )
43 def messages(self, request, pk=None):
44 """
45 Endpoint GET /chats/{travel_id}/messages/
46 Devuelve el historial de mensajes del chat del viaje indicado y si se pueden enviar mensajes
47 """
48 travel = self._get_travel(pk)
49 logger.info(f"User {request.user.username} requested messages for travel {travel.id_travel if travel else None}")
50 if travel is None: # Si el viaje no existe, se devuelve un error
51 return self._not_found()
53 # Si el usuario no es miembro del chat, se devuelve un error
54 if not user_is_member(travel, request.user):
55 return self._forbidden()
57 chat_name, can_send, messages = ChatService.get_conversation(travel)
58 serializer = ChatMessageSerializer(messages, many=True)
60 logger.info(f"User {request.user.username} retrieved {len(serializer.data)} messages for travel {travel.id_travel}")
61 return Response({
62 'chat_name': chat_name,
63 'can_send': can_send,
64 'results': serializer.data,
65 }, status=status.HTTP_200_OK)
67 @action(
68 detail=True,
69 methods=['post'],
70 url_path='mute'
71 )
72 def mute(self, request, pk=None):
73 """
74 Endpoint POST /chats/{travel_id}/mute/
75 Se pasa en el body el parametro: {"muted": true|false} para indicar si se quiere silenciar o reactivar las notificaciones
76 """
77 # Si el viaje no existe o el usuario no es miembro del chat, se devuelve un error
78 logger.info(f"User {request.user.username} requested to mute/unmute travel {pk}")
79 travel = self._get_travel(pk)
80 if travel is None: 80 ↛ 81line 80 didn't jump to line 81 because the condition on line 80 was never true
81 return self._not_found()
82 if not user_is_member(travel, request.user):
83 return self._forbidden()
85 # Se saca si el usuario queria silenciar o reactivar las notificaciones
86 muted = bool(request.data.get('muted', True))
87 ChatService.set_muted(travel, request.user, muted)
89 logger.info(f"User {request.user.username} updated mute status for travel {pk}")
90 return Response({'status': 'ok', 'is_muted': muted}, status=status.HTTP_200_OK)
92 @action(
93 detail=True,
94 methods=['post'],
95 url_path='archive'
96 )
97 def archive(self, request, pk=None):
98 """
99 Endpoint POST /chats/{travel_id}/archive/
100 Archiva el chat que mueve a la seccion de archivados
101 """
102 # Si el viaje no existe o el usuario no es miembro del chat, se devuelve un error
103 logger.info(f"User {request.user.username} requested to archive travel {pk}")
104 travel = self._get_travel(pk)
105 if travel is None: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true
106 return self._not_found()
107 if not user_is_member(travel, request.user): 107 ↛ 108line 107 didn't jump to line 108 because the condition on line 107 was never true
108 return self._forbidden()
110 ChatService.archive(travel, request.user)
112 logger.info(f"User {request.user.username} archived travel {pk}")
113 return Response({'status': 'ok'}, status=status.HTTP_200_OK)
115 @action(
116 detail=True,
117 methods=['post'],
118 url_path='unarchive'
119 )
120 def unarchive(self, request, pk=None):
121 """
122 Endpoint POST /chats/{travel_id}/unarchive/
123 Desarchiva el chat que lo devuelve a la lista principal.
124 """
125 # Si el viaje no existe o el usuario no es miembro del chat, se devuelve un error
126 logger.info(f"User {request.user.username} requested to unarchive travel {pk}")
127 travel = self._get_travel(pk)
128 if travel is None: 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true
129 return self._not_found()
130 if not user_is_member(travel, request.user): 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true
131 return self._forbidden()
133 ChatService.unarchive(travel, request.user)
135 logger.info(f"User {request.user.username} unarchived travel {pk}")
136 return Response({'status': 'ok'}, status=status.HTTP_200_OK)
138 def destroy(self, request, pk=None):
139 """
140 Endpoint DELETE /chats/{travel_id}/
141 El usuario sale del chat de forma permanente.
142 El conductor solo puede salir del chat cuando el viaje ha finalizado.
143 """
144 # Si el viaje no existe o el usuario no es miembro del chat, se devuelve un error
145 logger.info(f"User {request.user.username} requested to destroy chat for travel {pk}")
146 travel = self._get_travel(pk)
147 if travel is None: 147 ↛ 148line 147 didn't jump to line 148 because the condition on line 147 was never true
148 return self._not_found()
149 if not user_is_member(travel, request.user): 149 ↛ 150line 149 didn't jump to line 150 because the condition on line 149 was never true
150 return self._forbidden()
152 try:
153 ChatService.leave_chat(travel, request.user)
154 except DriverCannotLeaveError:
155 # El conductor no puede abandonar el chat de un viaje que aun no ha finalizado
156 return Response({
157 'status': 'error',
158 'message': 'No puedes salir del chat de tu viaje hasta que haya finalizado.',
159 'error_code': ErrorCodes.TRAVEL_ALREADY_STARTED,
160 }, status=status.HTTP_400_BAD_REQUEST)
162 return Response({'status': 'ok'}, status=status.HTTP_200_OK)
164 # Funcion para obtener un viaje por su id
165 def _get_travel(self, pk):
166 try:
167 return Travel.objects.select_related('creation_user', 'state').get(pk=pk, is_deleted=False)
168 except (Travel.DoesNotExist, ValueError):
169 return None
171 # Respuesta de error para los viajes no encontrados
172 def _not_found(self):
173 return Response({
174 'status': 'error',
175 'message': 'Viaje no encontrado.',
176 'error_code': ErrorCodes.TRAVEL_DONT_EXIST,
177 }, status=status.HTTP_404_NOT_FOUND)
179 # Respuesta de error para los usuarios que no son miembros del chat
180 def _forbidden(self):
181 return Response({
182 'status': 'error',
183 'message': 'No tienes acceso al chat de este viaje.',
184 'error_code': ErrorCodes.INSUFICIENT_CREDENTIALS,
185 }, status=status.HTTP_403_FORBIDDEN)