from __future__ import unicode_literals import six from django.apps import apps from django.db.models.signals import post_delete, post_save, pre_delete, pre_save from ..auth import channel_session, channel_session_user from ..channel import Group CREATE = 'create' UPDATE = 'update' DELETE = 'delete' class BindingMetaclass(type): """ Metaclass that tracks instantiations of its type. """ register_immediately = False binding_classes = [] def __new__(cls, name, bases, body): klass = type.__new__(cls, name, bases, body) if bases != (object, ): cls.binding_classes.append(klass) if cls.register_immediately: klass.register() return klass @classmethod def register_all(cls): for binding_class in cls.binding_classes: binding_class.register() cls.register_immediately = True @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 to serialize model = None # Only model fields that are listed in fields should be send by default # if you want to really send all fields, use fields = ['__all__'] fields = None exclude = None # Decorators channel_session_user = True channel_session = False # the kwargs the triggering signal (e.g. post_save) was emitted with signal_kwargs = None @classmethod def register(cls): """ Resolves models. """ # Connect signals for model in cls.get_registered_models(): pre_save.connect(cls.pre_save_receiver, sender=model) post_save.connect(cls.post_save_receiver, sender=model) pre_delete.connect(cls.pre_delete_receiver, sender=model) post_delete.connect(cls.post_delete_receiver, sender=model) @classmethod def get_registered_models(cls): """ Resolves the class model attribute if it's a string and returns it. """ # 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) # If neither fields nor exclude are not defined, raise an error if cls.fields is None and cls.exclude is None: raise ValueError("You must set the fields or exclude attribute on Binding %r!" % cls) # Optionally resolve model strings if isinstance(cls.model, six.string_types): cls.model = apps.get_model(cls.model) cls.model_label = "%s.%s" % ( cls.model._meta.app_label.lower(), cls.model._meta.object_name.lower(), ) return [cls.model] # Outbound binding @classmethod def encode(cls, stream, payload): """ Encodes stream + payload for outbound sending. """ raise NotImplementedError() @classmethod def pre_save_receiver(cls, instance, **kwargs): cls.pre_change_receiver(instance, CREATE if instance.pk is None else UPDATE) @classmethod def post_save_receiver(cls, instance, created, **kwargs): cls.post_change_receiver(instance, CREATE if created else UPDATE, **kwargs) @classmethod def pre_delete_receiver(cls, instance, **kwargs): cls.pre_change_receiver(instance, DELETE) @classmethod def post_delete_receiver(cls, instance, **kwargs): cls.post_change_receiver(instance, DELETE, **kwargs) @classmethod def pre_change_receiver(cls, instance, action): """ Entry point for triggering the binding from save signals. """ if action == CREATE: group_names = set() else: group_names = set(cls.group_names(instance, action)) if not hasattr(instance, '_binding_group_names'): instance._binding_group_names = {} instance._binding_group_names[cls] = group_names @classmethod def post_change_receiver(cls, instance, action, **kwargs): """ Triggers the binding to possibly send to its group. """ old_group_names = instance._binding_group_names[cls] if action == DELETE: new_group_names = set() else: new_group_names = set(cls.group_names(instance, action)) # if post delete, new_group_names should be [] self = cls() self.instance = instance # Django DDP had used the ordering of DELETE, UPDATE then CREATE for good reasons. self.send_messages(instance, old_group_names - new_group_names, DELETE, **kwargs) self.send_messages(instance, old_group_names & new_group_names, UPDATE, **kwargs) self.send_messages(instance, new_group_names - old_group_names, CREATE, **kwargs) def send_messages(self, instance, group_names, action, **kwargs): """ Serializes the instance and sends it to all provided group names. """ if not group_names: return # no need to serialize, bail. self.signal_kwargs = kwargs payload = self.serialize(instance, action) if payload == {}: return # nothing to send, bail. assert self.stream is not None message = self.encode(self.stream, payload) for group_name in group_names: group = Group(group_name) group.send(message) @classmethod def group_names(cls, 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 (e.g. {"pk": 12, "value": 42, "string": "some string"}) Kwargs are passed from the models save and delete methods. """ raise NotImplementedError() # Inbound binding @classmethod def trigger_inbound(cls, message, **kwargs): """ Triggers the binding to see if it will do something. Also acts as a consumer. """ # 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) @classmethod def get_handler(cls): """ Adds decorators to trigger_inbound. """ handler = cls.trigger_inbound if cls.channel_session_user: return channel_session_user(handler) elif cls.channel_session: return channel_session(handler) else: return handler @classmethod def consumer(cls, message, **kwargs): handler = cls.get_handler() handler(message, **kwargs) 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, action, pk): if action == "create": self.create(data) elif action == "update": self.update(pk, data) elif action == "delete": self.delete(pk) 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, pk, data): """ Updates the model with the data. """ raise NotImplementedError() def delete(self, pk): """ Deletes the model instance. """ self.model.objects.filter(pk=pk).delete()