from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework.parsers import MultiPartParser, FormParser from django.db import models from .models import Conversation, ConversationParticipant, Message, UserPresence from Auth.models import Profile, ProfileRole from GestionMessagerie.serializers import ( ConversationSerializer, MessageSerializer, ConversationCreateSerializer, UserPresenceSerializer, ProfileSimpleSerializer ) from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from django.utils import timezone import os import uuid import logging from django.core.files.storage import default_storage from django.core.files.base import ContentFile from django.db.models import Q logger = logging.getLogger(__name__) # ====================== MESSAGERIE INSTANTANÉE ====================== class InstantConversationListView(APIView): """ API pour lister les conversations instantanées d'un utilisateur """ @swagger_auto_schema( operation_description="Liste les conversations instantanées d'un utilisateur", responses={200: ConversationSerializer(many=True)} ) def get(self, request, user_id=None): try: user = Profile.objects.get(id=user_id) conversations = Conversation.objects.filter( participants__participant=user, participants__is_active=True, is_active=True ).distinct().order_by('-last_activity') serializer = ConversationSerializer(conversations, many=True, context={'user': user}) return Response(serializer.data, status=status.HTTP_200_OK) except Profile.DoesNotExist: return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class InstantConversationCreateView(APIView): """ API pour créer une nouvelle conversation instantanée """ @swagger_auto_schema( operation_description="Crée une nouvelle conversation instantanée", request_body=ConversationCreateSerializer, responses={201: ConversationSerializer} ) def post(self, request): serializer = ConversationCreateSerializer(data=request.data) if serializer.is_valid(): conversation = serializer.save() response_serializer = ConversationSerializer(conversation, context={'user': request.user}) return Response(response_serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class InstantMessageListView(APIView): """ API pour lister les messages d'une conversation """ @swagger_auto_schema( operation_description="Liste les messages d'une conversation", responses={200: MessageSerializer(many=True)} ) def get(self, request, conversation_id): try: conversation = Conversation.objects.get(id=conversation_id) messages = conversation.messages.filter(is_deleted=False).order_by('created_at') # Récupérer l'utilisateur actuel depuis les paramètres de requête user_id = request.GET.get('user_id') user = None if user_id: try: user = Profile.objects.get(id=user_id) except Profile.DoesNotExist: pass serializer = MessageSerializer(messages, many=True, context={'user': user}) return Response(serializer.data, status=status.HTTP_200_OK) except Conversation.DoesNotExist: return Response({'error': 'Conversation not found'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class InstantMessageCreateView(APIView): """ API pour envoyer un nouveau message instantané """ @swagger_auto_schema( operation_description="Envoie un nouveau message instantané", request_body=openapi.Schema( type=openapi.TYPE_OBJECT, properties={ 'conversation_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID de la conversation'), 'sender_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='ID de l\'expéditeur'), 'content': openapi.Schema(type=openapi.TYPE_STRING, description='Contenu du message'), 'message_type': openapi.Schema(type=openapi.TYPE_STRING, description='Type de message', default='text') }, required=['conversation_id', 'sender_id', 'content'] ), responses={201: MessageSerializer} ) def post(self, request): try: conversation_id = request.data.get('conversation_id') sender_id = request.data.get('sender_id') content = request.data.get('content', '').strip() message_type = request.data.get('message_type', 'text') if not all([conversation_id, sender_id, content]): return Response( {'error': 'conversation_id, sender_id, and content are required'}, status=status.HTTP_400_BAD_REQUEST ) # Vérifier que la conversation existe conversation = Conversation.objects.get(id=conversation_id) # Vérifier que l'expéditeur existe et peut envoyer dans cette conversation sender = Profile.objects.get(id=sender_id) participant = ConversationParticipant.objects.filter( conversation=conversation, participant=sender, is_active=True ).first() if not participant: return Response( {'error': 'You are not a participant in this conversation'}, status=status.HTTP_403_FORBIDDEN ) # Récupérer les données de fichier si disponibles file_url = request.data.get('file_url') file_name = request.data.get('file_name') file_type = request.data.get('file_type') file_size = request.data.get('file_size') # Créer le message message = Message.objects.create( conversation=conversation, sender=sender, content=content, message_type=message_type, file_url=file_url, file_name=file_name, file_type=file_type, file_size=file_size ) # Mettre à jour l'activité de la conversation conversation.last_activity = message.created_at conversation.save(update_fields=['last_activity']) serializer = MessageSerializer(message) return Response(serializer.data, status=status.HTTP_201_CREATED) except Conversation.DoesNotExist: return Response({'error': 'Conversation not found'}, status=status.HTTP_404_NOT_FOUND) except Profile.DoesNotExist: return Response({'error': 'Sender not found'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class InstantMarkAsReadView(APIView): """ API pour marquer une conversation comme lue """ @swagger_auto_schema( operation_description="Marque une conversation comme lue pour un utilisateur", request_body=openapi.Schema( type=openapi.TYPE_OBJECT, properties={ 'user_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='ID de l\'utilisateur') }, required=['user_id'] ), responses={200: openapi.Response('Success')} ) def post(self, request, conversation_id): try: user_id = request.data.get('user_id') if not user_id: return Response({'error': 'user_id is required'}, status=status.HTTP_400_BAD_REQUEST) participant = ConversationParticipant.objects.get( conversation_id=conversation_id, participant_id=user_id, is_active=True ) participant.last_read_at = timezone.now() participant.save(update_fields=['last_read_at']) return Response({'status': 'success'}, status=status.HTTP_200_OK) except ConversationParticipant.DoesNotExist: return Response({'error': 'Participant not found'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class UserPresenceView(APIView): """ API pour gérer la présence des utilisateurs """ @swagger_auto_schema( operation_description="Met à jour le statut de présence d'un utilisateur", request_body=openapi.Schema( type=openapi.TYPE_OBJECT, properties={ 'status': openapi.Schema(type=openapi.TYPE_STRING, description='Statut de présence') }, required=['status'] ), responses={200: UserPresenceSerializer} ) def post(self, request, user_id): try: user = Profile.objects.get(id=user_id) status_value = request.data.get('status') if status_value not in ['online', 'away', 'busy', 'offline']: return Response({'error': 'Invalid status'}, status=status.HTTP_400_BAD_REQUEST) presence, created = UserPresence.objects.get_or_create(user=user) presence.status = status_value presence.last_seen = timezone.now() presence.save() serializer = UserPresenceSerializer(presence) return Response(serializer.data, status=status.HTTP_200_OK) except Profile.DoesNotExist: return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @swagger_auto_schema( operation_description="Récupère le statut de présence d'un utilisateur", responses={200: UserPresenceSerializer} ) def get(self, request, user_id): try: user = Profile.objects.get(id=user_id) presence, created = UserPresence.objects.get_or_create(user=user) if created: presence.status = 'offline' presence.save() serializer = UserPresenceSerializer(presence) return Response(serializer.data, status=status.HTTP_200_OK) except Profile.DoesNotExist: return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class FileUploadView(APIView): """ API pour l'upload de fichiers dans la messagerie instantanée """ parser_classes = (MultiPartParser, FormParser) @swagger_auto_schema( operation_description="Upload un fichier pour la messagerie", manual_parameters=[ openapi.Parameter('file', openapi.IN_FORM, description="Fichier à uploader", type=openapi.TYPE_FILE, required=True), openapi.Parameter('conversation_id', openapi.IN_FORM, description="ID de la conversation", type=openapi.TYPE_INTEGER, required=True), openapi.Parameter('sender_id', openapi.IN_FORM, description="ID de l'expéditeur", type=openapi.TYPE_INTEGER, required=True), ], responses={ 200: openapi.Response('Success', openapi.Schema( type=openapi.TYPE_OBJECT, properties={ 'fileUrl': openapi.Schema(type=openapi.TYPE_STRING), 'fileName': openapi.Schema(type=openapi.TYPE_STRING), 'fileSize': openapi.Schema(type=openapi.TYPE_INTEGER), 'fileType': openapi.Schema(type=openapi.TYPE_STRING), } )), 400: 'Bad Request', 413: 'File too large', 415: 'Unsupported file type' } ) def post(self, request): try: file = request.FILES.get('file') conversation_id = request.data.get('conversation_id') sender_id = request.data.get('sender_id') if not file: return Response({'error': 'Aucun fichier fourni'}, status=status.HTTP_400_BAD_REQUEST) if not conversation_id or not sender_id: return Response({'error': 'conversation_id et sender_id requis'}, status=status.HTTP_400_BAD_REQUEST) # Vérifier que la conversation existe et que l'utilisateur y participe try: conversation = Conversation.objects.get(id=conversation_id) sender = Profile.objects.get(id=sender_id) # Vérifier que l'expéditeur participe à la conversation if not ConversationParticipant.objects.filter( conversation=conversation, participant=sender, is_active=True ).exists(): return Response({'error': 'Accès non autorisé à cette conversation'}, status=status.HTTP_403_FORBIDDEN) except (Conversation.DoesNotExist, Profile.DoesNotExist): return Response({'error': 'Conversation ou utilisateur introuvable'}, status=status.HTTP_404_NOT_FOUND) # Valider le type de fichier allowed_types = [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/plain' ] if file.content_type not in allowed_types: return Response({'error': 'Type de fichier non autorisé'}, status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE) # Valider la taille du fichier (10MB max) max_size = 10 * 1024 * 1024 # 10MB if file.size > max_size: return Response({'error': 'Fichier trop volumineux (max 10MB)'}, status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE) # Générer un nom de fichier unique file_extension = os.path.splitext(file.name)[1] unique_filename = f"{uuid.uuid4()}{file_extension}" # Chemin de stockage : messagerie/conversation_id/ storage_path = f"messagerie/{conversation_id}/{unique_filename}" # Sauvegarder le fichier file_path = default_storage.save(storage_path, ContentFile(file.read())) # Générer l'URL du fichier file_url = default_storage.url(file_path) if not file_url.startswith('http'): # Construire l'URL complète si nécessaire file_url = request.build_absolute_uri(file_url) return Response({ 'fileUrl': file_url, 'fileName': file.name, 'fileSize': file.size, 'fileType': file.content_type, 'filePath': file_path }, status=status.HTTP_200_OK) except Exception as e: return Response({'error': f'Erreur lors de l\'upload: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class InstantRecipientSearchView(APIView): """ API pour rechercher des destinataires pour la messagerie instantanée """ @swagger_auto_schema( operation_description="Recherche des destinataires pour la messagerie instantanée", manual_parameters=[ openapi.Parameter('establishment_id', openapi.IN_QUERY, description="ID de l'établissement", type=openapi.TYPE_INTEGER, required=True), openapi.Parameter('q', openapi.IN_QUERY, description="Terme de recherche", type=openapi.TYPE_STRING, required=True) ], responses={200: ProfileSimpleSerializer(many=True)} ) def get(self, request): try: establishment_id = request.query_params.get('establishment_id') search_query = request.query_params.get('q', '').strip() if not establishment_id: return Response({'error': 'establishment_id is required'}, status=status.HTTP_400_BAD_REQUEST) # Récupérer les IDs des profils actifs dans l'établissement profile_roles = ProfileRole.objects.filter( establishment_id=establishment_id, is_active=True ).values_list('profile_id', flat=True) # Rechercher les profils correspondants users = Profile.objects.filter(id__in=profile_roles) # Appliquer le filtre de recherche si un terme est fourni if search_query: users = users.filter( Q(first_name__icontains=search_query) | Q(last_name__icontains=search_query) | Q(email__icontains=search_query) ) # Exclure l'utilisateur actuel des résultats if request.user.is_authenticated: users = users.exclude(id=request.user.id) serializer = ProfileSimpleSerializer(users[:10], many=True) # Limiter à 10 résultats return Response(serializer.data, status=status.HTTP_200_OK) except Exception as e: return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class InstantConversationDeleteView(APIView): """ API pour supprimer (désactiver) une conversation instantanée """ @swagger_auto_schema( operation_description="Supprime une conversation instantanée (désactivation soft)", responses={200: openapi.Schema( type=openapi.TYPE_OBJECT, properties={ 'success': openapi.Schema(type=openapi.TYPE_BOOLEAN), 'message': openapi.Schema(type=openapi.TYPE_STRING) } )} ) def delete(self, request, conversation_id): try: # Récupérer la conversation par son ID UUID conversation = Conversation.objects.filter(id=conversation_id).first() if not conversation: return Response({'error': 'Conversation not found'}, status=status.HTTP_404_NOT_FOUND) # Suppression simple : désactiver la conversation conversation.is_active = False conversation.save() return Response({ 'success': True, 'message': 'Conversation deleted successfully' }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"Error deleting conversation: {str(e)}") return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)