Welcome to djangochannelsrestframework’s documentation!¶
Introduction¶
Django Channels Rest Framework¶
Django Channels Rest Framework provides a DRF like interface for building channels-v3 websocket consumers.
This project can be used alongside HyperMediaChannels and ChannelsMultiplexer to create a Hyper Media Style api over websockets. However Django Channels Rest Framework is also a free standing framework with the goal of providing an api that is familiar to DRF users.
theY4Kman has developed a useful Javascript client library dcrf-client to use with DCRF.
Installation¶
pip install djangochannelsrestframework
Thanks to¶
DCRF is based of a fork of Channels Api and of course inspired by Django Rest Framework.
Introduction¶
Django Channels Rest Framework¶
Django Channels Rest Framework provides a DRF like interface for building channels-v3 websocket consumers.
This project can be used alongside HyperMediaChannels and ChannelsMultiplexer to create a Hyper Media Style api over websockets. However Django Channels Rest Framework is also a free standing framework with the goal of providing an api that is familiar to DRF users.
theY4Kman has developed a useful Javascript client library dcrf-client to use with DCRF.
Installation¶
pip install djangochannelsrestframework
Thanks to¶
DCRF is based of a fork of Channels Api and of course inspired by Django Rest Framework.
Examples¶
This is a collection of examples using the djangochannelsrestframework library.
Generic Api Consumer¶
In DCRF you can create a GenericAsyncAPIConsumer that works much like a GenericAPIView in DRF: For a more indepth look into Rest Like Websocket consumers read this blog post.
We have a set of mixins for the consumer, that add different actions based on the CRUD operations.
ListModelMixin
this mixin adds the actionlist
, allows to retrieve all instances of a model class.RetrieveModelMixin
this mixin adds the actionretrieve
allows to retrieve an object based on the pk sent.PatchModelMixin
this mixin adds the actionpatch
, allows to patch an instance of a model.UpdateModelMixin
this mixin adds the actionupdate
, allows to update a model instance.CreateModelMixin
this mixin adds the actioncreate
, allows to create an instance based on the data sent.DeleteModelMixin
this mixin adds the actiondelete
, allows to delete an instance based on the pk sent.
Example¶
This example shows how to create a basic consumer for the django’s auth user model. We
are going to create a serializer class for it, and mixin with the GenericAsyncAPIConsumer
the action mixins.
# serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
class UserSerilizer(serailizers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email", "password"]
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
user = User(
email=validated_data['email'],
username=validated_data['username']
)
user.set_password(validated_data['password'])
user.save()
return user
# consumers.py
from django.contrib.auth.models import User
from .serializers import UserSerilizer
from djangochannelsrestframework.generics import GenericAsyncAPIConsumer
from djangochannelsrestframework.mixins import (
ListModelMixin,
RetrieveModelMixin,
PatchModelMixin,
UpdateModelMixin,
CreateModelMixin,
DeleteModelMixin,
)
class UserConsumer(
ListModelMixin,
RetrieveModelMixin,
PatchModelMixin,
UpdateModelMixin,
CreateModelMixin,
DeleteModelMixin,
GenericAsyncAPIConsumer,
):
queryset = User.objects.all()
serializer_class = UserSerilizer
# routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r"^ws/$", consumers.UserConsumer.as_asgi()),
]
How to use it¶
First we will create the web socket instance in javascript
.
const ws = new WebSocket("ws://localhost:8000/ws/")
ws.onmessage = function(e){
console.log(e)
}
Note
We must have a few users in our database for testing, if not, create them.
ws.send(JSON.stringify({
action: "list",
request_id: new Date().getTime(),
}))
/* The return response will be something like this.
{
"action": "list",
"errors": [],
"response_status": 200,
"request_id": 1550050,
"data": [
{'email': '1@example.com', 'id': 1, 'username': 'test 1'},
{'email': '2@example.com', 'id': 2, 'username': 'test 2'},
{'email': '3@example.com', 'id': 3, 'username': 'test 3'},
]
}
*/
ws.send(JSON.stringify({
action: "retrieve",
request_id: new Date().getTime(),
pk: 2,
}))
/* The return response will be something like this.
{
"action": "retrieve",
"errors": [],
"response_status": 200,
"request_id": 1550050,
"data": {'email': '2@example.com', 'id': 2, 'username': 'test 2'},
}
*/
ws.send(JSON.stringify({
action: "patch",
request_id: new Date().getTime(),
pk: 2,
data: {
email: "edited@example.com",
}
}))
/* The return response will be something like this.
{
"action": "patch",
"errors": [],
"response_status": 200,
"request_id": 1550050,
"data": {'email': 'edited@example.com', 'id': 2, 'username': 'test 2'},
}
*/
ws.send(JSON.stringify({
action: "update",
request_id: new Date().getTime(),
pk: 2,
data: {
username: "user 2",
}
}))
/* The return response will be something like this.
{
"action": "update",
"errors": [],
"response_status": 200,
"request_id": 1550050,
"data": {'email': 'edited@example.com', 'id': 2, 'username': 'user 2'},
}
*/
ws.send(JSON.stringify({
action: "create",
request_id: new Date().getTime(),
data: {
username: "new user 4",
password1: "testpassword123",
password2: "testpassword123",
email: "4@example.com",
}
}))
/* The return response will be something like this.
{
"action": "create",
"errors": [],
"response_status": 201,
"request_id": 1550050,
"data": {'email': '4@example.com', 'id': 4, 'username': 'new user 4'},
}
*/
ws.send(JSON.stringify({
action: "delete",
request_id: new Date().getTime(),
pk: 4,
}))
/* The return response will be something like this.
{
"action": "delete",
"errors": [],
"response_status": 204,
"request_id": 1550050,
"data": null,
}
*/
Custom actions¶
Consumer that aren’t bound to a Model.¶
We may want a consumer for handling certain actions that are not referred to any Django model. Maybe for
fetching data from an external api service, using requests
library or another async request lib.
# consumers.py
from djangochannelsrestframework.decorators import action
from djangochannelsrestframework.consumers import AsyncAPIConsumer
from rest_framework import status
class MyConsumer(AsyncAPIConsumer):
@action()
async def an_async_action(self, some=None, **kwargs):
# do something async
return {'response with': 'some message'}, status.HTTP_RESPONSE_OK
@action()
def a_sync_action(self, pk=None, **kwargs):
# do something sync
return {'response with': 'some message'}, status.HTTP_RESPONSE_OK
Consumer that is bound to a Model.¶
Inheritating from GenericAsyncAPIConsumer
we have access to methods like get_queryset
and get_object
,
this way we can perform operations in our django models though custom actions.
# serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
class UserSerilizer(serailizers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email", "password"]
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
user = User(
email=validated_data['email'],
username=validated_data['username']
)
user.set_password(validated_data['password'])
user.save()
return user
# consumers.py
from django.contrib.auth.models import User
from .serializers import UserSerilizer
from djangochannelsrestframework.generics import GenericAsyncAPIConsumer
from djangochannelsrestframework.decorators import action
class UserConsumer(GenericAsyncAPIConsumer):
queryset = User.objects.all()
serializer_class = UserSerilizer
@action()
async def send_email(self, pk=None, to=None, **kwargs):
user = await database_sync_to_async(self.get_object)(pk=pk)
# ... do some stuff
# remember to wrap all db actions in `database_sync_to_async`
return {}, 200 # return the contenct and the response code.
@action() # if the method is not async it is already wrapped in `database_sync_to_async`
def publish(self, pk=None, **kwargs):
user = self.get_object(pk=pk)
# ...
return {'pk': pk}, 200
Todo¶
TODO more detail example for fetching data from external API
Observer model instance¶
This mixin consumer lets you subscribe to all changes of a specific instance, and
also gives you access to the retrieve
action.
# serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
class UserSerilizer(serailizers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email", "password"]
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
user = User(
email=validated_data['email'],
username=validated_data['username']
)
user.set_password(validated_data['password'])
user.save()
return user
# consumers.py
from django.contrib.auth.models import User
from .serializers import UserSerilizer
from djangochannelsrestframework.generics import GenericAsyncAPIConsumer
from djangochannelsrestframework.observer import ObserverModelInstanceMixin
class UserConsumer(ObserverModelInstanceMixin, GenericAsyncAPIConsumer):
queryset = User.objects.all()
serializer_class = UserSerilizer
# routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r"^ws/$", consumers.UserConsumer.as_asgi()),
]
How to use it¶
First we will create the web socket instance in javascript
.
const ws = new WebSocket("ws://localhost:8000/ws/")
ws.onmessage = function(e){
console.log(e)
}
Note
We must have a few users in our database for testing, if not, create them.
Retrieve action.¶
ws.send(JSON.stringify({
action: "retrieve",
request_id: new Date().getTime(),
pk: 1,
}))
/* The return response will be something like this.
{
"action": "list",
"errors": [],
"response_status": 200,
"request_id": 1550050,
"data": {'email': '1@example.com', 'id': 1, 'username': 'test 1'},
}
*/
Subscription¶
Subscribe to a specific instance.
ws.send(JSON.stringify({
action: "retrieve",
request_id: new Date().getTime(),
pk: 1,
}))
/* After subscribing the response will be something like this.
{
"action": "subscribe_instance",
"errors": [],
"response_status": 201,
"request_id": 1550050,
"data": null,
}
*/
Changing the model instance in from the shell will fire the subscription event.
>>> from django.contrib.auth.models import User
>>> user = User.objects.get(pk=1)
>>> user.username = "edited user name"
>>> user.save()
After saving the model instance, in the console, we will see the subscription message.
{
"action": "update",
"errors": [],
"response_status": 200,
"request_id": 1550050,
"data": {'email': '1@example.com', 'id': 1, 'username': 'edited user name'},
}
Todo¶
More detail example.
View as consumer¶
Introduction¶
Suppose we already have a functional API that uses Django Rest Framework, and we
want to add some websocket functionality. We can use the view_as_consumer
decorator for accessing the same REST
methods.
Creating the serializers.¶
# serializers.py
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email"]
Creating the view sets.¶
# views.py
from rest_framework.viewsets import ModelViewSet
from django.contrib.auth.models import User
from .serializers import UserSerializer
class UserViewSet(ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
Routing the consumer¶
Using the same UserViewSet
we can map some basic websocket actions
to the REST methods. The mapped actions are:
create
-PUT
update
-PATCH
list
-GET
retrieve
-GET
# routing.py
from django.urls import re_path
from djangochannelsrestframework.consumers import view_as_consumer
from .views import UserViewSet
websocket_urlpatterns = [
re_path(r"^user/$", view_as_consumer(UserViewSet.as_view()))
]
Manual testing the output.¶
Now we will have a websocket client in javascript listening to the messages, after subscribing to the comment activity. This codeblock 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/user/")
const ws.onopen = function(){
ws.send(JSON.stringify({
action: "list",
request_id: new Date().getTime(),
}))
}
const ws.onmessage = function(e){
console.log(e)
}
Warning
At this point we should have some users in our database, otherwise create them
In the console we will have the following response assuming that we have some users in our database.
{
error: [],
data: [
{username: "user 1", id: 1, email: "1@example.com"},
{username: "user 2", id: 2, email: "2@example.com"},
],
action: "list",
response_status: 200,
request_id: 15050500
}
Model observer¶
Subscribing to all instances of a model.¶
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 all comment instances.
Creating models.¶
We will have the following models.py
file, with a user model, and a comment models 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 all instances of the Comment
model.
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.
# 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(Comments)
async def comment_activity(self, message: CommentSerializer, observer=None, **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)
@action()
async def subscribe_to_comment_activity(self, **kwargs):
await self.comment_activity.subscribe()
Manual testing the output.¶
Now we will have a websocket client in javascript listening to the messages, after subscribing to the comment activity. This codeblock 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)
}
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 ùser_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 see 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 something like this:
{
action: "subscribe_to_comment_activity",
errors: [],
response_status: 200,
request_id: 15606042,
data: {
id: 2,
text: "user 2 creates a second comment",
user: 2,
},
}
Conclusions¶
In this example we subscribed to all instances of the comment model, in the next section we will see how to filter them.
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 models 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(Comments)
async def comment_activity(self, message: CommentSerializer, observer=None, **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, **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()
Note
Without logging 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 codeblock can be used in the browser console.
Note
- In producction 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.
Consumers¶
- class djangochannelsrestframework.consumers.APIConsumerMetaclass(name, bases, body)[source]¶
Metaclass that records action methods
- class djangochannelsrestframework.consumers.AsyncAPIConsumer(*args, **kwargs)[source]¶
Be very inspired by django rest framework ViewSets
- async check_permissions(action: str, **kwargs)[source]¶
Check if the action should be permitted. Raises an appropriate exception if the request is not permitted.
- async get_permissions(action: str, **kwargs)[source]¶
Instantiates and returns the list of permissions that this view requires.
- djangochannelsrestframework.consumers.view_as_consumer(wrapped_view: Callable[[django.http.request.HttpRequest], django.http.response.HttpResponse], mapped_actions: Optional[Dict[str, str]] = None) → djangochannelsrestframework.consumers.DjangoViewAsConsumer[source]¶
- Wrap a django View so that it will be triggered by actions over this json
websocket consumer.
Generics¶
- class djangochannelsrestframework.generics.GenericAsyncAPIConsumer(*args, **kwargs)[source]¶
Base class for all other generic views.
- queryset¶
will be accesed when the method get_queryset is called.
- serializer_class¶
it should correspond with the queryset model, it will be useded for the return response.
- lookup_field¶
field used in the get_object method. Optional.
- lookup_url_kwarg¶
url parameter used it for the lookup.
- filter_queryset(queryset: django.db.models.query.QuerySet, **kwargs) → django.db.models.query.QuerySet[source]¶
Given a queryset, filter it with whichever filter backend is in use.
You are unlikely to want to override this method, although you may need to call it either from a list view, or from a custom get_object method if you want to apply the configured filtering backend to the default queryset.
- Parameters
queryset – cached queryset to filter.
kwargs – keyworded dictionary arguments.
- Returns
Filtered queryset.
- Todos:
Implement
- get_object(**kwargs) → django.db.models.base.Model[source]¶
Returns the object the view is displaying.
You may want to override this if you need to provide non-standard queryset lookups. Eg if objects are referenced using multiple keyword arguments in the url conf.
- Parameters
kwargs – keyworded dictionary, it can be use it for filtering the queryset.
- Returns
Model
object class.
Examples
>>> filtered_queryset = self.get_object(**{"field__gte": value}) # this way you could filter from the frontend.
- get_queryset(**kwargs) → django.db.models.query.QuerySet[source]¶
Get the list of items for this view. This must be an iterable, and may be a queryset. Defaults to using self.queryset.
This method should always be used rather than accessing self.queryset directly, as self.queryset gets evaluated only once, and those results are cached for all subsequent requests.
You may want to override this if you need to provide different querysets depending on the incoming request.
(Eg. return a list of items that is specific to the user)
- Parameters
kwargs – keyworded dictionary.
- Returns
Queryset attribute.
- get_serializer(action_kwargs: Optional[Dict] = None, *args, **kwargs) → rest_framework.serializers.Serializer[source]¶
Return the serializer instance that should be used for validating and deserializing input, and for serializing output.
- Parameters
action_kwargs – keyworded dictionary from the action.
args – listed arguments.
kwargs – keyworded dictionary arguments.
- Returns
Model serializer.
- get_serializer_class(**kwargs) → Type[rest_framework.serializers.Serializer][source]¶
Return the class to use for the serializer. Defaults to using self.serializer_class.
You may want to override this if you need to provide different serializations depending on the incoming request.
(Eg. admins get full serialization, others get basic serialization)
- Parameters
kwargs – keyworded dictionary arguments.
- Returns
Model serializer class.
Mixins¶
- class djangochannelsrestframework.mixins.CreateModelMixin[source]¶
Create model mixin.
- async create(data: dict, **kwargs) → Tuple[rest_framework.utils.serializer_helpers.ReturnDict, int][source]¶
Create action.
- Parameters
data – model data to create.
- Returns
Tuple with the serializer data and the status code.
Examples
#! consumers.py from .models import User from .serializers import UserSerializer from djangochannelsrestframework import permissions from djangochannelsrestframework.generics import GenericAsyncAPIConsumer from djangochannelsrestframework.mixins import CreateModelMixin class LiveConsumer(CreateModelMixin, GenericAsyncAPIConsumer): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = (permissions.AllowAny,)
#! routing.py from django.urls import re_path from .consumers import LiveConsumer websocket_urlpatterns = [ re_path(r'^ws/$', LiveConsumer.as_asgi()), ]
// html const ws = new WebSocket("ws://localhost:8000/ws/") ws.send(JSON.stringify({ action: "create", request_id: new Date().getTime(), data: { username: "test", password1: "testpassword123", password2: "testpassword123", } })) /* The response will be something like this. { "action": "create", "errors": [], "response_status": 201, "request_id": 150060530, "data": {'username': 'test', 'id': 42,}, } */
- class djangochannelsrestframework.mixins.DeleteModelMixin[source]¶
Delete model mixin
- async delete(**kwargs) → Tuple[None, int][source]¶
Retrieve action.
- Returns
Tuple with the serializer data and the status code.
Examples
#! consumers.py from .models import User from .serializers import UserSerializer from djangochannelsrestframework import permissions from djangochannelsrestframework.generics import GenericAsyncAPIConsumer from djangochannelsrestframework.mixins import DeleteModelMixin class LiveConsumer(DeleteModelMixin, GenericAsyncAPIConsumer): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = (permissions.AllowAny,)
#! routing.py from django.urls import re_path from .consumers import LiveConsumer websocket_urlpatterns = [ re_path(r'^ws/$', LiveConsumer.as_asgi()), ]
// html const ws = new WebSocket("ws://localhost:8000/ws/") ws.send(JSON.stringify({ action: "delete", request_id: new Date().getTime(), pk: 1, })) /* The response will be something like this. { "action": "delete", "errors": [], "response_status": 204, "request_id": 150000, "data": null, } */
- class djangochannelsrestframework.mixins.ListModelMixin[source]¶
List model mixin
- async list(**kwargs) → Tuple[rest_framework.utils.serializer_helpers.ReturnList, int][source]¶
List action.
- Returns
Tuple with the list of serializer data and the status code.
Examples
#! consumers.py from .models import User from .serializers import UserSerializer from djangochannelsrestframework import permissions from djangochannelsrestframework.generics import GenericAsyncAPIConsumer from djangochannelsrestframework.mixins import ListModelMixin class LiveConsumer(ListModelMixin, GenericAsyncAPIConsumer): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = (permissions.AllowAny,)
#! routing.py from django.urls import re_path from .consumers import LiveConsumer websocket_urlpatterns = [ re_path(r'^ws/$', LiveConsumer.as_asgi()), ]
// html const ws = new WebSocket("ws://localhost:8000/ws/") ws.send(JSON.stringify({ action: "list", request_id: new Date().getTime(), })) /* The response will be something like this. { "action": "list", "errors": [], "response_status": 200, "request_id": 1500000, "data": [ {"email": "42@example.com", "id": 1, "username": "test1"}, {"email": "45@example.com", "id": 2, "username": "test2"}, ], } */
- class djangochannelsrestframework.mixins.PaginatedModelListMixin[source]¶
- async list(**kwargs)[source]¶
List action.
- Returns
Tuple with the list of serializer data and the status code.
Examples
#! consumers.py from .models import User from .serializers import UserSerializer from djangochannelsrestframework import permissions from djangochannelsrestframework.generics import GenericAsyncAPIConsumer from djangochannelsrestframework.mixins import ListModelMixin class LiveConsumer(ListModelMixin, GenericAsyncAPIConsumer): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = (permissions.AllowAny,)
#! routing.py from django.urls import re_path from .consumers import LiveConsumer websocket_urlpatterns = [ re_path(r'^ws/$', LiveConsumer.as_asgi()), ]
// html const ws = new WebSocket("ws://localhost:8000/ws/") ws.send(JSON.stringify({ action: "list", request_id: new Date().getTime(), })) /* The response will be something like this. { "action": "list", "errors": [], "response_status": 200, "request_id": 1500000, "data": [ {"email": "42@example.com", "id": 1, "username": "test1"}, {"email": "45@example.com", "id": 2, "username": "test2"}, ], } */
- property paginator: Optional[any]¶
Gets the paginator class
- Returns
Pagination class. Optional.
- class djangochannelsrestframework.mixins.PatchModelMixin[source]¶
Patch model mixin
- async patch(data: dict, **kwargs) → Tuple[rest_framework.utils.serializer_helpers.ReturnDict, int][source]¶
Patch action.
- Returns
Tuple with the serializer data and the status code.
Examples
#! consumers.py from .models import User from .serializers import UserSerializer from djangochannelsrestframework import permissions from djangochannelsrestframework.generics import GenericAsyncAPIConsumer from djangochannelsrestframework.mixins import PatchModelMixin class LiveConsumer(PatchModelMixin, GenericAsyncAPIConsumer): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = (permissions.AllowAny,)
#! routing.py from django.urls import re_path from .consumers import LiveConsumer websocket_urlpatterns = [ re_path(r'^ws/$', LiveConsumer.as_asgi()), ]
// html const ws = new WebSocket("ws://localhost:8000/ws/") ws.send(JSON.stringify({ action: "patch", request_id: new Date().getTime(), pk: 1, data: { email: "00@example.com", }, })) /* The response will be something like this. { "action": "patch", "errors": [], "response_status": 200, "request_id": 150000, "data": {"email": "00@example.com", "id": 1, "username": "test1"}, } */
- class djangochannelsrestframework.mixins.RetrieveModelMixin[source]¶
Retrieve model mixin
- async retrieve(**kwargs) → Tuple[rest_framework.utils.serializer_helpers.ReturnDict, int][source]¶
Retrieve action.
- Returns
Tuple with the serializer data and the status code.
Examples
#! consumers.py from .models import User from .serializers import UserSerializer from djangochannelsrestframework import permissions from djangochannelsrestframework.generics import GenericAsyncAPIConsumer from djangochannelsrestframework.mixins import RetrieveModelMixin class LiveConsumer(RetrieveModelMixin, GenericAsyncAPIConsumer): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = (permissions.AllowAny,)
#! routing.py from django.urls import re_path from .consumers import LiveConsumer websocket_urlpatterns = [ re_path(r'^ws/$', LiveConsumer.as_asgi()), ]
// html const ws = new WebSocket("ws://localhost:8000/ws/") ws.send(JSON.stringify({ action: "retrieve", request_id: new Date().getTime(), pk: 1, })) /* The response will be something like this. { "action": "retrieve", "errors": [], "response_status": 200, "request_id": 1500000, "data": {"email": "42@example.com", "id": 1, "username": "test1"}, } */
- class djangochannelsrestframework.mixins.StreamedPaginatedListMixin[source]¶
- async list(action, request_id, **kwargs)[source]¶
List action.
- Returns
Tuple with the list of serializer data and the status code.
Examples
#! consumers.py from .models import User from .serializers import UserSerializer from djangochannelsrestframework import permissions from djangochannelsrestframework.generics import GenericAsyncAPIConsumer from djangochannelsrestframework.mixins import ListModelMixin class LiveConsumer(ListModelMixin, GenericAsyncAPIConsumer): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = (permissions.AllowAny,)
#! routing.py from django.urls import re_path from .consumers import LiveConsumer websocket_urlpatterns = [ re_path(r'^ws/$', LiveConsumer.as_asgi()), ]
// html const ws = new WebSocket("ws://localhost:8000/ws/") ws.send(JSON.stringify({ action: "list", request_id: new Date().getTime(), })) /* The response will be something like this. { "action": "list", "errors": [], "response_status": 200, "request_id": 1500000, "data": [ {"email": "42@example.com", "id": 1, "username": "test1"}, {"email": "45@example.com", "id": 2, "username": "test2"}, ], } */
- class djangochannelsrestframework.mixins.UpdateModelMixin[source]¶
Update model mixin
- async update(data: dict, **kwargs) → Tuple[rest_framework.utils.serializer_helpers.ReturnDict, int][source]¶
Retrieve action.
- Returns
Tuple with the serializer data and the status code.
Examples
#! consumers.py from .models import User from .serializers import UserSerializer from djangochannelsrestframework import permissions from djangochannelsrestframework.generics import GenericAsyncAPIConsumer from djangochannelsrestframework.mixins import UpdateModelMixin class LiveConsumer(UpdateModelMixin, GenericAsyncAPIConsumer): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = (permissions.AllowAny,)
#! routing.py from django.urls import re_path from .consumers import LiveConsumer websocket_urlpatterns = [ re_path(r'^ws/$', LiveConsumer.as_asgi()), ]
// html const ws = new WebSocket("ws://localhost:8000/ws/") ws.send(JSON.stringify({ action: "update", request_id: new Date().getTime(), pk: 1, data: { username: "test edited", }, })) /* The response will be something like this. { "action": "update", "errors": [], "response_status": 200, "request_id": 1500000, "data": {"email": "42@example.com", "id": 1, "username": "test edited"}, } */
Base observer¶
- class djangochannelsrestframework.observer.base_observer.BaseObserver(func, partition: str = '*')[source]¶
Base observer class
- serializer(func)[source]¶
Adds a Serializer to the model observer return.
Note
This is meant to use as a decorator.
Examples
TODO path to examples?
# 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)
# 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"]
# 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(Comments) async def comment_activity(self, message, observer=None, **kwargs): await self.send_json(message) @comment_activity.serializer def comment_activity(self, instance: Comment, action, **kwargs): return CommentSerializer(instance).data @action() async def subscribe_to_comment_activity(self, **kwargs): await self.comment_activity.subscribe()
Now we will have a websocket client in javascript listening to the messages, after subscribing to the comment activity. This codeblock can be used it in the browser console.
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) }
In the IPython shell we will create some comments for differnt users and in the browser console we will se the log.
Note
At this point we should have some users in our database, otherwise create them
>>> 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 consol 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 consol log we will se something like this:
{ action: "subscribe_to_comment_activity", errors: [], response_status: 200, request_id: 15606042, data: { id: 2, text: "user 2 creates a second comment", user: 2, }, }
As you can see in this example, we are subscribe to ALL ACTIVITY of the comment model.
Decorators¶
- djangochannelsrestframework.decorators.action(atomic=None, **kwargs)[source]¶
Mark a method as an action.
Permissions¶
Tutorial¶
Djangochannelsrestframework allow you to use DRF serializers easily with django Channels v3 In this tutorial we will use this library to improve the chat tutorial from django Channels.
In this tutorial we redo the channels tutorial to use DCRF consumers.
Tutorial Part 1: Basic Setup¶
In this tutorial we will build a simple chat server. It will have two pages:
An index view that lets you type the name of a chat room to join.
A room view that lets you see messages posted in a particular chat room.
The room view will use a WebSocket to communicate with the Django server and listen for any messages that are posted.
We assume that you are familiar with basic concepts for building a Django site. If not we recommend you complete the Django tutorial first and then come back to this tutorial.
We assume that you have Django installed already and the Channels Tutorial made.
- This will be the directory tree at the end of the Channels Tutorial and we will add the following python files:
serializers.py
models.py
routing.py
mysite/
manage.py
mysite/
__init__.py
asgi.py
settings.py
urls.py
wsgi.py
chat/
__init__.py
consumers.py
models.py
serializers.py
routing.py
templates/
chat/
index.html
room.html
tests.py
urls.py
views.py
Creating the Models¶
We will put the following code in the models.py
file, to handle current rooms, messages and current users.
from django.db import models
from django.conf import settings
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
class Room(models.Model):
name = models.CharField(max_length=255, null=False, blank=False, unique=True)
host = models.ForeignKey(User, on_delete=models.CASCADE, related_name="rooms")
current_users = models.ManyToManyField(User, related_name="current_rooms", blank=True)
def __str__(self):
return f"Room({self.name} {self.host})"
class Message(models.Model):
room = models.ForeignKey("chat.Room", on_delete=models.CASCADE, related_name="messages")
text = models.TextField(max_length=500)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="messages")
def __str__(self):
return f"Message({self.user} {self.room})"
Creating the Serializers¶
We will put the following code in the serializers.py
file, to handle the serialization of the models created.
from .models import User, Room, Message
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
exclude = ["password"]
class MessageSerializer(serializers.ModelSerializer):
created_at_formatted = serializers.SerializerMethodField()
user = UserSerializer()
class Meta:
model = Message
exclude = []
depth = 1
def get_created_at_formatted(self, obj:Message):
return obj.created_at.strftime("%d-%m-%Y %H:%M:%S")
class RoomSerializer(serializers.ModelSerializer):
last_message = serializers.SerializerMethodField()
messages = MessageSerializer(many=True, read_only=True)
class Meta:
model = Room
fields = ["pk", "name", "host", "messages", "current_users", "last_message"]
depth = 1
read_only_fields = ["messages", "last_message"]
def get_last_message(self, obj:Room):
return MessageSerializer(obj.messages.order_by('created_at').last()).data
Creating the Consumers¶
- In the
consumers.py
file we will create only the room consumer for: Joining and leaving a room.
Observing messages in that room.
Observing the current users in the room.
import json
from django.shortcuts import get_object_or_404
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.utils.timezone import now
from django.conf import settings
from typing import Generator
from djangochannelsrestframework.generics import GenericAsyncAPIConsumer, AsyncAPIConsumer
from djangochannelsrestframework.observer.generics import (ObserverModelInstanceMixin, action)
from djangochannelsrestframework.observer import model_observer
from .models import Room, Message, User
from .serializers import MessageSerializer, RoomSerializer, UserSerializer
class RoomConsumer(ObserverModelInstanceMixin, GenericAsyncAPIConsumer):
queryset = Room.objects.all()
serializer_class = RoomSerializer
lookup_field = "pk"
async def disconnect(self, code):
if hasattr(self, "room_subscribe"):
await self.remove_user_from_room(self.room_subscribe)
await self.notify_users()
await super().disconnect(code)
@action()
async def join_room(self, pk, **kwargs):
self.room_subscribe = pk
await self.add_user_to_room(pk)
await self.notify_users()
@action()
async def leave_room(self, pk, **kwargs):
await self.remove_user_from_room(pk)
@action()
async def create_message(self, message, **kwargs):
room:Room = await self.get_room(pk=self.room_subscribe)
await database_sync_to_async(Message.objects.create)(
room=room,
user=self.scope["user"],
text=message
)
@action()
async def subscribe_to_messages_in_room(self, pk, **kwargs):
await self.message_activity.subscribe(room=pk)
@model_observer(Message)
async def message_activity(self, message, observer=None, **kwargs):
await self.send_json(message)
@message_activity.groups_for_signal
def message_activity(self, instance: Message, **kwargs):
yield 'room__{instance.room_id}'
yield f'pk__{instance.pk}'
@message_activity.groups_for_consumer
def message_activity(self, room=None, **kwargs):
if room is not None:
yield f'room__{room}'
@message_activity.serializer
def message_activiy(self, instance:Message, action, **kwargs):
return dict(data=MessageSerializer(instance).data, action=action.value, pk=instance.pk)
async def notify_users(self):
room:Room = await self.get_room(self.room_subscribe)
for group in self.groups:
await self.channel_layer.group_send(
group,
{
'type':'update_users',
'usuarios':await self.current_users(room)
}
)
async def update_users(self, event:dict):
await self.send(text_data=json.dumps({'usuarios':event["usuarios"]}))
@database_sync_to_async
def get_room(self, pk:int)->Room:
return Room.objects.get(pk=pk)
@database_sync_to_async
def current_users(self, room:Room):
return [UserSerializer(user).data for user in room.current_users.all()]
@database_sync_to_async
def remove_user_from_room(self, room):
user:User = self.scope["user"]
user.current_rooms.remove(room)
@database_sync_to_async
def add_user_to_room(self, pk):
user:User = self.scope["user"]
if not user.current_rooms.filter(pk=self.room_subscribe).exists():
user.current_rooms.add(Room.objects.get(pk=pk))
Routing the Websocket¶
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/room/$', consumers.RoomConsumer.as_asgi()),
]
Tutorial Part 2: Templates¶
We will edit the views, urls and templates for posting a Room form, and joining it.
We will edit the index.html
file, for posting a new room.
{% extends "chat/layout.html" %}
{% block content %}
What chat room would you like to enter?<br>
<form method="POST">
<input id="room-name-input" name="name" type="text" size="100"><br>
<input id="room-name-submit" type="button" value="Enter">
</form>
{% endblock content %}
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('room/<int:pk>/', views.room, name='room'),
]
Editing existing views¶
We will edit the views.py
from django.shortcuts import render, reverse, get_object_or_404
from django.views.generic import TemplateView
from django.http import HttpResponseRedirect
from .models import User, Room, Message
def index(request):
if request.method == "POST":
name = request.POST.get("name", None)
if name:
room = Room.objects.create(name=name, host=request.user)
HttpResponseRedirect(reverse("room", args=[room.pk]))
return render(request, 'chat/index.html')
def room(request, pk):
room:Room = get_object_or_404(Room, pk=pk)
return render(request, 'chat/room.html', {
"room":room,
})
{% extends "chat/layout.html" %}
{% load static %}
{% block content %}
<textarea id="chat-log" cols="100" rows="20"></textarea><br>
<input id="chat-message-input" type="text" size="100"><br>
<input id="chat-message-submit" type="button" value="Send">
{% endblock content %}
{% block footer %}
<script>
const room_pk = "{{ room.pk }}";
const request_id = "{{ request.sessions.session_key }}";
const chatSocket = new WebSocket(`ws://${window.location.host}/ws/chat/`);
chatSocket.onopen = function(){
ws.send(
JSON.stringify({
pk:room_pk,
action:"join_room",
request_id:request_id,
})
);
ws.send(
JSON.stringify({
pk:room_pk,
action:"retrieve",
request_id:request_id,
})
);
ws.send(
JSON.stringify({
pk:room_pk,
action:"subscribe_to_messages_in_room",
request_id:request_id,
})
);
ws.send(
JSON.stringify({
pk:room_pk,
action:"subscribe_instance",
request_id:request_id,
})
);
};
chatSocket.onmessage = function (e) {
const message = JSON.parse(e.data);
switch (data.action) {
case "retrieve":
setRoom(old =>data.data);
setMessages(old=>data.messages);
break;
case "create":
setMessages(old=>[...old, data])
break;
default:
break;
}
break;
};
chatSocket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
};
$('#chat-message-input').focus();
$('#chat-message-input').on('keyup', function(e){
if (e.keyCode === 13) { // enter, return
document.querySelector('#chat-message-submit').click();
}
});
$('#chat-message-submit').on('click', function(e){
const message = $('#chat-message-input').val();
chatSocket.send(JSON.stringify({
'message': message
}));
$('#chat-message-input').val('') ;
});
</script>
{% endblock footer %}