| 1 | import urllib |
|---|
| 2 | import datetime |
|---|
| 3 | |
|---|
| 4 | from icalendar import Calendar |
|---|
| 5 | import recurring_ical_events |
|---|
| 6 | |
|---|
| 7 | from trac.config import ConfigSection |
|---|
| 8 | from trac.core import Component, implements |
|---|
| 9 | from trac.util.datefmt import format_time, user_time |
|---|
| 10 | from weekplan.core import IWeekPlanEventProvider |
|---|
| 11 | from weekplan.model import WeekPlanEvent |
|---|
| 12 | |
|---|
| 13 | |
|---|
| 14 | class ICalWeekPlanEventProvider(Component): |
|---|
| 15 | """Provides events from ical feeds like Google Calendar.""" |
|---|
| 16 | |
|---|
| 17 | implements(IWeekPlanEventProvider) |
|---|
| 18 | |
|---|
| 19 | ical_section = ConfigSection('week-plan-ical-feeds', |
|---|
| 20 | """Every option in the `[week-plan-ical-feeds]` section defines one ical |
|---|
| 21 | feed. The option name defines the plan name. The option value defines |
|---|
| 22 | the URL. |
|---|
| 23 | |
|---|
| 24 | '''Example:''' |
|---|
| 25 | {{{ |
|---|
| 26 | [week-plan-ical-feeds] |
|---|
| 27 | trac = https://trac.edgewall.org/roadmap?format=ics |
|---|
| 28 | }}} |
|---|
| 29 | """) |
|---|
| 30 | |
|---|
| 31 | def get_plan_prefixes(self): |
|---|
| 32 | return ['ical:'] |
|---|
| 33 | |
|---|
| 34 | def get_events(self, req, plan_ids, range): |
|---|
| 35 | def get_ical_entries(ical_url): |
|---|
| 36 | f = urllib.urlopen(ical_url) |
|---|
| 37 | content = f.read() |
|---|
| 38 | calendar = Calendar.from_ical(content) |
|---|
| 39 | entries = calendar.walk() |
|---|
| 40 | if range is not None: |
|---|
| 41 | r0, r1 = range |
|---|
| 42 | recurring_events = recurring_ical_events.of(calendar).between(r0, r1) |
|---|
| 43 | entries.extend(recurring_events) |
|---|
| 44 | return entries |
|---|
| 45 | |
|---|
| 46 | def is_relevant(entry): |
|---|
| 47 | if entry.name != "VEVENT": |
|---|
| 48 | return False |
|---|
| 49 | start = entry.get('dtstart') |
|---|
| 50 | end = entry.get('dtend') |
|---|
| 51 | if start is None: |
|---|
| 52 | return False |
|---|
| 53 | if end is None: |
|---|
| 54 | end = start |
|---|
| 55 | if range is not None: |
|---|
| 56 | r0, r1 = range |
|---|
| 57 | if not isinstance(start.dt, datetime.datetime): |
|---|
| 58 | r1 = r1.date() |
|---|
| 59 | if not isinstance(end.dt, datetime.datetime): |
|---|
| 60 | r0 = r0.date() |
|---|
| 61 | if end.dt < r0 or r1 < start.dt: |
|---|
| 62 | return False |
|---|
| 63 | return True |
|---|
| 64 | |
|---|
| 65 | def ical_entry_to_week_plan_event(entry, plan): |
|---|
| 66 | entry_id = entry.get('UID') |
|---|
| 67 | start = entry.get('dtstart') |
|---|
| 68 | end = entry.get('dtend') |
|---|
| 69 | summary = entry.get('summary') |
|---|
| 70 | description = entry.get('description') |
|---|
| 71 | if end is None: |
|---|
| 72 | end = start |
|---|
| 73 | title = str(summary) |
|---|
| 74 | if description: |
|---|
| 75 | title += ' ' + str(description) |
|---|
| 76 | if isinstance(start.dt, datetime.datetime): |
|---|
| 77 | title = user_time(req, format_time, start.dt) + ' ' + title |
|---|
| 78 | start = start.dt |
|---|
| 79 | end = end.dt |
|---|
| 80 | if isinstance(start, datetime.date): |
|---|
| 81 | start = datetime.datetime.combine(start, datetime.time(12, 0)) |
|---|
| 82 | if isinstance(end, datetime.date): |
|---|
| 83 | end = datetime.datetime.combine(end, datetime.time(12, 0)) |
|---|
| 84 | return WeekPlanEvent(str(entry_id), plan, title, start, end) |
|---|
| 85 | |
|---|
| 86 | events = [] |
|---|
| 87 | for plan_suffix, ical_url in self.ical_section.options(): |
|---|
| 88 | plan_id = 'ical:' + plan_suffix |
|---|
| 89 | if plan_ids and plan_id not in plan_ids: |
|---|
| 90 | continue |
|---|
| 91 | for entry in get_ical_entries(ical_url): |
|---|
| 92 | try: |
|---|
| 93 | if is_relevant(entry): |
|---|
| 94 | event = ical_entry_to_week_plan_event(entry, plan_id) |
|---|
| 95 | events.append(event) |
|---|
| 96 | except BaseException as e: |
|---|
| 97 | self.log.error("Error accessing week-plan '%s': %s", plan_id, e) |
|---|
| 98 | return events |
|---|