Andrei Koptev
Posted on April 1, 2021
One of solution asynchronous log. With Rest api and optional PUSH notification to ios/android devices
Our application consist of 2 parts:
-
Notifier
listen changes from models and fill notification model. With Rest API -
Pusher
listen changes fromNotifier
(Notification model) and send push to ios/android device
Requirements
-
Notification
model must be available throughAPI
(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
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 userB
(Gift
model)
Ok. Let's get a started!
- Firstly, create
notifier
application inside Django project - 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)
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
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
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()
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
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!
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)
...and urls:
urlpatterns = [
path('', NotificationListGenericApiView.as_view())
]
Now we can to list all notifications for a requested user.
Summary
- Our
notifier
as a separate module and only listenpost_save
signal from models - Our
notifier
as scalable. Add new model and addcontext
and it is all - Our
notifier
has ownAPI
-
notifier
work asynchronous
In the next part I show you pusher
- listen changes from notifier
and send push notification
to mobile devices
Posted on April 1, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.