Files
n3wt-school/Back-End/GestionMessagerie/serializers.py
2025-05-29 15:09:22 +02:00

268 lines
11 KiB
Python

from rest_framework import serializers
from Auth.models import Profile
from GestionMessagerie.models import Messagerie, Conversation, ConversationParticipant, Message, MessageRead, UserPresence
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
class ProfileSimpleSerializer(serializers.ModelSerializer):
"""Sérialiseur simple pour les profils utilisateur"""
class Meta:
model = Profile
fields = ['id', 'first_name', 'last_name', 'email']
class UserPresenceSerializer(serializers.ModelSerializer):
"""Sérialiseur pour la présence utilisateur"""
user = ProfileSimpleSerializer(read_only=True)
class Meta:
model = UserPresence
fields = ['user', 'status', 'last_seen', 'is_typing_in']
class MessageReadSerializer(serializers.ModelSerializer):
"""Sérialiseur pour les messages lus"""
participant = ProfileSimpleSerializer(read_only=True)
class Meta:
model = MessageRead
fields = ['participant', 'read_at']
class MessageSerializer(serializers.ModelSerializer):
"""Sérialiseur pour les messages instantanés"""
sender = ProfileSimpleSerializer(read_only=True)
read_by = MessageReadSerializer(many=True, read_only=True)
attachment = serializers.SerializerMethodField()
is_read = serializers.SerializerMethodField()
class Meta:
model = Message
fields = ['id', 'conversation', 'sender', 'content', 'message_type', 'file_url',
'file_name', 'file_size', 'file_type', 'attachment',
'created_at', 'updated_at', 'is_edited', 'is_deleted', 'read_by', 'is_read']
read_only_fields = ['id', 'created_at', 'updated_at']
def get_attachment(self, obj):
"""Retourne les informations du fichier attaché sous forme d'objet"""
if obj.file_url:
return {
'fileName': obj.file_name,
'fileSize': obj.file_size,
'fileType': obj.file_type,
'fileUrl': obj.file_url,
}
return None
def get_is_read(self, obj):
"""Détermine si le message est lu par l'utilisateur actuel"""
user = self.context.get('user')
if not user or not user.is_authenticated:
return False
# Si c'est le message de l'utilisateur lui-même, vérifier si quelqu'un d'autre l'a lu
if obj.sender == user:
# Pour les messages envoyés par l'utilisateur, vérifier si au moins un autre participant l'a explicitement lu
# Utiliser le modèle MessageRead pour une vérification précise
from .models import MessageRead
other_participants = obj.conversation.participants.exclude(participant=user).filter(is_active=True)
for participant in other_participants:
# Vérifier si ce participant a explicitement lu ce message
if MessageRead.objects.filter(message=obj, participant=participant.participant).exists():
return True
# Fallback: vérifier last_read_at seulement si l'utilisateur était en ligne récemment
# ou si last_read_at est postérieur à created_at (lecture explicite après réception)
if (participant.last_read_at and
participant.last_read_at > obj.created_at):
# Vérifier la présence de l'utilisateur pour s'assurer qu'il était en ligne
try:
from .models import UserPresence
user_presence = UserPresence.objects.filter(user=participant.participant).first()
# Si l'utilisateur était en ligne récemment (dans les 5 minutes suivant le message)
# ou si last_read_at est bien après created_at (lecture délibérée)
time_diff = participant.last_read_at - obj.created_at
if (user_presence and user_presence.last_seen and
user_presence.last_seen >= obj.created_at) or time_diff.total_seconds() > 10:
return True
except:
# En cas d'erreur, continuer avec la logique conservative
pass
return False
else:
# Pour les messages reçus, vérifier si l'utilisateur actuel l'a lu
# D'abord vérifier dans MessageRead pour une lecture explicite
from .models import MessageRead
if MessageRead.objects.filter(message=obj, participant=user).exists():
return True
# Fallback: vérifier last_read_at du participant
participant = obj.conversation.participants.filter(
participant=user,
is_active=True
).first()
if participant and participant.last_read_at:
# Seulement considérer comme lu si last_read_at est postérieur à created_at
return participant.last_read_at > obj.created_at
return False
class ConversationParticipantSerializer(serializers.ModelSerializer):
"""Sérialiseur pour les participants d'une conversation"""
participant = ProfileSimpleSerializer(read_only=True)
class Meta:
model = ConversationParticipant
fields = ['participant', 'joined_at', 'last_read_at', 'is_active']
class ConversationSerializer(serializers.ModelSerializer):
"""Sérialiseur pour les conversations"""
participants = ConversationParticipantSerializer(many=True, read_only=True)
last_message = serializers.SerializerMethodField()
unread_count = serializers.SerializerMethodField()
interlocuteur = serializers.SerializerMethodField()
class Meta:
model = Conversation
fields = ['id', 'name', 'conversation_type', 'created_at', 'updated_at',
'last_activity', 'is_active', 'participants', 'last_message', 'unread_count', 'interlocuteur']
read_only_fields = ['id', 'created_at', 'updated_at']
def get_last_message(self, obj):
last_message = obj.messages.filter(is_deleted=False).last()
if last_message:
return MessageSerializer(last_message).data
return None
def get_unread_count(self, obj):
user = self.context.get('user')
if not user or not user.is_authenticated:
return 0
participant = obj.participants.filter(participant=user).first()
if not participant:
return 0
# Nouvelle logique : compter les messages qui ne sont PAS dans MessageRead
# et qui ont été créés après last_read_at (ou tous si last_read_at est None)
# Base query : messages de la conversation, excluant les propres messages et les supprimés
# ET ne comptant que les messages textuels
base_query = obj.messages.filter(
is_deleted=False,
message_type='text' # Ne compter que les messages textuels
).exclude(sender=user)
# Si l'utilisateur n'a pas de last_read_at, tous les messages sont non lus
if not participant.last_read_at:
unread_from_timestamp = base_query
else:
# Messages créés après le dernier moment de lecture
unread_from_timestamp = base_query.filter(
created_at__gt=participant.last_read_at
)
# Soustraire les messages explicitement marqués comme lus dans MessageRead
from .models import MessageRead
read_message_ids = MessageRead.objects.filter(
participant=user,
message__conversation=obj
).values_list('message_id', flat=True)
# Compter les messages non lus = messages après last_read_at MOINS ceux explicitement lus
unread_count = unread_from_timestamp.exclude(
id__in=read_message_ids
).count()
return unread_count
def get_interlocuteur(self, obj):
"""Pour les conversations privées, retourne l'autre participant"""
user = self.context.get('user')
if not user or not user.is_authenticated or obj.conversation_type != 'private':
return None
# Trouver l'autre participant (pas l'utilisateur actuel)
other_participant = obj.participants.filter(is_active=True).exclude(participant=user).first()
if other_participant:
return ProfileSimpleSerializer(other_participant.participant).data
return None
class ConversationCreateSerializer(serializers.ModelSerializer):
"""Sérialiseur pour créer une conversation"""
participant_ids = serializers.ListField(
child=serializers.IntegerField(),
write_only=True
)
class Meta:
model = Conversation
fields = ['name', 'conversation_type', 'participant_ids']
def create(self, validated_data):
participant_ids = validated_data.pop('participant_ids')
conversation_type = validated_data.get('conversation_type', 'private')
# Pour les conversations privées, ne pas utiliser de nom spécifique
# Le nom sera géré côté frontend en affichant le nom de l'interlocuteur
if conversation_type == 'private':
validated_data['name'] = None
conversation = super().create(validated_data)
# Ajouter les participants
participants = []
for participant_id in participant_ids:
try:
participant = Profile.objects.get(id=participant_id)
ConversationParticipant.objects.create(
conversation=conversation,
participant=participant
)
participants.append(participant)
except Profile.DoesNotExist:
continue
# Notifier les participants via WebSocket de la nouvelle conversation
try:
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
channel_layer = get_channel_layer()
if channel_layer:
# Envoyer à chaque participant avec le bon contexte
for participant in participants:
# Sérialiser la conversation avec le contexte de ce participant
conversation_data = ConversationSerializer(conversation, context={'user': participant}).data
async_to_sync(channel_layer.group_send)(
f'user_{participant.id}',
{
'type': 'new_conversation_notification',
'conversation': conversation_data
}
)
except Exception as e:
# Log l'erreur mais ne pas interrompre la création de la conversation
import logging
logger = logging.getLogger(__name__)
logger.error(f"Erreur lors de la notification WebSocket de nouvelle conversation: {str(e)}")
return conversation
# Ancien sérialiseur conservé pour compatibilité
class MessageLegacySerializer(serializers.ModelSerializer):
destinataire_profil = serializers.SerializerMethodField()
emetteur_profil = serializers.SerializerMethodField()
class Meta:
model = Messagerie
fields = '__all__'
read_only_fields = ['date_envoi']
def get_destinataire_profil(self, obj):
return obj.destinataire.email
def get_emetteur_profil(self, obj):
return obj.emetteur.email