django-orchestra/orchestra/apps/orchestration/middlewares.py

124 lines
5.6 KiB
Python
Raw Normal View History

2014-05-08 16:59:35 +00:00
from threading import local
2014-10-03 14:02:11 +00:00
from django.core.urlresolvers import resolve
from django.db.models.signals import pre_delete, post_save, m2m_changed
2014-05-08 16:59:35 +00:00
from django.dispatch import receiver
from django.http.response import HttpResponseServerError
2014-07-17 16:09:24 +00:00
2014-05-08 16:59:35 +00:00
from orchestra.utils.python import OrderedSet
from .backends import ServiceBackend
from .helpers import message_user
from .models import BackendLog
from .models import BackendOperation as Operation
2014-07-17 16:09:24 +00:00
@receiver(post_save, dispatch_uid='orchestration.post_save_collector')
2014-05-08 16:59:35 +00:00
def post_save_collector(sender, *args, **kwargs):
2014-10-15 12:47:28 +00:00
if sender not in [BackendLog, Operation]:
2014-05-08 16:59:35 +00:00
OperationsMiddleware.collect(Operation.SAVE, **kwargs)
2014-07-17 16:09:24 +00:00
@receiver(pre_delete, dispatch_uid='orchestration.pre_delete_collector')
2014-05-08 16:59:35 +00:00
def pre_delete_collector(sender, *args, **kwargs):
2014-10-15 12:47:28 +00:00
if sender not in [BackendLog, Operation]:
2014-05-08 16:59:35 +00:00
OperationsMiddleware.collect(Operation.DELETE, **kwargs)
@receiver(m2m_changed, dispatch_uid='orchestration.m2m_collector')
def m2m_collector(sender, *args, **kwargs):
# m2m relations without intermediary models are shit
# model.post_save is not sent and by the time related.post_save is sent
# the objects are not accessible with RelatedManager.all()
# We have to use this inefficient technique of collecting the instances via m2m_changed.post_add
if kwargs.pop('action') == 'post_add':
for pk in kwargs['pk_set']:
kwargs['instance'] = kwargs['model'].objects.get(pk=pk)
OperationsMiddleware.collect(Operation.SAVE, **kwargs)
2014-05-08 16:59:35 +00:00
class OperationsMiddleware(object):
"""
Stores all the operations derived from save and delete signals and executes them
at the end of the request/response cycle
"""
# Thread local is used because request object is not available on model signals
thread_locals = local()
@classmethod
def get_pending_operations(cls):
# Check if an error poped up before OperationsMiddleware.process_request()
if hasattr(cls.thread_locals, 'request'):
request = cls.thread_locals.request
if not hasattr(request, 'pending_operations'):
request.pending_operations = OrderedSet()
return request.pending_operations
return set()
@classmethod
def collect(cls, action, **kwargs):
""" Collects all pending operations derived from model signals """
request = getattr(cls.thread_locals, 'request', None)
if request is None:
return
pending_operations = cls.get_pending_operations()
for backend in ServiceBackend.get_backends():
# Check if there exists a related instance to be executed for this backend
instances = []
2014-05-08 16:59:35 +00:00
if backend.is_main(kwargs['instance']):
instances = [(kwargs['instance'], action)]
2014-05-08 16:59:35 +00:00
else:
candidate = backend.get_related(kwargs['instance'])
if candidate:
if candidate.__class__.__name__ == 'ManyRelatedManager':
candidates = candidate.all()
else:
candidates = [candidate]
for candidate in candidates:
# Check if a delete for candidate is in pending_operations
delete = Operation.create(backend, candidate, Operation.DELETE)
if delete not in pending_operations:
# related objects with backend.model trigger save()
action = Operation.SAVE
instances.append((candidate, action))
for instance, action in instances:
# Maintain consistent state of pending_operations based on save/delete behaviour
2014-05-08 16:59:35 +00:00
# Prevent creating a deleted instance by deleting existing saves
if action == Operation.DELETE:
save = Operation.create(backend, instance, Operation.SAVE)
try:
pending_operations.remove(save)
except KeyError:
pass
else:
update_fields = kwargs.get('update_fields', None)
if update_fields:
2014-10-07 13:50:59 +00:00
# "update_fileds=[]" is a convention for explicitly executing backend
# i.e. account.disable()
2014-10-10 14:39:46 +00:00
if update_fields != []:
2014-10-07 13:50:59 +00:00
execute = False
for field in update_fields:
if field not in backend.ignore_fields:
execute = True
break
if not execute:
continue
2014-10-09 17:04:12 +00:00
operation = Operation.create(backend, instance, action)
if action != Operation.DELETE:
# usually we expect to be using last object state,
# except when we are deleting it
pending_operations.discard(operation)
pending_operations.add(operation)
2014-10-15 12:47:28 +00:00
2014-05-08 16:59:35 +00:00
def process_request(self, request):
""" Store request on a thread local variable """
type(self).thread_locals.request = request
def process_response(self, request, response):
""" Processes pending backend operations """
if not isinstance(response, HttpResponseServerError):
operations = type(self).get_pending_operations()
if operations:
logs = Operation.execute(operations)
2014-10-03 14:02:11 +00:00
if logs and resolve(request.path).app_name == 'admin':
2014-05-08 16:59:35 +00:00
message_user(request, logs)
return response