mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 15:33:22 +00:00
268 lines
11 KiB
Python
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 |