NAV
python

Introduction

CircleCi codecov

Django Messages DRF is an alternative and based on pinax-messages but using Django Rest Framework by making it easier to integrate with your existing project. It allows CRUD messages along with inbox and creating threads. Users can reply to messages and mark them as read.

A special thanks to pinax for inspiring me to do this and use some ideas.

Tested, easy to customize, up-to-date with Python 3.10 app that provided private user-to-user threaded messaging.

Supported Django and Python Versions

Django / Python 3.6 3.7 3.8 3.9 3.10
2.2 Yes Yes Yes Yes Yes
3.0 Yes Yes Yes Yes Yes
3.1 Yes Yes Yes Yes Yes
3.2 Yes Yes Yes Yes Yes
4.0 Yes Yes Yes Yes Yes

Installing

In order to install run pip:

pip install django_messages_drf

Then add django_messages_drf to your INSTALLED_APPS:

INSTALLED_APPS = [
  # ...
  "django_messages_drf",
  # ...
]

Run Django migrations to create django-messages-drf database tables:

python manage.py migrate

You'll also want to add django_messages_drf.urls into your main urlpatterns.

urlpatterns = [
    # other urls
    path("api/messages-drf/", include("django_messages_drf.urls", namespace="django_messages_drf")),
]

Remember to use at least Python 3.6

Process of installing uses default pip procedure like other django apps.

URLs

Overview

from django.urls import path
from . import views

# Change app_name when customizing endpoints in your app
app_name = "django_messages_drf"

urlpatterns = [
    path('inbox/', views.InboxListApiView.as_view(), name='inbox'),
    path('message/thread/<uuid>/', views.ThreadListApiView.as_view(), name='thread'),
    path('message/thread/<user_id>/send/', views.ThreadCRUDApiView.as_view(), name='thread-create'),
    path('message/thread/<uuid>/<user_id>/send/', views.ThreadCRUDApiView.as_view(), name='thread-send'),
    path('message/thread/<user_id>/<thread_id>/edit/', views.EditMessageApiView.as_view(), name='message-edit'),
    path('thread/<uuid>/delete', views.ThreadCRUDApiView.as_view(), name='thread-delete'),
]

App provides 6 endpoints with CRUD functionalities.

Inbox

path('inbox/', views.InboxListApiView.as_view(), name='inbox'),

This endpoint retrieves all threads that have been sent to current user (initiated by other users).

HTTP Request

GET http://localhost:8000/api/messages-drf/inbox/

List Messages

path('message/thread/<uuid>/', views.ThreadListApiView.as_view(), name='thread'),

This endpoint retrieves all messages that are within a thread.

Route Parameters

Parameter Required Description
uuid true The UUID of the thread.

HTTP Request

GET http://localhost:8000/api/message/thread/<uuid>/

Send First Message

path('message/thread/<user_id>/send/', views.ThreadCRUDApiView.as_view(), name='thread-create'),

This View can also take another url parameter - thread_uuid (see below)

This endpoint sends a new message to a user by initiating new thread.

Route Parameters

Parameter Required Description
user_id true The id of a user we want to send a message to.

Body Parameters

Parameter Description Method
message The content of the message POST
subject The subject of the message POST

HTTP Request

GET http://localhost:8000/api/messages-drf/message/thread/<user_id>/send/

Expand on thread

path('message/thread/<uuid>/<user_id>/send/', views.ThreadCRUDApiView.as_view(), name='thread-send'),

This is the same View that initiates a thread (see above).

This endpoint sends a reply to an existing message.

Route Parameters

Parameter Required Description
uuid true The id of a thread we want to send a reply to.
user_id true The id of a user we want to send a message to.

Body Parameters

Parameter Description Method
message The content of the message POST
subject The subject of the message POST

HTTP Request

GET http://localhost:8000/api/messages-drf/message/thread/<uuid>/<user_id>/send/

Edit message

path('message/thread/<user_id>/<thread_id>/edit/', views.EditMessageApiView.as_view(), name='message-edit'),

This endpoint edits a message from within a thread.

Route Parameters

