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
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-30 20:58 +0200
1import logging
3from django.db.models import Q
4from django.utils import timezone
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
12# Logger para ir almacenando los logs
13logger = logging.getLogger(__name__)
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'
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 """
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)
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)
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()
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()
59 # Si se le ha eliminado del chat, no se le devuelve
60 if membership and membership.is_removed:
61 continue
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
66 # Se escogen solo los chats que el usuario quiere
67 if want_archived != is_archived:
68 continue
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()
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 })
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 )
94 return results
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)
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
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
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
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
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
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()
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'])
164 logger.info(f"User {user.username} left the chat of travel {travel.id_travel}")
165 return membership
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)
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)
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 )
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)
204 return recipients