def _handle_changed_object(request, sender, instance, **kwargs): """ Fires when an object is created or updated. """ # Queue the object for processing once the request completes if kwargs.get("created"): action = ObjectChangeActionChoices.ACTION_CREATE elif "created" in kwargs: action = ObjectChangeActionChoices.ACTION_UPDATE elif kwargs.get("action") in ["post_add", "post_remove"] and kwargs["pk_set"]: # m2m_changed with objects added or removed action = ObjectChangeActionChoices.ACTION_UPDATE else: return # Record an ObjectChange if applicable if hasattr(instance, "to_objectchange"): objectchange = instance.to_objectchange(action) objectchange.user = _get_user_if_authenticated(request, objectchange) objectchange.request_id = request.id objectchange.save() # Enqueue webhooks enqueue_webhooks(instance, request.user, request.id, action) # Increment metric counters if action == ObjectChangeActionChoices.ACTION_CREATE: model_inserts.labels(instance._meta.model_name).inc() elif action == ObjectChangeActionChoices.ACTION_UPDATE: model_updates.labels(instance._meta.model_name).inc() # Housekeeping: 0.1% chance of clearing out expired ObjectChanges if settings.CHANGELOG_RETENTION and random.randint(1, 1000) == 1: cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION) ObjectChange.objects.filter(time__lt=cutoff).delete()
def __call__(self, request): # Initialize an empty list to cache objects being saved. _thread_locals.changed_objects = [] # Assign a random unique ID to the request. This will be used to associate multiple object changes made during # the same request. request.id = uuid.uuid4() # Connect our receivers to the post_save and post_delete signals. post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object') pre_delete.connect(handle_deleted_object, dispatch_uid='handle_deleted_object') # Provide a hook for purging the change cache purge_changelog.connect(purge_objectchange_cache) # Process the request response = self.get_response(request) # If the change cache is empty, there's nothing more we need to do. if not _thread_locals.changed_objects: return response # Create records for any cached objects that were changed. for instance, action in _thread_locals.changed_objects: # Refresh cached custom field values if action in [ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE]: if hasattr(instance, 'cache_custom_fields'): instance.cache_custom_fields() # Record an ObjectChange if applicable if hasattr(instance, 'to_objectchange'): objectchange = instance.to_objectchange(action) objectchange.user = request.user objectchange.request_id = request.id objectchange.save() # Enqueue webhooks enqueue_webhooks(instance, request.user, request.id, action) # Increment metric counters if action == ObjectChangeActionChoices.ACTION_CREATE: model_inserts.labels(instance._meta.model_name).inc() elif action == ObjectChangeActionChoices.ACTION_UPDATE: model_updates.labels(instance._meta.model_name).inc() elif action == ObjectChangeActionChoices.ACTION_DELETE: model_deletes.labels(instance._meta.model_name).inc() # Housekeeping: 1% chance of clearing out expired ObjectChanges. This applies only to requests which result in # one or more changes being logged. if settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1: cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION) purged_count, _ = ObjectChange.objects.filter( time__lt=cutoff ).delete() return response
def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs): """ Fires when an object is created or updated. """ def is_same_object(instance, webhook_data): return ( ContentType.objects.get_for_model(instance) == webhook_data['content_type'] and instance.pk == webhook_data['object_id'] and request.id == webhook_data['request_id'] ) if not hasattr(instance, 'to_objectchange'): return m2m_changed = False # Determine the type of change being made if kwargs.get('created'): action = ObjectChangeActionChoices.ACTION_CREATE elif 'created' in kwargs: action = ObjectChangeActionChoices.ACTION_UPDATE elif kwargs.get('action') in ['post_add', 'post_remove'] and kwargs['pk_set']: # m2m_changed with objects added or removed m2m_changed = True action = ObjectChangeActionChoices.ACTION_UPDATE else: return # Record an ObjectChange if applicable if hasattr(instance, 'to_objectchange'): if m2m_changed: ObjectChange.objects.filter( changed_object_type=ContentType.objects.get_for_model(instance), changed_object_id=instance.pk, request_id=request.id ).update( postchange_data=instance.to_objectchange(action).postchange_data ) else: objectchange = instance.to_objectchange(action) objectchange.user = request.user objectchange.request_id = request.id objectchange.save() # If this is an M2M change, update the previously queued webhook (from post_save) if m2m_changed and webhook_queue and is_same_object(instance, webhook_queue[-1]): instance.refresh_from_db() # Ensure that we're working with fresh M2M assignments webhook_queue[-1]['data'] = serialize_for_webhook(instance) webhook_queue[-1]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange'] else: enqueue_object(webhook_queue, instance, request.user, request.id, action) # Increment metric counters if action == ObjectChangeActionChoices.ACTION_CREATE: model_inserts.labels(instance._meta.model_name).inc() elif action == ObjectChangeActionChoices.ACTION_UPDATE: model_updates.labels(instance._meta.model_name).inc()
def _handle_changed_object(request, sender, instance, **kwargs): """ Fires when an object is created or updated. """ m2m_changed = False # Determine the type of change being made if kwargs.get("created"): action = ObjectChangeActionChoices.ACTION_CREATE elif "created" in kwargs: action = ObjectChangeActionChoices.ACTION_UPDATE elif kwargs.get("action") in ["post_add", "post_remove" ] and kwargs["pk_set"]: # m2m_changed with objects added or removed m2m_changed = True action = ObjectChangeActionChoices.ACTION_UPDATE else: return # Record an ObjectChange if applicable if hasattr(instance, "to_objectchange"): if m2m_changed: ObjectChange.objects.filter( changed_object_type=ContentType.objects.get_for_model( instance), changed_object_id=instance.pk, request_id=request.id, ).update(object_data=instance.to_objectchange(action).object_data) else: objectchange = instance.to_objectchange(action) objectchange.user = _get_user_if_authenticated( request, objectchange) objectchange.request_id = request.id objectchange.save() # Enqueue webhooks enqueue_webhooks(instance, request.user, request.id, action) # Increment metric counters if action == ObjectChangeActionChoices.ACTION_CREATE: model_inserts.labels(instance._meta.model_name).inc() elif action == ObjectChangeActionChoices.ACTION_UPDATE: model_updates.labels(instance._meta.model_name).inc() # Housekeeping: 0.1% chance of clearing out expired ObjectChanges changelog_retention = get_settings_or_config("CHANGELOG_RETENTION") if changelog_retention and random.randint(1, 1000) == 1: cutoff = timezone.now() - timedelta(days=changelog_retention) ObjectChange.objects.filter(time__lt=cutoff).delete()
def _handle_changed_object(request, sender, instance, **kwargs): """ Fires when an object is created or updated. """ m2m_changed = False # Determine the type of change being made if kwargs.get('created'): action = ObjectChangeActionChoices.ACTION_CREATE elif 'created' in kwargs: action = ObjectChangeActionChoices.ACTION_UPDATE elif kwargs.get('action') in ['post_add', 'post_remove'] and kwargs['pk_set']: # m2m_changed with objects added or removed m2m_changed = True action = ObjectChangeActionChoices.ACTION_UPDATE else: return # Record an ObjectChange if applicable if hasattr(instance, 'to_objectchange'): if m2m_changed: ObjectChange.objects.filter( changed_object_type=ContentType.objects.get_for_model(instance), changed_object_id=instance.pk, request_id=request.id ).update( postchange_data=instance.to_objectchange(action).postchange_data ) else: objectchange = instance.to_objectchange(action) objectchange.user = request.user objectchange.request_id = request.id objectchange.save() # Enqueue webhooks enqueue_webhooks(instance, request.user, request.id, action) # Increment metric counters if action == ObjectChangeActionChoices.ACTION_CREATE: model_inserts.labels(instance._meta.model_name).inc() elif action == ObjectChangeActionChoices.ACTION_UPDATE: model_updates.labels(instance._meta.model_name).inc() # Housekeeping: 0.1% chance of clearing out expired ObjectChanges if settings.CHANGELOG_RETENTION and random.randint(1, 1000) == 1: cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION) ObjectChange.objects.filter(time__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS)
def __call__(self, request): # Initialize an empty list to cache objects being saved. _thread_locals.changed_objects = [] # Assign a random unique ID to the request. This will be used to associate multiple object changes made during # the same request. request.id = uuid.uuid4() # Signals don't include the request context, so we're currying it into the post_delete function ahead of time. record_object_deleted = curry(_record_object_deleted, request) # Connect our receivers to the post_save and post_delete signals. post_save.connect(cache_changed_object, dispatch_uid='record_object_saved') post_delete.connect(record_object_deleted, dispatch_uid='record_object_deleted') # Process the request response = self.get_response(request) # Create records for any cached objects that were created/updated. for obj, action in _thread_locals.changed_objects: # Record the change if hasattr(obj, 'log_change'): obj.log_change(request.user, request.id, action) # Enqueue webhooks enqueue_webhooks(obj, request.user, request.id, action) # Increment metric counters if action == OBJECTCHANGE_ACTION_CREATE: model_inserts.labels(obj._meta.model_name).inc() elif action == OBJECTCHANGE_ACTION_UPDATE: model_updates.labels(obj._meta.model_name).inc() # Housekeeping: 1% chance of clearing out expired ObjectChanges if _thread_locals.changed_objects and settings.CHANGELOG_RETENTION and random.randint( 1, 100) == 1: cutoff = timezone.now() - timedelta( days=settings.CHANGELOG_RETENTION) purged_count, _ = ObjectChange.objects.filter( time__lt=cutoff).delete() return response
def _handle_changed_object(request, sender, instance, **kwargs): """ Fires when an object is created or updated. """ # Ignore object without the right method if not hasattr(instance, "to_objectchange"): return # Queue the object for processing once the request completes if kwargs.get("created"): action = ObjectChangeAction.CREATE elif "created" in kwargs: action = ObjectChangeAction.UPDATE elif kwargs.get("action") in ["post_add", "post_remove"] and kwargs["pk_set"]: # m2m_changed with objects added or removed action = ObjectChangeAction.UPDATE else: return # Record an object change change = instance.to_objectchange(action) change.user = request.user change.request_id = request.id change.save() # Enqueue webhooks enqueue_webhooks(instance, request.user, request.id, action) # Increment metric counters if action == ObjectChangeAction.CREATE: model_inserts.labels(instance._meta.model_name).inc() elif action == ObjectChangeAction.UPDATE: model_updates.labels(instance._meta.model_name).inc() # Housekeeping: 0.1% chance of clearing out expired ObjectChanges if settings.CHANGELOG_RETENTION and random.randint(1, 1000) == 1: date_limit = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION) ObjectChange.objects.filter(time__lt=date_limit)._raw_delete( using=DEFAULT_DB_ALIAS )
def handle_changed_object(sender, instance, **kwargs): """ Fires when an object is created or updated. """ # Ignore object without the right method if not hasattr(instance, "to_objectchange"): return request = get_request() m2m_changed = False def is_same_object(instance, webhook_data): return ( instance.pk == webhook_data["object_id"] and request.id == webhook_data["request_id"] ) # Queue the object for processing once the request completes if kwargs.get("created"): action = ObjectChangeAction.CREATE elif "created" in kwargs: action = ObjectChangeAction.UPDATE elif kwargs.get("action") in ["post_add", "post_remove"] and kwargs["pk_set"]: # m2m_changed with objects added or removed m2m_changed = True action = ObjectChangeAction.UPDATE else: return if m2m_changed: ObjectChange.objects.filter( changed_object_type=ContentType.objects.get_for_model(instance), changed_object_id=instance.pk, request_id=request.id, ).update(postchange_data=instance.to_objectchange(action).postchange_data) else: # Record an object change change = instance.to_objectchange(action) change.user = request.user change.request_id = request.id change.save() # If this is an M2M change, update the previously queued webhook (from post_save) webhook_queue = thread_locals.webhook_queue if m2m_changed and webhook_queue and is_same_object(instance, webhook_queue[-1]): instance.refresh_from_db() # Ensure that we're working with fresh M2M assignments webhook_queue[-1]["data"] = serialize_for_webhook(instance) webhook_queue[-1]["snapshots"]["postchange"] = get_snapshots(instance, action)[ "postchange" ] else: enqueue_object(webhook_queue, instance, request.user, request.id, action) # Increment metric counters if action == ObjectChangeAction.CREATE: model_inserts.labels(instance._meta.model_name).inc() elif action == ObjectChangeAction.UPDATE: model_updates.labels(instance._meta.model_name).inc() # Housekeeping: 0.1% chance of clearing out expired ObjectChanges if settings.CHANGELOG_RETENTION and random.randint(1, 1000) == 1: date_limit = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION) ObjectChange.objects.filter(time__lt=date_limit)._raw_delete( using=DEFAULT_DB_ALIAS )