django-auditlog/src/auditlog/diff.py

129 lines
4.7 KiB
Python
Raw Normal View History

from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Model, NOT_PROVIDED, DateTimeField
from django.utils import timezone
from django.utils.encoding import smart_text
2013-10-20 13:25:48 +00:00
def track_field(field):
"""
Returns whether the given field should be tracked by Auditlog.
Untracked fields are many-to-many relations and relations to the Auditlog LogEntry model.
:param field: The field to check.
:type field: Field
:return: Whether the given field should be tracked.
:rtype: bool
"""
from auditlog.models import LogEntry
# Do not track many to many relations
if field.many_to_many:
return False
# Do not track relations to LogEntry
if getattr(field, 'rel', None) is not None and field.rel.to == LogEntry:
return False
return True
2015-06-03 14:06:25 +00:00
def get_fields_in_model(instance):
"""
Returns the list of fields in the given model instance. Checks whether to use the official _meta API or use the raw
data. This method excludes many to many fields.
:param instance: The model instance to get the fields for
:type instance: Model
:return: The list of fields for the given model (instance)
:rtype: list
"""
assert isinstance(instance, Model)
# Check if the Django 1.8 _meta API is available
use_api = hasattr(instance._meta, 'get_fields') and callable(instance._meta.get_fields)
if use_api:
return [f for f in instance._meta.get_fields() if track_field(f)]
2015-06-03 14:06:25 +00:00
return instance._meta.fields
2015-05-14 23:25:44 +00:00
def model_instance_diff(old, new):
2013-10-20 13:25:48 +00:00
"""
2015-05-31 13:06:06 +00:00
Calculates the differences between two model instances. One of the instances may be ``None`` (i.e., a newly
created model or deleted model). This will cause all fields with a value to have changed (from ``None``).
2015-05-14 23:25:44 +00:00
:param old: The old state of the model instance.
:type old: Model
:param new: The new state of the model instance.
:type new: Model
:return: A dictionary with the names of the changed fields as keys and a two tuple of the old and new field values
as value.
2015-05-31 13:06:06 +00:00
:rtype: dict
2013-10-20 13:25:48 +00:00
"""
from auditlog.registry import auditlog
2013-10-20 13:25:48 +00:00
if not(old is None or isinstance(old, Model)):
raise TypeError("The supplied old instance is not a valid model instance.")
2013-10-20 13:25:48 +00:00
if not(new is None or isinstance(new, Model)):
raise TypeError("The supplied new instance is not a valid model instance.")
2013-10-20 13:25:48 +00:00
diff = {}
if old is not None and new is not None:
fields = set(old._meta.fields + new._meta.fields)
model_fields = auditlog.get_model_fields(new._meta.model)
2013-10-20 13:25:48 +00:00
elif old is not None:
2015-06-03 14:06:25 +00:00
fields = set(get_fields_in_model(old))
model_fields = auditlog.get_model_fields(old._meta.model)
2013-10-20 13:25:48 +00:00
elif new is not None:
2015-06-03 14:06:25 +00:00
fields = set(get_fields_in_model(new))
model_fields = auditlog.get_model_fields(new._meta.model)
2013-10-20 13:25:48 +00:00
else:
fields = set()
model_fields = None
2013-10-20 13:25:48 +00:00
# Check if fields must be filtered
if model_fields and (model_fields['include_fields'] or model_fields['exclude_fields']) and fields:
filtered_fields = []
if model_fields['include_fields']:
filtered_fields = [field for field in fields
if field.name in model_fields['include_fields']]
else:
filtered_fields = fields
if model_fields['exclude_fields']:
filtered_fields = [field for field in filtered_fields
if field.name not in model_fields['exclude_fields']]
fields = filtered_fields
2013-10-20 13:25:48 +00:00
for field in fields:
if isinstance(field, DateTimeField):
# DateTimeFields are timezone-aware, so we need to convert the field
# to its naive form before we can accuratly compare them for changes.
old_value = field.to_python(getattr(old, field.name, None))
if old_value is not None:
old_value = timezone.make_naive(old_value, timezone.utc)
new_value = field.to_python(getattr(new, field.name, None))
if new_value is not None:
new_value = timezone.make_naive(new_value, timezone.utc)
else:
try:
old_value = smart_text(getattr(old, field.name, None))
except ObjectDoesNotExist:
old_value = field.default if field.default is not NOT_PROVIDED else None
try:
new_value = smart_text(getattr(new, field.name, None))
except ObjectDoesNotExist:
new_value = None
2013-10-20 13:25:48 +00:00
if old_value != new_value:
diff[field.name] = (smart_text(old_value), smart_text(new_value))
2013-10-20 13:25:48 +00:00
if len(diff) == 0:
diff = None
return diff