from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django_choices_field import TextChoicesField from core.models.base import BaseModel from core.models.enums import ConversationTypeChoices class Conversation(BaseModel): """ Conversation thread that groups messages together. Can be linked to specific entities (Project, Service, Account, etc.) for context. """ subject = models.CharField( max_length=500, help_text="Conversation subject/title" ) conversation_type = TextChoicesField( choices_enum=ConversationTypeChoices, help_text="Type of conversation (DIRECT, GROUP, SUPPORT)" ) last_message_at = models.DateTimeField( null=True, blank=True, help_text="Timestamp of most recent message" ) is_archived = models.BooleanField( default=False, help_text="Whether this conversation is archived (system-wide)" ) # Generic foreign key for linking to any entity (Project, Service, Account, etc.) entity_content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, null=True, blank=True, related_name='conversations', help_text="Content type of the related entity" ) entity_object_id = models.UUIDField( null=True, blank=True, help_text="UUID of the related entity" ) entity = GenericForeignKey('entity_content_type', 'entity_object_id') # Generic foreign key for conversation creator created_by_content_type = models.ForeignKey( ContentType, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_conversations', help_text="Content type of the creator (TeamProfile or CustomerProfile)" ) created_by_object_id = models.UUIDField( null=True, blank=True, help_text="UUID of the creator" ) created_by = GenericForeignKey('created_by_content_type', 'created_by_object_id') metadata = models.JSONField( default=dict, blank=True, help_text="Additional conversation metadata" ) class Meta: db_table = 'conversations' ordering = ['-last_message_at', '-created_at'] indexes = [ models.Index(fields=['-last_message_at']), models.Index(fields=['entity_content_type', 'entity_object_id']), models.Index(fields=['conversation_type', '-last_message_at']), models.Index(fields=['created_by_content_type', 'created_by_object_id']), ] def __str__(self): entity_info = f" ({self.entity_content_type.model}:{self.entity_object_id})" if self.entity else "" return f"{self.subject}{entity_info}" class ConversationParticipant(BaseModel): """ Links users (TeamProfile or CustomerProfile) to conversations. Tracks per-user read status and preferences. """ conversation = models.ForeignKey( Conversation, on_delete=models.CASCADE, related_name='participants', help_text="Conversation this participant belongs to" ) # Generic foreign key to support both TeamProfile and CustomerProfile participant_content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, related_name='conversation_participants', help_text="Content type of the participant (TeamProfile or CustomerProfile)" ) participant_object_id = models.UUIDField( help_text="UUID of the participant" ) participant = GenericForeignKey('participant_content_type', 'participant_object_id') last_read_at = models.DateTimeField( null=True, blank=True, help_text="Timestamp when participant last read messages in this conversation" ) unread_count = models.PositiveIntegerField( default=0, help_text="Number of unread messages for this participant" ) is_muted = models.BooleanField( default=False, help_text="Whether participant has muted notifications for this conversation" ) is_archived = models.BooleanField( default=False, help_text="Whether participant has archived this conversation (user-specific)" ) joined_at = models.DateTimeField( auto_now_add=True, help_text="When participant joined the conversation" ) class Meta: db_table = 'conversation_participants' ordering = ['conversation', 'joined_at'] indexes = [ models.Index(fields=['participant_content_type', 'participant_object_id', 'is_archived']), models.Index(fields=['conversation', 'participant_content_type', 'participant_object_id']), models.Index(fields=['unread_count']), ] unique_together = [['conversation', 'participant_content_type', 'participant_object_id']] def __str__(self): return f"{self.participant} in {self.conversation.subject}" class Message(BaseModel): """ Individual message within a conversation. """ conversation = models.ForeignKey( Conversation, on_delete=models.CASCADE, related_name='messages', help_text="Conversation this message belongs to" ) # Generic foreign key for sender (TeamProfile or CustomerProfile) sender_content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, related_name='sent_messages', help_text="Content type of the sender" ) sender_object_id = models.UUIDField( help_text="UUID of the sender" ) sender = GenericForeignKey('sender_content_type', 'sender_object_id') body = models.TextField( help_text="Message content" ) # For message threading/replies reply_to = models.ForeignKey( 'self', on_delete=models.SET_NULL, null=True, blank=True, related_name='replies', help_text="Message this is replying to (for threading)" ) # Attachments stored as array of file references attachments = models.JSONField( default=list, blank=True, help_text="Array of attachment metadata (file paths, names, sizes, types)" ) is_system_message = models.BooleanField( default=False, help_text="Whether this is an automated system message" ) metadata = models.JSONField( default=dict, blank=True, help_text="Additional message metadata (formatting, mentions, etc.)" ) class Meta: db_table = 'messages' ordering = ['created_at'] indexes = [ models.Index(fields=['conversation', 'created_at']), models.Index(fields=['sender_content_type', 'sender_object_id', 'created_at']), models.Index(fields=['reply_to']), ] def __str__(self): preview = self.body[:50] + "..." if len(self.body) > 50 else self.body return f"Message from {self.sender} in {self.conversation.subject}: {preview}" class MessageReadReceipt(BaseModel): """ Tracks when individual messages are read by specific participants. Allows for fine-grained read tracking beyond conversation-level. """ message = models.ForeignKey( Message, on_delete=models.CASCADE, related_name='read_receipts', help_text="Message that was read" ) # Generic foreign key for reader (TeamProfile or CustomerProfile) reader_content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, related_name='message_reads', help_text="Content type of the reader" ) reader_object_id = models.UUIDField( help_text="UUID of the reader" ) reader = GenericForeignKey('reader_content_type', 'reader_object_id') read_at = models.DateTimeField( auto_now_add=True, help_text="When the message was read" ) class Meta: db_table = 'message_read_receipts' ordering = ['read_at'] indexes = [ models.Index(fields=['message', 'reader_content_type', 'reader_object_id']), models.Index(fields=['reader_content_type', 'reader_object_id', 'read_at']), ] unique_together = [['message', 'reader_content_type', 'reader_object_id']] def __str__(self): return f"{self.reader} read message {self.message_id} at {self.read_at}"