From 30ee804ce216ed35b9a38abfbb5b354d818d454c Mon Sep 17 00:00:00 2001 From: Paul Poulakis Date: Mon, 16 Dec 2013 11:05:23 +0200 Subject: [PATCH 01/10] Fix tzinfo is overidden by super's constructor default. --- schedule/periods.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/schedule/periods.py b/schedule/periods.py index 547c24fa..bb410c64 100644 --- a/schedule/periods.py +++ b/schedule/periods.py @@ -132,7 +132,7 @@ def __init__(self, events, date=None, parent_persisted_occurrences=None, tzinfo= if date is None: date = timezone.now() start, end = self._get_year_range(date) - super(Year, self).__init__(events, start, end, parent_persisted_occurrences) + super(Year, self).__init__(events, start, end, parent_persisted_occurrences, tzinfo=tzinfo) def get_months(self): return self.get_periods(Month) @@ -169,7 +169,7 @@ def __init__(self, events, date=None, parent_persisted_occurrences=None, date = timezone.now() start, end = self._get_month_range(date) super(Month, self).__init__(events, start, end, - parent_persisted_occurrences, occurrence_pool) + parent_persisted_occurrences, occurrence_pool, tzinfo=tzinfo) def get_weeks(self): return self.get_periods(Week) @@ -234,7 +234,7 @@ def __init__(self, events, date=None, parent_persisted_occurrences=None, date = timezone.now() start, end = self._get_week_range(date) super(Week, self).__init__(events, start, end, - parent_persisted_occurrences, occurrence_pool) + parent_persisted_occurrences, occurrence_pool, tzinfo=tzinfo) def prev_week(self): return Week(self.events, self.start - datetime.timedelta(days=7)) @@ -288,7 +288,7 @@ def __init__(self, events, date=None, parent_persisted_occurrences=None, date = timezone.now() start, end = self._get_day_range(date) super(Day, self).__init__(events, start, end, - parent_persisted_occurrences, occurrence_pool) + parent_persisted_occurrences, occurrence_pool, tzinfo=tzinfo) def _get_day_range(self, date): if isinstance(date, datetime.datetime): From 350162763a7e1ea3475e32e0cca564ab77dcf250 Mon Sep 17 00:00:00 2001 From: Paul Poulakis Date: Mon, 16 Dec 2013 14:06:47 +0200 Subject: [PATCH 02/10] Add tests for timezone aware periods. --- schedule/tests/test_periods.py | 84 +++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/schedule/tests/test_periods.py b/schedule/tests/test_periods.py index 7cebb85b..8629a024 100644 --- a/schedule/tests/test_periods.py +++ b/schedule/tests/test_periods.py @@ -5,7 +5,7 @@ from schedule.conf.settings import FIRST_DAY_OF_WEEK from schedule.models import Event, Rule, Calendar -from schedule.periods import Period, Month, Day, Year +from schedule.periods import Period, Month, Day, Year, Week class TestPeriod(TestCase): @@ -251,3 +251,85 @@ def testPeriodFromPool(self): period = Period(parent_period.events, start, end, parent_period.get_persisted_occurrences(), parent_period.occurrences) self.assertEquals(parent_period.occurrences, period.occurrences) + +class TestAwareDay(TestCase): + def setUp(self): + self.timezone = pytz.timezone('Europe/Amsterdam') + + start = self.timezone.localize(datetime.datetime(2008, 2, 7, 0, 20)) + end = self.timezone.localize(datetime.datetime(2008, 2, 7, 0, 21)) + self.event = Event( + title='One minute long event on january seventh 2008 at 00:20 in Amsterdam.', + start=start, + end=end, + ) + self.event.save() + + self.day = Day( + events=Event.objects.all(), + date=self.timezone.localize(datetime.datetime(2008, 2, 7, 9, 0)), + tzinfo=self.timezone, + ) + + def test_day_range(self): + start = datetime.datetime(2008, 2, 6, 23, 0, tzinfo=pytz.utc) + end = datetime.datetime(2008, 2, 7, 23, 0, tzinfo=pytz.utc) + + self.assertEqual(start, self.day.start) + self.assertEqual(end, self.day.end) + + def test_occurence(self): + self.assertEqual(self.event in [o.event for o in self.day.occurrences], True) + + + + +class TestAwareWeek(TestCase): + def setUp(self): + self.timezone = pytz.timezone('Europe/Amsterdam') + self.week = Week( + events=Event.objects.all(), + date=self.timezone.localize(datetime.datetime(2013, 12, 17, 9, 0)), + tzinfo=self.timezone, + ) + + def test_week_range(self): + start = datetime.datetime(2013, 12, 15, 23, 0, tzinfo=pytz.utc) + end = datetime.datetime(2013, 12, 22, 23, 0, tzinfo=pytz.utc) + + self.assertEqual(start, self.week.start) + self.assertEqual(end, self.week.end) + + +class TestAwareMonth(TestCase): + def setUp(self): + self.timezone = pytz.timezone('Europe/Amsterdam') + self.month = Month( + events=Event.objects.all(), + date=self.timezone.localize(datetime.datetime(2013, 12, 17, 9, 0)), + tzinfo=self.timezone, + ) + + def test_month_range(self): + start = datetime.datetime(2013, 11, 30, 23, 0, tzinfo=pytz.utc) + end = datetime.datetime(2013, 12, 31, 23, 0, tzinfo=pytz.utc) + + self.assertEqual(start, self.month.start) + self.assertEqual(end, self.month.end) + + +class TestAwareYear(TestCase): + def setUp(self): + self.timezone = pytz.timezone('Europe/Amsterdam') + self.year = Year( + events=Event.objects.all(), + date=self.timezone.localize(datetime.datetime(2013, 12, 17, 9, 0)), + tzinfo=self.timezone, + ) + + def test_year_range(self): + start = datetime.datetime(2012, 12, 31, 23, 0, tzinfo=pytz.utc) + end = datetime.datetime(2013, 12, 31, 23, 0, tzinfo=pytz.utc) + + self.assertEqual(start, self.year.start) + self.assertEqual(end, self.year.end) From 5d730cbab3d3b143068225d9dc8dc770659873de Mon Sep 17 00:00:00 2001 From: Paul Poulakis Date: Mon, 16 Dec 2013 14:17:24 +0200 Subject: [PATCH 03/10] Fix Period subclasses' get_***_range methods to return appropriate date ranges. --- schedule/periods.py | 52 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/schedule/periods.py b/schedule/periods.py index bb410c64..7c5975b0 100644 --- a/schedule/periods.py +++ b/schedule/periods.py @@ -142,15 +142,27 @@ def next_year(self): next = next_year def prev_year(self): - start = datetime.datetime(self.start.year - 1, self.start.month, self.start.day, tzinfo=self.tzinfo) + start = datetime.datetime(self.start.year - 1, self.start.month, self.start.day) return Year(self.events, start) prev = prev_year def _get_year_range(self, year): - start = datetime.datetime(year.year, datetime.datetime.min.month, - datetime.datetime.min.day, tzinfo=self.tzinfo) - end = datetime.datetime(year.year + 1, datetime.datetime.min.month, - datetime.datetime.min.day, tzinfo=self.tzinfo) + if self.tzinfo is not None: + local_start = self.tzinfo.localize( + datetime.datetime(year.year, datetime.datetime.min.month, datetime.datetime.min.day) + ) + local_end = self.tzinfo.localize( + datetime.datetime(year.year + 1, datetime.datetime.min.month, datetime.datetime.min.day) + ) + + start = local_start.astimezone(pytz.utc) + end = local_end.astimezone(pytz.utc) + else: + start = datetime.datetime(year.year, datetime.datetime.min.month, + datetime.datetime.min.day, tzinfo=self.tzinfo) + end = datetime.datetime(year.year + 1, datetime.datetime.min.month, + datetime.datetime.min.day, tzinfo=self.tzinfo) + return start, end def __unicode__(self): @@ -206,11 +218,21 @@ def next_year(self): def _get_month_range(self, month): year = month.year month = month.month - start = datetime.datetime.min.replace(year=year, month=month, tzinfo=self.tzinfo) + if self.tzinfo is not None: + local_start = self.tzinfo.localize(datetime.datetime.min.replace(year=year, month=month)) + start = local_start.astimezone(pytz.utc) + else: + start = datetime.datetime.min.replace(year=year, month=month, tzinfo=self.tzinfo) if month == 12: - end = start.replace(month=1, year=year + 1, tzinfo=self.tzinfo) + local_end = datetime.datetime.min.replace(month=1, year=year + 1, day=1) else: - end = start.replace(month=month + 1, tzinfo=self.tzinfo) + local_end = datetime.datetime.min.replace(month=month + 1, year=year, day=1) + + if self.tzinfo is not None: + local_end = self.tzinfo.localize(local_end) + end = local_end.astimezone(pytz.utc) + end = local_end.astimezone(pytz.utc) + return start, end def __unicode__(self): @@ -257,7 +279,11 @@ def _get_week_range(self, week): if isinstance(week, datetime.datetime): week = week.date() # Adjust the start datetime to midnight of the week datetime - start = datetime.datetime.combine(week, datetime.time.min).replace(tzinfo=self.tzinfo) + if self.tzinfo is not None: + local_start = self.tzinfo.localize(datetime.datetime.combine(week, datetime.time.min)) + start = local_start.astimezone(pytz.utc) + else: + start = datetime.datetime.combine(week, datetime.time.min).replace(tzinfo=self.tzinfo) # Adjust the start datetime to Monday or Sunday of the current week if FIRST_DAY_OF_WEEK == 1: # The week begins on Monday @@ -293,7 +319,13 @@ def __init__(self, events, date=None, parent_persisted_occurrences=None, def _get_day_range(self, date): if isinstance(date, datetime.datetime): date = date.date() - start = datetime.datetime.combine(date, datetime.time.min).replace(tzinfo=self.tzinfo) + #If tzinfo is not none get the local start of the day and convert it to utc. + if self.tzinfo is not None: + local_start = self.tzinfo.localize(datetime.datetime.combine(date, datetime.time.min)) + start = local_start.astimezone(pytz.utc) + else: + start = datetime.datetime.combine(date, datetime.time.min).replace(tzinfo=self.tzinfo) + end = start + datetime.timedelta(days=1) return start, end From 07929816fd4fd45d784cc2a222a42d9c1c238fdc Mon Sep 17 00:00:00 2001 From: Paul Poulakis Date: Mon, 16 Dec 2013 14:35:36 +0200 Subject: [PATCH 04/10] Add tests for commit 30ee804ce216 --- schedule/tests/test_periods.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/schedule/tests/test_periods.py b/schedule/tests/test_periods.py index 8629a024..85826b98 100644 --- a/schedule/tests/test_periods.py +++ b/schedule/tests/test_periods.py @@ -282,6 +282,38 @@ def test_occurence(self): self.assertEqual(self.event in [o.event for o in self.day.occurrences], True) +class TestTzInfoPersistence(TestCase): + def setUp(self): + self.timezone = pytz.timezone('Europe/Amsterdam') + self.day = Day( + events=Event.objects.all(), + date=self.timezone.localize(datetime.datetime(2013, 12, 17, 9, 0)), + tzinfo=self.timezone + ) + + self.week = Week( + events=Event.objects.all(), + date=self.timezone.localize(datetime.datetime(2013, 12, 17, 9, 0)), + tzinfo=self.timezone, + ) + + self.month = Month( + events=Event.objects.all(), + date=self.timezone.localize(datetime.datetime(2013, 12, 17, 9, 0)), + tzinfo=self.timezone, + ) + + self.year = Year( + events=Event.objects.all(), + date=self.timezone.localize(datetime.datetime(2013, 12, 17, 9, 0)), + tzinfo=self.timezone, + ) + + def test_persistence(self): + self.assertEqual(self.day.tzinfo, self.timezone) + self.assertEqual(self.week.tzinfo, self.timezone) + self.assertEqual(self.month.tzinfo, self.timezone) + self.assertEqual(self.year.tzinfo, self.timezone) class TestAwareWeek(TestCase): From fc5409d45480468d9be33e05da0114ec556477e9 Mon Sep 17 00:00:00 2001 From: Paul Poulakis Date: Mon, 16 Dec 2013 14:47:30 +0200 Subject: [PATCH 05/10] Ensure tzinfo propagation to subperiods. --- schedule/periods.py | 47 ++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/schedule/periods.py b/schedule/periods.py index 7c5975b0..719c78ed 100644 --- a/schedule/periods.py +++ b/schedule/periods.py @@ -115,14 +115,18 @@ def get_time_slot(self, start, end): return Period(self.events, start, end) return None - def create_sub_period(self, cls, start=None): + def create_sub_period(self, cls, start=None, tzinfo=None): + if tzinfo is None: + tzinfo = self.tzinfo start = start or self.start - return cls(self.events, start, self.get_persisted_occurrences(), self.occurrences) + return cls(self.events, start, self.get_persisted_occurrences(), self.occurrences, tzinfo) - def get_periods(self, cls): + def get_periods(self, cls, tzinfo=None): + if tzinfo is None: + tzinfo = self.tzinfo period = self.create_sub_period(cls) while period.start < self.end: - yield self.create_sub_period(cls, period.start) + yield self.create_sub_period(cls, period.start, tzinfo) period = period.next() @@ -138,15 +142,16 @@ def get_months(self): return self.get_periods(Month) def next_year(self): - return Year(self.events, self.end) + return Year(self.events, self.end, tzinfo=self.tzinfo) next = next_year def prev_year(self): start = datetime.datetime(self.start.year - 1, self.start.month, self.start.day) - return Year(self.events, start) + return Year(self.events, start, tzinfo=self.tzinfo) prev = prev_year def _get_year_range(self, year): + #If tzinfo is not none get the local start of the year and convert it to utc. if self.tzinfo is not None: local_start = self.tzinfo.localize( datetime.datetime(year.year, datetime.datetime.min.month, datetime.datetime.min.day) @@ -196,28 +201,29 @@ def get_day(self, daynumber): return self.create_sub_period(Day, date) def next_month(self): - return Month(self.events, self.end) + return Month(self.events, self.end, tzinfo=self.tzinfo) next = next_month def prev_month(self): start = (self.start - datetime.timedelta(days=1)).replace(day=1, tzinfo=self.tzinfo) - return Month(self.events, start) + return Month(self.events, start, tzinfo=self.tzinfo) prev = prev_month def current_year(self): - return Year(self.events, self.start) + return Year(self.events, self.start, tzinfo=self.tzinfo) def prev_year(self): start = datetime.datetime.min.replace(year=self.start.year - 1, tzinfo=self.tzinfo) - return Year(self.events, start) + return Year(self.events, start, tzinfo=self.tzinfo) def next_year(self): start = datetime.datetime.min.replace(year=self.start.year + 1, tzinfo=self.tzinfo) - return Year(self.events, start) + return Year(self.events, start, tzinfo=self.tzinfo) def _get_month_range(self, month): year = month.year month = month.month + #If tzinfo is not none get the local start of the month and convert it to utc. if self.tzinfo is not None: local_start = self.tzinfo.localize(datetime.datetime.min.replace(year=year, month=month)) start = local_start.astimezone(pytz.utc) @@ -259,18 +265,18 @@ def __init__(self, events, date=None, parent_persisted_occurrences=None, parent_persisted_occurrences, occurrence_pool, tzinfo=tzinfo) def prev_week(self): - return Week(self.events, self.start - datetime.timedelta(days=7)) + return Week(self.events, self.start - datetime.timedelta(days=7), tzinfo=self.tzinfo) prev = prev_week def next_week(self): - return Week(self.events, self.end) + return Week(self.events, self.end, tzinfo=self.tzinfo) next = next_week def current_month(self): - return Month(self.events, self.start) + return Month(self.events, self.start, tzinfo=self.tzinfo) def current_year(self): - return Year(self.events, self.start) + return Year(self.events, self.start, tzinfo=self.tzinfo) def get_days(self): return self.get_periods(Day) @@ -279,6 +285,7 @@ def _get_week_range(self, week): if isinstance(week, datetime.datetime): week = week.date() # Adjust the start datetime to midnight of the week datetime + #If tzinfo is not none get the local start of the week and convert it to utc. if self.tzinfo is not None: local_start = self.tzinfo.localize(datetime.datetime.combine(week, datetime.time.min)) start = local_start.astimezone(pytz.utc) @@ -337,18 +344,18 @@ def __unicode__(self): } def prev_day(self): - return Day(self.events, self.start - datetime.timedelta(days=1)) + return Day(self.events, self.start - datetime.timedelta(days=1), tzinfo=self.tzinfo) prev = prev_day def next_day(self): - return Day(self.events, self.end) + return Day(self.events, self.end, tzinfo=self.tzinfo) next = next_day def current_year(self): - return Year(self.events, self.start) + return Year(self.events, self.start, tzinfo=self.tzinfo) def current_month(self): - return Month(self.events, self.start) + return Month(self.events, self.start, tzinfo=self.tzinfo) def current_week(self): - return Week(self.events, self.start) + return Week(self.events, self.start, tzinfo=self.tzinfo) From e8defb45058bfbe43b6d762ded13bed382be3299 Mon Sep 17 00:00:00 2001 From: Paul Poulakis Date: Mon, 16 Dec 2013 17:46:12 +0200 Subject: [PATCH 06/10] Use an internal utc represantation. Period wont work with naive datetimes or dates. --- schedule/periods.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/schedule/periods.py b/schedule/periods.py index 719c78ed..89aaee78 100644 --- a/schedule/periods.py +++ b/schedule/periods.py @@ -1,6 +1,7 @@ -from django.conf import settings import pytz import datetime + +from django.conf import settings from django.template.defaultfilters import date as date_filter from django.utils.translation import ugettext from django.utils.dates import WEEKDAYS, WEEKDAYS_ABBR @@ -31,8 +32,8 @@ class Period(object): """ def __init__(self, events, start, end, parent_persisted_occurrences=None, occurrence_pool=None, tzinfo=pytz.utc): - self.start = start - self.end = end + self.utc_start = start.astimezone(pytz.utc) + self.utc_end = end.astimezone(pytz.utc) self.events = events self.tzinfo = self._get_tzinfo(tzinfo) self.occurrence_pool = occurrence_pool @@ -40,10 +41,10 @@ def __init__(self, events, start, end, parent_persisted_occurrences=None, self._persisted_occurrences = parent_persisted_occurrences def __eq__(self, period): - return self.start == period.start and self.end == period.end and self.events == period.events + return self.utc_start == period.utc_start and self.utc_end == period.utc_end and self.events == period.events def __ne__(self, period): - return self.start != period.start or self.end != period.end or self.events != period.events + return self.utc_start != period.utc_start or self.utc_end != period.utc_end or self.events != period.events def _get_tzinfo(self, tzinfo): return tzinfo if settings.USE_TZ else None @@ -52,7 +53,7 @@ def _get_sorted_occurrences(self): occurrences = [] if hasattr(self, "occurrence_pool") and self.occurrence_pool is not None: for occurrence in self.occurrence_pool: - if occurrence.start <= self.end and occurrence.end >= self.start: + if occurrence.start <= self.utc_end and occurrence.end >= self.utc_start: occurrences.append(occurrence) return occurrences for event in self.events: @@ -82,9 +83,9 @@ def classify_occurrence(self, occurrence): return None started = False ended = False - if self.start <= occurrence.start < self.end: + if self.utc_start <= occurrence.start < self.utc_end: started = True - if self.start <= occurrence.end < self.end: + if self.utc_start <= occurrence.end < self.utc_end: ended = True if started and ended: return {'occurrence': occurrence, 'class': 1} @@ -129,6 +130,14 @@ def get_periods(self, cls, tzinfo=None): yield self.create_sub_period(cls, period.start, tzinfo) period = period.next() + @property + def start(self): + return self.utc_start.astimezone(self.tzinfo) + + @property + def end(self): + return self.utc_end.astimezone(self.tzinfo) + class Year(Period): def __init__(self, events, date=None, parent_persisted_occurrences=None, tzinfo=pytz.utc): From 7268f2d1c2c1adca177707fa26a0b0ae0c59bc23 Mon Sep 17 00:00:00 2001 From: Paul Poulakis Date: Tue, 17 Dec 2013 15:16:59 +0200 Subject: [PATCH 07/10] Fix daylight saving time issue in periods.py --- schedule/periods.py | 73 +++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/schedule/periods.py b/schedule/periods.py index 89aaee78..867ee4db 100644 --- a/schedule/periods.py +++ b/schedule/periods.py @@ -161,21 +161,16 @@ def prev_year(self): def _get_year_range(self, year): #If tzinfo is not none get the local start of the year and convert it to utc. - if self.tzinfo is not None: - local_start = self.tzinfo.localize( - datetime.datetime(year.year, datetime.datetime.min.month, datetime.datetime.min.day) - ) - local_end = self.tzinfo.localize( - datetime.datetime(year.year + 1, datetime.datetime.min.month, datetime.datetime.min.day) - ) + naive_start = datetime.datetime(year.year, datetime.datetime.min.month, datetime.datetime.min.day) + naive_end = datetime.datetime(year.year + 1, datetime.datetime.min.month, datetime.datetime.min.day) + start = naive_start + end = naive_end + if self.tzinfo is not None: + local_start = self.tzinfo.localize(naive_start) + local_end = self.tzinfo.localize(naive_end) start = local_start.astimezone(pytz.utc) end = local_end.astimezone(pytz.utc) - else: - start = datetime.datetime(year.year, datetime.datetime.min.month, - datetime.datetime.min.day, tzinfo=self.tzinfo) - end = datetime.datetime(year.year + 1, datetime.datetime.min.month, - datetime.datetime.min.day, tzinfo=self.tzinfo) return start, end @@ -233,20 +228,19 @@ def _get_month_range(self, month): year = month.year month = month.month #If tzinfo is not none get the local start of the month and convert it to utc. - if self.tzinfo is not None: - local_start = self.tzinfo.localize(datetime.datetime.min.replace(year=year, month=month)) - start = local_start.astimezone(pytz.utc) - else: - start = datetime.datetime.min.replace(year=year, month=month, tzinfo=self.tzinfo) + naive_start = datetime.datetime.min.replace(year=year, month=month) if month == 12: - local_end = datetime.datetime.min.replace(month=1, year=year + 1, day=1) + naive_end = datetime.datetime.min.replace(month=1, year=year + 1, day=1) else: - local_end = datetime.datetime.min.replace(month=month + 1, year=year, day=1) + naive_end = datetime.datetime.min.replace(month=month + 1, year=year, day=1) + start = naive_start + end = naive_end if self.tzinfo is not None: - local_end = self.tzinfo.localize(local_end) + local_start = self.tzinfo.localize(naive_start) + local_end = self.tzinfo.localize(naive_end) + start = local_start.astimezone(pytz.utc) end = local_end.astimezone(pytz.utc) - end = local_end.astimezone(pytz.utc) return start, end @@ -294,24 +288,29 @@ def _get_week_range(self, week): if isinstance(week, datetime.datetime): week = week.date() # Adjust the start datetime to midnight of the week datetime - #If tzinfo is not none get the local start of the week and convert it to utc. - if self.tzinfo is not None: - local_start = self.tzinfo.localize(datetime.datetime.combine(week, datetime.time.min)) - start = local_start.astimezone(pytz.utc) - else: - start = datetime.datetime.combine(week, datetime.time.min).replace(tzinfo=self.tzinfo) + naive_start = datetime.datetime.combine(week, datetime.time.min) # Adjust the start datetime to Monday or Sunday of the current week if FIRST_DAY_OF_WEEK == 1: # The week begins on Monday - sub_days = start.isoweekday() - 1 + sub_days = naive_start.isoweekday() - 1 else: # The week begins on Sunday - sub_days = start.isoweekday() + sub_days = naive_start.isoweekday() if sub_days == 7: sub_days = 0 if sub_days > 0: - start = start - datetime.timedelta(days=sub_days) - end = start + datetime.timedelta(days=7) + naive_start = naive_start - datetime.timedelta(days=sub_days) + naive_end = naive_start + datetime.timedelta(days=7) + + if self.tzinfo is not None: + local_start = self.tzinfo.localize(naive_start) + local_end = self.tzinfo.localize(naive_end) + start = local_start.astimezone(pytz.utc) + end = local_end.astimezone(pytz.utc) + else: + start = naive_start + end = naive_end + return start, end def __unicode__(self): @@ -335,14 +334,18 @@ def __init__(self, events, date=None, parent_persisted_occurrences=None, def _get_day_range(self, date): if isinstance(date, datetime.datetime): date = date.date() - #If tzinfo is not none get the local start of the day and convert it to utc. + + naive_start = datetime.datetime.combine(date, datetime.time.min) + naive_end = datetime.datetime.combine(date + datetime.timedelta(days=1), datetime.time.min) if self.tzinfo is not None: - local_start = self.tzinfo.localize(datetime.datetime.combine(date, datetime.time.min)) + local_start = self.tzinfo.localize(naive_start) + local_end = self.tzinfo.localize(naive_end) start = local_start.astimezone(pytz.utc) + end = local_end.astimezone(pytz.utc) else: - start = datetime.datetime.combine(date, datetime.time.min).replace(tzinfo=self.tzinfo) + start = naive_start + end = naive_end - end = start + datetime.timedelta(days=1) return start, end def __unicode__(self): From c0b7447b9d6d60a73ab2c67599c6eafdb974634e Mon Sep 17 00:00:00 2001 From: Paul Poulakis Date: Tue, 14 Jan 2014 13:44:13 +0200 Subject: [PATCH 08/10] Fix tests for aware datetime objects. --- schedule/tests/test_periods.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/schedule/tests/test_periods.py b/schedule/tests/test_periods.py index 85826b98..7a089273 100644 --- a/schedule/tests/test_periods.py +++ b/schedule/tests/test_periods.py @@ -326,9 +326,10 @@ def setUp(self): ) def test_week_range(self): - start = datetime.datetime(2013, 12, 15, 23, 0, tzinfo=pytz.utc) - end = datetime.datetime(2013, 12, 22, 23, 0, tzinfo=pytz.utc) + start = self.timezone.localize(datetime.datetime(2013, 12, 15, 0, 0)) + end = self.timezone.localize(datetime.datetime(2013, 12, 22, 0, 0)) + self.assertEqual(self.week.tzinfo, self.timezone) self.assertEqual(start, self.week.start) self.assertEqual(end, self.week.end) @@ -338,14 +339,15 @@ def setUp(self): self.timezone = pytz.timezone('Europe/Amsterdam') self.month = Month( events=Event.objects.all(), - date=self.timezone.localize(datetime.datetime(2013, 12, 17, 9, 0)), + date=self.timezone.localize(datetime.datetime(2013, 11, 17, 9, 0)), tzinfo=self.timezone, ) def test_month_range(self): - start = datetime.datetime(2013, 11, 30, 23, 0, tzinfo=pytz.utc) - end = datetime.datetime(2013, 12, 31, 23, 0, tzinfo=pytz.utc) + start = self.timezone.localize(datetime.datetime(2013, 11, 1, 0, 0)) + end = self.timezone.localize(datetime.datetime(2013, 12, 1, 0, 0)) + self.assertEqual(self.month.tzinfo, self.timezone) self.assertEqual(start, self.month.start) self.assertEqual(end, self.month.end) @@ -360,8 +362,9 @@ def setUp(self): ) def test_year_range(self): - start = datetime.datetime(2012, 12, 31, 23, 0, tzinfo=pytz.utc) - end = datetime.datetime(2013, 12, 31, 23, 0, tzinfo=pytz.utc) + start = self.timezone.localize(datetime.datetime(2013, 1, 1, 0, 0)) + end = self.timezone.localize(datetime.datetime(2014, 1, 1, 0, 0)) + self.assertEqual(self.year.tzinfo, self.timezone) self.assertEqual(start, self.year.start) self.assertEqual(end, self.year.end) From 83e6b3076ef2cec49477c5bac473e623ff7a6137 Mon Sep 17 00:00:00 2001 From: Paul Poulakis Date: Tue, 14 Jan 2014 13:44:51 +0200 Subject: [PATCH 09/10] Fix periods for naive datetime objects. --- schedule/periods.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/schedule/periods.py b/schedule/periods.py index 867ee4db..276aed5f 100644 --- a/schedule/periods.py +++ b/schedule/periods.py @@ -32,8 +32,16 @@ class Period(object): """ def __init__(self, events, start, end, parent_persisted_occurrences=None, occurrence_pool=None, tzinfo=pytz.utc): - self.utc_start = start.astimezone(pytz.utc) - self.utc_end = end.astimezone(pytz.utc) + if start.tzinfo is not None: + self.utc_start = start.astimezone(pytz.utc) + else: + self.utc_start = pytz.utc.localize(start) + + if end.tzinfo is not None: + self.utc_end = end.astimezone(pytz.utc) + else: + self.utc_end = pytz.utc.localize(end) + self.events = events self.tzinfo = self._get_tzinfo(tzinfo) self.occurrence_pool = occurrence_pool @@ -132,11 +140,15 @@ def get_periods(self, cls, tzinfo=None): @property def start(self): - return self.utc_start.astimezone(self.tzinfo) + if self.tzinfo is not None: + return self.utc_start.astimezone(self.tzinfo) + return self.utc_start.replace(tzinfo=None) @property def end(self): - return self.utc_end.astimezone(self.tzinfo) + if self.tzinfo is not None: + return self.utc_end.astimezone(self.tzinfo) + return self.utc_end.replace(tzinfo=None) class Year(Period): @@ -201,7 +213,7 @@ def get_days(self): def get_day(self, daynumber): date = self.start if daynumber > 1: - date += datetime.timedelta(days=daynumber-1) + date += datetime.timedelta(days=daynumber - 1) return self.create_sub_period(Day, date) def next_month(self): From 280f8e7980f0b08792a172b4393d3e19ba9a5503 Mon Sep 17 00:00:00 2001 From: Paul Poulakis Date: Tue, 14 Jan 2014 14:53:25 +0200 Subject: [PATCH 10/10] Improve Periods timezone robustness. --- schedule/periods.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/schedule/periods.py b/schedule/periods.py index 276aed5f..46fdc5cf 100644 --- a/schedule/periods.py +++ b/schedule/periods.py @@ -32,15 +32,10 @@ class Period(object): """ def __init__(self, events, start, end, parent_persisted_occurrences=None, occurrence_pool=None, tzinfo=pytz.utc): - if start.tzinfo is not None: - self.utc_start = start.astimezone(pytz.utc) - else: - self.utc_start = pytz.utc.localize(start) - if end.tzinfo is not None: - self.utc_end = end.astimezone(pytz.utc) - else: - self.utc_end = pytz.utc.localize(end) + self.utc_start = self._normalize_timezone_to_utc(start, tzinfo) + + self.utc_end = self._normalize_timezone_to_utc(end, tzinfo) self.events = events self.tzinfo = self._get_tzinfo(tzinfo) @@ -48,6 +43,13 @@ def __init__(self, events, start, end, parent_persisted_occurrences=None, if parent_persisted_occurrences is not None: self._persisted_occurrences = parent_persisted_occurrences + def _normalize_timezone_to_utc(self, point_in_time, tzinfo): + if point_in_time.tzinfo is not None: + return point_in_time.astimezone(pytz.utc) + if tzinfo is not None: + return tzinfo.localize(point_in_time).astimezone(pytz.utc) + return pytz.utc.localize(point_in_time) + def __eq__(self, period): return self.utc_start == period.utc_start and self.utc_end == period.utc_end and self.events == period.events