From 2f8d996a103b2d87af844e2d944e5e0587b33214 Mon Sep 17 00:00:00 2001 From: Benjamin Murden Date: Thu, 19 May 2016 17:38:50 +0900 Subject: [PATCH] Update to add timezone support. In order to avoid timezone support warnings, change default values for metric items and gauges to timezone aware equivalents from django.utils.timezone. This requires some changes to move_to_mixpanel command so timestamps can be calculated correctly. To support more platforms, changed the use of strftime('%s') to a more compatible implementation that closely follows the Python 3 implementation of timestamp(). Added timedelta_total_seconds() to allow continued support for Python 2.6. Added tests for timestamp utility functions. --- .../management/commands/move_to_mixpanel.py | 5 ++-- app_metrics/models.py | 9 +++--- app_metrics/tests/base_tests.py | 30 +++++++++++++++++++ app_metrics/utils.py | 15 ++++++++++ 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/app_metrics/management/commands/move_to_mixpanel.py b/app_metrics/management/commands/move_to_mixpanel.py index 524e4df..7f6b2d2 100644 --- a/app_metrics/management/commands/move_to_mixpanel.py +++ b/app_metrics/management/commands/move_to_mixpanel.py @@ -2,7 +2,8 @@ from app_metrics.models import MetricItem from app_metrics.backends.mixpanel import metric -from app_metrics.utils import get_backend +from app_metrics.utils import get_backend, get_timestamp + class Command(NoArgsCommand): help = "Move MetricItems from the db backend to MixPanel" @@ -23,7 +24,7 @@ def handle_noargs(self, **options): for i in items: properties = { - 'time': i.created.strftime('%s'), + 'time': int(get_timestamp(i.created)), } metric(i.metric.slug, num=i.num, properties=properties) diff --git a/app_metrics/models.py b/app_metrics/models.py index 60af6f5..8774ce2 100644 --- a/app_metrics/models.py +++ b/app_metrics/models.py @@ -3,6 +3,7 @@ from django.db import models, IntegrityError from django.template.defaultfilters import slugify from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone from app_metrics.compat import User @@ -55,7 +56,7 @@ class MetricItem(models.Model): """ Individual metric items """ metric = models.ForeignKey(Metric, verbose_name=_('metric')) num = models.IntegerField(_('number'), default=1) - created = models.DateTimeField(_('created'), default=datetime.datetime.now) + created = models.DateTimeField(_('created'), default=timezone.datetime.now) class Meta: verbose_name = _('metric item') @@ -146,8 +147,8 @@ class Gauge(models.Model): name = models.CharField(_('name'), max_length=50) slug = models.SlugField(_('slug'), unique=True, max_length=60) current_value = models.DecimalField(_('current value'), max_digits=15, decimal_places=6, default='0.00') - created = models.DateTimeField(_('created'), default=datetime.datetime.now) - updated = models.DateTimeField(_('updated'), default=datetime.datetime.now) + created = models.DateTimeField(_('created'), default=timezone.datetime.now) + updated = models.DateTimeField(_('updated'), default=timezone.datetime.now) class Meta: verbose_name = _('gauge') @@ -160,5 +161,5 @@ def save(self, *args, **kwargs): if not self.id and not self.slug: self.slug = slugify(self.name) - self.updated = datetime.datetime.now() + self.updated = timezone.datetime.now() return super(Gauge, self).save(*args, **kwargs) diff --git a/app_metrics/tests/base_tests.py b/app_metrics/tests/base_tests.py index d422aaf..03a7352 100644 --- a/app_metrics/tests/base_tests.py +++ b/app_metrics/tests/base_tests.py @@ -8,6 +8,7 @@ from django.core import mail from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured +from django.utils import timezone from app_metrics.exceptions import TimerError from app_metrics.models import Metric, MetricItem, MetricDay, MetricWeek, MetricMonth, MetricYear, Gauge @@ -336,3 +337,32 @@ def test_mixpanel_op(self): def tearDown(self): settings.APP_METRICS_BACKEND = self.old_backend + +class TestTimezone(datetime.tzinfo): + def utcoffset(self, dt): + return datetime.timedelta(hours=-9) + +class TimestampTest(TestCase): + """ Test timestamp utilities """ + + def test_tz_timestamp(self): + dt = datetime.datetime(2016, 05, 01, 0, 0, 0, tzinfo=timezone.utc) + utc_ts = get_timestamp(dt) + dt_tz = datetime.datetime(2016, 05, 01, 0, 0, 0, tzinfo=TestTimezone()) + tz_ts = get_timestamp(dt_tz) + + # timestamp with a UTC offset of -9 hours should be ahead by 32400 seconds. + self.assertEqual(utc_ts, tz_ts - 32400) + + def test_naive_timestamp(self): + dt = datetime.datetime(2016, 05, 01, 0, 0, 0, tzinfo=None) + + # Naive datetime to timestamp will just use local timezone. Same + # behavior as using strftime('%s') on 'nix systems. + + # The above dt, even in a TZ of -12 hours would have to be more than + # or equal to 1462060800 (UTC) - 43200 seconds as a timestamp. + self.assertGreaterEqual(get_timestamp(dt), 1462060800 - 43200) + + # However, it should be less than or equal to the same +12 hours. + self.assertLessEqual(get_timestamp(dt), 1462060800 + 43200) diff --git a/app_metrics/utils.py b/app_metrics/utils.py index 8d6a727..61f1c19 100644 --- a/app_metrics/utils.py +++ b/app_metrics/utils.py @@ -3,6 +3,7 @@ import time from django.conf import settings from django.utils.importlib import import_module +from django.utils import timezone from app_metrics.exceptions import InvalidMetricsBackend, TimerError from app_metrics.models import Metric, MetricSet @@ -227,3 +228,17 @@ def get_previous_year(date): new = date return new.replace(year=new.year-1) +def timedelta_total_seconds(timedelta): + return timedelta.seconds + timedelta.days * 24 * 3600 + +def get_timestamp(dt): + if dt.tzinfo is None: + return time.mktime((dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, + -1, -1, -1)) + else: + timedelta = dt - datetime.datetime(1970, 1, 1, tzinfo=timezone.utc) + try: + return timedelta.total_seconds() + except AttributeError: + return timedelta_total_seconds(timedelta)