2019-02-23 22:11:49 +01:00
|
|
|
from DataSourceInterface import DataSourceInterface
|
2019-04-23 07:30:08 +02:00
|
|
|
from datetime import datetime, timezone, timedelta, date
|
2019-03-24 11:07:22 +01:00
|
|
|
from dateutil.rrule import rrulestr
|
|
|
|
from dateutil.parser import parse
|
|
|
|
import calendar
|
2020-05-08 10:10:31 +02:00
|
|
|
from CalendarEvent import CalendarEvent
|
2019-02-23 22:11:49 +01:00
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
|
2019-02-23 22:11:49 +01:00
|
|
|
class CalendarInterface (DataSourceInterface):
|
|
|
|
"""Interface for fetching and processing calendar event information."""
|
2019-07-13 08:05:35 +02:00
|
|
|
|
|
|
|
def __init__(self):
|
2019-04-18 08:30:08 +02:00
|
|
|
self.events = []
|
2020-04-19 22:25:50 +02:00
|
|
|
self.excluded_urls = []
|
2019-04-18 08:30:08 +02:00
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def reload(self):
|
2019-04-18 08:30:08 +02:00
|
|
|
if self.is_available() == False:
|
|
|
|
return
|
2019-03-24 11:07:22 +01:00
|
|
|
self.events = self.__get_events__()
|
|
|
|
self.events = self.__sort_events__(self.events)
|
2019-03-03 13:53:07 +01:00
|
|
|
|
2020-04-19 22:25:50 +02:00
|
|
|
def exclude_calendars(self, urls=[]):
|
|
|
|
self.excluded_urls = urls
|
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def __sort_events__(self, events):
|
|
|
|
events.sort(key=lambda x: x.begin_datetime)
|
2019-03-24 11:07:22 +01:00
|
|
|
return events
|
2019-03-03 13:53:07 +01:00
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def __sort_event_types__(self, events):
|
2019-04-24 14:27:45 +02:00
|
|
|
multiday = [ev for ev in events if ev.multiday]
|
|
|
|
allday = [ev for ev in events if ev.allday and ev.multiday == False]
|
2019-07-13 08:05:35 +02:00
|
|
|
timed = [ev for ev in events if ev.allday ==
|
|
|
|
False and ev.multiday == False]
|
2019-04-24 14:27:45 +02:00
|
|
|
return multiday + allday + timed
|
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def __get_events__(self):
|
2019-02-23 22:11:49 +01:00
|
|
|
raise NotImplementedError("Functions needs to be implemented")
|
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def get_upcoming_events(self, timespan=None, start_time=None):
|
2019-03-24 11:07:22 +01:00
|
|
|
if timespan is None:
|
|
|
|
timespan = timedelta(31)
|
2019-04-28 15:24:15 +02:00
|
|
|
if start_time == None:
|
|
|
|
local_tzinfo = datetime.now(timezone.utc).astimezone().tzinfo
|
|
|
|
start_time = datetime.now(local_tzinfo)
|
|
|
|
return self.__get_events_in_range__(start_time, timespan)
|
2019-03-03 13:53:07 +01:00
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def get_today_events(self):
|
2019-04-29 18:08:53 +02:00
|
|
|
return self.get_day_events(date.today())
|
2019-02-23 22:11:49 +01:00
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def get_day_events(self, day):
|
2019-04-23 07:30:08 +02:00
|
|
|
if type(day) is not type(date.today()):
|
2019-07-13 08:05:35 +02:00
|
|
|
raise TypeError(
|
|
|
|
"get_day_events only takes date-objects as parameters, not \"%s\"" % str(type(day)))
|
2019-04-26 09:45:50 +02:00
|
|
|
local_tzinfo = datetime.now(timezone.utc).astimezone().tzinfo
|
2019-07-13 08:05:35 +02:00
|
|
|
day_start = datetime(day.year, day.month, day.day,
|
|
|
|
0, 0, 0, 0, local_tzinfo)
|
2019-03-24 11:07:22 +01:00
|
|
|
return self.__get_events_in_range__(day_start, timedelta(1))
|
2019-02-23 22:11:49 +01:00
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def get_month_events(self, month=-1, year=-1):
|
2019-03-03 13:53:07 +01:00
|
|
|
if month < 0:
|
|
|
|
month = datetime.now().month
|
2019-05-12 17:40:42 +02:00
|
|
|
if year < 0:
|
|
|
|
year = datetime.now().year
|
2019-07-13 08:05:35 +02:00
|
|
|
|
2019-04-26 09:45:50 +02:00
|
|
|
local_tzinfo = datetime.now(timezone.utc).astimezone().tzinfo
|
2019-05-12 17:40:42 +02:00
|
|
|
month_start = datetime(year, month, 1, 0, 0, 0, 0, local_tzinfo)
|
2019-07-13 08:05:35 +02:00
|
|
|
month_days = calendar.monthrange(
|
|
|
|
month_start.year, month_start.month)[1]
|
2019-03-24 11:07:22 +01:00
|
|
|
return self.__get_events_in_range__(month_start, timedelta(month_days))
|
2019-02-23 22:11:49 +01:00
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def __get_events_in_range__(self, start, duration):
|
2019-03-24 11:07:22 +01:00
|
|
|
if self.events is None:
|
2019-03-03 13:53:07 +01:00
|
|
|
return []
|
2019-03-24 11:07:22 +01:00
|
|
|
|
|
|
|
if start.tzinfo is None:
|
2019-04-02 10:48:15 +02:00
|
|
|
raise TypeError("start datetime needs to be timezone-aware")
|
2019-03-24 11:07:22 +01:00
|
|
|
|
|
|
|
events_in_range = []
|
|
|
|
for event in self.events:
|
2020-04-19 22:25:50 +02:00
|
|
|
# Is excluded?
|
|
|
|
if event.calendar_url in self.excluded_urls:
|
|
|
|
continue
|
2020-05-08 10:10:31 +02:00
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
event_occurrence = self.__get_if_event_in_range__(
|
|
|
|
event, start, duration)
|
2019-03-24 11:07:22 +01:00
|
|
|
if event_occurrence:
|
|
|
|
events_in_range.extend(event_occurrence)
|
|
|
|
|
|
|
|
events_in_range = self.__sort_events__(events_in_range)
|
2019-04-24 14:27:45 +02:00
|
|
|
return self.__sort_event_types__(events_in_range)
|
2019-03-24 11:07:22 +01:00
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def __get_if_event_in_range__(self, event, start, duration):
|
2019-03-24 11:07:22 +01:00
|
|
|
'''Returns list or None'''
|
|
|
|
if event is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if event.rrule is None:
|
|
|
|
return self.__is_onetime_in_range__(event, start, duration)
|
|
|
|
else:
|
|
|
|
return self.__is_repeating_in_range__(event, start, duration)
|
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def __is_onetime_in_range__(self, event, start, duration):
|
2019-03-24 11:07:22 +01:00
|
|
|
if event.begin_datetime > start:
|
|
|
|
first_start = start
|
|
|
|
first_duration = duration
|
|
|
|
second_start = event.begin_datetime
|
|
|
|
else:
|
|
|
|
first_start = event.begin_datetime
|
|
|
|
first_duration = event.duration
|
|
|
|
second_start = start
|
|
|
|
|
|
|
|
if (second_start - first_start) < first_duration:
|
2019-07-13 08:05:35 +02:00
|
|
|
return [event]
|
2019-03-24 11:07:22 +01:00
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def __is_repeating_in_range__(self, event, start, duration):
|
2019-03-24 11:07:22 +01:00
|
|
|
end = start + duration
|
|
|
|
occurrences = []
|
|
|
|
|
2019-07-05 21:30:14 +02:00
|
|
|
try:
|
2019-07-13 08:05:35 +02:00
|
|
|
r_string = ""
|
|
|
|
r_string = self.__add_timezoneawarness__(event.rrule)
|
|
|
|
rule = rrulestr(r_string, dtstart=event.begin_datetime)
|
2019-07-05 21:30:14 +02:00
|
|
|
for occurrence in rule:
|
|
|
|
if occurrence - end > timedelta(0):
|
|
|
|
return occurrences
|
2019-07-13 08:05:35 +02:00
|
|
|
merged_event = self.__merge_event_data__(
|
|
|
|
event, start=occurrence)
|
2019-07-05 21:30:14 +02:00
|
|
|
if self.__is_onetime_in_range__(merged_event, start, duration):
|
|
|
|
occurrences.append(merged_event)
|
|
|
|
return occurrences
|
|
|
|
except Exception as ex:
|
2019-07-13 08:05:35 +02:00
|
|
|
print("\"is_repeating_in_range\" failed while processing: dtstart="+str(event.begin_datetime) +
|
|
|
|
" dtstart.tzinfo="+str(event.begin_datetime.tzinfo)+" rrule="+r_string)
|
2019-07-05 21:30:14 +02:00
|
|
|
raise ex
|
2019-03-24 11:07:22 +01:00
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def __merge_event_data__(self, event, start=None):
|
2020-05-08 10:10:31 +02:00
|
|
|
merged_event = CalendarEvent()
|
|
|
|
|
|
|
|
merged_event.begin_datetime = event.begin_datetime
|
|
|
|
merged_event.end_datetime = event.end_datetime
|
|
|
|
merged_event.duration = event.duration
|
|
|
|
merged_event.allday = event.allday
|
|
|
|
merged_event.multiday = event.multiday
|
|
|
|
merged_event.rrule = event.rrule
|
|
|
|
|
|
|
|
merged_event.title = event.title
|
|
|
|
merged_event.description = event.description
|
|
|
|
merged_event.attendees = event.attendees
|
|
|
|
merged_event.highlight = event.highlight
|
|
|
|
|
|
|
|
merged_event.calendar_name = event.calendar_name
|
|
|
|
merged_event.calendar_url = event.calendar_url
|
|
|
|
|
|
|
|
merged_event.location = event.location
|
|
|
|
merged_event.fetch_datetime = event.fetch_datetime
|
|
|
|
|
2019-03-24 11:07:22 +01:00
|
|
|
if start is not None:
|
2020-05-08 10:10:31 +02:00
|
|
|
merged_event.begin_datetime = start
|
|
|
|
merged_event.end_datetime = start + event.duration
|
2019-07-13 08:05:35 +02:00
|
|
|
|
2020-05-08 10:10:31 +02:00
|
|
|
return merged_event
|
2019-03-27 17:48:28 +01:00
|
|
|
|
2019-07-13 08:05:35 +02:00
|
|
|
def __add_timezoneawarness__(self, rrule):
|
2019-07-05 21:30:14 +02:00
|
|
|
"""UNTIL must be specified in UTC when DTSTART is timezone-aware (which it is)"""
|
2019-03-27 17:48:28 +01:00
|
|
|
if "UNTIL" not in rrule:
|
|
|
|
return rrule
|
|
|
|
|
|
|
|
timezone_str = "T000000Z"
|
2019-07-05 21:30:14 +02:00
|
|
|
until_template = "UNTIL=YYYYMMDD"
|
2019-03-27 17:48:28 +01:00
|
|
|
|
|
|
|
until_index = rrule.index("UNTIL")
|
|
|
|
|
2019-07-05 21:30:14 +02:00
|
|
|
tz_index = until_index + len(until_template)
|
2019-07-07 18:16:52 +02:00
|
|
|
if until_index < 0 or (tz_index < len(rrule) and rrule[tz_index] is "T"):
|
2019-04-09 10:08:15 +02:00
|
|
|
return rrule
|
2019-07-13 08:05:35 +02:00
|
|
|
|
2019-07-07 18:16:52 +02:00
|
|
|
if tz_index == len(rrule):
|
|
|
|
return rrule + timezone_str
|
|
|
|
else:
|
2019-07-13 08:05:35 +02:00
|
|
|
return rrule[:tz_index] + timezone_str + rrule[tz_index:]
|