Coverage for modules\chat\models.py: 91%

58 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-05 09:19 +0800

1from django.db import models 

2from core.models import TenantScopedModel, UUIDModel, TimeStampedModel 

3from modules.authentication.models import User 

4 

5class Channel(TenantScopedModel): 

6 """Chat channel or direct message conversation.""" 

7 TYPE_CHOICES = [ 

8 ('PUBLIC', 'Public Channel'), 

9 ('PRIVATE', 'Private Channel'), 

10 ('DIRECT', 'Direct Message'), 

11 ] 

12 

13 name = models.CharField(max_length=128) 

14 topic = models.CharField(max_length=255, blank=True, null=True) 

15 type = models.CharField(max_length=16, choices=TYPE_CHOICES, default='PUBLIC') 

16 created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_channels') 

17 

18 class Meta: 

19 ordering = ['name'] 

20 

21 def __str__(self): 

22 return f"#{self.name} ({self.type})" 

23 

24class ChannelMember(TenantScopedModel): 

25 """Membership in a chat channel.""" 

26 ROLE_CHOICES = [ 

27 ('ADMIN', 'Admin'), 

28 ('MEMBER', 'Member'), 

29 ] 

30 

31 channel = models.ForeignKey(Channel, on_delete=models.CASCADE, related_name='members') 

32 user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='channel_memberships') 

33 role = models.CharField(max_length=16, choices=ROLE_CHOICES, default='MEMBER') 

34 

35 class Meta: 

36 unique_together = ('channel', 'user') 

37 

38 def __str__(self): 

39 return f"{self.user.full_name} in #{self.channel.name}" 

40 

41class Message(TenantScopedModel): 

42 """ChatMessage sent in a channel.""" 

43 channel = models.ForeignKey(Channel, on_delete=models.CASCADE, related_name='messages') 

44 sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages') 

45 content = models.TextField() 

46 parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='replies') 

47 is_pinned = models.BooleanField(default=False) 

48 is_edited = models.BooleanField(default=False) 

49 

50 class Meta: 

51 ordering = ['created_at'] 

52 

53 def __str__(self): 

54 return f"{self.sender.full_name} in #{self.channel.name}: {self.content[:30]}" 

55 

56class MessageAttachment(UUIDModel, TimeStampedModel): 

57 """File attached to a chat message.""" 

58 tenant_id = models.UUIDField(db_index=True) 

59 message = models.ForeignKey(Message, on_delete=models.CASCADE, related_name='attachments') 

60 file_name = models.CharField(max_length=255) 

61 file_size = models.IntegerField() # bytes 

62 file_type = models.CharField(max_length=64) 

63 file_url = models.URLField(max_length=500) 

64 

65 def __str__(self): 

66 return self.file_name 

67 

68class MessageReaction(UUIDModel, TimeStampedModel): 

69 """Emoji reaction on a message.""" 

70 tenant_id = models.UUIDField(db_index=True) 

71 message = models.ForeignKey(Message, on_delete=models.CASCADE, related_name='reactions') 

72 user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='chat_reactions') 

73 emoji = models.CharField(max_length=16) 

74 

75 class Meta: 

76 unique_together = ('message', 'user', 'emoji') 

77 

78 def __str__(self): 

79 return f"{self.user.full_name} reacted {self.emoji}" 

80 

81class ReadReceipt(UUIDModel, TimeStampedModel): 

82 """Tracks the last message read by a user in a channel.""" 

83 tenant_id = models.UUIDField(db_index=True) 

84 channel = models.ForeignKey(Channel, on_delete=models.CASCADE, related_name='read_receipts') 

85 user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='chat_read_receipts') 

86 last_read_message = models.ForeignKey(Message, on_delete=models.SET_NULL, null=True, blank=True) 

87 

88 class Meta: 

89 unique_together = ('channel', 'user')