Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support USE_TZ In Django Configuration And Remove Potential Runtime Warnings #51

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ venv/

# Pycharm
.idea/

# VS Code
.vscode/
10 changes: 6 additions & 4 deletions localized_recurrence/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ def value_to_string(self, obj):
return str(time_delta_value)


DAYS_RE = re.compile(r"(?P<days>-?[0-9]*) days?, (?P<hours>[0-9]+):(?P<minutes>[0-9]+):(?P<seconds>[0-9]+\.?[0-9]*)")
NO_DAYS_RE = re.compile(r"(?P<hours>[0-9]+):(?P<minutes>[0-9]+):(?P<seconds>[0-9]+\.?[0-9]*)")


def parse_timedelta_string(string):
"""Parses strings from datetime.timedelta.__str__.

Expand All @@ -118,10 +122,8 @@ def parse_timedelta_string(string):
datetime.timedelta.__str__ returns a string in the form [D day[s],
][H]H:MM:SS[.UUUUUU], where D is negative for negative t.
"""
days_re = "(?P<days>-?[0-9]*) days?, (?P<hours>[0-9]+):(?P<minutes>[0-9]+):(?P<seconds>[0-9]+\.?[0-9]*)"
no_days_re = "(?P<hours>[0-9]+):(?P<minutes>[0-9]+):(?P<seconds>[0-9]+\.?[0-9]*)"
match_days = re.match(days_re, string)
match_no_days = re.match(no_days_re, string)
match_days = re.match(DAYS_RE, string)
match_no_days = re.match(NO_DAYS_RE, string)
if match_days:
return timedelta(**{k: float(v) for k, v in match_days.groupdict().items()})
elif match_no_days:
Expand Down
25 changes: 25 additions & 0 deletions localized_recurrence/migrations/0005_auto_20190131_1286.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-01-31 23:15
from __future__ import unicode_literals

from django.db import migrations, models

import localized_recurrence.models


class Migration(migrations.Migration):
dependencies = [
('localized_recurrence', '0004_auto_20161108_2151'),
]
operations = [
migrations.AlterField(
model_name='localizedrecurrence',
name='next_scheduled',
field=models.DateTimeField(default=localized_recurrence.models.default_scheduled),
),
migrations.AlterField(
model_name='localizedrecurrence',
name='previous_scheduled',
field=models.DateTimeField(default=localized_recurrence.models.default_scheduled),
),
]
37 changes: 27 additions & 10 deletions localized_recurrence/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import calendar

from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from timezone_field import TimeZoneField
import fleming
Expand All @@ -18,6 +20,13 @@
)


def default_scheduled():
tzinfo = None
if settings.USE_TZ:
tzinfo = pytz.timezone(settings.TIME_ZONE)
return datetime(1970, 1, 1, tzinfo=tzinfo)


