Coverage for chats\services\chat_service.py: 100.0%

88 statements  

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

1import logging 

2 

3from django.db.models import Q 

4from django.utils import timezone 

5 

6from chats.models import ( 

7 CHAT_MEMBER_REQUEST_STATES, Chat, ChatMembership, ChatMessage, 

8 chat_display_name, get_member_users, get_or_create_membership) 

9from chats.services.exceptions import DriverCannotLeaveError 

10from travels.models import RequestTravels, Travel 

11 

12# Logger para ir almacenando los logs 

13logger = logging.getLogger(__name__) 

14 

15# Codigo del estado finalizado de un viaje, ya que en este estado no se pueden enviar mensajes en los chats 

16FINISHED_TRAVEL_STATE = 'fnd' 

17 

18 

19# Servicio con la logica de negocio del subsistema de mensajeria 

20class ChatService: 

21 """ 

22 Centraliza las reglas de negocio de los chats de viaje. 

23 Lo usan tanto el ViewSet HTTP (listado, silenciar, archivar, salir) como el consumer de WebSocket  

24 """ 

25 

26 # Devuelve los chats de los viajes en los que participa el usuario 

27 @staticmethod 

28 def list_chats(user, want_archived=False): 

29 """ 

30 Arma el listado de chats del usuario, ya ordenado 

31 want_archived decide si se devuelven los archivados o los activos 

32 Devuelve una lista de diccionarios lista para serializar 

33 """ 

34 # Viajes donde el usuario es el conductor 

35 driver_travel_ids = Travel.objects.filter( 

36 creation_user=user, is_deleted=False 

37 ).values_list('pk', flat=True) 

38 

39 # Viajes donde el usuario es pasajero 

40 passenger_travel_ids = RequestTravels.objects.filter( 

41 user=user, 

42 status__code__in=CHAT_MEMBER_REQUEST_STATES, 

43 is_deleted=False, 

44 ).values_list('id_travel', flat=True) 

45 

46 # Se sacan los viajes en los que el usuario es conductor o pasajero 

47 travels = Travel.objects.filter( 

48 Q(pk__in=driver_travel_ids) | Q(pk__in=passenger_travel_ids), 

49 is_deleted=False, 

50 ).select_related('creation_user', 'state').distinct() 

51 

52 results = [] 

53 for travel in travels: 

54 # Se obtiene el chat 

55 chat = Chat.get_or_create_for_travel(travel) 

56 # Se obtiene la membresia del usuario en el chat, si existe 

57 membership = ChatMembership.objects.filter(chat=chat, user=user).first() 

58 

59 # Si se le ha eliminado del chat, no se le devuelve 

60 if membership and membership.is_removed: 

61 continue 

62 

63 # Se mira si el chat esta archivado o no para el usuario 

64 is_archived = membership is not None and membership.archived_at is not None 

65 

66 # Se escogen solo los chats que el usuario quiere 

67 if want_archived != is_archived: 

68 continue 

69 

70 # Se obtiene el ultimo mensaje del chat que no este eliminado 

71 last_message = chat.messages.filter(is_deleted=False).order_by('-created_at').first() 

72 

73 # Se añade el chat a la lista de resultados 

74 results.append({ 

75 'id': str(travel.id_travel), 

76 'chat_id': str(chat.id), 

77 'name': chat_display_name(travel), 

78 'travel_date': travel.travel_date, 

79 'last_message': last_message.content if last_message else None, 

80 'last_message_time': last_message.created_at if last_message else None, 

81 'is_muted': membership.is_muted if membership else False, 

82 'can_leave': ChatService.can_leave(travel, user), 

83 }) 

84 

85 # Se ordenan los resultados por la fecha del ultimo mensaje, si no hay mensajes, se ordena por la fecha del viaje 

86 results.sort( 

87 key=lambda item: ( 

88 1 if item['last_message_time'] else 0, 

89 item['last_message_time'] or item['travel_date'], 

90 ), 

91 reverse=True, 

92 ) 

93 

94 return results 

95 

96 # Indica si el usuario puede abandonar el chat del viaje 

97 @staticmethod 

98 def can_leave(travel, user): 

99 # El conductor solo puede salir del chat cuando el viaje ha finalizado 

100 is_driver = travel.creation_user_id == user.id 

101 return (not is_driver) or (travel.state.code == FINISHED_TRAVEL_STATE) 

102 

103 # Devuelve el historial de mensajes del chat de un viaje 

104 @staticmethod 

105 def get_conversation(travel): 