Parameter Required Description
thread_id true The id of a message we want to edit.
user_id true The id of a user we want to send a message to.

Body Parameters

Parameter Description Method
message The content of the message POST
subject The subject of the message POST

HTTP Request

GET http://localhost:8000/api/messages-drf/message/thread/<user_id>/<thread_id>/edit/

Views

Django Messages DRF comes initially with a set of views that allows you to apply in your projects. All the views are in Django Rest Framework and allowing customization up to a certain level.

All of the serializers are provided by the settings and allows overriding from there.

InboxListApiView

The main view for an inbox of a user where return an ordered list from the latest received to the first.

class InboxListApiView(DjangoMessageDRFAuthMixin, RequireUserContextView, ListAPIView):
    """
    Returns the Inbox the logged in User
    """
    serializer_class = InboxSerializer
    pagination_class = Pagination

    def get_queryset(self):
        queryset = Thread.inbox(self.request.user)
        return Thread.ordered(queryset)

Tips

We use a custom Pagination object that adds some more details to the default Django Pagination. You can have your own pagination object and override the default.

# Custom Pagination Applied to the view

from rest_framework import pagination

from django_messages_drf.views import InboxListApiView

class MyCustomPagination(pagination.PageNumberPagination):
  # Add custom pagination logic


class MyInboxListApiView(InboxListApiView):
  pagination_class = MyCustomPagination

You can also override the serializer_class default using the same principle.

# Custom Pagination Applied to the view

from rest_framework import serializers

from django_messages_drf.views import InboxListApiView

class MyCustomSerializer(serializers.ModelSerializer):
  # Add custom serializer logic


class MyInboxListApiView(InboxListApiView):
  serializer_class = MyCustomSerializer

Or combining both pagination and serializer_class in one.

# Custom Pagination Applied to the view

from rest_framework import pagination
from rest_framework import serializers

from django_messages_drf.views import InboxListApiView

class MyCustomPagination(pagination.PageNumberPagination):
  # Add custom pagination logic

class MyCustomSerializer(serializers.ModelSerializer):
  # Add custom serializer logic


class MyInboxListApiView(InboxListApiView):
  serializer_class = MyCustomSerializer
  pagination_class = MyCustomPagination

ThreadListApiView

class ThreadListApiView(DjangoMessageDRFAuthMixin, ThreadMixin, RequireUserContextView, ListAPIView):
    """
    Gets all the messages from a given thread
    """
    serializer_class = ThreadSerializer

    def get(self, request, *args, **kwargs):
        instance = self.get_thread()
        if not instance:
            raise NotFound(code=status.HTTP_404_NOT_FOUND)

        serializer = self.serializer_class(instance, context=self.get_serializer_context())
        return Response(serializer.data, status=status.HTTP_200_OK)

Tips

The same logic for ThreadListApiView is the same applied for InboxListApiView by overriding the default serializer_class.

# Custom Pagination Applied to the view

from rest_framework import serializers

from django_messages_drf.views import ThreadListApiView

class MyCustomSerializer(serializers.ModelSerializer):
  # Add custom serializer logic


class MyThreadListApiView(ThreadListApiView):
  serializer_class = MyCustomSerializer

ThreadCRUDApiView

class ThreadCRUDApiView(DjangoMessageDRFAuthMixin, ThreadMixin, RequireUserContextView, APIView):
    """
    View that allows the reply of a specific message as well as the
    We will apply some pagination to return a list for the results and therefore

    1. This API gets or creates the Thread
    2. If a UUID is passed, then a Thread is validated and created but if only a user_id is
    passed, then it will create a new thread and start a conversation.
    """
    serializer_class = ThreadReplySerializer

    def post(self, request, uuid=None, *args, **kwargs):
        """
        Replies a mensage in given thread
        """
        thread = self.get_thread() if uuid else None
        user = self.get_user()

        if not user:
            raise NotFound(code=status.HTTP_404_NOT_FOUND)

        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)

        subject = request.data.get('subject') or thread.subject
        if not thread:
            msg = Message.new_message(
                from_user=self.request.user, to_users=[user], subject=subject,
                content=request.data.get('message')
            )

        else:
            msg = Message.new_reply(thread, self.request.user, request.data.get('message'))
            thread.subject = subject
            thread.save()

        message = MessageSerializer(msg, context=self.get_serializer_context())
        return Response(message.data, status=status.HTTP_200_OK)

    def delete(self, request, *args, **kwargs):
        """
        Flags a thread as deleted a thread from the system.
        To remove completely, another permanent view can be added to execute the action.
        """
        thread = self.get_thread()
        if not thread:
            raise NotFound(code=status.HTTP_404_NOT_FOUND)

        thread.userthread_set.filter(user=request.user).update(deleted=True)
        return Response(status=status.HTTP_200_OK)

