Tutorial

This page will use an example to-do app called todolist with the following models and serializers:

# todolist/models.py
from django.db import models

class List(models.Model):
    name = models.CharField(max_length=64)

class Task(models.Model):
    text = models.CharField(max_length=140)
    done = models.BooleanField(default=False)
    list = models.ForeignKey("List", on_delete=models.CASCADE)

# todolist/serializers.py
from rest_framework import serializers

class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = ["id", "text", "done"]

class TodoListSerializer(serializers.ModelSerializer):
    tasks = TaskSerializer(many=True, read_only=True)
    class Meta:
        model = List
        fields = ["id", "name", "tasks"]

Server-side setup

django-rest-live extends the existing generic API views using a mixin called RealtimeMixin. In order to designate your view as realtime-capable, add RealtimeMixin to its superclasses:

from rest_framework.viewsets import ModelViewSet
from rest_live.mixins import RealtimeMixin

class TaskViewSet(ModelViewSet, RealtimeMixin):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer

Note that throughout this documentation we use ViewSets as our base class. It's important to note that django-rest-live works just as well with any generic view that defines a queryset attribute along with either a serializer_class atttribute or a get_serializer_class() method.

Just like parts of REST Framework which require knowledge of a backing model for a view, RealtimeMixin requires that you have a queryset attribute defined on your view, even if you have overridden the get_queryset() method. The DRF solution, recommended here as well, is to define an empty "sentinel" queryset on the view that RealtimeMixin can use to determine the model:

from rest_framework.viewsets import ModelViewSet
from rest_live.mixins import RealtimeMixin

class FilteredTaskViewSet(ModelViewSet, RealtimeMixin):
    serializer_class = TaskSerializer
    queryset = Task.objects.none()  # Empty queryset indicating the backing model for this view

    def get_queryset(self):  # Actual queryset for the view
        return Task.objects.filter(user=self.request.user)

The last backend step is to register your View in the RealtimeRouter you defined in the first setup step:

from rest_live.routers import RealtimeRouter

router = RealtimeRouter()
router.register(TaskViewSet)  # Register all ViewSets here

websockets = AuthMiddlewareStack(
    URLRouter([
        path("ws/subscribe/", router.as_consumer().as_asgi(), name="subscriptions"),
        # Other routing here...
    ])

Note: if using Channels version 2, omit the as_asgi() method.

Subscribing to single instances

Subscribing to a updates equires opening a WebSocket on the client connection to the URL you specified during setup. In our example case, that URL is /ws/subscribe/. After the connection is established, send a JSON message (using JSON.stringify()) in this format:

{
  "type": "subscribe",
  "id": 1337,
  "model": "todolist.Task",
  "action": "retrieve",
  "lookup_by": 1
}

You should generate the id client side. It's used to track the subscription you request throughout its lifetime, so it should be unique for this connection. We'll see how it's referenced both in error messages and broadcasts.

The model label should be in Django's standard app.modelname format. lookup_by should be the value of the lookup field for the model instance we're subscribing to. Since this defaults to pk, it's the conceptual equivalent of subscribing to the instance which would be returned from Task.objects.filter(pk=<value>).

The client should make RESTful HTTP requests for resources to determine which IDs it wants to subscribe to; there's no capability for querying built in to the Websocket API, just subscriptions and broadcasts.

When the Task with primary key 1 updates, a message in this format will be sent over the websocket:

{
  "type": "broadcast",
  "id": 1337,
  "model": "test_app.Todo",
  "action": "UPDATED",
  "instance": { "id": 1, "text": "test", "done": true }
}

Valid action values are UPDATED, CREATED, and DELETED. instance is the JSON-serialized model instance using the serializer defined in the view's serializer_class attribute or returned from the get_serializer_class method.

Unsubscribing is even simpler – simply pass the original request id along in a websocket message:

{
  "type": "unsubscribe",
  "id": 1337
}

Subscribing to lists

Being attached to a generic view with a get_queryset() method, you can also subscribe to updates to a view's queryset. The subscription looks like this:

{
  "id": 1338,
  "type": "subscribe",
  "model": "todolist.Task",
  "action": "list"
}

Note that lookup_by isn't used here since we're referring to the whole queryset. You'll receive broadcasts in the same format as shown above.

CREATE and DELETE actions are not the actual create and delete actions in the database, but are relative to their inclusion in the view's queryset. If an instance is created, or is modified such that it is now included in the queryset when it wasn't before, the action in the broadcast will be CREATED. If the instance is modified so that it is no longer included in the queryset, the action in the broadcast will be DELETED.

Note that DELETED actions can't be triggered from actual deletions from the database at this time.

request.user and request.session

Django REST Framework makes heavy use of the Request object as a general context throughout the framework. Permissions are a good example: each permission check gets passed the request object along with the current view in order to verify if a given request has permission to view an object.

However, broadcasts originate from database updates rather than an HTTP request, so django-rest-live uses the HTTP request that establishes the websocket connection as a basis for the request object accessible in views, permissions and serializers. request.user and request.session, normally populated via middleware, are available as expected.

Passing parameters to subscriptions

Views are often filtered in some way, using parameters in the URL passed as keyword arguments to the view, or as GET parameters after the ? in the URL. These arguments can be passed to DRF through extra fields on the initial subscription request to filter the queryset appropriately for a given subscription.

view.kwargs

Something that can't be inferred from the initial request are view keyword arguments, normally derived from the URL path to a resource in HTTP requests. django-rest-live allows you to declare view arguments in your subscription request using the view_kwargs key:

{
  "type": "subscribe",
  "id": 1339,
  "model": "todolist.Task",
  "action": "retrieve",
  "lookup_by": 29,
  "view_kwargs": {
    "list": 14
  }
}

As a rule of thumb, if you have angle brackets in your URL pattern, like title in path('articles/<slug:title>/', views.article), then you're providing your view with keyword arguments, and you most likely need to provide those arguments to your View when requesting subscriptions too.

request.query_params

If you use request.query_params in your view at all, potentially from filters on your queryset, you can also pass in query parameters to your subscription with the query_params key:

{
  "type": "subscribe",
  "id": 1340,
  "model": "todolist.Task",
  "action": "list",
  "query_params": {
    "active": true
  }
}

If you're getting an AttributeError in your View when receiving a broadcast but not when doing normal HTTP REST operations, then you're probably making use of an attribute we didn't think of. In that case, please open an issue describing your use case! It'll go a long way to making this library more useful to all.