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.core.exceptions import ImproperlyConfigured | ||||
| 
 | ||||
| from .binding.base import BindingMetaclass | ||||
| 
 | ||||
| 
 | ||||
| class ChannelsConfig(AppConfig): | ||||
| 
 | ||||
|  | @ -18,3 +20,5 @@ class ChannelsConfig(AppConfig): | |||
|         # Do django monkeypatches | ||||
|         from .hacks import 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