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

Reccuring all day events #80

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 33 additions & 9 deletions icalevents/icalparser.py
Expand Up @@ -11,6 +11,7 @@
from dateutil.tz import UTC, gettz

from icalendar import Calendar
from icalendar.windows_to_olson import WINDOWS_TO_OLSON
from icalendar.prop import vDDDLists, vText
from pytz import timezone

Expand Down Expand Up @@ -39,6 +40,7 @@ def __init__(self):
self.start = None
self.end = None
self.all_day = True
self.transparent = False
self.recurring = False
self.location = None
self.private = False
Expand Down Expand Up @@ -132,6 +134,7 @@ def copy_to(self, new_start=None, uid=None):
ne.attendee = self.attendee
ne.organizer = self.organizer
ne.private = self.private
ne.transparent = self.transparent
ne.uid = uid
ne.created = self.created
ne.last_modified = self.last_modified
Expand Down Expand Up @@ -200,6 +203,9 @@ def create_event(component, tz=UTC):
event_class = component.get('class')
event.private = event_class == 'PRIVATE' or event_class == 'CONFIDENTIAL'

if component.get('class'):
event.transparent = component.get('transp') == 'TRANSPARENT'
eigenmannmartin marked this conversation as resolved.
Show resolved Hide resolved

if component.get('created'):
event.created = normalize(component.get('created').dt, tz)

Expand All @@ -208,7 +214,8 @@ def create_event(component, tz=UTC):
elif event.created:
event.last_modified = event.created

if component.get('sequence'):
# sequence can be 0 - test for None instead
if not component.get('sequence') is None:
event.sequence = component.get('sequence')

if component.get("categories"):
Expand Down Expand Up @@ -267,6 +274,9 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):

# Keep track of the timezones defined in the calendar
timezones = {}
if 'X-WR-TIMEZONE' in calendar:
timezones[str(calendar['X-WR-TIMEZONE'])] = gettz(str(calendar['X-WR-TIMEZONE']))
eigenmannmartin marked this conversation as resolved.
Show resolved Hide resolved

for c in calendar.walk('VTIMEZONE'):
name = str(c['TZID'])
try:
Expand All @@ -282,19 +292,25 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
# assume it applies globally, otherwise UTC
if len(timezones) == 1:
cal_tz = gettz(list(timezones)[0])
if not cal_tz and str(c['TZID']) in WINDOWS_TO_OLSON:
cal_tz = gettz(WINDOWS_TO_OLSON[str(c['TZID'])])
eigenmannmartin marked this conversation as resolved.
Show resolved Hide resolved
else:
cal_tz = UTC

start = normalize(start, cal_tz)
end = normalize(end, cal_tz)

found = []
recurrence_ids = []

# Skip dates that are stored as exceptions.
exceptions = {}
for component in calendar.walk():
if component.name == "VEVENT":
e = create_event(component, cal_tz)

if 'RECURRENCE-ID' in component:
recurrence_ids.append((e.uid, component['RECURRENCE-ID'].dt, e.sequence))

if 'EXDATE' in component:
# Deal with the fact that sometimes it's a list and
Expand All @@ -313,8 +329,11 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
# use it; otherwise, attempt to load the rules from pytz.
start_tz = None
end_tz = None
if e.all_day and e.recurring:
eigenmannmartin marked this conversation as resolved.
Show resolved Hide resolved
# Keep the timezone around if to apply later
start_tz = e.start.tzinfo
end_tz = e.end.tzinfo

if e.all_day:
# Start and end times for all day events must not have
# a timezone because the specification forbids the
# RRULE UNTIL from having a timezone. On the other
Expand All @@ -336,7 +355,7 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
start_tz = timezones[str(e.start.tzinfo)]
else:
try:
start_tz = timezone(str(e.start.tzinfo))
start_tz = e.start.tzinfo
eigenmannmartin marked this conversation as resolved.
Show resolved Hide resolved
except:
pass

Expand All @@ -345,7 +364,7 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
end_tz = timezones[str(e.end.tzinfo)]
else:
try:
end_tz = timezone(str(e.end.tzinfo))
end_tz = e.end.tzinfo
except:
pass

Expand All @@ -364,8 +383,7 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
if e.recurring:
# Unfold recurring events according to their rrule
rule = parse_rrule(component, cal_tz)
dur = e.end - e.start
after = start - dur
after = start - duration
eigenmannmartin marked this conversation as resolved.
Show resolved Hide resolved

for dt in rule.between(after, end, inc=True):
if start_tz is None:
Expand All @@ -376,7 +394,7 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
# date of *this* occurrence. This handles the case where the
# recurrence has crossed over the daylight savings time boundary.
naive = datetime(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
dtstart = start_tz.localize(naive)
dtstart = normalize(naive, tz=start_tz)

ecopy = e.copy_to(dtstart, e.uid)

Expand All @@ -394,7 +412,8 @@ def parse_events(content, start=None, end=None, default_span=timedelta(days=7)):
exdate = "%04d%02d%02d" % (e.start.year, e.start.month, e.start.day)
if exdate not in exceptions:
found.append(e)
return found
# Filter out all events that are moved as indicated by the recurrence-id prop
return [event for event in found if e.sequence is None or not (event.uid, event.start, e.sequence) in recurrence_ids]
Hultner marked this conversation as resolved.
Show resolved Hide resolved


def parse_rrule(component, tz=UTC):
Expand All @@ -418,6 +437,11 @@ def parse_rrule(component, tz=UTC):
if type(rdtstart) is datetime:
rdtstart = normalize(rdtstart, tz=tz)

if type(rdtstart) is date:
for index, rru in enumerate(rrules):
if 'UNTIL' in rru:
rrules[index]['UNTIL'] = [dt.date() if type(dt) is datetime else dt for dt in rru['UNTIL']]
eigenmannmartin marked this conversation as resolved.
Show resolved Hide resolved

# Parse the rrules, might return a rruleset instance, instead of rrule
rule = rrulestr('\n'.join(x.to_ical().decode() for x in rrules),
dtstart=rdtstart)
Expand All @@ -431,7 +455,7 @@ def parse_rrule(component, tz=UTC):

# Add exdates to the rruleset
for exd in extract_exdates(component):
rule.exdate(exd)
rule.exdate(exd.replace(tzinfo=None) if type(rdtstart) is date else exd)
eigenmannmartin marked this conversation as resolved.
Show resolved Hide resolved

# TODO: What about rdates and exrules?

Expand Down