Asynchronous log with API and PUSH. Part 1

a1k89

Andrei Koptev

Posted on April 1, 2021

Asynchronous log with API and PUSH. Part 1

One of solution asynchronous log. With Rest api and optional PUSH notification to ios/android devices

Our application consist of 2 parts:

  1. Notifier listen changes from models and fill notification model. With Rest API
  2. Pusher listen changes from Notifier (Notification model) and send push to ios/android device

Requirements

  • Notification model must be available through API (list of notifications for concrete user)
  • Notification model have optional case: PUSH. If yes - send PUSH (ios/android)
  • Our application must be separate from other applications (is like as service)
  • Use only Django signals to fill notification model
  • asynchronous
  • Different icons, title, body and data in each message
  • Notifier work only in one direction: listen changes from models -> create notification in model -> (optional) send push

Important

  • Full code of application here
  • How to install and configure celery here

Part 1: Notifier

For example, we want to create notification and send push in case:

  • New article was created (Article model)
  • New comment was created (ArticleComment model)
  • User A send gift to user B (Gift model)

Ok. Let's get a started!

  1. Firstly, create notifier application inside Django project
  2. Create models.py inside it and add:
class Notification(models.Model):
    uid = models.UUIDField(default=uuid.uuid4,
                           editable=True,
                           unique=True)
    name = models.CharField(max_length=250)
    body = models.CharField(max_length=250)
    recipients = models.ManyToManyField(get_user_model())
    is_notify_screen = models.BooleanField(default=True)
    is_push = models.BooleanField(default=True)
    client_action = models.CharField(choices=ClientAction.choices,
                                     default=ClientAction.NONE,
                                     max_length=250)

    icon = models.CharField(blank=True,
                            max_length=250,
                            choices=Icon.choices,
                            default=Icon.INFO)

Enter fullscreen mode Exit fullscreen mode

3.Register models.
Now we want to register models and connect it with post_save signal.

Our application must be fully separate and work only in one direction: listen changes and create notification.

Inside your app create tasks.py and add:

models = [
    Article,
    ArticleComment,
    Gift,
]

def register_models():
    for model in models:
        post_save.connect(_post_save,
                           sender=model,
                           dispatch_uid=uuid.uuid4())


def _post_save(sender, instance, **kwargs):
    """
    Handler for our post_save.
    PostSaveContext is a namedtuple - context from sync post_save to async post_save handler

    """
    created = kwargs.get( 'created' )
    if not created:
        return

    class_name = instance.__class__.__name__
    action = f'{class_name}_POST_SAVE'.upper()

    post_save_context = PostSaveContext( module=instance.__module__,
                                         class_name=class_name,
                                         instance_identifier=instance.pk,
                                         action=action )

    transaction.on_commit(
        lambda: _async_post_save_handler.delay( post_save_context._asdict() )
    )


@app.task
def _async_post_save_handler(post_save_context):
    perform_action( post_save_context )


def perform_action(post_save_context):
    """
    The core of notifier

    """
    module = post_save_context.get( 'module' )
    class_name = post_save_context.get( 'class_name' )
    instance_identifier = post_save_context.get( 'instance_identifier' )
    action = post_save_context.get( 'action' )

    module = module.split( '.' )[1]
    model = apps.get_model( app_label=module,
                            model_name=class_name )

    instance = model.objects \
        .filter( pk=instance_identifier ) \
        .first()

    if instance is None:
        return
Enter fullscreen mode Exit fullscreen mode

Good!.
Now we may listen creation instances from each model in models and transfer information to _async_post_save_handler.

The next step:fill our notification model

Because we have different content from models, I think the good solution create context fabric (ContextFactory) and pass instance and action to it.

4.Create factory.py file and past to it:

class ContextFactory:
    def __init__(self, instance, action,  *args, **kwargs):
        self.instance = instance
        self.action = action
        self.__dict__.update(kwargs)

    def create_context(self):
        if self.action == NotifierAction.ARTICLE_POST_SAVE.value:
            recipients = self.instance.recipients.all()
            body = self.instance.body
            data = Data(name='New article available',
                        body=body,
                        recipients=recipients,
                        client_action=ClientAction.OPEN_ARTICLE,
                        is_notify_screen=True,
                        is_push=True,
                        icon=Icon.MAIL)

            return data

        if self.action == NotifierAction.ARTICLE_COMMENT_POST_SAVE.value:
            recipients = self.instance.article.recipients.all()
            data = Data(name='New comment',
                        body=self.instance.body,
                        client_action=ClientAction.OPEN_MESSAGE,
                        recipients=recipients,
                        is_notify_screen=True,
                        is_push=True,
                        icon=Icon.INFO)
            return data

        if self.action == NotifierAction.GIFT_POST_SAVE.value:
            recipients = [self.instance.owner]
            data = Data(
                name='Gift for you!',
                body='You got a new gift!',
                recipients=recipients,
                client_action=ClientAction.OPEN_GIFTS,
                is_notify_screen=True,
                is_push=True,
                icon=Icon.GIFT
            )

            return data

Enter fullscreen mode Exit fullscreen mode

Ok. Now we may create notification instance. I like to use Managers for models.
Yep, many developers like to use services to create instance or change it.
But for me Managers is a good place to any model manipulations.

5.Create manager file and past to it:

class NotificationManager(models.Manager):
    def create_notification(self, context: Data):
        context_dict = context._asdict()
        instance = self.create(**context_dict)  # Create notification firstly
        if instance:
            for recipient in context.recipients:
                instance.recipients.add(recipient)  # Add User to m2m

class Notification(models.Model):
    ....
    actions = NotificationManager()

Enter fullscreen mode Exit fullscreen mode

6.Now please add in perform_action method:

def perform_action(post_save_context):
     ...

    factory = ContextFactory(instance, action)
    context = factory.create_context()  # Create context to our model
    Notification.actions.create_notification(context)  # Create new notification
Enter fullscreen mode Exit fullscreen mode

7.Finally, we must to start models listener:

from django.apps import AppConfig

class NotifierConfig(AppConfig):
    name = 'apps.notifier'

    def ready(self):
        from apps.notifier.tasks import register_models
        register_models() # Here!
Enter fullscreen mode Exit fullscreen mode

Run celery, run project and create instance Dialog or Gift.
Now in our Notification model we can see new notifications.

8.Create api python package inside notifier. And add:

  • serializers.py
  • views.py
  • urls.py

views.py for our module:

class NotificationListGenericApiView(generics.ListAPIView):
    """
    Get a list of notifications for a current user with pagination. Readonly

    """
    serializer_class = NotificationSerializer
    queryset = Notification.objects.all()

    def get_queryset(self):
        return Notification\
            .actions\
            .for_user(self.request.user)
Enter fullscreen mode Exit fullscreen mode

...and urls:

urlpatterns = [
    path('', NotificationListGenericApiView.as_view())
]
Enter fullscreen mode Exit fullscreen mode

Now we can to list all notifications for a requested user.

Summary

  1. Our notifier as a separate module and only listen post_save signal from models
  2. Our notifier as scalable. Add new model and add context and it is all
  3. Our notifier has own API
  4. notifier work asynchronous

In the next part I show you pusher - listen changes from notifier and send push notification to mobile devices

💖 💪 🙅 🚩
a1k89
Andrei Koptev

Posted on April 1, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related