Asynchronous log with API and PUSH. Part 1
Tags: django, python
Published: 2021-04-01 11:00:49

### 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](https://github.com/a1k89/notifier-demo)
* How to install and configure celery [here](https://dev.to/a1k89/how-to-install-and-configure-celery-for-django-4o8e)

#### 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:
```python
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)

```

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:
```python
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
```

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:
```python
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

```
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:
```python
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()

```

6.Now please add in  `perform_action` method:
```python
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
```

7.Finally, we must to start models listener:
```python
from django.apps import AppConfig

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

    def ready(self):
        from apps.notifier.tasks import register_models
        register_models() # Here!
```

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:

```python
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)
```

...and urls:
```python
urlpatterns = [
    path('', NotificationListGenericApiView.as_view())
]
```

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