Source code for unit.dataimport.ics

# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
#
# This file is part of Timeline.
#
# Timeline is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Timeline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Timeline.  If not, see <http://www.gnu.org/licenses/>.


import wx

from timelinelib.calendar.gregorian.time import GregorianDelta
from timelinelib.canvas.data.exceptions import TimelineIOError
from timelinelib.dataimport.ics import import_db_from_ics
from timelinelib.test.cases.tmpdir import TmpDirTestCase
from timelinelib.test.utils import human_time_to_gregorian


ICS_CONTENT = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN

BEGIN:VEVENT
CATEGORIES:MEETING 1, MEETING 2
UID:uid1@example.com
DTSTAMP:19970714T170000Z
ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com
DTSTART:19970714T170000Z
DTEND:19970715T035959Z
SUMMARY:Bastille Day Party
DESCRIPTION:Steve and John to review newest proposal material
END:VEVENT

END:VCALENDAR
"""

ICS_WITH_TODO_CONTENT = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ABC Corporation//NONSGML My Product//EN

BEGIN:VTODO
DTSTAMP:19980130T134500Z
SEQUENCE:2
UID:uid4@example.com
DUE:19980415T235959
STATUS:NEEDS-ACTION
SUMMARY:Submit Income Taxes

BEGIN:VALARM
ACTION:AUDIO
TRIGGER:19980414T120000
ATTACH;FMTTYPE=audio/basic:http://example.com/pub/audio-
 files/ssbanner.aud
REPEAT:4
DURATION:PT1H
END:VALARM

END:VTODO

END:VCALENDAR
"""

ICS_WITH_TODO_CONTENT_NO_ALARM = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ABC Corporation//NONSGML My Product//EN

BEGIN:VTODO
DTSTAMP:19980130T134500Z
SEQUENCE:2
UID:uid4@example.com
DUE:19980415T235959
STATUS:NEEDS-ACTION
SUMMARY:Submit Income Taxes
END:VTODO

END:VCALENDAR
"""

ICS_WITH_TODO_CONTENT_NO_DUE_NO_ALARM = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ABC Corporation//NONSGML My Product//EN

BEGIN:VTODO
DTSTAMP:19980130T134500Z
SEQUENCE:2
UID:uid4@example.com
STATUS:NEEDS-ACTION
SUMMARY:Submit Income Taxes
END:VTODO

END:VCALENDAR
"""

ICS_WITH_TODO_CONTENT_NO_SUMMARY = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ABC Corporation//NONSGML My Product//EN

BEGIN:VTODO
DTSTAMP:19980130T134500Z
SEQUENCE:2
UID:uid4@example.com
DUE:19980415T235959
STATUS:NEEDS-ACTION
END:VTODO

END:VCALENDAR
"""

ICS_CONTENT_WITHOUT_DTEND = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN

BEGIN:VEVENT
CATEGORIES:MEETING 1, MEETING 2
UID:uid1@example.com
DTSTAMP:19970714T170000Z
ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com
DTSTART:19970714T170000Z
SUMMARY:Bastille Day Party
DESCRIPTION:Steve and John to review newest proposal material
END:VEVENT

END:VCALENDAR
"""

ICS_CONTENT_WITH_DURATION = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN

BEGIN:VEVENT
CATEGORIES:MEETING 1, MEETING 2
UID:uid1@example.com
DTSTAMP:19970714T170000Z
DURATION:PT1H
ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com
DTSTART:19970714T170000Z
SUMMARY:Bastille Day Party
DESCRIPTION:Steve and John to review newest proposal material
END:VEVENT

END:VCALENDAR
"""

INVALID_ICS_CONTENT = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN

BEGIN:VEVENT
CATEGORIES:MEETING1
UID:uid1@example.com
DTSTAMP:19970714T170000Z
ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com
DTSTART:19970714T170000Z
DTEND:19970715T035959Z
SUMMARY:Bastille Day Party


END:VCA
"""


