4464: VCard import/export needs update to new surname and spec incompatibilities

svn: r16745
This commit is contained in:
Michiel Nauta 2011-03-02 16:35:23 +00:00
parent 28387648b7
commit 1794a5e500
5 changed files with 1521 additions and 195 deletions

View File

@ -0,0 +1,91 @@
<?xml version="1.0"?>
<!--
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$
-->
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:g="http://gramps-project.org/xml/1.4.0/">
<!--
Transform a Gramps XML file into "canonical form", that is strip the
timestamps in the change attributes, and order all elements for which
the id attribute is used. The idea is that "canonical Gramps XML" files
can be compared with eachother with the help of ordinary diff tools.
-->
<xsl:output method="xml"/>
<xsl:param name="replace_handles"/>
<xsl:key name="primary_obj" match="g:person|g:family|g:event|g:placeobj|g:source|g:repository|g:object|g:note|g:tag" use="@handle"/>
<xsl:template match="*|@*|text()">
<xsl:copy>
<xsl:apply-templates select="*|@*|text()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="@change">
</xsl:template>
<xsl:template match="g:researcher">
<xsl:copy/>
</xsl:template>
<xsl:template match="g:people|g:families|g:events|g:places|g:sources|g:repositories|g:objects|g:notes|g:tags">
<xsl:copy>
<xsl:apply-templates select="*">
<xsl:sort select="@id"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="@handle">
<xsl:choose>
<xsl:when test="$replace_handles='ID'">
<xsl:attribute name="handle">
<xsl:value-of select="../@id"/>
</xsl:attribute>
</xsl:when>
<xsl:when test="$replace_handles='strip'">
</xsl:when>
<xsl:otherwise>
<xsl:copy/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="@hlink">
<xsl:choose>
<xsl:when test="$replace_handles='ID'">
<xsl:attribute name="hlink">
<xsl:value-of select="key('primary_obj',.)/@id"/>
</xsl:attribute>
</xsl:when>
<xsl:when test="$replace_handles='strip'">
</xsl:when>
<xsl:otherwise>
<xsl:copy/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>

View File

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

View File

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

View File

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

View File

@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE database PUBLIC "-//GRAMPS//DTD GRAMPS XML %s//EN"
"http://gramps-project.org/xml/%s/grampsxml.dtd">
<database xmlns="http://gramps-project.org/xml/%s/">
<header>
<created date="%04d-%02d-%02d" version="%s"/>
<researcher/>
</header>""" % \
(GRAMPS_XML_VERSION, GRAMPS_XML_VERSION, GRAMPS_XML_VERSION,
date[0], date[1], date[2], VERSION)
expect_str = self.header + """<people><person handle="I0000" """ \
"""id="I0000"><gender>U</gender><name type="Birth Name">""" \
"""<surname>Lastname</surname></name></person></people></database>"""
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()