diff --git a/schedule/periods.py b/schedule/periods.py index 547c24fa..46fdc5cf 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,19 +32,29 @@ 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 = 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) self.occurrence_pool = occurrence_pool 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.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 +63,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 +93,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} @@ -115,16 +126,32 @@ 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() + @property + def start(self): + if self.tzinfo is not None: + return self.utc_start.astimezone(self.tzinfo) + return self.utc_start.replace(tzinfo=None) + + @property + def end(self): + if self.tzinfo is not None: + return self.utc_end.astimezone(self.tzinfo) + return self.utc_end.replace(tzinfo=None) + class Year(Period): def __init__(self, events, date=None, parent_persisted_occurrences=None, tzinfo=pytz.utc): @@ -132,25 +159,33 @@ 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) 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, tzinfo=self.tzinfo) - return Year(self.events, start) + start = datetime.datetime(self.start.year - 1, self.start.month, self.start.day) + return Year(self.events, start, tzinfo=self.tzinfo) 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 tzinfo is not none get the local start of the year and convert it to utc. + 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) + return start, end def __unicode__(self): @@ -169,7 +204,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) @@ -180,37 +215,47 @@ 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): - 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 - start = datetime.datetime.min.replace(year=year, month=month, tzinfo=self.tzinfo) + #If tzinfo is not none get the local start of the month and convert it to utc. + naive_start = datetime.datetime.min.replace(year=year, month=month) if month == 12: - end = start.replace(month=1, year=year + 1, tzinfo=self.tzinfo) + naive_end = datetime.datetime.min.replace(month=1, year=year + 1, day=1) else: - end = start.replace(month=month + 1, tzinfo=self.tzinfo) + 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_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) + return start, end def __unicode__(self): @@ -234,21 +279,21 @@ 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)) + 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) @@ -257,19 +302,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 - 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): @@ -288,13 +343,23 @@ 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): date = date.date() - start = datetime.datetime.combine(date, datetime.time.min).replace(tzinfo=self.tzinfo) - end = start + datetime.timedelta(days=1) + + 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(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): @@ -305,18 +370,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) diff --git a/schedule/tests/test_periods.py b/schedule/tests/test_periods.py index 7cebb85b..7a089273 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,120 @@ 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 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): + 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 = 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) + + +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, 11, 17, 9, 0)), + tzinfo=self.timezone, + ) + + def test_month_range(self): + 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) + + +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 = 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)