A DateTime field extension that automatically stores the timezone, and the computed UTC equivalent. This field needs the pytz library.
The field adds two new fields to the model, with the same name of the UTCDateTimeField field, and a suffix. For an UTCDateTimeField named 'updated', the model will contain
-
an 'updated' field, which holds the local datetime
-
an 'updated_utc' field, which holds the corresponding UTC datetime
-
an 'updated_tz' field, which holds the field timezone name
The timezone can vary between model instances, just set the 'xxx_tz' field to the desired timezone before saving.
UTCDateTimeField supports a single optional keyword argument 'default_tz', in addition to the DateTimeField standard ones, to let the user choose a provider for a default timezone when no timezone has been set. Its value can be
-
None (or the argument missing), in which case the default settings.TIME_ZONE will be used
-
a callable, which will be called when the 'xxx_tz' field is emtpy, which should return a timezone name
-
a string, which will be used to access a model attribute or call a model method, which should return a timezone name
If the timezone name points to a non-existent timezone, a warning will be issued and the default settings.TIME_ZONE value will be used.
The field will also add three properties to the model, to access the pytz timezone instance, and the offset aware datetimes for local time and UTC. For the same 'updated' field instance we used above, the three properties added to the model will be:
-
updated_timezone
-
updated_offset_aware
-
updated_utc_offset_aware
A brief example on how to use UTCDateTimeField:
class UTCDateTimeTest(models.Model):
"""
>>> import datetime
>>> d = datetime.datetime(2007, 8, 24, 16, 46, 34, 762627)
# new instance, tz from model method
>>> t = UTCDateTimeTest(updated=d)
>>> t.save()
>>> t.updated
datetime.datetime(2007, 8, 24, 16, 46, 34, 762627)
>>> t.updated_utc
datetime.datetime(2007, 8, 24, 14, 46, 34, 762627)
>>> t.updated_tz
'Europe/Rome'
>>> t.updated_timezone
<DstTzInfo 'Europe/Rome' CET+1:00:00 STD>
>>> t.updated_offset_aware
datetime.datetime(2007, 8, 24, 16, 46, 34, 762627, tzinfo=<DstTzInfo 'Europe/Rome' CEST+2:00:00 DST>)
>>> t.updated_utc_offset_aware
datetime.datetime(2007, 8, 24, 14, 46, 34, 762627, tzinfo=<UTC>)
>>>
"""
updated = UTCDateTimeField(default_tz='get_tz')
def get_tz(self):
return 'Europe/Rome'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | import datetime
import warnings
import pytz
from django.conf import settings
from django.db import models
from django.dispatch import dispatcher
from django.db.models import signals
try:
pytz_exc = (pytz.UnknownTimeZoneError,)
except AttributeError:
# older versions of pytz
pytz_exc = (IOError, KeyError)
TIMEZONES = tuple((t, t) for t in pytz.all_timezones)
class UTCDateTimeField(models.DateTimeField):
def __init__(self, *args, **kw):
self.default_tz = kw.get('default_tz')
if 'default_tz' in kw:
del(kw['default_tz'])
super(UTCDateTimeField, self).__init__(*args, **kw)
self._name = None
def get_internal_type(self):
return 'DateTimeField'
def _model_init(self, signal, sender, instance, **kw):
setattr(instance, "_%s" % self._name, getattr(instance, self._name))
setattr(instance, "_%s_utc" % self._name, getattr(instance, "%s_utc" % self._name))
setattr(instance, "_%s_tz" % self._name, getattr(instance, "%s_tz" % self._name))
def _model_pre_save(self, signal, sender, instance, **kw):
dt = getattr(instance, self._name)
utc = getattr(instance, "%s_utc" % self._name)
tz = getattr(instance, "%s_tz" % self._name)
if not tz:
# try to get the default
if isinstance(self.default_tz, basestring):
tz = getattr(instance, self.default_tz)
if isinstance(tz, basestring):
# use it as a timezone
pass
elif callable(tz):
self.default_tz = tz
tz = None
else:
warnings.warn("UTCDateTimeField cannot use default_tz %s in model %s" % (self.default_tz, instance))
self.default_tz = lambda: getattr(settings, 'TIME_ZONE', 'UTC')
tz = None
if callable(self.default_tz):
tz = self.default_tz()
if not tz or not isinstance(tz, basestring):
warnings.warn("UTCDateTimeField cannot use default timezone %s, using Django default" % tz)
tz = getattr(settings, 'TIME_ZONE', 'UTC')
if not tz:
tz = getattr(settings, 'TIME_ZONE', 'UTC')
setattr(instance, "%s_tz" % self._name, tz)
_dt = getattr(instance, "_%s" % self._name)
_utc = getattr(instance, "_%s_utc" % self._name)
_tz = getattr(instance, "_%s_tz" % self._name)
if isinstance(tz, unicode):
tz = tz.encode('utf8')
try:
timezone = pytz.timezone(tz)
except pytz_exc, e:
warnings.warn("Error in timezone: %s" % e)
setattr(instance, "%s_tz" % self._name, 'UTC')
timezone = pytz.UTC
if not dt and not utc:
# do nothing
return
elif dt and dt != _dt:
utc = UTCDateTimeField.utc_from_localtime(dt, timezone)
setattr(instance, "%s_utc" % self._name, utc)
elif utc and (utc != _utc or tz != _tz):
dt = UTCDateTimeField.localtime_from_utc(utc, timezone)
setattr(instance, "_%s" % self._name, dt)
elif not dt:
dt = UTCDateTimeField.localtime_from_utc(utc, timezone)
setattr(instance, "_%s" % self._name, dt)
elif not utc:
utc = UTCDateTimeField.utc_from_localtime(dt, timezone)
setattr(instance, "%s_utc" % self._name, utc)
if dt.tzinfo:
dt = dt.replace(tzinfo=None)
if utc.tzinfo:
utc = utc.replace(tzinfo=None)
setattr(instance, "_%s" % self._name, dt)
setattr(instance, "_%s_utc" % self._name, utc)
setattr(instance, "_%s_tz" % self._name, tz)
setattr(instance, "%s" % self._name, dt)
setattr(instance, "%s_utc" % self._name, utc)
def _model_post_save(self, signal, sender, instance, **kw):
setattr(instance, "%s" % self._name, getattr(instance, "_%s" % self._name))
setattr(instance, "%s_utc" % self._name, getattr(instance, "_%s_utc" % self._name))
def contribute_to_class(self, cls, name):
self._name = name
super(UTCDateTimeField, self).contribute_to_class(cls, name)
self.utc_field = models.DateTimeField(blank=True)
self.creation_counter = self.utc_field.creation_counter + 1
models.DateTimeField.contribute_to_class(self.utc_field, cls, '%s_utc' % name)
self.tz_field = models.CharField(max_length=38, blank=True, choices=TIMEZONES, default=getattr(settings, 'TIME_ZONE', 'UTC'))
self.creation_counter = self.tz_field.creation_counter + 1
models.CharField.contribute_to_class(self.tz_field, cls, '%s_tz' % name)
# add the properties for offset-aware datetimes
def get_timezone(s):
tz = getattr(s, '%s_tz' % name)
if isinstance(tz, unicode):
tz = tz.encode('utf8')
return pytz.timezone(tz)
setattr(cls, "%s_timezone" % name, property(get_timezone))
def get_dt_offset_aware(s):
dt = getattr(s, "%s_utc_offset_aware" % name)
tz = getattr(s, "%s_timezone" % name)
return dt.astimezone(tz)
setattr(cls, "%s_offset_aware" % name, property(get_dt_offset_aware))
def get_utc_offset_aware(s):
return getattr(s, '%s_utc' % name).replace(tzinfo=pytz.utc)
setattr(cls, "%s_utc_offset_aware" % name, property(get_utc_offset_aware))
signals.post_init.connect(self._model_init, sender=cls)
signals.pre_save.connect(self._model_pre_save, sender=cls)
signals.post_save.connect(self._model_post_save, sender=cls)
@staticmethod
def localtime_from_utc(utc, tz):
dt = utc.replace(tzinfo=pytz.utc)
return dt.astimezone(tz)
@staticmethod
def utc_from_localtime(dt, tz):
dt = dt.replace(tzinfo=tz)
_dt = tz.normalize(dt)
if dt.tzinfo != _dt.tzinfo:
# Houston, we have a problem...
# find out which one has a dst offset
if _dt.tzinfo.dst(_dt):
_dt -= _dt.tzinfo.dst(_dt)
else:
_dt += dt.tzinfo.dst(dt)
return _dt.astimezone(pytz.utc)
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
- Help text hyperlinks by sa2812 1 year, 7 months ago
Comments
Updated to work with Django 1.1+ (I use it with trunk == 1.2)
#
You mean this code can be used with Django 1.1+?
#
Please login first before commenting.