mirror of
https://github.com/jazzband/django-auditlog.git
synced 2026-04-26 09:44:45 +00:00
* Modify ``change`` field to be a json field. Storing the object changes as a json is preferred because it allows SQL queries to access the change values. This work moves the burden of handling json objects from an implementation of python's json library in this package and puts it instead onto the ORM. Ultimately, having the text field store the changes was leaving them less accessible to external systems and code that is written outside the scope of the django auditlog. This change was accomplished by updating the field type on the model and then removing the JSON dumps invocations on write and JSON loads invocations on read. Test were updated to assert equality of dictionaries rather than equality of JSON parsable text. Separately, it was asserted that postgres will make these changes to existing data. Therefore, existing postgres installations should update the type of existing field values without issue. * Add test coverage for messages exceeding char len The "Modify change field to be a json field" commit reduced test coverage on the mixins.py file by 0.03%. The reduction in coverage was the result of reducing the number of operations required to achieve the desired state. An additional test was added to increase previously uncovered code. The net effect is an increase in test case coverage. * Add line to changelog Better markdown formatting Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com> * Update CHANGELOG text format More specific language in the improvement section regarding `LogEntry.change` Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com> * Update migration to show Django version 4.0 Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com> * Update CHANGELOG to show breaking change Running the migration to update the field type of `LogEntry.change` is a breaking change. Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com> * Update serial order of migrations * Adjust manager method for compatibility The create log method on the LogEntry manager required an additional kwarg for a call to create an instance regardless of a change or not. This felt brittle anyway. The reason it had worked prior to these changes was that the `change` kwarg was sending a string "null" and not a None when there were no changes. Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
163 lines
4.8 KiB
Python
163 lines
4.8 KiB
Python
from functools import wraps
|
|
|
|
from django.conf import settings
|
|
|
|
from auditlog.context import threadlocal
|
|
from auditlog.diff import model_instance_diff
|
|
from auditlog.models import LogEntry
|
|
from auditlog.signals import post_log, pre_log
|
|
|
|
|
|
def check_disable(signal_handler):
|
|
"""
|
|
Decorator that passes along disabled in kwargs if any of the following is true:
|
|
- 'auditlog_disabled' from threadlocal is true
|
|
- raw = True and AUDITLOG_DISABLE_ON_RAW_SAVE is True
|
|
"""
|
|
|
|
@wraps(signal_handler)
|
|
def wrapper(*args, **kwargs):
|
|
if not getattr(threadlocal, "auditlog_disabled", False) and not (
|
|
kwargs.get("raw") and settings.AUDITLOG_DISABLE_ON_RAW_SAVE
|
|
):
|
|
signal_handler(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
|
|
@check_disable
|
|
def log_create(sender, instance, created, **kwargs):
|
|
"""
|
|
Signal receiver that creates a log entry when a model instance is first saved to the database.
|
|
|
|
Direct use is discouraged, connect your model through :py:func:`auditlog.registry.register` instead.
|
|
"""
|
|
if created:
|
|
_create_log_entry(
|
|
action=LogEntry.Action.CREATE,
|
|
instance=instance,
|
|
sender=sender,
|
|
diff_old=None,
|
|
diff_new=instance,
|
|
)
|
|
|
|
|
|
@check_disable
|
|
def log_update(sender, instance, **kwargs):
|
|
"""
|
|
Signal receiver that creates a log entry when a model instance is changed and saved to the database.
|
|
|
|
Direct use is discouraged, connect your model through :py:func:`auditlog.registry.register` instead.
|
|
"""
|
|
if not instance._state.adding:
|
|
update_fields = kwargs.get("update_fields", None)
|
|
old = sender.objects.filter(pk=instance.pk).first()
|
|
_create_log_entry(
|
|
action=LogEntry.Action.UPDATE,
|
|
instance=instance,
|
|
sender=sender,
|
|
diff_old=old,
|
|
diff_new=instance,
|
|
fields_to_check=update_fields,
|
|
)
|
|
|
|
|
|
@check_disable
|
|
def log_delete(sender, instance, **kwargs):
|
|
"""
|
|
Signal receiver that creates a log entry when a model instance is deleted from the database.
|
|
|
|
Direct use is discouraged, connect your model through :py:func:`auditlog.registry.register` instead.
|
|
"""
|
|
if instance.pk is not None:
|
|
_create_log_entry(
|
|
action=LogEntry.Action.DELETE,
|
|
instance=instance,
|
|
sender=sender,
|
|
diff_old=instance,
|
|
diff_new=None,
|
|
)
|
|
|
|
|
|
def log_access(sender, instance, **kwargs):
|
|
"""
|
|
Signal receiver that creates a log entry when a model instance is accessed in a AccessLogDetailView.
|
|
|
|
Direct use is discouraged, connect your model through :py:func:`auditlog.registry.register` instead.
|
|
"""
|
|
if instance.pk is not None:
|
|
_create_log_entry(
|
|
action=LogEntry.Action.ACCESS,
|
|
instance=instance,
|
|
sender=sender,
|
|
diff_old=None,
|
|
diff_new=None,
|
|
force_log=True,
|
|
)
|
|
|
|
|
|
def _create_log_entry(
|
|
action, instance, sender, diff_old, diff_new, fields_to_check=None, force_log=False
|
|
):
|
|
pre_log_results = pre_log.send(
|
|
sender,
|
|
instance=instance,
|
|
action=action,
|
|
)
|
|
error = None
|
|
try:
|
|
changes = model_instance_diff(
|
|
diff_old, diff_new, fields_to_check=fields_to_check
|
|
)
|
|
|
|
if force_log or changes:
|
|
LogEntry.objects.log_create(
|
|
instance,
|
|
action=action,
|
|
changes=changes,
|
|
force_log=force_log,
|
|
)
|
|
except BaseException as e:
|
|
error = e
|
|
finally:
|
|
post_log.send(
|
|
sender,
|
|
instance=instance,
|
|
action=action,
|
|
error=error,
|
|
pre_log_results=pre_log_results,
|
|
)
|
|
if error:
|
|
raise error
|
|
|
|
|
|
def make_log_m2m_changes(field_name):
|
|
"""Return a handler for m2m_changed with field_name enclosed."""
|
|
|
|
@check_disable
|
|
def log_m2m_changes(signal, action, **kwargs):
|
|
"""Handle m2m_changed and call LogEntry.objects.log_m2m_changes as needed."""
|
|
if action not in ["post_add", "post_clear", "post_remove"]:
|
|
return
|
|
|
|
if action == "post_clear":
|
|
changed_queryset = kwargs["model"].objects.all()
|
|
else:
|
|
changed_queryset = kwargs["model"].objects.filter(pk__in=kwargs["pk_set"])
|
|
|
|
if action in ["post_add"]:
|
|
LogEntry.objects.log_m2m_changes(
|
|
changed_queryset,
|
|
kwargs["instance"],
|
|
"add",
|
|
field_name,
|
|
)
|
|
elif action in ["post_remove", "post_clear"]:
|
|
LogEntry.objects.log_m2m_changes(
|
|
changed_queryset,
|
|
kwargs["instance"],
|
|
"delete",
|
|
field_name,
|
|
)
|
|
|
|
return log_m2m_changes
|