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