Skip to content

Models

We decided to use UUIDs to make harder to make associations by using it but not using as primary key.


List of Models

  1. Thread
  2. UserThread
  3. Message

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.