Tips

The same logic for ThreadCRUDApiView is the same applied for InboxListApiView by overriding the default serializer_class.

# Custom Pagination Applied to the view

from rest_framework import serializers

from django_messages_drf.views import ThreadCRUDApiView

class MyCustomSerializer(serializers.ModelSerializer):
  # Add custom serializer logic


class MyThreadCRUDApiView(ThreadCRUDApiView):
  serializer_class = MyCustomSerializer

EditMessageApiView

class EditMessageApiView(DjangoMessageDRFAuthMixin, ThreadMixin, RequireUserContextView, APIView):
    """
    Edits a message sent from a user in a given thread
    """
    serializer_class = EDIT_MESSAGE_SERIALIZER

    def get_instance(self, user, message_uuid):
        """
        Checks of the message exists
        """
        try:
            return Message.objects.get(sender=user, uuid=message_uuid)
        except Message.DoesNotExist:
            return

    def get_serializer_context(self):
        context = super().get_serializer_context()
        context.update({
            'thread': self.get_thead_by_id(),
        })
        return context

    def put(self, request, user_id, thread_id, *args, **kwargs):
        """
        Edits a mensage in given thread.

        1. Gets the user_id from the URL.
        2. From the request.data gets the uuid of the message
        3. Validates
        4. Saves and returns
        """
        user = self.get_user()

        if not user:
            raise NotFound()

        if (not user.pk == request.user.pk):
            raise PermissionDenied()

        # Get the instance of the message for a given user
        instance = self.get_instance(user, request.data.get('uuid'))
        if not instance:
            raise NotFound()

        serializer = self.serializer_class(instance, data=request.data, context=self.get_serializer_context())
        serializer.is_valid(raise_exception=True)
        instance = serializer.save()

        message = MessageSerializer(instance, context=self.get_serializer_context())
        return Response(message.data, status=status.HTTP_200_OK)

General Tip

  1. The views follow a similar structure and design everywhere but they can also be overwritten in a normal Django way.
  2. Checkout the settings page to see how to override the variables.

Mixins

Mixins are a super useful tool when it comes to apply the DRY principles or share functionalities across the platform.

RequireUserContextView

A simplification of a get_serializer_context that can be applied on every serializer that needs the user in the context.

class RequireUserContextView(GenericAPIView):
    """
    Handles with Generics of views
    """
    def get_serializer(self, *args, **kwargs):
        """
        Return the serializer instance that should be used for validating and
        deserializing input, and for serializing output.
        """
        serializer_class = self.get_serializer_class()
        kwargs['context'] = self.get_serializer_context()
        return serializer_class(*args, **kwargs)

    def get_serializer_context(self):
        context = super().get_serializer_context()
        context.update({
            'request': self.request,
            'user': self.request.user,
        })
        return context

ThreadMixin

All things common to a thread.

class ThreadMixin:
    """
    Everything related with a thread, is placed here.
    """
    def get_thread(self):
        """Gets the thread"""
        try:
            return Thread.objects.get(uuid=self.kwargs.get('uuid'))
        except Thread.DoesNotExist:
            return

    def get_user(self):
        """Gets a User to whom which the message will be sent"""
        try:
            return get_user_model().objects.get(pk=self.kwargs.get('user_id'))
        except get_user_model().DoesNotExist:
            return

    def get_thead_by_id(self):
        """Gets a thread by id"""
        try:
            return Thread.objects.get(id=self.kwargs.get('thread_id'))
        except Thread.DoesNotExist:
            return

