diff --git a/data/gramps_canonicalize.xsl b/data/gramps_canonicalize.xsl
new file mode 100644
index 000000000..274d39105
--- /dev/null
+++ b/data/gramps_canonicalize.xsl
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/plugins/export/ExportVCard.py b/src/plugins/export/ExportVCard.py
index c77535cfa..b06f5b09d 100644
--- a/src/plugins/export/ExportVCard.py
+++ b/src/plugins/export/ExportVCard.py
@@ -5,6 +5,7 @@
# Copyright (C) 2005-2008 Donald N. Allingham
# Copyright (C) 2008 Brian G. Matherly
# Copyright (C) 2010 Jakim Friant
+# Copyright (C) 2011 Michiel D. Nauta
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -22,7 +23,7 @@
#
# $Id$
-"Export Persons to vCard."
+"Export Persons to vCard (RFC 2426)."
#-------------------------------------------------------------------------
#
@@ -30,8 +31,7 @@
#
#-------------------------------------------------------------------------
import sys
-import os
-from gen.ggettext import gettext as _
+from textwrap import TextWrapper
#------------------------------------------------------------------------
#
@@ -46,17 +46,88 @@ log = logging.getLogger(".ExportVCard")
# GRAMPS modules
#
#-------------------------------------------------------------------------
+from gen.ggettext import gettext as _
from ExportOptions import WriterOptionBox
-from Filters import GenericFilter, Rules, build_filter_model
+import const
from gen.lib import Date
-import Errors
-from glade import Glade
+from gen.lib.urltype import UrlType
+from gen.lib.eventtype import EventType
+from gen.display.name import displayer as _nd
-class CardWriter(object):
- def __init__(self, database, filename, msg_callback, option_box=None, callback=None):
+
+#-------------------------------------------------------------------------
+#
+# ExportOpenFileContextManager class
+#
+#-------------------------------------------------------------------------
+class ExportOpenFileContextManager:
+ """Context manager to open a file or stdout for writing."""
+ def __init__(self, filename):
+ self.filename = filename
+ self.filehandle = None
+
+ def __enter__(self):
+ if self.filename == '-':
+ self.filehandle = sys.stdout
+ else:
+ self.filehandle = open(self.filename, 'w')
+ return self.filehandle
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if self.filehandle and self.filename != '-':
+ self.filehandle.close()
+ return False
+
+#-------------------------------------------------------------------------
+#
+# Support Functions
+#
+#-------------------------------------------------------------------------
+def exportData(database, filename, msg_callback, option_box=None, callback=None):
+ """Function called by Gramps to export data on persons in VCard format."""
+ cardw = VCardWriter(database, filename, option_box, callback)
+ try:
+ cardw.export_data()
+ except EnvironmentError, msg:
+ msg_callback(_("Could not create %s") % filename, str(msg))
+ return False
+ except:
+ # Export shouldn't bring Gramps down.
+ msg_callback(_("Could not create %s") % filename)
+ return False
+ return True
+
+#-------------------------------------------------------------------------
+#
+# VCardWriter class
+#
+#-------------------------------------------------------------------------
+class VCardWriter(object):
+ """Class to create a file with data in VCard format."""
+ LINELENGTH = 73 # unclear if the 75 chars of spec includes \r\n.
+ ESCAPE_CHAR = '\\'
+ TOBE_ESCAPED = ['\\', ',', ';'] # order is important
+ LINE_CONTINUATION = [' ', '\t']
+
+ @staticmethod
+ def esc(data):
+ """Escape the special chars of the VCard protocol."""
+ if type(data) == type('string') or type(data) == type(u'string'):
+ for char in VCardWriter.TOBE_ESCAPED:
+ data = data.replace(char, VCardWriter.ESCAPE_CHAR + char)
+ return data
+ elif type(data) == type([]):
+ return list(map(VCardWriter.esc, data))
+ elif type(data) == type(()):
+ return tuple(map(VCardWriter.esc, data))
+ else:
+ raise TypeError(_("VCard escaping is not implemented for "
+ "data type %s.") % str(type(data)))
+
+ def __init__(self, database, filename, option_box=None, callback=None):
self.db = database
self.filename = filename
- self.msg_callback = msg_callback
+ self.filehandle = None
self.option_box = option_box
self.callback = callback
if callable(self.callback): # callback is really callable
@@ -68,10 +139,20 @@ class CardWriter(object):
self.option_box.parse_options()
self.db = option_box.get_filtered_database(self.db)
+ self.txtwrp = TextWrapper(width=self.LINELENGTH,
+ expand_tabs=False,
+ replace_whitespace=False,
+ drop_whitespace=False,
+ subsequent_indent=self.LINE_CONTINUATION[0])
+ self.count = 0
+ self.total = 0
+
def update_empty(self):
+ """Progress can't be reported."""
pass
def update_real(self):
+ """Report progress."""
self.count += 1
newval = int(100*self.count/self.total)
if newval != self.oldval:
@@ -79,96 +160,180 @@ class CardWriter(object):
self.oldval = newval
def writeln(self, text):
- #self.g.write('%s\n' % (text.encode('iso-8859-1')))
- self.g.write('%s\n' % (text.encode(sys.getfilesystemencoding())))
+ """
+ Write a property of the VCard to file.
- def export_data(self, filename):
+ Can't cope with nested VCards, section 2.4.2 of RFC 2426.
+ """
+ sysencoding = sys.getfilesystemencoding()
+ self.filehandle.write('%s\r\n' % '\r\n'.join(
+ [line.encode(sysencoding) for line in self.txtwrp.wrap(text)]))
- self.dirname = os.path.dirname (filename)
- try:
- self.g = open(filename,"w")
- except IOError,msg:
- msg2 = _("Could not create %s") % filename
- self.msg_callback(msg2, str(msg))
- return False
- except:
- self.msg_callback(_("Could not create %s") % filename)
- return False
-
- self.count = 0
- self.oldval = 0
- self.total = len([x for x in self.db.iter_person_handles()])
- for key in self.db.iter_person_handles():
- self.write_person(key)
- self.update()
-
- self.g.close()
+ def export_data(self):
+ """Open the file and loop over everyone two write their VCards."""
+ with ExportOpenFileContextManager(self.filename) as self.filehandle:
+ if self.filehandle:
+ self.count = 0
+ self.oldval = 0
+ self.total = self.db.get_number_of_people()
+ for key in self.db.iter_person_handles():
+ self.write_person(key)
+ self.update()
return True
def write_person(self, person_handle):
+ """Create a VCard for the specified person."""
person = self.db.get_person_from_handle(person_handle)
if person:
- self.writeln("BEGIN:VCARD");
+ self.write_header()
prname = person.get_primary_name()
-
- self.writeln("FN:%s" % prname.get_regular_name())
- self.writeln("N:%s;%s;%s;%s;%s" %
- (prname.get_surname(),
- prname.get_first_name(),
- person.get_nick_name(),
- prname.get_surname_prefix(),
- prname.get_suffix()
- )
- )
- if prname.get_title():
- self.writeln("TITLE:%s" % prname.get_title())
-
- birth_ref = person.get_birth_ref()
- if birth_ref:
- birth = self.db.get_event_from_handle(birth_ref.ref)
- if birth:
- b_date = birth.get_date_object()
- mod = b_date.get_modifier()
- if (mod != Date.MOD_TEXTONLY and
- not b_date.is_empty() and
- not mod == Date.MOD_SPAN and
- not mod == Date.MOD_RANGE):
- (day, month, year, sl) = b_date.get_start_date()
- if day > 0 and month > 0 and year > 0:
- self.writeln("BDAY:%s-%02d-%02d" % (year, month,
- day))
+ self.write_formatted_name(prname)
+ self.write_name(prname)
+ self.write_sortstring(prname)
+ self.write_nicknames(person, prname)
+ self.write_birthdate(person)
+ self.write_addresses(person)
+ self.write_urls(person)
+ self.write_occupation(person)
+ self.write_footer()
- address_list = person.get_address_list()
- for address in address_list:
- postbox = ""
- ext = ""
- street = address.get_street()
- city = address.get_city()
- state = address.get_state()
- zip = address.get_postal_code()
- country = address.get_country()
- if street or city or state or zip or country:
- self.writeln("ADR:%s;%s;%s;%s;%s;%s;%s" %
- (postbox, ext, street, city,state, zip, country))
-
- phone = address.get_phone()
- if phone:
- self.writeln("TEL:%s" % phone)
-
- url_list = person.get_url_list()
- for url in url_list:
- href = url.get_path()
- if href:
- self.writeln("URL:%s" % href)
+ def write_header(self):
+ """Write the opening lines of a VCard."""
+ self.writeln("BEGIN:VCARD")
+ self.writeln("VERSION:3.0")
+ self.writeln("PRODID:-//Gramps//NONSGML %s %s//EN" %
+ (const.PROGRAM_NAME, const.VERSION))
- self.writeln("END:VCARD");
- self.writeln("");
+ def write_footer(self):
+ """Write the closing lines of a VCard."""
+ self.writeln("END:VCARD")
+ self.writeln("")
+
+ def write_formatted_name(self, prname):
+ """Write the compulsory FN property of VCard."""
+ regular_name = prname.get_regular_name().strip()
+ title = prname.get_title()
+ if title:
+ regular_name = "%s %s" % (title, regular_name)
+ self.writeln("FN:%s" % self.esc(regular_name))
+
+ def write_name(self, prname):
+ """Write the compulsory N property of a VCard."""
+ family_name = ''
+ given_name = ''
+ additional_names = ''
+ hon_prefix = ''
+ suffix = ''
+
+ primary_surname = prname.get_primary_surname()
+ surname_list = prname.get_surname_list()
+ if not surname_list[0].get_primary():
+ surname_list.remove(primary_surname)
+ surname_list.insert(0, primary_surname)
+ family_name = ','.join(self.esc([("%s %s %s" % (surname.get_prefix(),
+ surname.get_surname(), surname.get_connector())).strip()
+ for surname in surname_list]))
+
+ call_name = prname.get_call_name()
+ if call_name:
+ given_name = self.esc(call_name)
+ additional_name_list = prname.get_first_name().split()
+ if call_name in additional_name_list:
+ additional_name_list.remove(call_name)
+ additional_names = ','.join(self.esc(additional_name_list))
+ else:
+ name_list = prname.get_first_name().split()
+ if len(name_list) > 0:
+ given_name = self.esc(name_list[0])
+ if len(name_list) > 1:
+ additional_names = ','.join(self.esc(name_list[1:]))
+ # Alternate names are ignored because names just don't add up:
+ # if one name is Jean and an alternate is Paul then you can't
+ # conclude the Jean Paul is also an alternate name of that person.
+
+ # Assume all titles/suffixes that apply are present in primary name.
+ hon_prefix = ','.join(self.esc(prname.get_title().split()))
+ suffix = ','.join(self.esc(prname.get_suffix().split()))
+
+ self.writeln("N:%s;%s;%s;%s;%s" % (family_name, given_name,
+ additional_names, hon_prefix, suffix))
+
+ def write_sortstring(self, prname):
+ """Write the SORT-STRING property of a VCard."""
+ # TODO only add sort-string if needed
+ self.writeln("SORT-STRING:%s" % self.esc(_nd.sort_string(prname)))
+
+ def write_nicknames(self, person, prname):
+ """Write the NICKNAME property of a VCard."""
+ nicknames = [x.get_nick_name() for x in person.get_alternate_names()
+ if x.get_nick_name()]
+ if prname.get_nick_name():
+ nicknames.insert(0, prname.get_nick_name())
+ if len(nicknames) > 0:
+ self.writeln("NICKNAME:%s" % (','.join(self.esc(nicknames))))
+
+ def write_birthdate(self, person):
+ """Write the BDAY property of a VCard."""
+ birth_ref = person.get_birth_ref()
+ if birth_ref:
+ birth = self.db.get_event_from_handle(birth_ref.ref)
+ if birth:
+ b_date = birth.get_date_object()
+ mod = b_date.get_modifier()
+ if (mod != Date.MOD_TEXTONLY and
+ not b_date.is_empty() and
+ not mod == Date.MOD_SPAN and
+ not mod == Date.MOD_RANGE):
+ (day, month, year, slash) = b_date.get_start_date()
+ if day > 0 and month > 0 and year > 0:
+ self.writeln("BDAY:%s-%02d-%02d" % (year, month, day))
+
+ def write_addresses(self, person):
+ """Write ADR and TEL properties of a VCard."""
+ address_list = person.get_address_list()
+ for address in address_list:
+ postbox = ""
+ ext = ""
+ street = address.get_street()
+ city = address.get_city()
+ state = address.get_state()
+ zipcode = address.get_postal_code()
+ country = address.get_country()
+ if street or city or state or zipcode or country:
+ self.writeln("ADR:%s;%s;%s;%s;%s;%s;%s" % self.esc(
+ (postbox, ext, street, city, state, zipcode, country)))
+
+ phone = address.get_phone()
+ if phone:
+ self.writeln("TEL:%s" % phone)
+
+ def write_urls(self, person):
+ """Write URL and EMAIL properties of a VCard."""
+ url_list = person.get_url_list()
+ for url in url_list:
+ href = url.get_path()
+ if href:
+ if url.get_type() == UrlType(UrlType.EMAIL):
+ if href.startswith('mailto:'):
+ href = href[len('mailto:'):]
+ self.writeln("EMAIL:%s" % self.esc(href))
+ else:
+ self.writeln("URL:%s" % self.esc(href))
+
+ def write_occupation(self, person):
+ """
+ Write ROLE property of a VCard.
+
+ Use the most recent occupation event.
+ """
+ event_refs = person.get_primary_event_ref_list()
+ events = [event for event in
+ [self.db.get_event_from_handle(ref.ref) for ref in event_refs]
+ if event.get_type() == EventType(EventType.OCCUPATION)]
+ if len(events) > 0:
+ events.sort(cmp=lambda x, y: cmp(x.get_date_object(),
+ y.get_date_object()))
+ occupation = events[-1].get_description()
+ if occupation:
+ self.writeln("ROLE:%s" % occupation)
-#-------------------------------------------------------------------------
-#
-#
-#
-#-------------------------------------------------------------------------
-def exportData(database, filename, msg_callback, option_box=None, callback=None):
- cw = CardWriter(database, filename, msg_callback, option_box, callback)
- return cw.export_data(filename)
diff --git a/src/plugins/export/test/exportVCard_test.py b/src/plugins/export/test/exportVCard_test.py
new file mode 100644
index 000000000..caa198aa1
--- /dev/null
+++ b/src/plugins/export/test/exportVCard_test.py
@@ -0,0 +1,199 @@
+#
+# Gramps - a GTK+/GNOME based genealogy program
+#
+# Copyright (C) 2011 Michiel D. Nauta
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# $Id$
+
+"""
+Unittest for export to VCard
+
+To be called from src directory.
+"""
+
+# Uses vcf for input, would be better to use Gramps-XML, but import of
+# Gramps-XML via stdin is hard.
+
+import unittest
+import sys
+import os
+sys.path.append(os.curdir)
+sys.path.append(os.path.join(os.curdir, 'plugins', 'export'))
+import subprocess
+
+from const import VERSION
+import Errors
+import ExportVCard
+
+class VCardCheck(unittest.TestCase):
+ def setUp(self):
+ self.expect = ["BEGIN:VCARD", "VERSION:3.0", "PRODID:-//Gramps//NONSGML Gramps %s//EN" % VERSION, "FN:Lastname", "N:Lastname;;;;", "SORT-STRING:" + "Lastname".ljust(55), "END:VCARD"]
+ self.input_list = ["BEGIN:VCARD", "VERSION:3.0", "FN:Lastname", "N:Lastname;;;;", "END:VCARD"]
+
+ def do_test(self, input_str, expect_str, debug=False):
+ process = subprocess.Popen('python gramps.py -i - -f vcf -e - -f vcf',
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=True)
+ result_str, err_str = process.communicate(input_str)
+ if err_str:
+ print err_str
+ if debug:
+ print result_str
+ print expect_str
+ self.assertEqual(result_str, expect_str)
+
+ def test_base(self):
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ def test_esc_string_none(self):
+ self.assertEqual(ExportVCard.VCardWriter.esc("nothing"), "nothing")
+
+ def test_esc_string_all(self):
+ self.assertEqual(ExportVCard.VCardWriter.esc("backslash\\_comma,_semicolon;"),
+ "backslash\\\\_comma\\,_semicolon\\;")
+
+ def test_esc_string_list(self):
+ self.assertEqual(ExportVCard.VCardWriter.esc(["comma,", "semicolon;"]),["comma\\,", "semicolon\\;"])
+
+ def test_esc_string_tuple(self):
+ self.assertEqual(ExportVCard.VCardWriter.esc(("comma,", "semicolon;")),("comma\\,", "semicolon\\;"))
+
+ def test_esc_string_wrongtype(self):
+ self.assertRaises(TypeError, ExportVCard.VCardWriter.esc,
+ {"comma,":"semicolon;"})
+
+ def test_write_formatted_name_title(self):
+ self.input_list[3] = "N:Lastname;;;Sir.;"
+ self.expect[3] = "FN:Sir. Lastname"
+ self.expect[4] = "N:Lastname;;;Sir.;"
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ def test_write_name_multiple_surname(self):
+ self.input_list[3] = "N:van Oranje,Nassau;;;;"
+ self.expect[3] = "FN:van Oranje Nassau"
+ self.expect[4] = "N:van Oranje,Nassau;;;;"
+ self.expect[5] = "SORT-STRING:" + "Oranje".ljust(55)
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ def test_write_name_callname(self):
+ self.input_list[2] = "FN:A B C Lastname"
+ self.input_list[3] = "N:Lastname;B;A,C;;"
+ self.expect[3] = "FN:A B C Lastname"
+ self.expect[4] = "N:Lastname;B;A,C;;"
+ self.expect[5] = "SORT-STRING:" + "Lastname".ljust(25) + "A B C".ljust(30)
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ #def test_write_name_callname_in_addnames(self):
+ # impossible to test with VCF input, need XML
+
+ def test_write_name_no_callname(self):
+ self.input_list[2] = "FN:A B C Lastname"
+ self.input_list[3] = "N:Lastname;A;B,C;;"
+ self.expect[3] = "FN:A B C Lastname"
+ self.expect[4] = "N:Lastname;A;B,C;;"
+ self.expect[5] = "SORT-STRING:" + "Lastname".ljust(25) + "A B C".ljust(30)
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ def test_write_name_no_additional_names(self):
+ self.input_list[2] = "FN:A Lastname"
+ self.input_list[3] = "N:Lastname;A;;;"
+ self.expect[3] = "FN:A Lastname"
+ self.expect[4] = "N:Lastname;A;;;"
+ self.expect[5] = "SORT-STRING:" + "Lastname".ljust(25) + "A".ljust(30)
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ def test_write_name_honprefix(self):
+ self.input_list[3] = "N:Lastname;;;Sir;"
+ self.expect[3] = "FN:Sir Lastname"
+ self.expect[4] = "N:Lastname;;;Sir;"
+ self.expect[5] = "SORT-STRING:" + "Lastname".ljust(55)
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ def test_write_name_honsuffix(self):
+ self.input_list[3] = "N:Lastname;;;;Jr."
+ self.expect[3] = "FN:Lastname\, Jr."
+ self.expect[4] = "N:Lastname;;;;Jr."
+ self.expect[5] = "SORT-STRING:" + "Lastname".ljust(55)+ "Jr."
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+
+ def test_nicknames_regular(self):
+ self.input_list.insert(4, "NICKNAME:Nick,N.")
+ self.expect.insert(6, "NICKNAME:Nick,N.")
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ #def test_nicknames_primary_nick(self)
+ # impossible to test with VCF input, need XML
+
+ def test_write_birthdate_regular(self):
+ self.input_list.insert(4, "BDAY:2001-02-28")
+ self.expect.insert(6, "BDAY:2001-02-28")
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ #def test_write_birthdate_empty(self):
+ #def test_write_birhtdate_textonly(self):
+ #def test_write_birthdate_span(self):
+ #def test_write_birthdate_range(self):
+ # impossible to test with VCF input, need XML
+
+ def test_write_addresses_regular(self):
+ self.input_list.insert(4, "ADR:pobox;bis;street;place;province;zip;country")
+ self.expect.insert(6, "ADR:;;pobox bis street;place;province;zip;country")
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ def test_write_addresses_phone(self):
+ self.input_list.insert(4, "TEL:01234-56789")
+ self.expect.insert(6, "TEL:01234-56789")
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ def test_write_urls_email(self):
+ self.input_list.insert(4, "EMAIL:me@example.com")
+ self.expect.insert(6, "EMAIL:me@example.com")
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ #def test_write_urls_emial_mailto(self):
+ # impossible to test with VCF input, need XML
+
+ def test_write_urls_url(self):
+ self.input_list.insert(4, "URL:http://www.example.org")
+ self.expect.insert(6, "URL:http://www.example.org")
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ def test_write_occupation_regular(self):
+ self.input_list.insert(4, "ROLE:carpenter")
+ self.expect.insert(6, "ROLE:carpenter")
+ self.do_test("\r\n".join(self.input_list),
+ "\r\n".join(self.expect) + '\r\n\r\n')
+
+ #def test_write_occupation_lastdate(self):
+ # impossible to test with VCF input, need XML
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/plugins/import/ImportVCard.py b/src/plugins/import/ImportVCard.py
index 0a9974a33..2ea351383 100644
--- a/src/plugins/import/ImportVCard.py
+++ b/src/plugins/import/ImportVCard.py
@@ -3,6 +3,7 @@
#
# Copyright (C) 2000-2006 Martin Hawlisch, Donald N. Allingham
# Copyright (C) 2008 Brian G. Matherly
+# Copyright (C) 2011 Michiel D. Nauta
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -21,13 +22,14 @@
# $Id$
-"Import from vCard"
+"Import from vCard (RFC 2426)"
#-------------------------------------------------------------------------
#
# standard python modules
#
#-------------------------------------------------------------------------
+import sys
import re
import time
from gen.ggettext import gettext as _
@@ -53,52 +55,198 @@ from QuestionDialog import ErrorDialog
#-------------------------------------------------------------------------
#
-#
+# ImportOpenFileContextManager class
#
#-------------------------------------------------------------------------
-def importData(database, filename, cb=None):
+class ImportOpenFileContextManager:
+ """Context manager to open a file or stdin for reading."""
+ def __init__(self, filename):
+ self.filename = filename
+ self.filehandle = None
- try:
- g = VCardParser(database,filename)
- except IOError,msg:
- ErrorDialog(_("%s could not be opened\n") % filename,str(msg))
- return
+ def __enter__(self):
+ if self.filename == '-':
+ # TODO how to add U to mode?
+ self.filehandle = sys.stdin
+ else:
+ self.filehandle = open(self.filename, "rU")
+ return self.filehandle
- try:
- status = g.parse_vCard_file()
- except IOError,msg:
- errmsg = _("%s could not be opened\n") % filename
- ErrorDialog(errmsg,str(msg))
- return
+ def __exit__(self, exc_type, exc_value, traceback):
+ if self.filename != '-':
+ self.filehandle.close()
+ return False
#-------------------------------------------------------------------------
#
+# Support Functions
#
+#-------------------------------------------------------------------------
+def importData(database, filename, cb_progress=None):
+ """Function called by Gramps to import data on persons in VCard format."""
+ parser = VCardParser(database, filename)
+ try:
+ return parser.parse_vCard_file()
+ except EnvironmentError, msg:
+ ErrorDialog(_("%s could not be opened\n") % filename, str(msg))
+ return
+ except Errors.GedcomError, msg:
+ ErrorDialog(_("%s could not be opened\n") % filename, str(msg))
+ return
+
+
+def splitof_nameprefix(name):
+ """
+ Return a (prefix, Surname) tuple by splitting on first uppercase char.
+
+ Shame on Python for not supporting [[:upper:]] in re!
+ """
+ look_for_capital = False
+ for i, char in enumerate(name):
+ if look_for_capital:
+ if char.isupper():
+ return (name[:i].rstrip(), name[i:])
+ else:
+ look_for_capital = False
+ if not char.isalpha():
+ look_for_capital = True
+ return ('', name)
+
+
+def fitin(prototype, receiver, element):
+ """
+ Return the index in string receiver at which element should be inserted
+ to match part of prototype.
+
+ Assume that the part of receiver that is not tested does match.
+ Don't split to work with lists because element may contain a space!
+ Example: fitin("Mr. Gaius Julius Caesar", "Gaius Caesar", "Julius") = 6
+
+ :param prototype: Partly to be matched by inserting element in receiver.
+ :type prototype: str
+ :param receiver: Space separated words that miss words to match prototype.
+ :type receiver: str
+ :param element: Words that need to be inserted; error if not in prototype.
+ :type element: str
+ :returns: Returns index where element fits in receiver, -1 if receiver
+ not in prototype, or throws IndexError if element at end receiver.
+ :rtype: int
+ """
+ receiver_idx = 0
+ receiver_chunks = receiver.split()
+ element_idx = prototype.index(element)
+ i = 0
+ idx = prototype.find(receiver_chunks[i])
+ while idx < element_idx:
+ if idx == -1:
+ return -1
+ receiver_idx += len(receiver_chunks[i]) + 1
+ i += 1
+ idx = prototype.find(receiver_chunks[i])
+ return receiver_idx
+
+#-------------------------------------------------------------------------
+#
+# VCardParser class
#
#-------------------------------------------------------------------------
class VCardParser(object):
- def __init__(self, dbase, file):
- self.db = dbase
- self.f = open(file,"rU")
- self.filename = file
+ """Class to read data in VCard format from a file."""
+ DATE_RE = re.compile(r'^(\d{4}-\d{1,2}-\d{1,2})|(?:(\d{4})-?(\d\d)-?(\d\d))')
+ GROUP_RE = re.compile(r'^(?:[-0-9A-Za-z]+\.)?(.+)$') # see RFC 2425 sec5.8.2
+ ESCAPE_CHAR = '\\'
+ TOBE_ESCAPED = ['\\', ',', ';'] # order is important
+ LINE_CONTINUATION = [' ', '\t']
+
+ @staticmethod
+ def name_value_split(data):
+ """Property group.name:value split is on first unquoted colon."""
+ colon_idx = data.find(':')
+ if colon_idx < 1:
+ return ()
+ quote_count = data.count('"', 0, colon_idx)
+ while quote_count % 2 == 1:
+ colon_idx = data.find(':', colon_idx + 1)
+ quote_count = data.count('"', 0, colon_idx)
+ group_name, value = data[:colon_idx], data[colon_idx+1:]
+ name_parts = VCardParser.GROUP_RE.match(group_name)
+ return (name_parts.group(1), value)
+
+ @staticmethod
+ def unesc(data):
+ """Remove VCard escape sequences."""
+ if type(data) == type('string'):
+ for char in reversed(VCardParser.TOBE_ESCAPED):
+ data = data.replace(VCardParser.ESCAPE_CHAR + char, char)
+ return data
+ elif type(data) == type([]):
+ return list(map(VCardParser.unesc, data))
+ else:
+ raise TypeError(_("VCard unescaping is not implemented for "
+ "data type %s.") % str(type(data)))
+
+ @staticmethod
+ def count_escapes(strng):
+ """Count the number of escape characters at the end of a string"""
+ count = 0
+ for char in reversed(strng):
+ if char != VCardParser.ESCAPE_CHAR:
+ return count
+ count += 1
+ return count
+
+ @staticmethod
+ def split_unescaped(strng, sep):
+ """Split on sep if sep is unescaped."""
+ strng_parts = strng.split(sep)
+ for i in reversed(xrange(len(strng_parts[:]))):
+ if VCardParser.count_escapes(strng_parts[i]) % 2 == 1:
+ # the sep was escaped so undo split
+ appendix = strng_parts.pop(i+1)
+ strng_parts[i] += sep + appendix
+ return strng_parts
+
+ def __init__(self, dbase, filename):
+ self.database = dbase
+ self.filename = filename
+ self.formatted_name = ''
+ self.name_parts = ''
+ self.filehandle = None
+ self.next_line = None
+ self.trans = None
+ self.version = None
+ self.person = None
def get_next_line(self):
- line = self.f.readline()
+ """
+ Read and return the line with the next property of the VCard.
+
+ Also if it spans multiple lines (RFC 2425 sec.5.8.1).
+ """
+ line = self.next_line
+ self.next_line = self.filehandle.readline()
+ while self.next_line and self.next_line[0] in self.LINE_CONTINUATION:
+ line = line.rstrip("\n")
+ #TODO perhaps next lines superflous because of rU open parameter?
+ if len(line) > 0 and line[-1] == "\r":
+ line = line[:-1]
+ line += self.next_line[1:]
+ self.next_line = self.filehandle.readline()
if line:
line = line.strip()
else:
line = None
return line
-
- def parse_vCard_file(self):
- with DbTxn(_("vCard import"), self.db, batch=True) as self.trans:
- self.db.disable_signals()
- t = time.time()
- self.person = None
- line_reg = re.compile('^([^:]+)+:(.*)$')
- try:
- while 1:
+ def parse_vCard_file(self):
+ """Read each line of the input file and act accordingly."""
+ tym = time.time()
+ self.person = None
+ with DbTxn(_("vCard import"), self.database, batch=True) as self.trans:
+ self.database.disable_signals()
+ with ImportOpenFileContextManager(self.filename) as self.filehandle:
+ self.next_line = self.filehandle.readline()
+ while True:
line = self.get_next_line()
if line is None:
break
@@ -107,109 +255,277 @@ class VCardParser(object):
if line.find(":") == -1:
continue
- line_parts = line_reg.match( line)
+ line_parts = self.name_value_split(line)
if not line_parts:
continue
- fields = line_parts.group(1).split(";")
+ # No check for escaped ; because only fields[0] is used.
+ fields = line_parts[0].split(";")
- #for field in line_parts.groups():
- # print " p "+field
- #for field in fields:
- # print " f "+field
-
- if fields[0] == "BEGIN":
+ property_name = fields[0].upper()
+ if property_name == "BEGIN":
self.next_person()
- elif fields[0] == "END":
+ elif property_name == "END":
self.finish_person()
- elif fields[0] == "FN":
- self.set_nick_name(fields, line_parts.group(2))
- elif fields[0] == "N":
- self.add_name(fields, line_parts.group(2))
- elif fields[0] == "ADR":
- self.add_address(fields, line_parts.group(2))
- elif fields[0] == "TEL":
- self.add_phone(fields, line_parts.group(2))
- elif fields[0] == "BDAY":
- self.add_birthday(fields, line_parts.group(2))
- elif fields[0] == "TITLE":
- self.add_title(fields, line_parts.group(2))
- elif fields[0] == "URL":
- self.add_url(fields, line_parts.group(2))
+ elif property_name == "VERSION":
+ self.check_version(fields, line_parts[1])
+ elif property_name == "FN":
+ self.add_formatted_name(fields, line_parts[1])
+ elif property_name == "N":
+ self.add_name_parts(fields, line_parts[1])
+ elif property_name == "NICKNAME":
+ self.add_nicknames(fields, line_parts[1])
+ elif property_name == "SORT-STRING":
+ self.add_sortas(fields, line_parts[1])
+ elif property_name == "ADR":
+ self.add_address(fields, line_parts[1])
+ elif property_name == "TEL":
+ self.add_phone(fields, line_parts[1])
+ elif property_name == "BDAY":
+ self.add_birthday(fields, line_parts[1])
+ elif property_name == "ROLE":
+ self.add_occupation(fields, line_parts[1])
+ elif property_name == "URL":
+ self.add_url(fields, line_parts[1])
+ elif property_name == "EMAIL":
+ self.add_email(fields, line_parts[1])
+ elif property_name == "PRODID":
+ # Included cause VCards made by Gramps have this prop.
+ pass
else:
- LOG.warn("Token >%s< unknown. line skipped: %s" % (fields[0],line))
- except Errors.GedcomError, err:
- self.errmsg(str(err))
-
- t = time.time() - t
- msg = ngettext('Import Complete: %d second','Import Complete: %d seconds', t ) % t
-
- self.db.enable_signals()
- self.db.request_rebuild()
-
+ LOG.warn("Token >%s< unknown. line skipped: %s" %
+ (fields[0],line))
+ self.database.enable_signals()
+
+ tym = time.time() - tym
+ msg = ngettext('Import Complete: %d second',
+ 'Import Complete: %d seconds', tym ) % tym
+ LOG.debug(msg)
+
+ self.database.request_rebuild()
return None
def finish_person(self):
+ """All info has been collected, write to database."""
if self.person is not None:
- self.db.add_person(self.person,self.trans)
+ if self.add_name():
+ self.database.add_person(self.person, self.trans)
self.person = None
def next_person(self):
+ """A VCard for another person is started."""
if self.person is not None:
- self.db.add_person(self.person,self.trans)
+ self.finish_person()
+ LOG.warn("BEGIN property not properly closed by END property, "
+ "Gramps can't cope with nested VCards.")
self.person = gen.lib.Person()
+ self.formatted_name = ''
+ self.name_parts = ''
- def set_nick_name(self, fields, data):
- if self.person is not None:
- attr = gen.lib.Attribute()
- attr.set_type(gen.lib.AttributeType.NICKNAME)
- attr.set_value(data)
- self.person.add_attribute(attr)
+ def check_version(self, fields, data):
+ """Check the version of the VCard, only version 3.0 is supported."""
+ self.version = data
+ if self.version != "3.0":
+ raise Errors.GedcomError(_("Import of VCards version %s is not "
+ "supported by Gramps.") % self.version)
+
+ def add_formatted_name(self, fields, data):
+ """Read the FN property of a VCard."""
+ if not self.formatted_name:
+ self.formatted_name = self.unesc(str(data)).strip()
+
+ def add_name_parts(self, fields, data):
+ """Read the N property of a VCard."""
+ if not self.name_parts:
+ self.name_parts = data.strip()
+
+ def add_name(self):
+ """
+ Add the name to the person.
+
+ Returns True on success, False on failure.
+ """
+ if not self.name_parts.strip():
+ LOG.warn("VCard is malformed missing the compulsory N property, "
+ "so there is no name; skip it.")
+ return False
+ if not self.formatted_name:
+ LOG.warn("VCard is malformed missing the compulsory FN property, "
+ "get name from N alone.")
+ data_fields = self.split_unescaped(self.name_parts, ';')
+ if len(data_fields) != 5:
+ LOG.warn("VCard is malformed wrong number of name components.")
- def add_name(self, fields, data):
- data_fields = data.split(";")
name = gen.lib.Name()
- name.set_type(gen.lib.NameType(gen.lib.NameType.AKA))
- name.set_surname(data_fields[0])
- name.set_first_name(data_fields[1])
- if data_fields[2]:
- name.set_first_name(data_fields[1]+" "+data_fields[2])
- name.set_surname_prefix(data_fields[3])
- name.set_suffix(data_fields[4])
+ name.set_type(gen.lib.NameType(gen.lib.NameType.BIRTH))
+
+ if data_fields[0].strip():
+ # assume first surname is primary
+ for surname_str in self.split_unescaped(data_fields[0], ','):
+ surname = gen.lib.Surname()
+ prefix, sname = splitof_nameprefix(self.unesc(surname_str))
+ surname.set_surname(sname.strip())
+ surname.set_prefix(prefix.strip())
+ name.add_surname(surname)
+
+ if len(data_fields) > 1 and data_fields[1].strip():
+ given_name = ' '.join(self.unesc(
+ self.split_unescaped(data_fields[1], ',')))
+ else:
+ given_name = ''
+ if len(data_fields) > 2 and data_fields[2].strip():
+ additional_names = ' '.join(self.unesc(
+ self.split_unescaped(data_fields[2], ',')))
+ else:
+ additional_names = ''
+ self.add_firstname(given_name.strip(), additional_names.strip(), name)
+
+ if len(data_fields) > 3 and data_fields[3].strip():
+ name.set_title(' '.join(self.unesc(
+ self.split_unescaped(data_fields[3], ','))))
+ if len(data_fields) > 4 and data_fields[4].strip():
+ name.set_suffix(' '.join(self.unesc(
+ self.split_unescaped(data_fields[4], ','))))
self.person.set_primary_name(name)
+ return True
- def add_title(self, fields, data):
- name = gen.lib.Name()
- name.set_type(gen.lib.NameType(gen.lib.NameType.AKA))
- name.set_title(data)
- self.person.add_alternate_name(name)
+ def add_firstname(self, given_name, additional_names, name):
+ """
+ Combine given_name and additional_names and add as firstname to name.
+
+ If possible try to add given_name as call name.
+ """
+ default = "%s %s" % (given_name, additional_names)
+ if self.formatted_name:
+ if given_name:
+ if additional_names:
+ given_name_pos = self.formatted_name.find(given_name)
+ if given_name_pos != -1:
+ add_names_pos = self.formatted_name.find(additional_names)
+ if add_names_pos != -1:
+ if given_name_pos <= add_names_pos:
+ firstname = default
+ # Uncertain if given name is used as callname
+ else:
+ firstname = "%s %s" % (additional_names,
+ given_name)
+ name.set_call_name(given_name)
+ else:
+ idx = fitin(self.formatted_name, additional_names,
+ given_name)
+ if idx == -1:
+ # Additional names is not in formatted name
+ firstname = default
+ else: # Given name in middle of additional names
+ firstname = "%s%s %s" % (additional_names[:idx],
+ given_name, additional_names[idx:])
+ name.set_call_name(given_name)
+ else: # Given name is not in formatted name
+ firstname = default
+ else: # There are no additional_names
+ firstname = given_name
+ else: # There is no given_name
+ firstname = additional_names
+ else: # There is no formatted name
+ firstname = default
+ name.set_first_name(firstname.strip())
+ return
+
+ def add_nicknames(self, fields, data):
+ """Read the NICKNAME property of a VCard."""
+ for nick in self.split_unescaped(data, ','):
+ nickname = nick.strip()
+ if nickname:
+ name = gen.lib.Name()
+ name.set_nick_name(self.unesc(nickname))
+ self.person.add_alternate_name(name)
+
+ def add_sortas(self, fields, data):
+ """Read the SORT-STRING property of a VCard."""
+ #TODO
+ pass
def add_address(self, fields, data):
- data_fields = data.split(";")
- addr = gen.lib.Address()
- addr.set_street(data_fields[0]+data_fields[1]+data_fields[2])
- addr.set_city(data_fields[3])
- addr.set_state(data_fields[4])
- addr.set_postal_code(data_fields[5])
- addr.set_country(data_fields[6])
- self.person.add_address(addr)
+ """Read the ADR property of a VCard."""
+ data_fields = self.split_unescaped(data, ';')
+ data_fields = [x.strip() for x in self.unesc(data_fields)]
+ if ''.join(data_fields):
+ addr = gen.lib.Address()
+ def add_street(strng):
+ if strng:
+ already = addr.get_street()
+ if already:
+ addr.set_street("%s %s" % (already, strng))
+ else:
+ addr.set_street(strng)
+ addr.add_street = add_street
+ set_func = ['add_street', 'add_street', 'add_street', 'set_city',
+ 'set_state', 'set_postal_code', 'set_country']
+ for i, data in enumerate(data_fields):
+ if i >= len(set_func):
+ break
+ getattr(addr, set_func[i])(data)
+ self.person.add_address(addr)
def add_phone(self, fields, data):
- addr = gen.lib.Address()
- addr.set_phone(data)
- self.person.add_address(addr)
+ """Read the TEL property of a VCard."""
+ tel = data.strip()
+ if tel:
+ addr = gen.lib.Address()
+ addr.set_phone(self.unesc(tel))
+ self.person.add_address(addr)
def add_birthday(self, fields, data):
- event = gen.lib.Event()
- event.set_type(gen.lib.EventType(gen.lib.EventType.BIRTH))
- self.db.add_event(event,self.trans)
-
- event_ref = gen.lib.EventRef()
- event_ref.set_reference_handle(event.get_handle())
- self.person.set_birth_ref(event_ref)
+ """Read the BDAY property of a VCard."""
+ date_str = data.strip()
+ date_match = VCardParser.DATE_RE.match(date_str)
+ if date_match:
+ if date_match.group(2):
+ date_str = "%s-%s-%s" % (date_match.group(2),
+ date_match.group(3), date_match.group(4))
+ else:
+ date_str = date_match.group(1)
+ event = gen.lib.Event()
+ event.set_type(gen.lib.EventType(gen.lib.EventType.BIRTH))
+ date = gen.lib.Date()
+ date.set_yr_mon_day(*[int(x, 10) for x in date_str.split('-')])
+ event.set_date_object(date)
+ self.database.add_event(event, self.trans)
+
+ event_ref = gen.lib.EventRef()
+ event_ref.set_reference_handle(event.get_handle())
+ self.person.set_birth_ref(event_ref)
+ else:
+ LOG.warn("Date %s not in appropriate format yyyy-mm-dd, "
+ "line skipped." % date_str)
+
+ def add_occupation(self, fields, data):
+ """Read the ROLE property of a VCard."""
+ occupation = data.strip()
+ if occupation:
+ event = gen.lib.Event()
+ event.set_type(gen.lib.EventType(gen.lib.EventType.OCCUPATION))
+ event.set_description(self.unesc(occupation))
+ self.database.add_event(event, self.trans)
+
+ event_ref = gen.lib.EventRef()
+ event_ref.set_reference_handle(event.get_handle())
+ self.person.add_event_ref(event_ref)
def add_url(self, fields, data):
- url = gen.lib.Url()
- url.set_path(data)
- self.person.add_url(url)
+ """Read the URL property of a VCard."""
+ href = data.strip()
+ if href:
+ url = gen.lib.Url()
+ url.set_path(self.unesc(href))
+ self.person.add_url(url)
+
+ def add_email(self, fields, data):
+ """Read the EMAIL property of a VCard."""
+ email = data.strip()
+ if email:
+ url = gen.lib.Url()
+ url.set_type(gen.lib.UrlType(gen.lib.UrlType.EMAIL))
+ url.set_path(self.unesc(email))
+ self.person.add_url(url)
diff --git a/src/plugins/import/test/importVCard_test.py b/src/plugins/import/test/importVCard_test.py
new file mode 100644
index 000000000..8e410bcf9
--- /dev/null
+++ b/src/plugins/import/test/importVCard_test.py
@@ -0,0 +1,555 @@
+#
+# Gramps - a GTK+/GNOME based genealogy program
+#
+# Copyright (C) 2011 Michiel D. Nauta
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# $Id$
+
+"""
+Unittest of import of VCard
+
+To be called from src directory.
+"""
+
+#TODO id number depend on user format.
+
+# in case of a failing test, add True as last parameter to do_test to see the output.
+
+from cStringIO import StringIO
+import time
+import unittest
+import sys
+import os
+sys.path.append(os.curdir)
+sys.path.append(os.path.join(os.curdir, 'plugins','import'))
+sys.path.append(os.path.join(os.curdir, 'plugins', 'lib'))
+import subprocess
+import libxml2
+import libxslt
+
+from libgrampsxml import GRAMPS_XML_VERSION
+
+from const import ROOT_DIR, VERSION
+import Errors
+import ImportVCard
+from ImportVCard import VCardParser
+
+class VCardCheck(unittest.TestCase):
+ def setUp(self):
+ date = time.localtime(time.time())
+ libxml2.keepBlanksDefault(0)
+ styledoc = libxml2.parseFile(os.path.join(ROOT_DIR,
+ "../data/gramps_canonicalize.xsl"))
+ self.style = libxslt.parseStylesheetDoc(styledoc)
+ self.header = """
+
+
+ """ % \
+ (GRAMPS_XML_VERSION, GRAMPS_XML_VERSION, GRAMPS_XML_VERSION,
+ date[0], date[1], date[2], VERSION)
+ expect_str = self.header + """U""" \
+ """Lastname"""
+ self.gramps = libxml2.readDoc(expect_str, '', None, libxml2.XML_PARSE_NONET)
+ self.person = self.gramps.getRootElement().firstElementChild().\
+ nextElementSibling().firstElementChild()
+ self.name = self.person.firstElementChild().nextElementSibling()
+ self.vcard = ["BEGIN:VCARD", "VERSION:3.0", "FN:Lastname",
+ "N:Lastname;;;;", "END:VCARD"]
+
+ def tearDown(self):
+ self.style.freeStylesheet()
+ #self.gramps.freeDoc() # makes is crash
+
+ def string2canonicalxml(self, input_str, buf):
+ if type(input_str) == type('string'):
+ doc = libxml2.readDoc(input_str, '', None, libxml2.XML_PARSE_NONET)
+ elif isinstance(input_str, libxml2.xmlDoc):
+ doc = input_str
+ else:
+ raise TypeError
+ param = {'replace_handles':"'ID'"}
+ canonical_doc = self.style.applyStylesheet(doc, param)
+ canonical_doc.saveFormatFileTo(buf, 'UTF-8', 1)
+ doc.freeDoc()
+ canonical_doc.freeDoc()
+ return
+
+ def do_test(self, input_str, expect_str, debug=False):
+ expect_canonical_strfile = StringIO()
+ buf = libxml2.createOutputBuffer(expect_canonical_strfile, 'UTF-8')
+ self.string2canonicalxml(expect_str, buf)
+
+ process = subprocess.Popen('python gramps.py -i - -f vcf -e - -f gramps',
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=True)
+ result_str, err_str = process.communicate(input_str)
+ if err_str:
+ print err_str
+ result_canonical_strfile = StringIO()
+ buf2 = libxml2.createOutputBuffer(result_canonical_strfile, 'UTF-8')
+ self.string2canonicalxml(result_str, buf2)
+
+ if debug:
+ print result_canonical_strfile.getvalue()
+ print expect_canonical_strfile.getvalue()
+ self.assertEqual(result_canonical_strfile.getvalue(),
+ expect_canonical_strfile.getvalue())
+ expect_canonical_strfile.close()
+ result_canonical_strfile.close()
+
+ def test_base(self):
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_splitof_nameprefix_noprefix(self):
+ self.assertEqual(ImportVCard.splitof_nameprefix("Noprefix"), ('',"Noprefix"))
+
+ def test_splitof_nameprefix_prefix(self):
+ self.assertEqual(ImportVCard.splitof_nameprefix("van Prefix"), ('van',"Prefix"))
+
+ def test_splitof_nameprefix_doublespace(self):
+ self.assertEqual(ImportVCard.splitof_nameprefix("van Prefix"), ('van',"Prefix"))
+
+ def test_fitin_regular(self):
+ self.assertEqual(ImportVCard.fitin("Mr. Gaius Julius Caesar",
+ "Gaius Caesar", "Julius"), 6)
+
+ def test_fitin_wrong_receiver(self):
+ self.assertEqual(ImportVCard.fitin("A B C", "A D", "B"), -1)
+
+ def test_fitin_wrong_element(self):
+ self.assertRaises(ValueError, ImportVCard.fitin, "A B C", "A C", "D")
+
+ def test_fitin_last_element(self):
+ self.assertRaises(IndexError, ImportVCard.fitin, "A B C", "A B", "C")
+
+ def test_name_value_split_begin_colon(self):
+ self.vcard.insert(4, ":email@example.com")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_name_value_split_quoted_colon(self):
+ self.vcard.insert(4, 'TEL;TYPE="FANCY:TYPE":01234-56789')
+ address = self.person.newChild(None, 'address', None)
+ address.newChild(None, 'phone', '01234-56789')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_name_value_split_grouped(self):
+ self.vcard[1] = "group." + self.vcard[1]
+ self.vcard[3] = "group." + self.vcard[3]
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_unesc_string(self):
+ self.assertEqual(VCardParser.unesc("TEL:012\\\\345\\,67\\;89"),
+ "TEL:012\\345,67;89")
+
+ def test_unesc_list(self):
+ self.assertEqual(VCardParser.unesc(["Last\,name", "First\;name"]),
+ ["Last,name", "First;name"])
+
+ def test_unesc_tuple(self):
+ self.assertRaises(TypeError, VCardParser.unesc, ("Last\,name", "First\;name"))
+
+ def test_count_escapes_null(self):
+ self.assertEqual(VCardParser.count_escapes("Lastname"), 0)
+
+ def test_count_escapes_one(self):
+ self.assertEqual(VCardParser.count_escapes("Lastname\\"), 1)
+
+ def test_count_escapes_two(self):
+ self.assertEqual(VCardParser.count_escapes(r"Lastname\\"), 2)
+
+ def test_split_unescaped_regular(self):
+ self.assertEqual(VCardParser.split_unescaped("Lastname;Firstname", ';'),
+ ["Lastname", "Firstname"])
+
+ def test_split_unescaped_one(self):
+ self.assertEqual(VCardParser.split_unescaped("Lastname\\;Firstname", ';'),
+ ["Lastname\\;Firstname",])
+
+ def test_split_unescaped_two(self):
+ self.assertEqual(VCardParser.split_unescaped("Lastname\\\\;Firstname", ';'),
+ ["Lastname\\\\", "Firstname",])
+
+ def test_split_unescaped_three(self):
+ self.assertEqual(VCardParser.split_unescaped(r"Lastname\\\;Firstname", ';'),
+ [r"Lastname\\\;Firstname",])
+
+ def test_get_next_line_continuation(self):
+ self.vcard.insert(4, "TEL:01234-\r\n 56789")
+ address = self.person.newChild(None, 'address', None)
+ address.newChild(None, 'phone', '01234-56789')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_get_next_line_cr(self):
+ self.vcard.insert(4, "TEL:01234-56789\r")
+ address = self.person.newChild(None, 'address', None)
+ address.newChild(None, 'phone', '01234-56789')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_get_next_line_strip(self):
+ self.vcard.insert(4, "TEL:01234-56789 ")
+ address = self.person.newChild(None, 'address', None)
+ address.newChild(None, 'phone', '01234-56789')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_get_next_line_none(self):
+ self.vcard.insert(4, "")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_parse_vCard_file_no_colon(self):
+ self.vcard.insert(4, "TEL;01234-56789")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_parse_vCard_file_lowercase(self):
+ self.vcard.insert(4, "tel:01234-56789")
+ address = self.person.newChild(None, 'address', None)
+ address.newChild(None, 'phone', '01234-56789')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_parse_vCard_unknown_property(self):
+ self.vcard.insert(4, "TELEPHONE:01234-56789")
+ self.gramps = self.gramps
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_next_person_no_end(self):
+ self.vcard[4] = "BEGIN:VCARD"
+ self.vcard.extend(["VERSION:3.0", "FN:Another", "N:Another;;;;", "END:VCARD"])
+ people = self.gramps.getRootElement().firstElementChild().nextElementSibling()
+ person = people.newChild(None, 'person', None)
+ person.newProp('handle', 'I0001')
+ person.newProp('id', 'I0001')
+ person.newChild(None, 'gender', 'U')
+ name = person.newChild(None, 'name', None)
+ name.newProp('type', 'Birth Name')
+ name.newChild(None, 'surname', 'Another')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_check_version(self):
+ self.vcard.extend(["BEGIN:VCARD", "VERSION:3.7", "FN:Another",
+ "N:Another;;;;", "END:VCARD"])
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_formatted_name_twice(self):
+ self.vcard[2] = "FN:Lastname B A"
+ self.vcard[3] = "N:Lastname;A;B;;"
+ self.vcard.insert(4, "FN:Lastname A B")
+ name = self.person.firstElementChild().nextElementSibling()
+ surname = name.firstElementChild()
+ firstname = name.newChild(None, "first", "B A")
+ callname = name.newChild(None, "call", "A")
+ callname.addNextSibling(surname)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_name_parts_twice(self):
+ self.vcard.insert(4, "N:Another;;;;")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_name_regular(self):
+ self.vcard[2] = "FN:Mr. Firstname Middlename Lastname Jr."
+ self.vcard[3] = "N:Lastname;Firstname;Middlename;Mr.;Jr."
+ name = self.person.firstElementChild().nextElementSibling()
+ surname = name.firstElementChild()
+ firstname = name.newChild(None, 'first', 'Firstname Middlename')
+ firstname.addNextSibling(surname)
+ name.newChild(None, 'suffix', 'Jr.')
+ name.newChild(None, 'title', 'Mr.')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_name_multisurname(self):
+ self.vcard[2] = "FN:Lastname Lastname2"
+ self.vcard[3] = "N:Lastname,Lastname2;;;;"
+ surname = self.name.newChild(None, 'surname', 'Lastname2')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_name_prefixsurname(self):
+ self.vcard[2] = "FN:van d'Alembert"
+ self.vcard[3] = "N:van d'Alembert;;;;"
+ surname = self.name.firstElementChild()
+ surname.setContent('Alembert')
+ surname.newProp('prefix', "van d'")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_name_only_surname(self):
+ self.vcard[3] = "N:Lastname"
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_name_upto_firstname(self):
+ self.vcard[2] = "FN:Firstname Lastname"
+ self.vcard[3] = "N:Lastname; Firstname;"
+ surname = self.name.firstElementChild()
+ first = self.name.newChild(None, 'first', 'Firstname')
+ first.addNextSibling(surname)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_name_empty(self):
+ self.vcard[2] = "FN:Lastname"
+ self.vcard[3] = "N: "
+ people = self.gramps.getRootElement().firstElementChild().nextElementSibling()
+ people.unlinkNode()
+ people.freeNode()
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_firstname_regular(self):
+ self.vcard[2] = "FN:A B Lastname"
+ self.vcard[3] = "N:Lastname;A;B;;"
+ surname = self.name.firstElementChild()
+ firstname = self.name.newChild(None, 'first', 'A B')
+ firstname.addNextSibling(surname)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_firstname_callname(self):
+ self.vcard[2] = "FN:A B Lastname"
+ self.vcard[3] = "N:Lastname;B;A;;"
+ surname = self.name.firstElementChild()
+ firstname = self.name.newChild(None, 'first', 'A B')
+ callname = self.name.newChild(None, 'call', 'B')
+ callname.addNextSibling(surname)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_firstname_incomplete_fn(self):
+ self.vcard[2] = "FN:A Lastname"
+ self.vcard[3] = "N:Lastname;A;B;;"
+ surname = self.name.firstElementChild()
+ firstname = self.name.newChild(None, 'first', 'A B')
+ firstname.addNextSibling(surname)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_firstname_middle(self):
+ self.vcard[2] = "FN:A B C Lastname"
+ self.vcard[3] = "N:Lastname;B;A C;;"
+ surname = self.name.firstElementChild()
+ firstname = self.name.newChild(None, 'first', 'A B C')
+ callname = self.name.newChild(None, 'call', 'B')
+ callname.addNextSibling(surname)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_firstname_fn_not_given(self):
+ self.vcard[2] = "FN:B Lastname"
+ self.vcard[3] = "N:Lastname;A;B;;"
+ surname = self.name.firstElementChild()
+ firstname = self.name.newChild(None, 'first', 'A B')
+ firstname.addNextSibling(surname)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_firstname_no_addnames(self):
+ self.vcard[2] = "FN:A Lastname"
+ self.vcard[3] = "N:Lastname;A;;;"
+ surname = self.name.firstElementChild()
+ firstname = self.name.newChild(None, 'first', 'A')
+ firstname.addNextSibling(surname)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_firstname_no_givenname(self):
+ self.vcard[2] = "FN:A Lastname"
+ self.vcard[3] = "N:Lastname;;A;;"
+ surname = self.name.firstElementChild()
+ firstname = self.name.newChild(None, 'first', 'A')
+ firstname.addNextSibling(surname)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_firstname_no_fn(self):
+ self.vcard[2] = "FN:"
+ self.vcard[3] = "N:Lastname;;A;;"
+ surname = self.name.firstElementChild()
+ firstname = self.name.newChild(None, 'first', 'A')
+ firstname.addNextSibling(surname)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_nicknames_single(self):
+ self.vcard.insert(4, "NICKNAME:Ton")
+ name = self.person.newChild(None, "name", None)
+ name.newProp("alt", "1")
+ name.newProp("type", "Birth Name")
+ name.newChild(None, "nick", "Ton")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_nicknames_empty(self):
+ self.vcard.insert(4, "NICKNAME:")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_nicknames_multiple(self):
+ self.vcard.insert(4, "NICKNAME:A,B\,C")
+ name = self.person.newChild(None, "name", None)
+ name.newProp("alt", "1")
+ name.newProp("type", "Birth Name")
+ name.newChild(None, "nick", "A")
+ name = self.person.newChild(None, "name", None)
+ name.newProp("alt", "1")
+ name.newProp("type", "Birth Name")
+ name.newChild(None, "nick", "B,C")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_address_all(self):
+ self.vcard.insert(4, "ADR:box 1;bis;Broadway 11; New York; New York; NY1234; USA")
+ address = self.person.newChild(None, 'address', None)
+ address.newChild(None, 'street', 'box 1 bis Broadway 11')
+ address.newChild(None, 'city', 'New York')
+ address.newChild(None, 'state', 'New York')
+ address.newChild(None, 'country', 'USA')
+ address.newChild(None, 'postal', 'NY1234')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_address_too_many(self):
+ self.vcard.insert(4, "ADR:;;Broadway 11; New\,York; ;; USA; Earth")
+ address = self.person.newChild(None, 'address', None)
+ address.newChild(None, 'street', 'Broadway 11')
+ address.newChild(None, 'city', 'New,York')
+ address.newChild(None, 'country', 'USA')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_address_too_few(self):
+ self.vcard.insert(4, "ADR:;;Broadway 11; New York")
+ address = self.person.newChild(None, 'address', None)
+ address.newChild(None, 'street', 'Broadway 11')
+ address.newChild(None, 'city', 'New York')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_address_empty(self):
+ self.vcard.insert(4, "ADR: ")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_phone_regular(self):
+ self.vcard.insert(4, "TEL:01234-56789")
+ address = self.person.newChild(None, 'address', None)
+ address.newChild(None, 'phone', '01234-56789')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_phone_empty(self):
+ self.vcard.insert(4, "TEL: ")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_birthday_regular(self):
+ self.vcard.insert(4, 'BDAY:2001-09-28')
+ eventref = self.person.newChild(None, 'eventref', None)
+ eventref.newProp('hlink', 'E0000')
+ eventref.newProp('role', 'Primary')
+ events = self.gramps.getRootElement().newChild(None, 'events', None)
+ event = events.newChild(None, 'event', None)
+ event.newProp('handle', 'E0000')
+ event.newProp('id', 'E0000')
+ event.newChild(None, 'type', 'Birth')
+ date = event.newChild(None, 'dateval', None)
+ date.newProp('val', '2001-09-28')
+ people = self.gramps.getRootElement().firstElementChild().nextElementSibling()
+ events.addNextSibling(people)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_birthday_datetime(self):
+ self.vcard.insert(4, 'BDAY:2001-09-28T09:23:47Z')
+ eventref = self.person.newChild(None, 'eventref', None)
+ eventref.newProp('hlink', 'E0000')
+ eventref.newProp('role', 'Primary')
+ events = self.gramps.getRootElement().newChild(None, 'events', None)
+ event = events.newChild(None, 'event', None)
+ event.newProp('handle', 'E0000')
+ event.newProp('id', 'E0000')
+ event.newChild(None, 'type', 'Birth')
+ date = event.newChild(None, 'dateval', None)
+ date.newProp('val', '2001-09-28')
+ people = self.gramps.getRootElement().firstElementChild().nextElementSibling()
+ events.addNextSibling(people)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_birthday_no_dash(self):
+ self.vcard.insert(4, 'BDAY:20010928')
+ eventref = self.person.newChild(None, 'eventref', None)
+ eventref.newProp('hlink', 'E0000')
+ eventref.newProp('role', 'Primary')
+ events = self.gramps.getRootElement().newChild(None, 'events', None)
+ event = events.newChild(None, 'event', None)
+ event.newProp('handle', 'E0000')
+ event.newProp('id', 'E0000')
+ event.newChild(None, 'type', 'Birth')
+ date = event.newChild(None, 'dateval', None)
+ date.newProp('val', '2001-09-28')
+ people = self.gramps.getRootElement().firstElementChild().nextElementSibling()
+ events.addNextSibling(people)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_birthday_one_dash(self):
+ self.vcard.insert(4, 'BDAY:2001-0928')
+ eventref = self.person.newChild(None, 'eventref', None)
+ eventref.newProp('hlink', 'E0000')
+ eventref.newProp('role', 'Primary')
+ events = self.gramps.getRootElement().newChild(None, 'events', None)
+ event = events.newChild(None, 'event', None)
+ event.newProp('handle', 'E0000')
+ event.newProp('id', 'E0000')
+ event.newChild(None, 'type', 'Birth')
+ date = event.newChild(None, 'dateval', None)
+ date.newProp('val', '2001-09-28')
+ people = self.gramps.getRootElement().firstElementChild().nextElementSibling()
+ events.addNextSibling(people)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_birthday_ddmmyyyy(self):
+ self.vcard.insert(4, "BDAY:28-09-2001")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ #def test_add_birthday_non_existant(self):
+ # # This test fails, I think gen.lib.date.set_yr_mon_day should raise
+ # # an error if a wrong date is entered.
+ # self.vcard.insert(4, 'BDAY:2001-13-28')
+ # self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_birthday_empty(self):
+ self.vcard.insert(4, "BDAY: ")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_occupation_regular(self):
+ self.vcard.insert(4, "ROLE:scarecrow")
+ eventref = self.person.newChild(None, 'eventref', None)
+ eventref.newProp('hlink', 'E0000')
+ eventref.newProp('role', 'Primary')
+ events = self.gramps.getRootElement().newChild(None, 'events', None)
+ event = events.newChild(None, 'event', None)
+ event.newProp('handle', 'E0000')
+ event.newProp('id', 'E0000')
+ event.newChild(None, 'type', 'Occupation')
+ event.newChild(None, 'description', 'scarecrow')
+ people = self.gramps.getRootElement().firstElementChild().nextElementSibling()
+ events.addNextSibling(people)
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_occupation_empty(self):
+ self.vcard.insert(4, "ROLE: ")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_url_regular(self):
+ self.vcard.insert(4, "URL:http://www.example.com")
+ url = self.person.newChild(None, 'url', None)
+ url.newProp('href', 'http://www.example.com')
+ url.newProp('type', 'Unknown')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_url_empty(self):
+ self.vcard.insert(4, "URL: ")
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+ def test_add_email(self):
+ self.vcard.insert(4, "EMAIL:me@example.org")
+ url = self.person.newChild(None, 'url', None)
+ url.newProp('href', 'me@example.org')
+ url.newProp('type', 'E-mail')
+ self.do_test("\r\n".join(self.vcard), self.gramps)
+
+
+if __name__ == "__main__":
+ unittest.main()