Make it so Django's timezone-aware DateTimeFields remain unchanged when Django resets the timezone. DateFields are not timezone-aware, so they do not need fixing.

Django converts the timezone of DateTimeFields to UTC when the field gets saved. This makes it so when you update a model which includes a DateTimeField, and your server is running in another timezone, the AuditLog will think you changed the timestamp, while it actually is the same time, but in another timezone. This commit adds a specific check in the model_instance_diff function for DateTimeField models (and any subclasses of it), which converts the old and new values to UTC before comparing them to see if they've actually changed. It also adds tests to see if the code works properly.

The extra test_setting (USE_TZ) is added because timezone support is disabled if it is not specified, this setting enables it.
This commit is contained in:
Kevin Alberts 2017-01-11 20:07:10 +01:00
parent 7458e44669
commit 6f4c6eb8a2
4 changed files with 102 additions and 10 deletions

View file

@ -1,7 +1,8 @@
from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Model, NOT_PROVIDED
from django.db.models import Model, NOT_PROVIDED, DateTimeField
from django.utils import timezone
from django.utils.encoding import smart_text
@ -97,15 +98,26 @@ def model_instance_diff(old, new):
fields = filtered_fields
for field in fields:
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
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)
try:
new_value = smart_text(getattr(new, field.name, None))
except ObjectDoesNotExist:
new_value = None
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
if old_value != new_value:
diff[field.name] = (smart_text(old_value), smart_text(new_value))

View file

@ -106,6 +106,18 @@ class AdditionalDataIncludedModel(models.Model):
}
return object_details
class DateTimeFieldModel(models.Model):
"""
A model with a DateTimeField, used to test DateTimeField
changes are detected properly.
"""
label = models.CharField(max_length=100)
timestamp = models.DateTimeField()
history = AuditlogHistoryField()
auditlog.register(SimpleModel)
auditlog.register(AltPrimaryKeyModel)
auditlog.register(ProxyModel)
@ -115,3 +127,4 @@ auditlog.register(ManyRelatedModel.related.through)
auditlog.register(SimpleIncludeModel, include_fields=['label'])
auditlog.register(SimpleExcludeModel, exclude_fields=['text'])
auditlog.register(AdditionalDataIncludedModel)
auditlog.register(DateTimeFieldModel)

View file

@ -24,3 +24,5 @@ DATABASES = {
}
ROOT_URLCONF = []
USE_TZ = True

View file

@ -4,11 +4,14 @@ from django.core.exceptions import ValidationError
from django.db.models.signals import pre_save
from django.http import HttpResponse
from django.test import TestCase, RequestFactory
from django.utils import timezone
from auditlog.middleware import AuditlogMiddleware
from auditlog.models import LogEntry
from auditlog.registry import auditlog
from auditlog_tests.models import SimpleModel, AltPrimaryKeyModel, ProxyModel, \
SimpleIncludeModel, SimpleExcludeModel, RelatedModel, ManyRelatedModel, AdditionalDataIncludedModel
SimpleIncludeModel, SimpleExcludeModel, RelatedModel, ManyRelatedModel, AdditionalDataIncludedModel, \
DateTimeFieldModel
class SimpleModelTest(TestCase):
@ -220,6 +223,68 @@ class AdditionalDataModelTest(TestCase):
msg="Related model's id is logged")
class DateTimeFieldModelTest(TestCase):
"""Tests if DateTimeField changes are recognised correctly"""
utc_plus_one = timezone.get_fixed_timezone(datetime.timedelta(hours=1))
def test_model_with_same_time(self):
timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc)
dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp)
dtm.save()
self.assertTrue(dtm.history.count() == 1, msg="There is one log entry")
# Change timestamp to same datetime and timezone
timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc)
dtm.timestamp = timestamp
dtm.save()
# Nothing should have changed
self.assertTrue(dtm.history.count() == 1, msg="There is one log entry")
def test_model_with_different_timezone(self):
timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc)
dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp)
dtm.save()
self.assertTrue(dtm.history.count() == 1, msg="There is one log entry")
# Change timestamp to same datetime in another timezone
timestamp = datetime.datetime(2017, 1, 10, 13, 0, tzinfo=self.utc_plus_one)
dtm.timestamp = timestamp
dtm.save()
# Nothing should have changed
self.assertTrue(dtm.history.count() == 1, msg="There is one log entry")
def test_model_with_different_time(self):
timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc)
dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp)
dtm.save()
self.assertTrue(dtm.history.count() == 1, msg="There is one log entry")
# Change timestamp to another datetime in the same timezone
timestamp = datetime.datetime(2017, 1, 10, 13, 0, tzinfo=timezone.utc)
dtm.timestamp = timestamp
dtm.save()
# The time should have changed.
self.assertTrue(dtm.history.count() == 2, msg="There are two log entries")
def test_model_with_different_time_and_timezone(self):
timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc)
dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp)
dtm.save()
self.assertTrue(dtm.history.count() == 1, msg="There is one log entry")
# Change timestamp to another datetime and another timezone
timestamp = datetime.datetime(2017, 1, 10, 14, 0, tzinfo=self.utc_plus_one)
dtm.timestamp = timestamp
dtm.save()
# The time should have changed.
self.assertTrue(dtm.history.count() == 2, msg="There are two log entries")
class UnregisterTest(TestCase):
def setUp(self):
auditlog.unregister(SimpleModel)