Package: djangochannelsrestframework
Django Channels Rest Framework provides a DRF like interface for buildingchannels-v4websocket consumers.This project can be used alongsideHyperMediaChannelsandChannelsMultiplexerto 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.theY4Kmanhas developed a useful Javascript client librarydcrf-clientto use with DCRF.Thanks toDCRF is based of a fork ofChannels Apiand of course inspired byDjango Rest Framework.DocumentationReadTheDocsInstallpipinstalldjangochannelsrestframeworkWarningIn your application definition when you declare your consumers it is very important to use the.as_asgi()class method (e.g.MyConsumer.as_asgi()). Youshould nothave any instances ofMyConsumer()in your code base.A Generic Api ConsumerIn DCRF you can create aGenericAsyncAPIConsumerthat works much like aGenericAPIViewin DRF: For a more indepth look into Rest-Like Websocket consumers read this blogpost.from.importmodelsfrom.importserializersfromdjangochannelsrestframeworkimportpermissionsfromdjangochannelsrestframework.genericsimportGenericAsyncAPIConsumerfromdjangochannelsrestframework.mixinsimport(ListModelMixin,PatchModelMixin,UpdateModelMixin,CreateModelMixin,DeleteModelMixin,)classLiveConsumer(ListModelMixin,GenericAsyncAPIConsumer):queryset=models.Test.objects.all()serializer_class=serializers.TestSerializerpermission_classes=(permissions.IsAuthenticated,)Because this class uses theListModelMixin, one has access to thelistaction.One may use the same exact querysets andserializer_classutilized in their DRF Views, but must omit the DRF permissions. Permissions are to be imported fromdjangochannelsrestframework, which provides the standardAllowAnyandIsAuthenticatedpermissions.To call an action from the client send a websocket message:{action: "list", "request_id": 42}There are a selection of mixins that expose the common CRUD actions:ListModelMixin-listPatchModelMixin-patchCreateModelMixin-createRetrieveModelMixin-retrieveUpdateModelMixin-updateDeleteModelMixin-deleteObserving a Model instanceConsumer that let you subscribe to changes on an instance:classTestConsumer(ObserverModelInstanceMixin,GenericAsyncAPIConsumer):queryset=get_user_model().objects.all()serializer_class=UserSerializerthis exposes theretrieve,subscribe_instanceandunsubscribe_instanceactions.To subscribe send:{"action":"subscribe_instance","pk":42,# the id of the instance you are subscribing to"request_id":4# this id will be used for all result updates.}Actions will be sent down out from the server:{"action":"update","errors":[],"response_status":200,"request_id":4,"data":{'email':'42@example.com','id':42,'username':'thenewname'},}Adding Custom actionsclassUserConsumer(GenericAsyncAPIConsumer):queryset=get_user_model().objects.all()serializer_class=UserSerializer@action()asyncdefsend_email(self,pk=None,to=None,**kwargs):user=awaitdatabase_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 context and the response code.@action()# if the method is not async it is already wrapped in `database_sync_to_async`defpublish(self,pk=None,**kwargs):user=self.get_object(pk=pk)# ...return{'pk':pk},200Consumers that are not bound to ModelsYou can also create consumers that are not at all related to any models.fromdjangochannelsrestframework.decoratorsimportactionfromdjangochannelsrestframework.consumersimportAsyncAPIConsumerclassMyConsumer(AsyncAPIConsumer):@action()asyncdefan_async_action(self,some=None,**kwargs):# do something asyncreturn{'response with':'some message'},200@action()defa_sync_action(self,pk=None,**kwargs):# do something syncreturn{'response with':'some message'},200Using your normal views over a websocket connectionfromdjangochannelsrestframework.consumersimportview_as_consumerapplication=ProtocolTypeRouter({"websocket":AuthMiddlewareStack(URLRouter([url(r"^front(end)/$",view_as_consumer(YourDjangoView)),])),})In this situation if your view needs to read theGETquery string values you can provides these using thequeryoption.
And if the view method reads parameters from the URL you can provides these with theparameters.Sending the following over your WS connection will result in a GET request being evaluated on your View.{action:"retrieve",query:{"user_id":42}parameters:{"project_id":92}}Subscribing to a signal.One can subscribe to a customSignalutilizing theobserverdecorator.Here we have a custom signal that will be triggered when a user join a chat.# signals.pyfromdjango.dispatch.dispatcherimportSignaljoined_chat_signal=Signal()Now we will create the consumer with two actions, one for subscribing to our custom signal for specific chat, and another one
for manually trigger the signal.# consumers.pyfromdjangochannelsrestframework.consumersimportAsyncAPIConsumerfromdjangochannelsrestframework.decoratorsimportactionfromdjangochannelsrestframework.observerimportobserverfromrest_frameworkimportstatusfrom.signalsimportjoined_chat_signalfrom.serializersimportUserSerializerclassTestConsumer(AsyncAPIConsumer):@action()defjoin_chat(self,chat_id,**kwargs):serializer=UserSerializer(instance=self.scope['user'])joined_chat_signal.send(sender='join_chat',data=serializer.data,**kwargs)return{},status.HTTP_204_NO_CONTENT@observer(signal=joined_chat_signal)asyncdefjoined_chat_handler(self,data,observer=None,action=None,subscribing_request_ids=[],**kwargs):forrequest_idinsubscribing_request_ids:awaitself.reply(action='joined_chat',data=data,status=status.HTTP_200_OK,request_id=request_id)@joined_chat_handler.serializerdefjoin_chat_handler(self,sender,data,**kwargs):# the data comes from the signal.send and will be available in the observerreturndata@joined_chat_handler.groups_for_signaldefjoined_chat_handler(self,instance,**kwargs):yieldf'chat__{instance}'@joined_chat_handler.groups_for_consumerdefjoined_chat_handler(self,chat,**kwargs):ifchat:yieldf'chat__{chat}'@action()asyncdefsubscribe_joined(self,chat_id,request_id,**kwargs):awaitself.joined_chat_handler.subscribe(chat_id,request_id=request_id)Subscribing to all instances of a modelOne can subscribe to all instances of a model by utilizing themodel_observer.fromdjangochannelsrestframework.observerimportmodel_observer@model_observer(models.Test)asyncdefmodel_activity(self,message,observer=None,action=None,**kwargs):# send activity to your frontendawaitself.send_json(message)This method will send messages to the client on all CRUD operations made through the Django ORM. Theactionarg here it will take values such ascreate,deleteandupdateyou should consider passing this to your frontend client.Note: These notifications do not include bulk updates, such asmodels.Test.objects.filter(name="abc").update(name="newname")WARNINGWhen using this to decorate a method to avoid the method firing multiple
times you should ensure that if there are multiple@model_observerwrapped methods for the same model type within a single file that each
method has a different name.Subscribing to amodel_observerYou can do this in a few placed, a common example is in thewebsocket_connectmethod.asyncdefwebsocket_connect(self,message):# Super Saveawaitsuper().websocket_connect(message)# Initialized operationawaitself.activities_change.subscribe()This method utilizes the previously mentionedmodel_activitymethod to subscribe to all instances of the current Consumer’s model.One can also subscribe by creating a custom actionAnother way is overrideAsyncAPIConsumer.accept(self, **kwargs)classModelConsumerObserver(AsyncAPIConsumer):asyncdefaccept(self,**kwargs):awaitsuper().accept(**kwargs)awaitself.model_change.subscribe()@model_observer(models.Test)asyncdefmodel_change(self,message,action=None,**kwargs):"""
        This method is evaluated once for every user that subscribed,
        here you have access to info about the user by reading `self.scope`

        However it is best to avoid doing DB quires here since if you have lots of
        subscribers to a given instance you will end up with a LOT of database traffic.
        """awaitself.send_json(message)# If you want the data serialized instead of pk@model_change.serializerdefmodel_serialize(self,instance,action,**kwargs):"""
        This block is evaluated before the data is sent over the channel layer
        this means you are unable to access information
        such as the user that it will be sent to.

        If you need the user info when serializing then you can do the serialization
        in the above method.
        """returnTestSerializer(instance).dataNoteNew Feature!
Now you can rewrite this as:classModelConsumerObserver(AsyncAPIConsumer):asyncdefaccept(self,**kwargs):awaitsuper().accept(**kwargs)awaitself.model_change.subscribe()@model_observer(models.Test,serializer_class=TestSerializer)asyncdefmodel_change(self,message,action=None,**kwargs):# in this case since we subscribe int he `accept` method# we do not expect to have any `subscribing_request_ids` to loop over.awaitself.reply(data=message,action=action)Subscribing to a filtered list of modelsIn most situations you want to filter the set of models that you subscribe to.To do this we need to split the model updates intogroupsand then in the consumer subscribe to the groups that we want/have permission to see.classMyConsumer(AsyncAPIConsumer):# This class MUST subclass `AsyncAPIConsumer` to use `@model_observer`@model_observer(models.Classroom)asyncdefclassroom_change_handler(self,message,observer=None,action=None,subscribing_request_ids=[],**kwargs):# due to not being able to make DB QUERIES when selecting a group# maybe do an extra check here to be sure the user has permission# send activity to your frontendforrequest_idinsubscribing_request_ids:# we can send a separate message for each subscribing request# this lets ws clients rout these messages.awaitself.send_json(dict(body=message,action=action,request_id=request_id))# note if we do not pass `request_id` to the `subscribe` method# then `subscribing_request_ids` will be and empty list.@classroom_change_handler.groups_for_signaldefclassroom_change_handler(self,instance:models.Classroom,**kwargs):# this block of code is called very often *DO NOT make DB QUERIES HERE*yieldf'-school__{instance.school_id}'yieldf'-pk__{instance.pk}'@classroom_change_handler.groups_for_consumerdefclassroom_change_handler(self,school=None,classroom=None,**kwargs):# This is called when you subscribe/unsubscribeifschoolisnotNone:yieldf'-school__{school.pk}'ifclassroomisnotNone:yieldf'-pk__{classroom.pk}'@action()asyncdefsubscribe_to_classrooms_in_school(self,school_pk,request_id,**kwargs):# check user has permission to do thisawaitself.classroom_change_handler.subscribe(school=school,request_id=request_id)@action()asyncdefsubscribe_to_classroom(self,classroom_pk,request_id,**kwargs):# check user has permission to do thisawaitself.classroom_change_handler.subscribe(classroom=classroom,request_id=request_id)