# 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)