CurrentThreadDefault

Similar to CurrentThreadDefault, this mixin allows a similar behaviour to be injected into the serializer fields as long as the thread is passed into the context.

class CurrentThreadDefault:
    requires_context = True

    def __call__(self, serializer_field):
        return serializer_field.context['thread']

    def __repr__(self):
        return '%s()' % self.__class__.__name__

Examples

# serializers.py
from django_messages.drf.mixins import CurrentThreadDefault


class MessageSerializer(serializers.ModelSerializer):
    uuid = serializers.UUIDField(required=True)
    subject = serializers.CharField(required=True)
    content = serializers.CharField(
        required=True, allow_null=False, allow_blank=False, error_messages={
            'blank': _("The message cannot be empty"),
        }
    )
    sender = serializers.HiddenField(default=serializers.CurrentUserDefault())
    thread = serializers.HiddenField(default=CurrentThreadDefault())

Models

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

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.

Pagination

Two custom pagination classes are provided for the application. The information was gathered from here.

Pagination class

class Pagination(pagination.PageNumberPagination):
    """
    Custom paginator for REST API responses
    'links': {
               'next': next page url,
               'previous': previous page url
            },
            'count': number of records fetched,
            'total_pages': total number of pages,
            'next': bool has next page,
            'previous': bool has previous page,
            'results': result set
    })
    """

    def get_paginated_response(self, data):
        return Response({
            'links': {
                'next': self.get_next_link(),
                'previous': self.get_previous_link()
            },
            'pagination': {
                'previous_page': self.page.number - 1 if self.page.number != 1 else None,
                'current_page': self.page.number,
                'next_page': self.page.number + 1 if self.page.has_next() else None,
                'page_size': self.page_size
            },
            'count': self.page.paginator.count,
            'total_pages': self.page.paginator.num_pages,
            'next': self.page.has_next(),
            'previous': self.page.has_previous(),
            'results': data
        })

SimplePagination

class SimplePagination(pagination.PageNumberPagination):
    """
    Custom paginator for REST API responses
    """
    def get_paginated_response(self, data):
        return Response({
            'records_filtered': self.page.paginator.count,
            'data': data
        })

Permissions

A small set of permissions are set in the app to make sure the data is safer and secure and those can be also extended.

AccessMixin

Base class of all permission mixins of Django Messages DRF. Adds an extension for the permissions of Django Rest Framework where you can now append into a list instead of repeating on every class.

class AccessMixin(metaclass=DjangoMessageDRFAuthMeta):
    """
    Django rest framework doesn't append permission_classes on inherited models which can cause
    issues when it comes to call an API programmatically, this way we create a metaclass that will
    read from a property custom from our subclasses and will append to the default
    `permission_classes` on the subclasses of AccessMixin.
    """
    pass

DjangoMessageDRFAuthMixin

Base class of all views of the application and sets the principle that every view inheriting from this will validate the user authentication.

class DjangoMessageDRFAuthMixin(AccessMixin, APIView):
    """
    Base APIView requiring login credentials to access it from the inside of the platform
    Or via request (if known)
    """
    permissions = [IsAuthenticated]
    pagination_class = None

    def __init__(self, *args, **kwargs) -> None:
        """
        Checks if the views contain the `permissions` attribute and overrides the
        `permission_classes`.
        """
        super().__init__(*args, **kwargs)
        self.permission_classes = self.permissions
        if self.pagination_class:
            try:
                rest_settings = settings.REST_FRAMEWORK
            except AttributeError:
                rest_settings = {}
            page_size = rest_settings.get('PAGE_SIZE', 50)
            self.pagination_class.page_size = page_size

Examples

Using the DjangoMessageDRFAuthMixin as a base we can now start creating our own views without thinking about replicating the permission_classes.

With DjangoMessageDRFAuthMixin

from rest_framework.views import APIView

from django_messages_drf.permissions import DjangoMessageDRFAuthMixin
from my_app.permissions import MyPermission


