Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Commit

Permalink
Improve generation and handling of repeating all-day events
Browse files Browse the repository at this point in the history
- make RRULE UNTIL constraint for recurrences the last microsecond of
  the repeat end date, so the repeat end date is included in the
  generated occurrence set
- ensure all-day events span a full day (less one microsecond) based
  on their start & end times
- improve `time_range_string` description for all-day events spanning
  just the single day
- add unit test for generating repeat occurrences for every day in Oct
- minor bug and formatting fixes.
  • Loading branch information
jmurty committed Oct 18, 2016
1 parent bb9b078 commit e43a172
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 27 deletions.
39 changes: 27 additions & 12 deletions icekit_events/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ def zero_datetime(dt, tz=None):
if dt is None:
return None
if tz is None:
tz = pytz.utc
return dt.replace(
hour=0, minute=0, second=0, microsecond=0, tzinfo=pytz.utc)
tz = get_current_timezone()
return coerce_naive(dt).replace(hour=0, minute=0, second=0, microsecond=0)


def default_starts():
Expand Down Expand Up @@ -453,6 +452,7 @@ def start_times_set(self):
def get_absolute_url(self):
return reverse('icekit_events_eventbase_detail', args=(self.slug,))


class GeneratorException(Exception):
pass

Expand Down Expand Up @@ -489,10 +489,12 @@ class EventRepeatsGenerator(AbstractBaseModel):
'event repeats.'),
null=True,
)
start = models.DateTimeField('first start',
start = models.DateTimeField(
'first start',
default=default_starts,
db_index=True)
end = models.DateTimeField('first end',
end = models.DateTimeField(
'first end',
default=default_ends,
db_index=True)
is_all_day = models.BooleanField(
Expand Down Expand Up @@ -567,9 +569,17 @@ def _build_complete_rrule(self, start_dt=None, until=None):
rrule_spec += "\nRDATE:%s" % format_naive_ical_dt(start_dt)
else:
rrule_spec += "\nRRULE:%s" % self.recurrence_rule
# Apply this event's end repeat
rrule_spec += ";UNTIL=%s" % format_naive_ical_dt(
until - timedelta(seconds=1))
# Apply this event's end repeat date as an *exclusive* UNTIL
# constraint. UNTIL in RRULE specs is inclusive by default, so we
# fake exclusivity by adjusting the end time by a microsecond.
if self.is_all_day:
# For all-day generator, make the UNTIL constraint the last
# microsecond of the repeat end date to ensure the end date is
# included in the generated set as users expect.
until += timedelta(days=1, microseconds=-1)
else:
until -= timedelta(microseconds=1)
rrule_spec += ";UNTIL=%s" % format_naive_ical_dt(until)
return rrule_spec

def save(self, *args, **kwargs):
Expand Down Expand Up @@ -615,7 +625,8 @@ def save(self, *args, **kwargs):
# UTC timezone when we save an all-day occurrence
if self.is_all_day:
self.start = zero_datetime(self.start)
self.end = zero_datetime(self.end)
self.end = zero_datetime(self.end) \
+ timedelta(days=1, microseconds=-1)

super(EventRepeatsGenerator, self).save(*args, **kwargs)

Expand Down Expand Up @@ -738,9 +749,13 @@ class Meta:

def time_range_string(self):
if self.is_all_day:
return u"""{0} - {1}, all day""".format(
datefilter(self.local_start, DATE_FORMAT),
datefilter(self.local_end, DATE_FORMAT))
if self.duration <= timedelta(days=1):
return u"""{0}, all day""".format(
datefilter(self.local_start, DATE_FORMAT))
else:
return u"""{0} - {1}, all day""".format(
datefilter(self.local_start, DATE_FORMAT),
datefilter(self.local_end, DATE_FORMAT))
else:
return u"""{0} - {1}""".format(
datefilter(self.local_start, DATETIME_FORMAT),
Expand Down
47 changes: 32 additions & 15 deletions icekit_events/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,17 +152,17 @@ def test_event_with_eventrepeatsgenerators(self):
13 * 2, daily_wend_occurrences.count())
self.assertEqual(
5, # Saturday
daily_wend_occurrences[0].start.weekday())
coerce_naive(daily_wend_occurrences[0].start).weekday())
self.assertEqual(
6, # Sunday
daily_wend_occurrences[1].start.weekday())
coerce_naive(daily_wend_occurrences[1].start).weekday())
# Start and end dates of all-day occurrences are zeroed
self.assertEqual(
models.zero_datetime(daily_wend_occurrences[0].start),
daily_wend_occurrences[0].start)
time(0, 0),
daily_wend_occurrences[0].start.astimezone(djtz.get_current_timezone()).time())
self.assertEqual(
models.zero_datetime(daily_wend_occurrences[0].end),
daily_wend_occurrences[0].end)
time(0, 0),
daily_wend_occurrences[0].end.astimezone(djtz.get_current_timezone()).time())
#######################################################################
# Delete "Daily" repeat generator
#######################################################################
Expand Down Expand Up @@ -220,17 +220,15 @@ def test_event_with_user_modified_occurrences(self):
self.assertEqual(2, event.occurrences.count())
all_day_occurrence = event.occurrences.all()[1]
self.assertTrue(timed_occurrence.is_user_modified)
self.assertEqual(
models.zero_datetime(all_day_start), all_day_occurrence.start)
self.assertEqual(
models.zero_datetime(all_day_start), all_day_occurrence.end)
# Start and end dates of all-day occurrences are zeroed
self.assertEqual(
models.zero_datetime(all_day_occurrence.start),
all_day_occurrence.start)
time(0, 0),
all_day_occurrence.start.astimezone(
djtz.get_current_timezone()).time())
self.assertEqual(
models.zero_datetime(all_day_occurrence.end),
all_day_occurrence.end)
time(0, 0),
all_day_occurrence.end.astimezone(
djtz.get_current_timezone()).time())
#######################################################################
# Cancel first (timed) event
#######################################################################
Expand Down Expand Up @@ -863,7 +861,7 @@ def test_duration(self):
).duration
)
self.assertEquals(
timedelta(),
timedelta(days=1, microseconds=-1),
G(
models.EventRepeatsGenerator,
is_all_day=True,
Expand Down Expand Up @@ -946,6 +944,25 @@ def test_unlimited_daily_repeating_generator(self):
(self.naive_start + timedelta(days=91), self.naive_end + timedelta(days=91)),
next(start_and_end_times))

def test_daily_repeating_every_day_in_month(self):
start = djtz.datetime(2016,10,1, 0,0)
end = djtz.datetime(2016,10,1, 0,0)
repeat_end = djtz.datetime(2016,10,31, 0,0)
generator = G(
models.EventRepeatsGenerator,
start=start,
end=end,
is_all_day=True,
recurrence_rule='FREQ=DAILY',
repeat_end=repeat_end,
)
# Repeating generator has expected date entries in its RRULESET
rruleset = generator.get_rruleset()
self.assertTrue(31, rruleset.count())
self.assertTrue(coerce_naive(start) in rruleset)
self.assertTrue(
coerce_naive(djtz.datetime(2016,10,31, 0,0)) in rruleset)


class TestEventOccurrences(TestCase):

Expand Down

0 comments on commit e43a172

Please sign in to comment.