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
« 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
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 ]
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')
18 class Meta:
19 ordering = ['name']
21 def __str__(self):
22 return f"#{self.name} ({self.type})"
24class ChannelMember(TenantScopedModel):
25 """Membership in a chat channel."""
26 ROLE_CHOICES = [
27 ('ADMIN', 'Admin'),
28 ('MEMBER', 'Member'),
29 ]
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')
35 class Meta:
36 unique_together = ('channel', 'user')
38 def __str__(self):
39 return f"{self.user.full_name} in #{self.channel.name}"
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)
50 class Meta:
51 ordering = ['created_at']
53 def __str__(self):
54 return f"{self.sender.full_name} in #{self.channel.name}: {self.content[:30]}"
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)
65 def __str__(self):
66 return self.file_name
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)
75 class Meta:
76 unique_together = ('message', 'user', 'emoji')
78 def __str__(self):
79 return f"{self.user.full_name} reacted {self.emoji}"
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)
88 class Meta:
89 unique_together = ('channel', 'user')