class MyCustomView(DjangoMessageDRFAuthMixin, APiView):
  """
  My Custom view that will do things
  """
  permissions = [MyPermission]

Importing the APIView is optional since the DjangoMessageDRFAuthMixin already implements it.

Behind the scenes, Django Messages DRF is appending the permissions to permission_classes of Django Rest Framework, which means that if we query for the permission_classes we would have:

permission_classes = [IsAuthenticated, MyPermission]

Without DjangoMessageDRFAuthMixin

from rest_framework.views import APIView

from rest_framework.permissions import IsAuthenticated
from my_app.permissions import MyPermission


class BaseView(APiView):
  permission_classes = [IsAuthenticated]


class MyCustomView(BaseView):
  """
  My Custom view that will do things
  """
  permission_classes = [MyPermission]

This won't have the same result as the DjangoMessageDRFAuthMixin because what is doing is actually reassigning the permission_classes from the BaseView to the MyCustomView.

Serializers

Django Messages DRF like with the views, also comes with a set of serializers that allows you to apply in your project but you can and should build your own with your own use cases.

The way the serializers are built are the default ones from Django Rest Framework.

Inbox

A simple example for an inbox serializer.

class InboxSerializer(serializers.ModelSerializer):
    """
    Serializer for the inbox.
    """
    sent_at = serializers.DateTimeField(source='first_message.sent_at')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.useruser = self.context.get('user')

    class Meta:
        model = Thread
        fields = ('uuid', 'subject', 'sent_at')

Sender

A sender for Django Messages DRF is a Django user and can be whatever you decided that u.

class SenderSerializer(serializers.ModelSerializer):
    class Meta:
        model = get_user_model()
        fields = ('first_name', 'last_name', 'email')

Serializer Settings

Django Messages DRF allows overriding some settings for the views, which means, instead of creating a new view just to apply your own serializer, you can simply override the setting and Django Messages DRF will apply it on the current views.

None of the below settings are required to be added to your settings.py. This is only if you wish to override the current defaults.

Overriding

In your settings.py.

Setting Name View Default
DJANGO_MESSAGES_DRF_INBOX_SERIALIZER InboxListApiView InboxSerializer
DJANGO_MESSAGES_DRF_THREAD_SERIALIZER ThreadListApiView ThreadSerializer
DJANGO_MESSAGES_DRF_MESSAGE_SERIALIZER ThreadCRUDApiView ThreadReplySerializer
DJANGO_MESSAGES_DRF_EDIT_MESSAGE_SERIALIZER EditMessageApiView EditMessageSerializer

Usage

Overriding is based on import_string from your settings.py.

Examples

# settings.py

DJANGO_MESSAGES_DRF_INBOX_SERIALIZER = 'myapp.serializers.MyCustomInboxSerializer'
DJANGO_MESSAGES_DRF_THREAD_SERIALIZER = 'myapp.serializers.MyCustomThreadSerializer'

If none of the settings is overridden or is None , then it will default to the original.

Behaviour Settings

Django Messages DRF allows overriding some behaviours.

Overriding

In your settings.py.

Setting Name Behaviour Type Default
DJANGO_MESSAGES_MARK_NEW_THREAD_MESSAGE_AS_DELETED Mark the first message sent as deleted Boolean True

Utils

Some useful utils are provided with the project to make it easier to reuse across.

AuditModel

class AuditModel(models.Model):
    """A common audit model for tracking"""
    created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True)
    modified_at = models.DateTimeField(null=False, blank=False, auto_now=True)

Adding the AuditModel to a model will add an audit trailing to it making it easier to filter by dates.

This can be extended and add more information such as created_by or modified_by where those are users of the application.

Signals

Message sent

We only provide one signal at the moment. This signal fires off after every message.

message_sent = Signal(providing_args=["message", "thread", "reply"])

Release Notes

1.0.6

1.0.5

1.0.4

1.0.3

Added

Fixed

1.0.2

Added

Fixed

Updated

1.0.0

License

Copyright (c) 2020-present Tiago Silva and contributors under the MIT license.

Errors

Mainterners:

Tarsil