class LocalizedRecurrenceQuerySet(models.query.QuerySet):
def update_schedule(self, time=None):
"""
Expand Down Expand Up @@ -125,8 +134,8 @@ class LocalizedRecurrence(models.Model):
interval = models.CharField(max_length=18, default='DAY', choices=INTERVAL_CHOICES)
offset = models.DurationField(default=timedelta(0))
timezone = TimeZoneField(default='UTC')
previous_scheduled = models.DateTimeField(default=datetime(1970, 1, 1))
next_scheduled = models.DateTimeField(default=datetime(1970, 1, 1))
previous_scheduled = models.DateTimeField(default=default_scheduled)
next_scheduled = models.DateTimeField(default=default_scheduled)

objects = LocalizedRecurrenceManager()

Expand Down Expand Up @@ -221,15 +230,23 @@ def utc_of_next_schedule(self, current_time):
return next_scheduled_utc


def _prepare_datetime_field_value(value, make_aware=None):
if make_aware is None:
make_aware = settings.USE_TZ
if value is None or not make_aware or timezone.is_aware(value):
return value
return timezone.make_aware(value)


def _update_schedule(recurrences, time=None):
"""
Update the schedule times for all the provided recurrences.
"""
time = time or datetime.utcnow()
for recurrence in recurrences:
recurrence.next_scheduled = recurrence.utc_of_next_schedule(time)
recurrence.previous_scheduled = time
recurrence.save()
"""
Update the schedule times for all the provided recurrences.
"""
time = time or datetime.utcnow()
for recurrence in recurrences:
recurrence.next_scheduled = _prepare_datetime_field_value(recurrence.utc_of_next_schedule(time))
recurrence.previous_scheduled = _prepare_datetime_field_value(time)
recurrence.save()


def _replace_with_offset(dt, offset, interval):
Expand Down
88 changes: 87 additions & 1 deletion localized_recurrence/tests/models_tests.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from datetime import datetime, timedelta

import django
from django.test import TestCase
from django_dynamic_fixture import G
from django.utils import timezone
import pytz

from ..models import LocalizedRecurrence, LocalizedRecurrenceQuerySet
from ..models import _replace_with_offset, _update_schedule
from ..models import _replace_with_offset, _update_schedule, _prepare_datetime_field_value


class LocalizedRecurrenceUpdateTest(TestCase):
Expand Down Expand Up @@ -548,3 +550,87 @@ def test_bad_interval(self):
interval_in = 'blah'
with self.assertRaises(ValueError):
_replace_with_offset(dt_in, td_in, interval_in)


class LocalizedRecurrenceUseTzSetting(TestCase):

def test_with_and_without_use_tz(self):
lr_with_use_tz = G(LocalizedRecurrence, timezone=pytz.timezone('US/Eastern'))
lr_with_use_tz_utc = G(LocalizedRecurrence, timezone=pytz.UTC)
with self.settings(USE_TZ=True, TIME_ZONE='UTC'):
lr_with_use_tz.update_schedule()
lr_with_use_tz_utc.update_schedule()
self.assertTrue(timezone.is_aware(lr_with_use_tz.next_scheduled))
self.assertTrue(timezone.is_aware(lr_with_use_tz_utc.next_scheduled))

lr_without_use_tz = G(LocalizedRecurrence)
with self.settings(USE_TZ=False):
lr_without_use_tz.update_schedule()
self.assertFalse(timezone.is_aware(lr_without_use_tz.next_scheduled))

self.assertEqual(lr_with_use_tz_utc.next_scheduled.replace(tzinfo=None), lr_without_use_tz.next_scheduled)

def test_prepare_datetime_field(self):
self.assertIsNone(_prepare_datetime_field_value(None))
naive_now = datetime.now()
aware_now = naive_now.astimezone(pytz.timezone('US/Pacific'))
with self.settings(USE_TZ=True):
self.assertEqual(naive_now, _prepare_datetime_field_value(naive_now, make_aware=False))
self.assertEqual(aware_now, _prepare_datetime_field_value(aware_now))

def test_next_and_previous_scheduled_defaults(self):
with self.settings(USE_TZ=True, TIME_ZONE='UTC'):
lr = G(LocalizedRecurrence, timezone=pytz.timezone('US/Eastern'))
self.assertEqual(lr.previous_scheduled, datetime(1970, 1, 1, tzinfo=pytz.UTC))
self.assertEqual(lr.next_scheduled, datetime(1970, 1, 1, tzinfo=pytz.UTC))
with self.settings(USE_TZ=True, TIME_ZONE='US/Pacific'):
lr = G(LocalizedRecurrence, timezone=pytz.timezone('US/Eastern'))
self.assertEqual(lr.previous_scheduled, datetime(1970, 1, 1, tzinfo=pytz.timezone('US/Pacific')))
self.assertEqual(lr.next_scheduled, datetime(1970, 1, 1, tzinfo=pytz.timezone('US/Pacific')))
with self.settings(USE_TZ=False, TIME_ZONE='US/Pacific'):
lr = G(LocalizedRecurrence, timezone=pytz.timezone('US/Eastern'))
self.assertEqual(lr.previous_scheduled, datetime(1970, 1, 1))
self.assertEqual(lr.next_scheduled, datetime(1970, 1, 1))

def test_database_with_or_without_tz_in_utc(self):
scheduled_with_utc_tz = datetime(2018, 10, 10, tzinfo=pytz.UTC)
scheduled_with_est_tz = datetime(2018, 10, 10, tzinfo=pytz.timezone('US/Eastern'))
scheduled_without_tz = datetime(2018, 10, 10)
# SQLite backend does not support timezone-aware datetimes when USE_TZ is False.
with self.settings(USE_TZ=True, TIME_ZONE="UTC"):
lr_with_utc_tz = G(LocalizedRecurrence, next_scheduled=scheduled_with_utc_tz,
previous_scheduled=scheduled_with_utc_tz, timezone=pytz.UTC)
lr_with_est_tz = G(LocalizedRecurrence, next_scheduled=scheduled_with_est_tz,
previous_scheduled=scheduled_with_est_tz, timezone=pytz.timezone('US/Eastern'))
lr_without_tz = G(LocalizedRecurrence, timezone=None, next_scheduled=scheduled_without_tz,
previous_scheduled=scheduled_without_tz)
with self.settings(USE_TZ=True, TIME_ZONE="UTC"):
self.assertEqual(lr_with_utc_tz.next_scheduled, lr_without_tz.next_scheduled.replace(tzinfo=pytz.UTC))
self.assertEqual(lr_with_utc_tz.next_scheduled.replace(tzinfo=None),
lr_with_est_tz.next_scheduled.replace(tzinfo=None))

with self.settings(USE_TZ=False):
# Now make next_scheduled non-aware, and make sure that still works
# (Django automatically converts non-aware to aware in the database)
lr_with_utc_tz.previous_scheduled = datetime(2018, 10, 11)
lr_with_utc_tz.next_scheduled = datetime(2018, 10, 11)
lr_with_utc_tz.save()
lr_with_utc_tz.refresh_from_db()
self.assertEqual(lr_with_utc_tz.next_scheduled,
lr_without_tz.next_scheduled + timedelta(days=1))

version = django.VERSION
# assertWarns is only Django 2.1+
if version[0] >= 2 and version[1] >= 1: # pragma: no cover
with self.settings(USE_TZ=True, TIME_ZONE="UTC"):
# Now make next_scheduled non-aware, and make sure that still works
with self.assertWarnsRegex(RuntimeWarning, r"DateTimeField LocalizedRecurrence\."
r"(next|previous)\_scheduled "
r"received a naive datetime \(2018-10-12 00:00:00\) "
r"while time zone support is active\."):
lr_with_utc_tz.previous_scheduled = datetime(2018, 10, 12)
lr_with_utc_tz.next_scheduled = datetime(2018, 10, 12)
lr_with_utc_tz.save()
lr_with_utc_tz.refresh_from_db()
self.assertEqual(lr_with_utc_tz.next_scheduled.replace(tzinfo=None),
lr_without_tz.next_scheduled + timedelta(days=2))