mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
feat: mise en place de la messagerie [#17]
This commit is contained in:
@ -1,15 +1,266 @@
|
||||
from rest_framework import serializers
|
||||
from Auth.models import Profile
|
||||
from GestionMessagerie.models import Messagerie
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user