[docs]class describe_import_ics(TmpDirTestCase):
[docs] def given_ics_file(self, name): self.ics_file_path = self.get_tmp_path("ics_file.txt") with open(self.ics_file_path, "w") as f: f.write(name)
[docs] def when_ics_file_imported(self, options=None): return import_db_from_ics(self.ics_file_path, options=options)
[docs]class describe_import_vevent_from_ics(describe_import_ics):
[docs] def test_can_import_events_from_ics_file(self): self.given_ics_file(ICS_CONTENT) db = self.when_ics_file_imported() event_names = [event.get_text() for event in db.get_all_events()] self.assertEqual(len(event_names), 1) self.assertEqual(event_names[0], "Bastille Day Party")
[docs] def test_can_import_todo_events_from_ics_file(self): self.given_ics_file(ICS_WITH_TODO_CONTENT) db = self.when_ics_file_imported(options=[False, True, False]) self.assertEqual(len(db.get_all_events()), 1) event = db.get_first_event() self.assertEqual(event.get_text(), "Submit Income Taxes") self.assertEqual(event.get_description(), None) self.assertEqual(event.get_time_period().start_time, human_time_to_gregorian("14 Apr 1998 12:00:00")) self.assertEqual(event.get_time_period().end_time, human_time_to_gregorian("15 Apr 1998 23:59:59"))
[docs] def test_can_import_events_from_ics_file_with_no_dtend(self): self.given_ics_file(ICS_CONTENT_WITHOUT_DTEND) db = self.when_ics_file_imported() event = db.get_all_events()[0] self.assertFalse(event.get_time_period().is_period())
[docs] def test_can_import_events_from_ics_file_with_duration(self): self.given_ics_file(ICS_CONTENT_WITH_DURATION) db = self.when_ics_file_imported() event = db.get_all_events()[0] self.assertEqual(event.get_time_period().delta(), GregorianDelta(60 * 60))
[docs] def test_can_import_categories_from_ics_file(self): self.given_ics_file(ICS_CONTENT) db = import_db_from_ics(self.ics_file_path) category_names = [category.get_name() for category in db.get_categories()] self.assertEqual(len(category_names), 2) self.assertEqual(category_names[0], "MEETING 1") self.assertEqual(category_names[1], "MEETING 2")
[docs] def test_invalid_file_raises_exception(self): self.assertRaises(TimelineIOError, import_db_from_ics, "...")
[docs] def test_invalid_file_content_raises_exception(self): self.given_ics_file(INVALID_ICS_CONTENT) self.assertRaises(TimelineIOError, import_db_from_ics, self.ics_file_path)
[docs]class describe_import_vtodo_from_ics(describe_import_ics):
[docs] def test_can_import_todo_events_from_ics_file(self): self.given_ics_file(ICS_WITH_TODO_CONTENT) db = self.when_ics_file_imported(options=[False, True, False]) self.assertEqual(len(db.get_all_events()), 1) event = db.get_first_event() self.assertEqual(event.get_text(), "Submit Income Taxes") self.assertEqual(event.get_description(), None) self.assertEqual(event.get_time_period().start_time, human_time_to_gregorian("14 Apr 1998 12:00:00")) self.assertEqual(event.get_time_period().end_time, human_time_to_gregorian("15 Apr 1998 23:59:59"))
[docs] def test_can_import_todo_events_without_alarm_from_ics_file(self): self.given_ics_file(ICS_WITH_TODO_CONTENT_NO_ALARM) db = self.when_ics_file_imported() self.assertEqual(len(db.get_all_events()), 1) event = db.get_first_event() self.assertEqual(event.get_text(), "Submit Income Taxes") self.assertEqual(event.get_description(), None) self.assertEqual(event.get_time_period().start_time, human_time_to_gregorian("15 Apr 1998 23:59:59")) self.assertEqual(event.get_time_period().end_time, human_time_to_gregorian("15 Apr 1998 23:59:59"))
[docs] def test_cant_import_todo_events_without_due_ics_file(self): self.given_ics_file(ICS_WITH_TODO_CONTENT_NO_DUE_NO_ALARM) db = self.when_ics_file_imported() self.assertEqual(len(db.get_all_events()), 0)
[docs] def test_can_import_todo_events_without_summary(self): self.given_ics_file(ICS_WITH_TODO_CONTENT_NO_SUMMARY) db = self.when_ics_file_imported() self.assertEqual(len(db.get_all_events()), 1) event = db.get_first_event() self.assertEqual(event.get_text(), "")
[docs] def setUp(self): describe_import_ics.setUp(self) self.app = self.get_wxapp() self.locale = wx.Locale(wx.LANGUAGE_DEFAULT)
[docs] def tearDown(self): self.destroy_wxapp(self.app) describe_import_ics.tearDown(self)