106 """ 

107 Devuelve la tupla (nombre del chat, se pueden enviar mensajes, mensajes) 

108 """ 

109 chat = Chat.get_or_create_for_travel(travel) 

110 messages = chat.messages.filter(is_deleted=False).select_related('user').order_by('created_at') 

111 return chat_display_name(travel), ChatService.can_send_messages(travel), messages 

112 

113 # Indica si en el chat del viaje todavia se pueden enviar mensajes 

114 @staticmethod 

115 def can_send_messages(travel): 

116 # Solo se pueden enviar mensajes mientras el viaje no haya finalizado 

117 return travel.state.code != FINISHED_TRAVEL_STATE 

118 

119 # Silencia o reactiva las notificaciones del chat para un usuario 

120 @staticmethod 

121 def set_muted(travel, user, muted): 

122 chat = Chat.get_or_create_for_travel(travel) 

123 membership = get_or_create_membership(chat, user) 

124 membership.is_muted = muted # Se mutea o desmutea el chat para el usuario 

125 membership.save(update_fields=['is_muted', 'updated_at']) 

126 return membership 

127 

128 # Archiva el chat, que lo mueve a la seccion de archivados 

129 @staticmethod 

130 def archive(travel, user): 

131 chat = Chat.get_or_create_for_travel(travel) 

132 membership = get_or_create_membership(chat, user) 

133 # Se archiva poniendo fecha a archived_at 

134 membership.archived_at = timezone.now() 

135 membership.save(update_fields=['archived_at', 'updated_at']) 

136 return membership 

137 

138 # Desarchiva el chat, que lo devuelve a la lista principal 

139 @staticmethod 

140 def unarchive(travel, user): 

141 chat = Chat.get_or_create_for_travel(travel) 

142 membership = get_or_create_membership(chat, user) 

143 # Se desarchiva poniendo archived_at a None 

144 membership.archived_at = None 

145 membership.save(update_fields=['archived_at', 'updated_at']) 

146 return membership 

147 

148 # El usuario sale del chat de forma permanente 

149 @staticmethod 

150 def leave_chat(travel, user): 

151 """ 

152 Marca al usuario como eliminado del chat 

153 Lanza DriverCannotLeaveError si es el conductor y el viaje aun no ha finalizado 

154 """ 

155 if not ChatService.can_leave(travel, user): 

156 logger.warning(f"User {user.username} attempted to leave chat for travel {travel.id_travel} before it finished") 

157 raise DriverCannotLeaveError() 

158 

159 chat = Chat.get_or_create_for_travel(travel) 

160 membership = get_or_create_membership(chat, user) 

161 membership.is_removed = True 

162 membership.save(update_fields=['is_removed', 'updated_at']) 

163 

164 logger.info(f"User {user.username} left the chat of travel {travel.id_travel}") 

165 return membership 

166 

167 # Guarda un mensaje en el chat de un viaje 

168 @staticmethod 

169 def save_message(travel_id, chat, user, content): 

170 """ 

171 Crea el mensaje, salvo que el viaje ya haya finalizado 

172 Devuelve None en ese caso, para que el consumer sepa que no debe difundirlo 

173 """ 

174 state_code = Travel.objects.filter(pk=travel_id).values_list('state', flat=True).first() 

175 if state_code == FINISHED_TRAVEL_STATE: # Se valida que el viaje no haya finalizado 

176 return None 

177 return ChatMessage.objects.create(chat=chat, user=user, content=content) 

178 

179 # Decide a que miembros del chat hay que enviarles la notificacion push 

180 @staticmethod 

181 def members_to_notify(chat, travel, sender, present_ids): 

182 """ 

183 Devuelve los miembros que deben recibir el push de un mensaje nuevo 

184 Se excluye al que lo envia, a los que tienen el chat abierto en ese 

185 momento y a los que lo tienen silenciado o de los que se les ha eliminado. 

186 """ 

187 sender_id = str(sender.id) 

188 

189 excluded_ids = set( 

190 str(uid) for uid in ChatMembership.objects.filter( 

191 chat=chat, 

192 ).filter( 

193 Q(is_muted=True) | Q(is_removed=True) 

194 ).values_list('user_id', flat=True) 

195 ) 

196 

197 recipients = [] 

198 for member in get_member_users(travel): 

199 member_id = str(member.id) 

200 if member_id == sender_id or member_id in present_ids or member_id in excluded_ids: 

201 continue 

202 recipients.append(member) 

203 

204 return recipients