Filtered model observer
Subscribing to a filtered list of models.
Introduction
In this first example, we will create a User
model with a Comment
related model,
create the serializers for each one. And create a Consumer
for the User
model, with
a model observer method for watching all changes of the current user.
Creating models.
We will have the following models.py
file, with a User
model, and a Comment
model that is related to the user.
# models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
class Comment(models.Model):
text = models.TextField()
user = models.ForeignKey(User, related_name="comments", on_delete=models.CASCADE)
date = models.DatetimeField(auto_now_add=True)
Creating the serializers.
In the serializers.py
file, we will have the serializers for the models in the models.py
file.
# serializers.py
from rest_framework import serializers
from .models import User, Comment
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email"]
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ["id", "text", "user"]
Creating the consumers.
Now in the consumers.py
file, we will create or
websocket consumer for the users, with a model
observer method for the Comment
model, filtered for the current user.
These are the important methods of the class.
A method, called
comment_activity
decorated with themodel_observer
decorator and as argument we will add theComment
model.A
subscribe_to_comment_activity
action
to subscribe themodel_observer
method.A method (it can be named the same as the
model_observer
method) decorated with the@comment_activity.serializer
, this will return the serializer based on the instance.
Warning
The user must be logged to subscribe this method, because we will access the self.scope["user"]
# consumers.py
from djangochannelsrestframework.consumers import GenericAsyncAPIConsumer
from djangochannelsrestframework.observer import model_observer
from djangochannelsrestframework.decorators import action
from .serializers import UserSerializer, CommentSerializer
from .models import User, Comment
class MyConsumer(GenericAsyncAPIConsumer):
queryset = User.objects.all()
serializer_class = UserSerializer
@model_observer(Comment)
async def comment_activity(
self,
message: CommentSerializer,
observer=None,
subscribing_request_ids=[],
**kwargs
):
await self.send_json(message.data)
@comment_activity.serializer
def comment_activity(self, instance: Comment, action, **kwargs) -> CommentSerializer:
"""This will return the comment serializer"""
return CommentSerializer(instance)
@comment_activity.groups_for_signal
def comment_activity(self, instance: Comment, **kwargs):
# this block of code is called very often *DO NOT make DB QUERIES HERE*
yield f'-user__{instance.user_id}' #! the string **user** is the ``Comment's`` user field.
@comment_activity.groups_for_consumer
def comment_activity(self, school=None, classroom=None, **kwargs):
# This is called when you subscribe/unsubscribe
yield f'-user__{self.scope["user"].pk}'
@action()
async def subscribe_to_comment_activity(self, request_id, **kwargs):
# We will check if the user is authenticated for subscribing.
if "user" in self.scope and self.scope["user"].is_authenticated:
await self.comment_activity.subscribe(request_id=request_id)
Note
If the user is not logged in, we will have to access the user
using the pk or any other
unique field.
Example:
...
class MyConsumer(GenericAsyncAPIConsumer):
...
@action()
async def subscribe_to_comment_activity(self, user_pk, **kwargs):
# We will check if the user is authenticated for subscribing.
user = await database_sync_to_async(User.objects.get)(pk=user_pk)
await self.comment_activity.subscribe(user=user)
Manual testing the output.
Now we will have a websocket client in javascript listening to the messages, after subscribing to the comment activity. This code block can be used in the browser console.
Note
- In production the
ws:
iswss:
, we can check it with the following code: const ws_schema = window.location.protocol === "http:" ? "ws:" : "wss:";
const ws = new WebSocket("ws://localhost:8000/ws/my-consumer/")
const ws.onopen = function(){
ws.send(JSON.stringify({
action: "subscribe_to_comment_activity",
request_id: new Date().getTime(),
}))
}
const ws.onmessage = function(e){
console.log(e)
}
Note
- The subscribe method doesn’t require being logged:
const ws = new WebSocket("ws://localhost:8000/ws/my-consumer/") const ws.onopen = function(){ ws.send(JSON.stringify({ action: "subscribe_to_comment_activity", request_id: new Date().getTime(), user_pk: 1, // This field is the argument in the // subscribe method, and the pk correspond to the user. })) } const ws.onmessage = function(e){ console.log(e) }
In the IPython shell we will create some comments for different users and in the browser console we will see the log.
Warning
At this point we should have some users in our database, otherwise create them.
We will create a comment using the user_1
and we will see the log in the browser console.
>>> from my_app.models import User, Comment
>>> user_1 = User.objects.get(pk=1)
>>> user_2 = User.objects.get(pk=2)
>>> Comment.objects.create(text="user 1 creates a new comment", user=user_1)
In the console log we will se something like this:
{
action: "subscribe_to_comment_activity",
errors: [],
response_status: 200,
request_id: 15606042,
data: {
id: 1,
text: "user 1 creates a new comment",
user: 1
}
}
Now we will create a comment with the user_2
.
>>> Comment.objects.create(text="user 2 creates a second comment", user=user_2)
In the console log we will see nothing, because this comment was created by the user_2
.
Conclusions
In this example we subscribe to the filtered instances of the comment model.