mirror of
https://github.com/django/daphne.git
synced 2025-07-10 16:02:18 +03:00
First version of binding code
This commit is contained in:
parent
af606ff895
commit
62d4782dbd
|
@ -1,6 +1,8 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
from .binding.base import BindingMetaclass
|
||||||
|
|
||||||
|
|
||||||
class ChannelsConfig(AppConfig):
|
class ChannelsConfig(AppConfig):
|
||||||
|
|
||||||
|
@ -18,3 +20,5 @@ class ChannelsConfig(AppConfig):
|
||||||
# Do django monkeypatches
|
# Do django monkeypatches
|
||||||
from .hacks import monkeypatch_django
|
from .hacks import monkeypatch_django
|
||||||
monkeypatch_django()
|
monkeypatch_django()
|
||||||
|
# Instantiate bindings
|
||||||
|
BindingMetaclass.register_all()
|
||||||
|
|
1
channels/binding/__init__.py
Normal file
1
channels/binding/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .base import Binding
|
176
channels/binding/base.py
Normal file
176
channels/binding/base.py
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.db.models.signals import post_save, post_delete
|
||||||
|
|
||||||
|
from ..channel import Group
|
||||||
|
|
||||||
|
|
||||||
|
class BindingMetaclass(type):
|
||||||
|
"""
|
||||||
|
Metaclass that tracks instantiations of its type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
binding_classes = []
|
||||||
|
|
||||||
|
def __new__(cls, name, bases, body):
|
||||||
|
klass = type.__new__(cls, name, bases, body)
|
||||||
|
if bases != (object, ):
|
||||||
|
cls.binding_classes.append(klass)
|
||||||
|
return klass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_all(cls):
|
||||||
|
for binding_class in cls.binding_classes:
|
||||||
|
binding_class.register()
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(BindingMetaclass)
|
||||||
|
class Binding(object):
|
||||||
|
"""
|
||||||
|
Represents a two-way data binding from channels/groups to a Django model.
|
||||||
|
Outgoing binding sends model events to zero or more groups.
|
||||||
|
Incoming binding takes messages and maybe applies the action based on perms.
|
||||||
|
|
||||||
|
To implement outbound, implement:
|
||||||
|
- group_names, which returns a list of group names to send to
|
||||||
|
- serialize, which returns message contents from an instance + action
|
||||||
|
|
||||||
|
To implement inbound, implement:
|
||||||
|
- deserialize, which returns pk, data and action from message contents
|
||||||
|
- has_permission, which says if the user can do the action on an instance
|
||||||
|
- create, which takes the data and makes a model instance
|
||||||
|
- update, which takes data and a model instance and applies one to the other
|
||||||
|
|
||||||
|
Outbound will work once you implement the functions; inbound requires you
|
||||||
|
to place one or more bindings inside a protocol-specific Demultiplexer
|
||||||
|
and tie that in as a consumer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(cls):
|
||||||
|
"""
|
||||||
|
Resolves models.
|
||||||
|
"""
|
||||||
|
# If model is None directly on the class, assume it's abstract.
|
||||||
|
if cls.model is None:
|
||||||
|
if "model" in cls.__dict__:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise ValueError("You must set the model attribute on Binding %r!" % cls)
|
||||||
|
# Optionally resolve model strings
|
||||||
|
if isinstance(cls.model, six.string_types):
|
||||||
|
cls.model = apps.get_model(cls.model)
|
||||||
|
# Connect signals
|
||||||
|
post_save.connect(cls.save_receiver, sender=cls.model)
|
||||||
|
post_delete.connect(cls.delete_receiver, sender=cls.model)
|
||||||
|
|
||||||
|
# Outbound binding
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def save_receiver(cls, instance, created, **kwargs):
|
||||||
|
"""
|
||||||
|
Entry point for triggering the binding from save signals.
|
||||||
|
"""
|
||||||
|
cls.trigger_outbound(instance, "create" if created else "update")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete_receiver(cls, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
Entry point for triggering the binding from save signals.
|
||||||
|
"""
|
||||||
|
cls.trigger_outbound(instance, "delete")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def trigger_outbound(cls, instance, action):
|
||||||
|
"""
|
||||||
|
Triggers the binding to possibly send to its group.
|
||||||
|
"""
|
||||||
|
self = cls()
|
||||||
|
self.instance = instance
|
||||||
|
# Check to see if we're covered
|
||||||
|
for group_name in self.group_names(instance, action):
|
||||||
|
group = Group(group_name)
|
||||||
|
group.send(self.serialize(instance, action))
|
||||||
|
|
||||||
|
def group_names(self, instance, action):
|
||||||
|
"""
|
||||||
|
Returns the iterable of group names to send the object to based on the
|
||||||
|
instance and action performed on it.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def serialize(self, instance, action):
|
||||||
|
"""
|
||||||
|
Should return a serialized version of the instance to send over the
|
||||||
|
wire (return value must be a dict suitable for sending over a channel -
|
||||||
|
e.g., to send JSON as a WebSocket text frame, you must return
|
||||||
|
{"text": json.dumps(instance_serialized_as_dict)}
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
# Inbound binding
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def trigger_inbound(cls, message):
|
||||||
|
"""
|
||||||
|
Triggers the binding to see if it will do something.
|
||||||
|
We separate out message serialization to a consumer, so this gets
|
||||||
|
native arguments.
|
||||||
|
"""
|
||||||
|
# Late import as it touches models
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
self = cls()
|
||||||
|
self.message = message
|
||||||
|
# Deserialize message
|
||||||
|
self.action, self.pk, self.data = self.deserialize(self.message)
|
||||||
|
self.user = getattr(self.message, "user", AnonymousUser())
|
||||||
|
# Run incoming action
|
||||||
|
self.run_action(self.action, self.pk, self.data)
|
||||||
|
|
||||||
|
def deserialize(self, message):
|
||||||
|
"""
|
||||||
|
Returns action, pk, data decoded from the message. pk should be None
|
||||||
|
if action is create; data should be None if action is delete.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def has_permission(self, user, action, pk):
|
||||||
|
"""
|
||||||
|
Return True if the user can do action to the pk, False if not.
|
||||||
|
User may be AnonymousUser if no auth hooked up/they're not logged in.
|
||||||
|
Action is one of "create", "delete", "update".
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def run_action(self, action, pk, data):
|
||||||
|
"""
|
||||||
|
Performs the requested action. This version dispatches to named
|
||||||
|
functions by default for update/create, and handles delete itself.
|
||||||
|
"""
|
||||||
|
# Check to see if we're allowed
|
||||||
|
if self.has_permission(self.user, pk):
|
||||||
|
if action == "create":
|
||||||
|
self.create(data)
|
||||||
|
elif action == "update":
|
||||||
|
self.update(self.model.get(pk=pk), data)
|
||||||
|
elif action == "delete":
|
||||||
|
self.model.filter(pk=pk).delete()
|
||||||
|
else:
|
||||||
|
raise ValueError("Bad action %r" % action)
|
||||||
|
|
||||||
|
def create(self, data):
|
||||||
|
"""
|
||||||
|
Creates a new instance of the model with the data.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def update(self, instance, data):
|
||||||
|
"""
|
||||||
|
Updates the model with the data.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
79
channels/binding/websockets.py
Normal file
79
channels/binding/websockets.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .base import Binding
|
||||||
|
from ..generic.websockets import JsonWebsocketConsumer
|
||||||
|
|
||||||
|
|
||||||
|
class WebsocketBinding(Binding):
|
||||||
|
"""
|
||||||
|
Websocket-specific outgoing binding subclass that uses JSON encoding.
|
||||||
|
|
||||||
|
To implement outbound, implement:
|
||||||
|
- group_names, which returns a list of group names to send to
|
||||||
|
- serialize_data, which returns JSON-safe data from a model instance
|
||||||
|
|
||||||
|
To implement inbound, implement:
|
||||||
|
- has_permission, which says if the user can do the action on an instance
|
||||||
|
- create, which takes incoming data and makes a model instance
|
||||||
|
- update, which takes incoming data and a model instance and applies one to the other
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Mark as abstract
|
||||||
|
|
||||||
|
model = None
|
||||||
|
|
||||||
|
# Outbound
|
||||||
|
|
||||||
|
def serialize(self, instance, action):
|
||||||
|
return {
|
||||||
|
"text": json.dumps({
|
||||||
|
"model": "%s.%s" % (
|
||||||
|
instance._meta.app_label.lower(),
|
||||||
|
instance._meta.object_name.lower(),
|
||||||
|
),
|
||||||
|
"action": action,
|
||||||
|
"pk": instance.pk,
|
||||||
|
"data": self.serialize_data(instance),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def serialize_data(self, instance):
|
||||||
|
"""
|
||||||
|
Serializes model data into JSON-compatible types.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
# Inbound
|
||||||
|
|
||||||
|
def deserialize(self, message):
|
||||||
|
content = json.loads(message['text'])
|
||||||
|
action = content['action']
|
||||||
|
pk = content.get('pk', None)
|
||||||
|
data = content.get('data', None)
|
||||||
|
return action, pk, data
|
||||||
|
|
||||||
|
|
||||||
|
class WebsocketBindingDemultiplexer(JsonWebsocketConsumer):
|
||||||
|
"""
|
||||||
|
Allows you to combine multiple Bindings as one websocket consumer.
|
||||||
|
Subclass and provide a custom list of Bindings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
http_user = True
|
||||||
|
warn_if_no_match = True
|
||||||
|
bindings = None
|
||||||
|
|
||||||
|
def receive(self, content):
|
||||||
|
# Sanity check
|
||||||
|
if self.bindings is None:
|
||||||
|
raise ValueError("Demultiplexer has no bindings!")
|
||||||
|
# Find the matching binding
|
||||||
|
model_label = content['model']
|
||||||
|
triggered = False
|
||||||
|
for binding in self.bindings:
|
||||||
|
if binding.model_label == model_label:
|
||||||
|
binding.trigger_inbound(self.message)
|
||||||
|
triggered = True
|
||||||
|
# At least one of them should have fired.
|
||||||
|
if not triggered and self.warn_if_no_match:
|
||||||
|
raise ValueError("No binding found for model %s" % model_label)
|
Loading…
Reference in New Issue
Block a user