mirror of
				https://github.com/django/daphne.git
				synced 2025-10-30 23:37:25 +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