Skip to content

Commit

Permalink
Added on success transaction signals which is called after successful…
Browse files Browse the repository at this point in the history
… response/command/tast
  • Loading branch information
matllubos committed Jan 19, 2018
1 parent 0deeca9 commit a62480f
Show file tree
Hide file tree
Showing 15 changed files with 674 additions and 65 deletions.
5 changes: 0 additions & 5 deletions .travis.yml
Expand Up @@ -6,16 +6,11 @@ python:
- "3.5"

env:
- DJANGO_VERSION=1.7
- DJANGO_VERSION=1.8
- DJANGO_VERSION=1.9

matrix:
exclude:
- python: "3.5"
env: DJANGO_VERSION=1.6
- python: "3.5"
env: DJANGO_VERSION=1.7
- python: "3.3"
env: DJANGO_VERSION=1.9

Expand Down
134 changes: 117 additions & 17 deletions chamber/models/__init__.py
@@ -1,20 +1,27 @@
from __future__ import unicode_literals

import copy

import collections

from itertools import chain

import six

from six import python_2_unicode_compatible

from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.db.models.base import ModelBase
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text

from chamber.exceptions import PersistenceException
from chamber.patch import Options
from chamber.shortcuts import change_and_save, change, bulk_change_and_save

from .fields import * # NOQA exposing classes and functions as a module API
from .signals import dispatcher_post_save, dispatcher_pre_save


def many_to_many_field_to_dict(field, instance):
Expand Down Expand Up @@ -60,18 +67,25 @@ def model_to_dict(instance, fields=None, exclude=None):

@python_2_unicode_compatible
class ChangedFields(object):
"""
Class stores changed fields and its initial and current values.
"""

def __init__(self, instance):
self.instance = instance
self.initial_values = self.get_instance_dict(instance)
def __init__(self, initial_dict):
self._initial_dict = initial_dict

def get_instance_dict(self, instance):
return model_to_dict(instance, fields=(field.name for field in instance._meta.fields))
@property
def initial_values(self):
return self._initial_dict

@property
def current_values(self):
raise NotImplementedError

@property
def diff(self):
d1 = self.initial_values
d2 = self.get_instance_dict(self.instance)
d2 = self.current_values
return {k: ValueChange(v, d2[k]) for k, v in d1.items() if v != d2[k]}

def __setitem__(self, key, item):
Expand Down Expand Up @@ -126,6 +140,40 @@ def __str__(self):
return repr(self.diff)


class DynamicChangedFields(ChangedFields):
"""
Dynamic changed fields are changed with the instance changes.
"""

def __init__(self, instance):
super(DynamicChangedFields, self).__init__(self._get_instance_dict(instance))
self.instance = instance

def _get_instance_dict(self, instance):
return model_to_dict(instance, fields=(field.name for field in instance._meta.fields))

@property
def current_values(self):
return self._get_instance_dict(self.instance)

def get_static_changes(self):
return StaticChangedFields(self.initial_values, self.current_values)


class StaticChangedFields(ChangedFields):
"""
Static changed fields are immutable. The origin instance changes will not have an affect.
"""

def __init__(self, initial_dict, current_dict):
super(StaticChangedFields, self).__init__(initial_dict)
self._current_dict = current_dict

@property
def current_values(self):
return self._current_dict


class ComparableModelMixin(object):

def equals(self, obj, comparator):
Expand All @@ -145,6 +193,7 @@ def compare(self, a, b):


class AuditModel(models.Model):

