middleware.py 3.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import random
  2. import threading
  3. import uuid
  4. from datetime import timedelta
  5. from django.conf import settings
  6. from django.db.models.signals import post_delete, post_save
  7. from django.utils import timezone
  8. from django.utils.functional import curry
  9. from extras.webhooks import enqueue_webhooks
  10. from .constants import (
  11. OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE,
  12. )
  13. from .models import ObjectChange
  14. _thread_locals = threading.local()
  15. def cache_changed_object(instance, **kwargs):
  16. action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE
  17. # Cache the object for further processing was the response has completed.
  18. _thread_locals.changed_objects.append(
  19. (instance, action)
  20. )
  21. def _record_object_deleted(request, instance, **kwargs):
  22. # Force resolution of request.user in case it's still a SimpleLazyObject. This seems to happen
  23. # occasionally during tests, but haven't been able to determine why.
  24. assert request.user.is_authenticated
  25. # Record that the object was deleted
  26. if hasattr(instance, 'log_change'):
  27. instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
  28. enqueue_webhooks(instance, request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
  29. class ObjectChangeMiddleware(object):
  30. """
  31. This middleware performs two functions in response to an object being created, updated, or deleted:
  32. 1. Create an ObjectChange to reflect the modification to the object in the changelog.
  33. 2. Enqueue any relevant webhooks.
  34. The post_save and pre_delete signals are employed to catch object modifications, however changes are recorded a bit
  35. differently for each. Objects being saved are cached into thread-local storage for action *after* the response has
  36. completed. This ensures that serialization of the object is performed only after any related objects (e.g. tags)
  37. have been created. Conversely, deletions are acted upon immediately, so that the serialized representation of the
  38. object is recorded before it (and any related objects) are actually deleted from the database.
  39. """
  40. def __init__(self, get_response):
  41. self.get_response = get_response
  42. def __call__(self, request):
  43. # Initialize an empty list to cache objects being saved.
  44. _thread_locals.changed_objects = []
  45. # Assign a random unique ID to the request. This will be used to associate multiple object changes made during
  46. # the same request.
  47. request.id = uuid.uuid4()
  48. # Signals don't include the request context, so we're currying it into the pre_delete function ahead of time.
  49. record_object_deleted = curry(_record_object_deleted, request)
  50. # Connect our receivers to the post_save and pre_delete signals.
  51. post_save.connect(cache_changed_object, dispatch_uid='record_object_saved')
  52. post_delete.connect(record_object_deleted, dispatch_uid='record_object_deleted')
  53. # Process the request
  54. response = self.get_response(request)
  55. # Create records for any cached objects that were created/updated.
  56. for obj, action in _thread_locals.changed_objects:
  57. # Record the change
  58. if hasattr(obj, 'log_change'):
  59. obj.log_change(request.user, request.id, action)
  60. # Enqueue webhooks
  61. enqueue_webhooks(obj, request.user, request.id, action)
  62. # Housekeeping: 1% chance of clearing out expired ObjectChanges
  63. if _thread_locals.changed_objects and settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1:
  64. cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
  65. purged_count, _ = ObjectChange.objects.filter(
  66. time__lt=cutoff
  67. ).delete()
  68. return response