summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2015-03-28 20:31:30 +0100
committerThomas Bruederli <bruederli@kolabsys.com>2015-03-28 20:31:30 +0100
commit821e6de04a968f98363503f83705bf350a41a3bd (patch)
treea4a801140f0c1dd28c4d41bed1c2ef75058bee3e
parent2cca820836c530518474b45d27441b4b4be9d923 (diff)
downloadpykolab-821e6de04a968f98363503f83705bf350a41a3bd.tar.gz
Fully implement pykolab.xml.Contact.to_dict() + add unit tests for it (#4974)
-rw-r--r--pykolab/xml/__init__.py6
-rw-r--r--pykolab/xml/contact.py297
-rw-r--r--tests/unit/test-019-contact.py351
3 files changed, 649 insertions, 5 deletions
diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py
index 20e4763..99a269b 100644
--- a/pykolab/xml/__init__.py
+++ b/pykolab/xml/__init__.py
@@ -3,6 +3,9 @@ from attendee import InvalidAttendeeParticipantStatusError
from attendee import participant_status_label
from contact import Contact
+from contact import ContactIntegrityError
+from contact import contact_from_string
+from contact import contact_from_message
from contact_reference import ContactReference
from recurrence_rule import RecurrenceRule
@@ -46,6 +49,8 @@ __all__ = [
"todo_from_message",
"note_from_string",
"note_from_message",
+ "contact_from_string",
+ "contact_from_message",
"property_label",
"property_to_string",
"compute_diff",
@@ -58,6 +63,7 @@ errors = [
"InvalidAttendeeParticipantStatusError",
"TodoIntegrityError",
"NoteIntegrityError",
+ "ContactIntegrityError",
]
__all__.extend(errors)
diff --git a/pykolab/xml/contact.py b/pykolab/xml/contact.py
index 97987d9..2b08f9a 100644
--- a/pykolab/xml/contact.py
+++ b/pykolab/xml/contact.py
@@ -1,9 +1,111 @@
import kolabformat
+import datetime
+import pytz
+import base64
+
+from pykolab.xml import utils as xmlutils
+from pykolab.xml.utils import ustr
+
+def contact_from_vcard(string):
+ # TODO: implement this
+ pass
+
+def contact_from_string(string):
+ _xml = kolabformat.readContact(string, False)
+ return Contact(_xml)
+
+def contact_from_message(message):
+ contact = None
+ if message.is_multipart():
+ for part in message.walk():
+ if part.get_content_type() == "application/vcard+xml":
+ payload = part.get_payload(decode=True)
+ contact = contact_from_string(payload)
+
+ # append attachment parts to Contact object
+ elif contact and part.has_key('Content-ID'):
+ contact._attachment_parts.append(part)
+
+ return contact
+
class Contact(kolabformat.Contact):
type = 'contact'
+ related_map = {
+ 'manager': kolabformat.Related.Manager,
+ 'assistant': kolabformat.Related.Assistant,
+ 'spouse': kolabformat.Related.Spouse,
+ 'children': kolabformat.Related.Child,
+ None: kolabformat.Related.NoRelation,
+ }
+
+ addresstype_map = {
+ 'home': kolabformat.Address.Home,
+ 'work': kolabformat.Address.Work,
+ }
+
+ phonetype_map = {
+ 'home': kolabformat.Telephone.Home,
+ 'work': kolabformat.Telephone.Work,
+ 'text': kolabformat.Telephone.Text,
+ 'main': kolabformat.Telephone.Voice,
+ 'homefax': kolabformat.Telephone.Fax,
+ 'workfax': kolabformat.Telephone.Fax,
+ 'mobile': kolabformat.Telephone.Cell,
+ 'video': kolabformat.Telephone.Video,
+ 'pager': kolabformat.Telephone.Pager,
+ 'car': kolabformat.Telephone.Car,
+ 'other': kolabformat.Telephone.Textphone,
+ }
+
+ emailtype_map = {
+ 'home': kolabformat.Email.Home,
+ 'work': kolabformat.Email.Work,
+ 'other': kolabformat.Email.Work,
+ }
+
+ urltype_map = {
+ 'homepage': kolabformat.Url.NoType,
+ 'blog': kolabformat.Url.Blog,
+ }
+
+ keytype_map = {
+ 'pgp': kolabformat.Key.PGP,
+ 'pkcs7': kolabformat.Key.PKCS7_MIME,
+ None: kolabformat.Key.Invalid,
+ }
+
+ gender_map = {
+ 'female': kolabformat.Contact.Female,
+ 'male': kolabformat.Contact.Male,
+ None: kolabformat.Contact.NotSet,
+ }
+
+ properties_map = {
+ 'uid': 'get_uid',
+ 'lastmodified-date': 'get_lastmodified',
+ 'fn': 'name',
+ 'nickname': 'nickNames',
+ 'title': 'titles',
+ 'email': 'emailAddresses',
+ 'tel': 'telephones',
+ 'url': 'urls',
+ 'im': 'imAddresses',
+ 'address': 'addresses',
+ 'note': 'note',
+ 'freebusyurl': 'freeBusyUrl',
+ 'birthday': 'bDay',
+ 'anniversary': 'anniversary',
+ 'categories': 'categories',
+ 'lang': 'languages',
+ 'gender': 'get_gender',
+ 'gpspos': 'gpsPos',
+ 'key': 'keys',
+ }
+
def __init__(self, *args, **kw):
+ self._attachment_parts = []
kolabformat.Contact.__init__(self, *args, **kw)
def get_uid(self):
@@ -14,6 +116,16 @@ class Contact(kolabformat.Contact):
self.__str__()
return kolabformat.getSerializedUID()
+ def get_lastmodified(self):
+ try:
+ _datetime = self.lastModified()
+ if _datetime == None or not _datetime.isValid():
+ self.__str__()
+ except:
+ return datetime.datetime.now(pytz.utc)
+
+ return xmlutils.from_cdatetime(self.lastModified(), True)
+
def get_email(self, preferred=True):
if preferred:
return self.emailAddresses()[self.emailAddressPreferredIndex()]
@@ -39,11 +151,186 @@ class Contact(kolabformat.Contact):
self.setEmailAddresses(emails, preferred_email_index)
def set_name(self, name):
- self.setName(name)
+ self.setName(ustr(name))
+
+ def get_gender(self, translated=True):
+ _gender = self.gender()
+ if translated:
+ return self._translate_value(_gender, self.gender_map)
+ return _gender
+
+ def _translate_value(self, val, map):
+ name_map = dict([(v, k) for (k, v) in map.iteritems()])
+ return name_map[val] if name_map.has_key(val) else 'UNKNOWN'
+
+ def to_dict(self):
+ if not self.isValid():
+ return None
+
+ data = self._names2dict(self.nameComponents())
+
+ for p, getter in self.properties_map.iteritems():
+ val = None
+ if hasattr(self, getter):
+ val = getattr(self, getter)()
+ if isinstance(val, kolabformat.cDateTime):
+ val = xmlutils.from_cdatetime(val, True)
+ elif isinstance(val, kolabformat.vectori):
+ val = [int(x) for x in val]
+ elif isinstance(val, kolabformat.vectors):
+ val = [str(x) for x in val]
+ elif isinstance(val, kolabformat.vectortelephone):
+ val = [self._struct2dict(x, 'number', self.phonetype_map) for x in val]
+ elif isinstance(val, kolabformat.vectoremail):
+ val = [self._struct2dict(x, 'address', self.emailtype_map) for x in val]
+ elif isinstance(val, kolabformat.vectorurl):
+ val = [self._struct2dict(x, 'url', self.urltype_map) for x in val]
+ elif isinstance(val, kolabformat.vectorkey):
+ val = [self._struct2dict(x, 'key', self.keytype_map) for x in val]
+ elif isinstance(val, kolabformat.vectoraddress):
+ val = [self._address2dict(x) for x in val]
+ elif isinstance(val, kolabformat.vectorgeo):
+ val = [[x.latitude, x.longitude] for x in val]
+
+ if val is not None:
+ data[p] = val
+
+ affiliations = self.affiliations()
+ if len(affiliations) > 0:
+ _affiliation = self._affiliation2dict(affiliations[0])
+ if _affiliation.has_key('address'):
+ data['address'].extend(_affiliation['address'])
+ _affiliation.pop('address', None)
+ data.update(_affiliation)
+
+ data.update(self._relateds2dict(self.relateds()))
+
+ if self.photoMimetype():
+ data['photo'] = dict(mimetype=self.photoMimetype(), base64=base64.b64encode(self.photo()))
+ elif self.photo():
+ data['photo'] = dict(uri=self.photo())
+
+ return data
- def to_ditc(self):
- # TODO: implement this
- return dict(name=self.name())
+ def _names2dict(self, namecomp):
+ names_map = {
+ 'surname': 'surnames',
+ 'given': 'given',
+ 'additional': 'additional',
+ 'prefix': 'prefixes',
+ 'suffix': 'suffixes',
+ }
+
+ data = dict()
+
+ for p, getter in names_map.iteritems():
+ val = None
+ if hasattr(namecomp, getter):
+ val = getattr(namecomp, getter)()
+ if isinstance(val, kolabformat.vectors):
+ val = [str(x) for x in val][0] if len(val) > 0 else None
+ if val is not None:
+ data[p] = val
+
+ return data
+
+ def _affiliation2dict(self, affiliation):
+ props_map = {
+ 'organization': 'organisation',
+ 'department': 'organisationalUnits',
+ 'role': 'roles',
+ }
+
+ data = dict()
+
+ for p, getter in props_map.iteritems():
+ val = None
+ if hasattr(affiliation, getter):
+ val = getattr(affiliation, getter)()
+ if isinstance(val, kolabformat.vectors):
+ val = [str(x) for x in val][0] if len(val) > 0 else None
+ if val is not None:
+ data[p] = val
+
+ data.update(self._relateds2dict(affiliation.relateds(), True))
+
+ addresses = affiliation.addresses()
+ if len(addresses):
+ data['address'] = [self._address2dict(adr, 'office') for adr in addresses]
+
+ return data
+
+ def _address2dict(self, adr, adrtype=None):
+ props_map = {
+ 'label': 'label',
+ 'street': 'street',
+ 'locality': 'locality',
+ 'region': 'region',
+ 'code': 'code',
+ 'country': 'country',
+ }
+ addresstype_map = dict([(v, k) for (k, v) in self.addresstype_map.iteritems()])
+
+ data = dict()
+
+ if adrtype is None:
+ adrtype = addresstype_map.get(adr.types(), None)
+
+ if adrtype is not None:
+ data['type'] = adrtype
+
+ for p, getter in props_map.iteritems():
+ val = None
+ if hasattr(adr, getter):
+ val = getattr(adr, getter)()
+ if isinstance(val, kolabformat.vectors):
+ val = [str(x) for x in val][0] if len(val) > 0 else None
+ if val is not None:
+ data[p] = val
+
+ return data
+
+ def _relateds2dict(self, relateds, aslist=True):
+ data = dict()
+
+ related_map = dict([(v, k) for (k, v) in self.related_map.iteritems()])
+ for rel in relateds:
+ reltype = related_map.get(rel.relationTypes(), None)
+ val = rel.uri() if rel.type() == kolabformat.Related.Uid else rel.text()
+ if reltype and val is not None:
+ if aslist:
+ if not data.has_key(reltype):
+ data[reltype] = []
+ data[reltype].append(val)
+ else:
+ data[reltype] = val
+
+ return data
+
+ def _struct2dict(self, struct, propname, map):
+ type_map = dict([(v, k) for (k, v) in map.iteritems()])
+ result = dict()
+
+ if hasattr(struct, 'types'):
+ result['type'] = type_map.get(struct.types(), None)
+ elif hasattr(struct, 'type'):
+ result['type'] = type_map.get(struct.type(), None)
+
+ if hasattr(struct, propname):
+ result[propname] = getattr(struct, propname)()
+
+ return result
def __str__(self):
- return kolabformat.writeContact(self)
+ xml = kolabformat.writeContact(self)
+ error = kolabformat.error()
+
+ if error == None or not error:
+ return xml
+ else:
+ raise ContactIntegrityError, kolabformat.errorMessage()
+
+
+class ContactIntegrityError(Exception):
+ def __init__(self, message):
+ Exception.__init__(self, message)
diff --git a/tests/unit/test-019-contact.py b/tests/unit/test-019-contact.py
new file mode 100644
index 0000000..d5366f6
--- /dev/null
+++ b/tests/unit/test-019-contact.py
@@ -0,0 +1,351 @@
+import datetime
+import pytz
+import unittest
+import kolabformat
+
+from pykolab.xml import Contact
+from pykolab.xml import ContactIntegrityError
+from pykolab.xml import contact_from_string
+from pykolab.xml import contact_from_message
+from email import message_from_string
+
+xml_contact = """<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
+<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0">
+ <vcard>
+ <uid>
+ <uri>urn:uuid:437656b2-d55e-11e4-a43b-080027b7afc5</uri>
+ </uid>
+ <x-kolab-version>
+ <text>3.1.0</text>
+ </x-kolab-version>
+ <prodid>
+ <text>Roundcube-libkolab-1.1 Libkolabxml-1.2</text>
+ </prodid>
+ <rev>
+ <timestamp>20150328T152236Z</timestamp>
+ </rev>
+ <kind>
+ <text>individual</text>
+ </kind>
+ <fn>
+ <text>Sample Dude</text>
+ </fn>
+ <n>
+ <surname>Dude</surname>
+ <given>Sample</given>
+ <additional>M.</additional>
+ <prefix>Dr.</prefix>
+ <suffix>Jr.</suffix>
+ </n>
+ <note>
+ <text>This is a sample contact for testing</text>
+ </note>
+ <title>
+ <text>Head of everything</text>
+ </title>
+ <group name="Affiliation">
+ <org>
+ <text>Kolab Inc.</text>
+ <text>R&amp;D Department</text>
+ </org>
+ <related>
+ <parameters>
+ <type>
+ <text>x-manager</text>
+ </type>
+ </parameters>
+ <text>Jane Manager</text>
+ </related>
+ <related>
+ <parameters>
+ <type>
+ <text>x-assistant</text>
+ </type>
+ </parameters>
+ <text>Mrs. Moneypenny</text>
+ </related>
+ <adr>
+ <parameters/>
+ <pobox/>
+ <ext/>
+ <street>O-steet</street>
+ <locality>San Francisco</locality>
+ <region>CA</region>
+ <code>55550</code>
+ <country>USA</country>
+ </adr>
+ </group>
+ <url>
+ <uri>www.kolab.org</uri>
+ </url>
+ <adr>
+ <parameters>
+ <type>
+ <text>home</text>
+ </type>
+ </parameters>
+ <pobox/>
+ <ext/>
+ <street>Homestreet 11</street>
+ <locality>Hometown</locality>
+ <region/>
+ <code>12345</code>
+ <country>Germany</country>
+ </adr>
+ <adr>
+ <parameters>
+ <type>
+ <text>work</text>
+ </type>
+ </parameters>
+ <pobox/>
+ <ext/>
+ <street>Workstreet 22</street>
+ <locality>Worktown</locality>
+ <region/>
+ <code>4567</code>
+ <country>Switzerland</country>
+ </adr>
+ <nickname>
+ <text>the dude</text>
+ </nickname>
+ <related>
+ <parameters>
+ <type>
+ <text>spouse</text>
+ </type>
+ </parameters>
+ <text>Leia</text>
+ </related>
+ <related>
+ <parameters>
+ <type>
+ <text>child</text>
+ </type>
+ </parameters>
+ <text>Jay</text>
+ </related>
+ <related>
+ <parameters>
+ <type>
+ <text>child</text>
+ </type>
+ </parameters>
+ <text>Bob</text>
+ </related>
+ <bday>
+ <date>20010401</date>
+ </bday>
+ <anniversary>
+ <date>20100705</date>
+ </anniversary>
+ <photo>
+ <uri>data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7</uri>
+ </photo>
+ <gender>
+ <sex>M</sex>
+ </gender>
+ <tel>
+ <parameters>
+ <type>
+ <text>home</text>
+ </type>
+ </parameters>
+ <text>+49-555-11223344</text>
+ </tel>
+ <tel>
+ <parameters>
+ <type>
+ <text>work</text>
+ </type>
+ </parameters>
+ <text>+49-555-44556677</text>
+ </tel>
+ <tel>
+ <parameters>
+ <type>
+ <text>cell</text>
+ </type>
+ </parameters>
+ <text>+41-777-55588899</text>
+ </tel>
+ <impp>
+ <uri>jabber:dude@kolab.org</uri>
+ </impp>
+ <email>
+ <parameters>
+ <type>
+ <text>home</text>
+ </type>
+ </parameters>
+ <text>home@kolab.org</text>
+ </email>
+ <email>
+ <parameters>
+ <type>
+ <text>work</text>
+ </type>
+ </parameters>
+ <text>work@kolab.org</text>
+ </email>
+ <key>
+ <uri>data:application/pgp-keys;base64,LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tDQpWZXJzaW9uOiBHbnVQRy9NYWNHUEcyIHYyLjAuMjINCg0KbVFHaUJFSVNOcUVSQkFDUnovb3J5L0JEY3pBWUFUR3JnTSt5WDgzV2pkaUVrNmZKNFFUekk2ZFZ1TkxTNy4uLg0KLS0tLS1FTkQgUEdQIFBVQkxJQyBLRVkgQkxPQ0stLS0tLQ==</uri>
+ </key>
+ </vcard>
+</vcards>
+"""
+
+contact_mime_message = """MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_4ff5155d75dc1328b7f5fe10ddce8d24"
+From: john.doe@example.org
+To: john.doe@example.org
+Date: Mon, 13 Apr 2015 15:26:44 +0200
+X-Kolab-Type: application/x-vnd.kolab.contact
+X-Kolab-Mime-Version: 3.0
+Subject: 05cfc56d-2bb3-46d1-ada4-5f5310337fb2
+User-Agent: Roundcube Webmail/1.2-git
+
+--=_4ff5155d75dc1328b7f5fe10ddce8d24
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=ISO-8859-1
+
+This is a Kolab Groupware object. To view this object you will need an emai=
+l client that understands the Kolab Groupware format. For a list of such em=
+ail clients please visit http://www.kolab.org/
+
+--=_4ff5155d75dc1328b7f5fe10ddce8d24
+Content-Transfer-Encoding: 8bit
+Content-Type: application/vcard+xml; charset=UTF-8;
+ name=kolab.xml
+Content-Disposition: attachment;
+ filename=kolab.xml;
+ size=1636
+
+<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
+<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0">
+ <vcard>
+ <uid>
+ <uri>urn:uuid:05cfc56d-2bb3-46d1-ada4-5f5310337fb2</uri>
+ </uid>
+ <x-kolab-version>
+ <text>3.1.0</text>
+ </x-kolab-version>
+ <prodid>
+ <text>Roundcube-libkolab-1.1 Libkolabxml-1.1</text>
+ </prodid>
+ <rev>
+ <timestamp>20150413T132644Z</timestamp>
+ </rev>
+ <kind>
+ <text>individual</text>
+ </kind>
+ <fn>
+ <text>User One</text>
+ </fn>
+ <n>
+ <surname>User One</surname>
+ <given>DAV</given>
+ </n>
+ <note>
+ <text>This is a Kolab contact</text>
+ </note>
+ <tel>
+ <parameters>
+ <type>
+ <text>home</text>
+ </type>
+ </parameters>
+ <text>+1555224488</text>
+ </tel>
+ <email>
+ <parameters>
+ <type>
+ <text>home</text>
+ </type>
+ </parameters>
+ <text>dav.user01@example.org</text>
+ </email>
+ <email>
+ <parameters>
+ <type>
+ <text>home</text>
+ </type>
+ </parameters>
+ <text>user.one@example.org</text>
+ </email>
+ </vcard>
+</vcards>
+
+--=_4ff5155d75dc1328b7f5fe10ddce8d24--
+"""
+
+class TestContactXML(unittest.TestCase):
+ contact = Contact()
+
+ def assertIsInstance(self, _value, _type):
+ if hasattr(unittest.TestCase, 'assertIsInstance'):
+ return unittest.TestCase.assertIsInstance(self, _value, _type)
+ else:
+ if (type(_value)) == _type:
+ return True
+ else:
+ raise AssertionError, "%s != %s" % (type(_value), _type)
+
+ def test_001_minimal(self):
+ self.contact.set_name("test")
+ self.assertEqual("test", self.contact.name())
+ self.assertIsInstance(self.contact.__str__(), str)
+
+ def test_002_full(self):
+ self.contact.set_name("test")
+ # TODO: add more setters and getter tests here
+
+ def test_010_load_from_xml(self):
+ contact = contact_from_string(xml_contact)
+ self.assertEqual(contact.get_uid(), '437656b2-d55e-11e4-a43b-080027b7afc5')
+ self.assertEqual(contact.name(), 'Sample Dude')
+
+ def test_011_load_from_message(self):
+ contact = contact_from_message(message_from_string(contact_mime_message))
+ self.assertEqual(contact.get_uid(), '05cfc56d-2bb3-46d1-ada4-5f5310337fb2')
+ self.assertEqual(contact.name(), 'User One')
+
+ def test_020_to_dict(self):
+ data = contact_from_string(xml_contact).to_dict()
+
+ self.assertIsInstance(data, dict)
+ self.assertIsInstance(data['lastmodified-date'], datetime.datetime)
+ self.assertEqual(data['uid'], '437656b2-d55e-11e4-a43b-080027b7afc5')
+ self.assertEqual(data['fn'], 'Sample Dude')
+ self.assertEqual(data['given'], 'Sample')
+ self.assertEqual(data['surname'], 'Dude')
+ self.assertEqual(data['prefix'], 'Dr.')
+ self.assertEqual(data['suffix'], 'Jr.')
+ self.assertIsInstance(data['birthday'], datetime.date)
+ self.assertIsInstance(data['anniversary'], datetime.date)
+ self.assertEqual(data['organization'], 'Kolab Inc.')
+ self.assertEqual(data['department'], 'R&D Department')
+ self.assertEqual(data['manager'], ['Jane Manager'])
+ self.assertEqual(data['note'], 'This is a sample contact for testing')
+ self.assertEqual(len(data['address']), 3)
+ self.assertEqual(data['address'][0]['type'], 'home')
+ self.assertEqual(data['address'][1]['type'], 'work')
+ self.assertEqual(data['address'][2]['type'], 'office')
+ self.assertEqual(len(data['tel']), 3)
+ self.assertEqual(data['tel'][0]['type'], 'home')
+ self.assertEqual(data['tel'][0]['number'], '+49-555-11223344')
+ self.assertEqual(data['tel'][1]['type'], 'work')
+ self.assertEqual(data['tel'][2]['type'], 'mobile')
+ self.assertEqual(len(data['email']), 2)
+ self.assertEqual(data['email'][0]['type'], 'home')
+ self.assertEqual(data['email'][0]['address'], 'home@kolab.org')
+ self.assertEqual(len(data['url']), 1)
+ self.assertEqual(len(data['key']), 1)
+ self.assertEqual(data['key'][0]['type'], 'pgp')
+ self.assertIsInstance(data['photo'], dict)
+ self.assertEqual(data['photo']['mimetype'], 'image/gif')
+
+
+if __name__ == '__main__':
+ unittest.main() \ No newline at end of file