Package Gnumed :: Package timelinelib :: Package dataimport :: Module ics |
[frames] | no frames]

# Source Code for Module Gnumed.timelinelib.dataimport.ics

      1  # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
      2  #
      3  # This file is part of Timeline.
      4  #
      5  # Timeline is free software: you can redistribute it and/or modify
      6  # it under the terms of the GNU General Public License as published by
      7  # the Free Software Foundation, either version 3 of the License, or
      8  # (at your option) any later version.
      9  #
     10  # Timeline is distributed in the hope that it will be useful,
     11  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13  # GNU General Public License for more details.
     14  #
     15  # You should have received a copy of the GNU General Public License
     16  # along with Timeline.  If not, see <http://www.gnu.org/licenses/>.
     17
     18
     19  from datetime import date
     20  from datetime import datetime
     21  from datetime import timedelta
     22  from os.path import abspath
     23  import random
     24
     25  from icalendar import Calendar
     26
     27  from timelinelib.calendar.gregorian.gregorian import GregorianDateTime
     28  from timelinelib.calendar.gregorian.time import GregorianDelta
     29  from timelinelib.canvas.data.db import MemoryDB
     30  from timelinelib.canvas.data.exceptions import TimelineIOError
     31  from timelinelib.canvas.data import Category
     32  from timelinelib.canvas.data import Event
     33  from timelinelib.utils import ex_msg
     34  from timelinelib.wxgui.utils import display_warning_message
     35
     36
     37 -class IcsLoader(object):
     38
     39 -    def load(self, db, path, options):
     40          self.vevents_not_loaded = []
     41          (self.add_location_to_description,
     42           self.use_trigger_as_start_date,
     43           self.use_trigger_as_alert) = options
     44          self.events = []
     45          self.categories = []
     46          self.category_names = []
     47          file_contents = self._read_file_content(path)
     48          cal = self._read_calendar_object(file_contents)
     49          self._load_events(cal, db)
     50          self._load_todos(cal, db)
     51          self._save_data_in_db(db)
     52          self._report_on_vevents_not_loaded()
     53
     54 -    def _load_events(self, cal, db):
     55          for vevent in cal.walk("VEVENT"):
     56              self._load_vevent(db, vevent)
     57              self._load_categories(vevent)
     58
     59 -    def _load_todos(self, cal, db):
     60          for vtodo in cal.walk("VTODO"):
     61              self._load_vtodo(db, vtodo)
     62              self._load_categories(vtodo)
     63
     64 -    def _load_vtodo(self, db, vtodo):
     65          try:
     66              start, end = self._extract_todo_start_end(vtodo)
     67              txt = self._get_event_name(vtodo)
     68              event = Event().update(start, end, txt)
     69              event.set_description(self._extract_todo_description(vtodo))
     70              event.set_alert(self._extract_todo_alert(vtodo))
     71              self.events.append(event)
     72          except KeyError:
     73              pass
     74
     75 -    def _extract_todo_start_end(self, vtodo):
     76          end = self._extract_todo_end(vtodo)
     77          if self.use_trigger_as_start_date:
     78              start = self._extract_todo_start(vtodo)
     79              if start is None:
     80                  start = end
     81          else:
     82              start = end
     83          return start, end
     84
     85 -    def _extract_todo_end(self, vtodo):
     86          end = self._get_value(vtodo, "due")
     87          if end:
     88              return self._convert_to_datetime(end)
     89          else:
     90              raise KeyError("Start time not found")
     91
     92 -    def _extract_todo_start(self, vtodo):
     93          valarm = self._get_first_subelement(vtodo, "VALARM")
     94          if valarm:
     95              start = self._get_value(valarm, "trigger")
     96              if start:
     97                  return self._convert_to_datetime(start)
     98
     99 -    def _extract_todo_description(self, vtodo):
    100          if self.add_location_to_description:
    101              if "location" in vtodo:
    102                  return "%s: %s" % (_("Location"), vtodo["location"])
    103              else:
    104                  return None
    105          else:
    106              return None
    107
    108 -    def _extract_todo_alert(self, vtodo):
    109          if self.use_trigger_as_alert:
    110              start = self._extract_todo_start(vtodo)
    111              if start is not None:
    112                  return (start, "")
    113
    114 -    def _read_calendar_object(self, file_contents):
    115          try:
    116              return Calendar.from_ical(file_contents)
    117          except Exception as pe:
    118              msg1 = _("Unable to read calendar data.")
    119              msg2 = "\n\n" + ex_msg(pe)
    120              raise TimelineIOError(msg1 + msg2)
    121
    122 -    def _read_file_content(self, path):
    123          ics_file = None
    124          try:
    125              ics_file = open(path, "rb")
    126              return ics_file.read()
    127          except IOError as e:
    128              msg = _("Unable to read from file '%s'.")
    129              whole_msg = (msg + "\n\n%s") % (abspath(path), e)
    130              raise TimelineIOError(whole_msg)
    131          finally:
    132              if ics_file is not None:
    133                  ics_file.close()
    134
    135 -    def _save_data_in_db(self, db):
    136          for event in self.events:
    137              db.save_event(event)
    138          for category in self.categories:
    139              db.save_category(category)
    140
    141 -    def _load_vevent(self, db, vevent):
    142          try:
    143              start, end = self._extract_start_end(vevent)
    144              txt = self._get_event_name(vevent)
    145              event = Event().update(start, end, txt)
    146              event.set_description(self._get_description(vevent))
    147              self.events.append(event)
    148          except:
    149              self.vevents_not_loaded.append((txt, start, end))
    150
    151 -    def _load_categories(self, vevent):
    152          if "categories" in vevent:
    153              categories_names = [cat.strip() for cat in vevent["categories"].split(",") if len(cat.strip()) > 0]
    154              for category_name in categories_names:
    155                  if category_name not in self.category_names:
    156                      self.categories.append(Category().update(
    157                          category_name,
    158                          self._get_random_color(),
    159                          None
    160                      ))
    161                      self.category_names.append(category_name)
    162
    163 -    def _get_random_color(self):
    164          return (random.randint(0, 255),
    165                  random.randint(0, 255),
    166                  random.randint(0, 255))
    167
    168 -    def _get_event_name(self, vevent):
    169          return self._get_first_value(vevent, ["summary", "description"], "")
    170
    171 -    def _get_description(self, vevent):
    172          if self.add_location_to_description:
    173              if "location" in vevent:
    174                  sep = ""
    175                  if "description" in vevent:
    176                      sep = "\n\n"
    177                  return "%s%s%s: %s" % (self._get_value(vevent, "description", ""),
    178                                        sep,
    179                                        _("Location"),
    180                                        self._get_value(vevent, "location", ""))
    181              else:
    182                  return self._get_value(vevent, "description", None)
    183          else:
    184              return self._get_value(vevent, "description", None)
    185
    186 -    def _get_first_value(self, element, key_list, not_found_value=None):
    187          for key in key_list:
    188              if key in element:
    189                  return element[key]
    190          return not_found_value
    191
    192 -    def _get_value(self, element, key, not_found_value=None):
    193          if key in element:
    194              return element.decoded(key)
    195          return not_found_value
    196
    197 -    def _extract_start_end(self, vevent):
    198          start = self._convert_to_datetime(vevent.decoded("dtstart"))
    199          if "dtend" in vevent:
    200              end = self._convert_to_datetime(vevent.decoded("dtend"))
    201          elif "duration" in vevent:
    202              end = start + self._convert_to_timedelta(vevent.decoded("duration"))
    203          else:
    204              end = self._convert_to_datetime(vevent.decoded("dtstart"))
    205          return (start, end)
    206
    207 -    def _convert_to_timedelta(self, t):
    208          if isinstance(t, timedelta):
    209              return GregorianDelta(t.seconds)
    210          else:
    211              return GregorianDelta(0)
    212
    213 -    def _convert_to_datetime(self, d):
    214          if isinstance(d, datetime):
    215              return GregorianDateTime(d.year, d.month, d.day, d.hour, d.minute, d.second).to_time()
    216          elif isinstance(d, date):
    217              return GregorianDateTime.from_ymd(d.year, d.month, d.day).to_time()
    218          else:
    219              raise TimelineIOError("Unknown date.")
    220
    221 -    def _get_first_subelement(self, parent, subelement_name):
    222          subelements = self._get_subelements(parent, subelement_name)
    223          if len(subelements) > 0:
    224              return subelements[0]
    225
    226 -    def _get_subelements(self, parent, subelement_name):
    227          return parent.walk(subelement_name)
    228
    229 -    def _report_on_vevents_not_loaded(self):
    230          if len(self.vevents_not_loaded) > 0:
    231              message = ("Some events couldn't be loaded!\n" +
    232                         "The first 10 failing events are shown below.\n" +
    233                         self._format_vevents_not_loaded())
    234              display_warning_message(message)
    235
    236 -    def _format_vevents_not_loaded(self):
    237          return "\n".join(["Name='%s'  Start=%s  End=%s" % (txt,
    238                                            self._time_to_text(start),
    239                                            self._time_to_text(end)) for txt, start, end in self.vevents_not_loaded][:10])
    240
    241 -    def _time_to_text(self, time):
    242          d = "%d-%02d-%02d " % GregorianDateTime.from_time(time).to_date_tuple()
    243          t = "%02d.%02d.%02d" % GregorianDateTime.from_time(time).to_time_tuple()
    244          return d + t
    245
    246
    247 -def import_db_from_ics(path, options_dialog=None, options=None):
    248      global events
    249      events = []
    250      if options is None:
    251          options = [False, False, False]
    252      db = MemoryDB()
    253      db.set_readonly()
    254      if options_dialog is not None:
    255          dlg = options_dialog()
    256          dlg.ShowModal()
    257          options = (dlg.get_import_location(),
    258                     dlg.get_trigger_as_start_time(),
    259                     dlg.get_trigger_as_alarm())
    260          dlg.Destroy()
    261      IcsLoader().load(db, path, options)
    262      db.clear_transactions()
    263      return db
    264