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

1import logging 

2 

3from rest_framework import viewsets, status 

4from rest_framework.decorators import action 

5from rest_framework.permissions import IsAuthenticated 

6from rest_framework.response import Response 

7 

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 

14 

15logger = logging.getLogger(__name__) 

16 

17# Endpoints de los chats 

18class ChatViewSet(viewsets.ViewSet): 

19 permission_classes = [IsAuthenticated] 

20 

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' 

31 

32 results = ChatService.list_chats(user, want_archived) 

33 

34 logger.info(f"User {user.username} retrieved {len(results)} chats (archived={want_archived})") 

35 

36 return Response({'results': results}, status=status.HTTP_200_OK) 

37 

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

52 

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

56 

57 chat_name, can_send, messages = ChatService.get_conversation(travel) 

58 serializer = ChatMessageSerializer(messages, many=True) 

59 

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) 

66 

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

84 

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) 

88 

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) 

91 

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

109 

110 ChatService.archive(travel, request.user) 

111 

112 logger.info(f"User {request.user.username} archived travel {pk}") 

113 return Response({'status': 'ok'}, status=status.HTTP_200_OK) 

114 

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

132 

133 ChatService.unarchive(travel, request.user) 

134 

135 logger.info(f"User {request.user.username} unarchived travel {pk}") 

136 return Response({'status': 'ok'}, status=status.HTTP_200_OK) 

137 

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

151 

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) 

161 

162 return Response({'status': 'ok'}, status=status.HTTP_200_OK) 

163 

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 

170 

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) 

178 

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)