created_at = models.DateTimeField(verbose_name=_('created at'), null=False, blank=False, auto_now_add=True,
db_index=True)
changed_at = models.DateTimeField(verbose_name=_('changed at'), null=False, blank=False, auto_now=True,
Expand All @@ -170,16 +219,44 @@ def send(self):
class SmartQuerySet(models.QuerySet):

def fast_distinct(self):
"""
Because standard distinct used on the all fields are very slow and works only with PostgreSQL database
this method provides alternative to the standard distinct method.
:return: qs with unique objects
"""
return self.model.objects.filter(pk__in=self.values_list('pk', flat=True))

def change_and_save(self, **chaned_fields):
"""
Changes a given `changed_fields` on each object in the queryset, saves objects
and returns the changed objects in the queryset.
"""
bulk_change_and_save(self, **chaned_fields)
return self.filter()


class SmartModelBase(ModelBase):
"""
Smart model meta class that register dispatchers to the post or pre save signals.
"""

def __new__(cls, name, bases, attrs):

new_cls = super(SmartModelBase, cls).__new__(cls, name, bases, attrs)
for dispatcher in new_cls.dispatchers:
dispatcher.connect(new_cls)
return new_cls

class SmartModel(AuditModel):

class SmartModel(six.with_metaclass(SmartModelBase, AuditModel)):

objects = SmartQuerySet.as_manager()

dispatchers = []

def __init__(self, *args, **kwargs):
super(SmartModel, self).__init__(*args, **kwargs)
self.changed_fields = ChangedFields(self)
self.changed_fields = DynamicChangedFields(self)
self.post_save = Signal(self)

@property
Expand Down Expand Up @@ -241,34 +318,35 @@ def _pre_save(self, *args, **kwargs):
def _call_pre_save(self, *args, **kwargs):
self._pre_save(*args, **kwargs)

def _call_dispatcher_group(self, group_name, change, changed_fields, *args, **kwargs):
if hasattr(self, group_name):
for dispatcher in getattr(self, group_name):
dispatcher(self, change, changed_fields, *args, **kwargs)

def _save(self, is_cleaned_pre_save=None, is_cleaned_post_save=None, force_insert=False, force_update=False,
using=None, update_fields=None, *args, **kwargs):

is_cleaned_pre_save = (
self._smart_meta.is_cleaned_pre_save if is_cleaned_pre_save is None else is_cleaned_pre_save
)
is_cleaned_post_save = (
self._smart_meta.is_cleaned_post_save if is_cleaned_post_save is None else is_cleaned_post_save
)

origin = self.__class__

change = bool(self.pk)
kwargs.update(self._get_save_extra_kwargs())

self._call_pre_save(change, self.changed_fields, *args, **kwargs)
if is_cleaned_pre_save:
self._clean_pre_save(*args, **kwargs)
self._call_dispatcher_group('pre_save_dispatchers', change, self.changed_fields, *args, **kwargs)

dispatcher_pre_save.send(sender=origin, instance=self, change=change,
changed_fields=self.changed_fields.get_static_changes(),
*args, **kwargs)
super(SmartModel, self).save(force_insert=force_insert, force_update=force_update, using=using,
update_fields=update_fields)
self._call_post_save(change, self.changed_fields, *args, **kwargs)
if is_cleaned_post_save:
self._clean_post_save(*args, **kwargs)
self._call_dispatcher_group('post_save_dispatchers', change, self.changed_fields, *args, **kwargs)
dispatcher_post_save.send(sender=origin, instance=self, change=change,
changed_fields=self.changed_fields.get_static_changes(),
*args, **kwargs)
self.post_save.send()

def _post_save(self, *args, **kwargs):
Expand All @@ -283,7 +361,7 @@ def save(self, *args, **kwargs):
self._save(*args, **kwargs)
else:
self._save(*args, **kwargs)
self.changed_fields = ChangedFields(self)
self.changed_fields = DynamicChangedFields(self)

def _pre_delete(self, *args, **kwargs):
pass
Expand Down Expand Up @@ -318,6 +396,28 @@ def delete(self, *args, **kwargs):
else:
self._delete(*args, **kwargs)

def refresh_from_db(self, *args, **kwargs):
super().refresh_from_db(*args, **kwargs)
return self

def change(self, **changed_fields):
"""
Changes a given `changed_fields` on this object and returns itself.
:param changed_fields: fields to change
:return: self
"""
change(self, **changed_fields)
return self

def change_and_save(self, **changed_fields):
"""
Changes a given `changed_fields` on this object, saves it and returns itself.
:param changed_fields: fields to change
:return: self
"""
change_and_save(self, **changed_fields)
return self

class Meta:
abstract = True

Expand Down
51 changes: 35 additions & 16 deletions chamber/models/dispatchers.py
@@ -1,31 +1,47 @@
from __future__ import unicode_literals

from collections import defaultdict

from django.core.exceptions import ImproperlyConfigured

from chamber.utils.transaction import (
on_success, OnSuccessHandler, OneTimeOnSuccessHandler, InstanceOneTimeOnSuccessHandler
)

from .signals import dispatcher_post_save


class BaseDispatcher(object):
"""
Base dispatcher class that can be subclassed to call a handler based on a change in some a SmartModel.
If you subclass, be sure the __call__ method does not change signature.
"""

signal = None

def _validate_init_params(self):
if not callable(self.handler):
raise ImproperlyConfigured('Registered handler must be a callable.')

def __init__(self, handler, *args, **kwargs):
def __init__(self, handler, signal=None):
self.handler = handler
self._validate_init_params()
self._connected = defaultdict(list)
self._signal = signal if signal is not None else self.signal

def connect(self, sender):
self._signal.connect(self, sender=sender)

def __call__(self, obj, *args, **kwargs):
def __call__(self, instance, **kwargs):
"""
`obj` ... instance of the SmartModel where the handler is being called
`instance` ... instance of the SmartModel where the handler is being called
Some dispatchers require additional params to evaluate the handler can be dispatched,
these are hidden in args and kwargs.
"""
if self._can_dispatch(obj, *args, **kwargs):
self.handler(obj)
if self._can_dispatch(instance, **kwargs):
self.handler(instance=instance, **kwargs)

def _can_dispatch(self, obj, *args, **kwargs):
def _can_dispatch(self, instance, *args, **kwargs):
raise NotImplementedError


Expand All @@ -41,39 +57,42 @@ def _validate_init_params(self):
"""
pass

def __init__(self, handler, property_name):
def __init__(self, handler, property_name, signal=None):
self.property_name = property_name
super(PropertyDispatcher, self).__init__(handler, property_name)
super(PropertyDispatcher, self).__init__(handler, signal)

def _can_dispatch(self, obj, *args, **kwargs):
return getattr(obj, self.property_name)
def _can_dispatch(self, instance, **kwargs):
return getattr(instance, self.property_name)


class CreatedDispatcher(BaseDispatcher):
"""
Calls registered handler if and only if an instance of the model is being created.
"""

def _can_dispatch(self, obj, change, *args, **kwargs):
def _can_dispatch(self, instance, change, **kwargs):
return not change


class StateDispatcher(BaseDispatcher):

"""
Use this class to register a handler for transition of a model to a certain state.
"""

def _validate_init_params(self):
super(StateDispatcher, self)._validate_init_params()
if self.field_value not in {value for value, _ in self.enum.choices}:
raise ImproperlyConfigured('Enum of FieldDispatcher does not contain {}.'.format(self.field_value))

def __init__(self, handler, enum, field, field_value):
def __init__(self, handler, enum, field, field_value, signal=None):
self.enum = enum
self.field = field
self.field_value = field_value

super(StateDispatcher, self).__init__(handler, enum, field, field_value)
super(StateDispatcher, self).__init__(handler, signal=signal)

def _can_dispatch(self, obj, change, changed_fields, *args, **kwargs):
return self.field.get_attname() in changed_fields and getattr(obj, self.field.get_attname()) == self.field_value
def _can_dispatch(self, instance, change, changed_fields, *args, **kwargs):
return (
self.field.get_attname() in changed_fields and
getattr(instance, self.field.get_attname()) == self.field_value
)
5 changes: 5 additions & 0 deletions chamber/models/signals.py
@@ -0,0 +1,5 @@
from django.dispatch import Signal


dispatcher_pre_save = Signal()
dispatcher_post_save = Signal()
Empty file.

0 comments on commit a62480f

Please sign in to comment.