Models¶
We decided to use UUIDs to make harder to make associations by using it but not using as primary key.
List of Models¶
Thread¶
class Thread(AuditModel):
"""Main model where a thread is created. This model only contains a subject
and a ManyToMany relationship with the users.
Django by default creates an 'invisible' model when ManyToMany is declared
but we can override the default and point to our own model.
A `uuid` field is declared as a way to
"""
uuid = models.UUIDField(blank=False, null=False, editable=False, default=uuid4)
subject = models.CharField(max_length=150)
users = models.ManyToManyField(settings.AUTH_USER_MODEL, through="UserThread")
Thread is the main model and some sort of source of truth.
Functions¶
@classmethod
def inbox(cls, user):
"""Returns the inbox of a given user"""
return cls.objects.filter(userthread__user=user, userthread__deleted=False)
@classmethod
def deleted(cls, user):
"""Returns the deleted messages of a given user"""
return cls.objects.filter(userthread__user=user, userthread__deleted=True)
@classmethod
def unread(cls, user):
"""Returns all the unread messages of a given user"""
return cls.objects.filter(
userthread__user=user,
userthread__deleted=False,
userthread__unread=True
)
@property
def first_message(self):
"""Returns the first message"""
return self.messages.all()[0]
@property
def latest_message(self):
"""Returs the last message"""
return self.messages.order_by("-sent_at")[0]
@classmethod
def ordered(cls, objs):
"""
Returns the iterable ordered the correct way, this is a class method
because we don"t know what the type of the iterable will be.
"""
objs = list(objs)
objs.sort(key=lambda o: o.latest_message.sent_at, reverse=True)
return objs
@classmethod
def get_thread_users(cls):
"""Returns all the users from the thread"""
return cls.users.all()
def earliest_message(self, user_to_exclude=None):
"""
Returns the earliest message of the thread
:param user_to_exclude: Returns a list of the messages excluding a given user. This is
particulary useful for showing the earliest message sent in a thread between two different
users
"""
try:
return self.messages.exclude(sender=user_to_exclude).earliest('sent_at')
except Message.DoesNotExist:
return
def last_message(self):
"""
Returns the latest message of the thread. Is the reverse of the `earliest_message`
"""
try:
return self.messages.all().latest('sent_at')
except Message.DoesNotExist:
return
def last_message_excluding_user(self, user_to_exclude=None):
"""
Returns the latest message of the thread. Is the reverse of the `earliest_message`
:param user_to_exclude: Returns a list of the messages excluding a given user. This is
particulary useful for showing the latest message sent in a thread between two different
users.
"""
queryset = self.messages.all()
try:
if user_to_exclude:
queryset = queryset.exclude(sender=user_to_exclude)
return queryset.latest('sent_at')
except Message.DoesNotExist:
return
def unread_messages(self, user):
"""
Gets the unread messages from User in a given Thread.
Example:
'''
t = Thread.objects.first()
user = User.objects.first()
unread = t.uread_messages(user)
'''
"""
return self.userthread_set.filter(user=user, deleted=False, unread=True, thread=self)
def is_user_first_message(self, user):
"""
Checks if the user started the thread
:return: Bool
"""
try:
message = self.messages.earliest('sent_at')
except Message.DoesNotExist:
return False
return bool(message.sender.pk == user.pk)
UserThread¶
class UserThread(models.Model):
"""Maps the user and the thread. This model was used to override the default ManyToMany
relationship table generated by django.
"""
uuid = models.UUIDField(blank=False, null=False, default=uuid4, editable=False,)
thread = models.ForeignKey(Thread, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
unread = models.BooleanField()
deleted = models.BooleanField()
This model is a substitution of the default generated by ManyToMany of Django.
Message¶
class Message(models.Model):
"""
Message model where creates threads, user threads and mapping between them.
"""
uuid = models.UUIDField(blank=False, null=False, default=uuid4, editable=False)
thread = models.ForeignKey(Thread, related_name="messages", on_delete=models.CASCADE)
sender = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="sent_messages", on_delete=models.CASCADE)
sent_at = models.DateTimeField(default=timezone.now)
content = models.TextField()
Functions¶
@classmethod
def new_reply(cls, thread, user, content):
"""
Create a new reply for an existing Thread. Mark thread as unread for all other participants,
and mark thread as read by replier. We want an atomic operation as we can't afford having
lost data between tables and causing problems with data integrity.
"""
with transaction.atomic():
try:
msg = cls.objects.create(thread=thread, sender=user, content=content)
thread.userthread_set.exclude(user=user).update(deleted=False, unread=True)
thread.userthread_set.filter(user=user).update(deleted=False, unread=False)
message_sent.send(sender=cls, message=msg, thread=thread, reply=True)
except OperationalError as e:
log.exception(e)
return
return msg
@classmethod
def new_message(cls, from_user, to_users, subject, content):
"""
Create a new Message and Thread. Mark thread as unread for all recipients, and
mark thread as read and deleted from inbox by creator. We want an atomic operation as we
also can't afford having lost data between tables and causing problems with data integrity.
"""
with transaction.atomic():
try:
thread = Thread.objects.create(subject=subject)
for user in to_users:
thread.userthread_set.create(user=user, deleted=False, unread=True)
thread.userthread_set.create(user=from_user, deleted=True, unread=False)
msg = cls.objects.create(thread=thread, sender=from_user, content=content)
message_sent.send(sender=cls, message=msg, thread=thread, reply=False)
except OperationalError as e:
log.exception(e)
return
return msg
def get_absolute_url(self):
return self.thread.get_absolute_url()
Tips¶
When creating a new message, the default behavior is calling the new_message
or reply_message
,
depending of the type.