Wiki

Clone wiki

django-portablecontacts / Home

{{{
#!parserest

Portable Contacts
=================

The Portable Contacts specification is designed to make it easier for 
developers to give their users a secure way to access the address books and
friends lists they have built up all over the web. Specifically, it seeks
to create a common access pattern and contact schema that any site can 
provide, well-specified authentication and access rules, standard libraries 
that can work with any site, and absolutely minimal complexity, with the 
lightest possible toolchain requirements for developers.

Workflow Overview
-----------------

A Consumer wishing to access a user's data via Portable Contacts must start 
with an Initial Identifier for the Service Provider containing the user's 
data, usually provided by the user. In many cases, this may be the domain name 
of the Service Provider's web site, such as sample.site.org, but may be a more 
specific URL, such as the OpenID identifier of the user, if available. 
Consumers then perform Discovery on the Initial Identifier to determine where 
the Portable Contacts endpoint for this Service Provider resides. 
If successful, the Consumer may then attempt to request information from that 
endpoint. If the endpoint contains private data, the Service Provider will 
return an authorization challenge, and the Consumer must then guide the user 
through an appropriate authorization flow to obtain the credentials necessary 
to access this private data. Upon successful authorization, the Consumer may 
request data from the Portable Contacts endpoint using these authorization 
credentials. Whether accessing public or private data, Consumers may request 
a specific subset of the user's data using standard Query Parameters. 
Upon a successful request, the data is returned in the response, and the 
Consumer may then parse the response data and use it as desired. The following 
sections detail each of these steps.

Installation
------------

There are 3 basic steps in order to install this Django application, edit your
settings and:

    * Add ``portablecontacts`` to your ``INSTALLED_APPS``
    * Add ``portablecontacts.middlewares.PortableContactsMiddleware`` to your 
      ``MIDDLEWARE_CLASSES`` (there is no particular order for now)
    * Add a ``SERIALIZATION_MODULES`` setting like that::
    
        SERIALIZATION_MODULES = {
            "poco-xml"    : "portablecontacts.serializers.xml_serializer",
            "poco-json"   : "portablecontacts.serializers.json",
        }


That's it! Optionally, you can specify your own Contact model class with 
``PORTABLE_CONTACTS_MODEL`` setting, for instance::

    PORTABLE_CONTACTS_MODEL = 'yourapp.CustomContact'

Note that some fields are required by the specification. A complete example of
what can be done is available in portablecontacts_example folder.

Initialization
--------------

As a Django app, we need to create a user and contacts in order to test (as
described in Appendix A of specifications)::

    >>> from django.contrib.auth.models import User
    >>> david = User.objects.create_user(username='david',
    ...     email='david@example.com', password='toto')
    >>> from portablecontacts.models import Contact
    >>> minimal_contact = Contact.objects.create(userName='Minimal Contact',
    ...     user=david)
    >>> minimal_contact
    <Contact: Minimal Contact>
    >>> mork = Contact.objects.create(familyName='Hashimoto', 
    ...     givenName='Mork', # TODO: add more info
    ...     user=david)
    >>> mork
    <Contact: Mork Hashimoto>

Now let's add some information to this contact::

    >>> from portablecontacts.models import ContactEmail
    >>> from portablecontacts.constants import WORK, HOME, OTHER, MOBILE, AIM, \
    ...     THUMBNAIL
    >>> contact_email_work = ContactEmail.objects.create(type=WORK, 
    ...     value='mhashimoto-04@plaxo.com', primary=True, contact=mork)
    >>> contact_email_work
    <ContactEmail: mhashimoto-04@plaxo.com (work) for Mork Hashimoto>
    >>> contact_email_home = ContactEmail.objects.create(type=HOME, 
    ...     value='mhashimoto-04@plaxo.com', contact=mork)
    >>> contact_email_home
    <ContactEmail: mhashimoto-04@plaxo.com (home) for Mork Hashimoto>

    >>> from portablecontacts.models import ContactUrl
    >>> contact_url_work = ContactUrl.objects.create(type=WORK,
    ...     value='http://www.seeyellow.com', contact=mork)
    >>> contact_url_work
    <ContactUrl: http://www.seeyellow.com (work) for Mork Hashimoto>
    >>> contact_url_home = ContactUrl.objects.create(type=HOME,
    ...     value='http://www.angryalien.com', contact=mork)
    >>> contact_url_home
    <ContactUrl: http://www.angryalien.com (home) for Mork Hashimoto>
    
    >>> from portablecontacts.models import ContactPhoneNumber
    >>> contact_phone_work = ContactPhoneNumber.objects.create(type=WORK,
    ...     value='KLONDIKE5', contact=mork)
    >>> contact_phone_work
    <ContactPhoneNumber: KLONDIKE5 (work) for Mork Hashimoto>
    >>> contact_phone_mobile = ContactPhoneNumber.objects.create(type=MOBILE,
    ...     value='650-123-4567', contact=mork)
    >>> contact_phone_mobile
    <ContactPhoneNumber: 650-123-4567 (mobile) for Mork Hashimoto>
    
    >>> from portablecontacts.models import ContactIm
    >>> contact_im = ContactIm.objects.create(type=AIM,
    ...     value='plaxodev8', contact=mork)
    >>> contact_im
    <ContactIm: plaxodev8 (aim) for Mork Hashimoto>

    >>> from portablecontacts.models import ContactPhoto
    >>> contact_photo = ContactPhoto.objects.create(type=THUMBNAIL,
    ...     value='http://sample.site.org/photos/12345.jpg', contact=mork)
    >>> contact_photo
    <ContactPhoto: http://sample.site.org/photos/12345.jpg (thumbnail) for Mork Hashimoto>

    >>> from portablecontacts.models import ContactTag
    >>> contact_tag = ContactTag.objects.create(
    ...     value='plaxo guy', contact=mork)
    >>> contact_tag
    <ContactTag: plaxo guy for Mork Hashimoto>

    >>> from portablecontacts.models import ContactRelationship
    >>> contact_relationship = ContactRelationship.objects.create(
    ...     value='friend', contact=mork)
    >>> contact_relationship
    <ContactRelationship: friend for Mork Hashimoto>

    >>> from portablecontacts.models import ContactAddress
    >>> contact_address = ContactAddress.objects.create(type=HOME,
    ...     streetAddress='742 Evergreen Terrace\nSuite 123', 
    ...     locality='Springfield', region='VT', postalCode='12345',
    ...     country='USA', contact=mork)
    >>> contact_address
    <ContactAddress: 742 Evergreen Terrace
    Suite 123
    Springfield VT 12345 USA>
    
    >>> from portablecontacts.models import ContactOrganization
    >>> contact_organization = ContactOrganization.objects.create(
    ...     name='Burns Worldwide', title='Head Bee Guy', contact=mork)
    >>> contact_organization
    <ContactOrganization: Burns Worldwide (Head Bee Guy) for Mork Hashimoto>

    >>> from portablecontacts.models import ContactAccount
    >>> contact_account = ContactAccount.objects.create(
    ...     domain='plaxo.com', userid='2706', contact=mork)
    >>> contact_account
    <ContactAccount: 2706 (plaxo.com) for Mork Hashimoto>

We need a contact for self too::

    >>> self_contact = Contact.objects.create(preferredUsername='david',
    ...     familyName='Larlet', givenName='David',
    ...     user=david)
    >>> self_contact
    <Contact: David Larlet>


Discovery
---------

Portable Contacts API endpoint is discoverable from the domain root using 
XRDS-Simple (Hammer-Lahav, E., "XRDS-Simple 1.0," .) 
(previously known as YADIS). The API is identified by the Service Type 
http://portablecontacts.net/spec/1.0 and the corresponding URI is the Base URL 
for the API. The Base URL MUST NOT contain any query string, as additional 
path information and query string variables MAY be appended by Consumers as 
part of forming the request (as described in detail below)::

    >>> from django.test.client import Client
    >>> c = Client()
    >>> response = c.get("/", **{'HTTP_ACCEPT': 'application/xrds+xml'})
    >>> print response.content
    <XRDS xmlns="xri://$xrds">
      <XRD xmlns:simple="http://xrds-simple.net/core/1.0" 
       xmlns="xri://$XRD*($v*2.0)" version="2.0">
        <Type>xri://$xrds*simple</Type>
        <Service>
          <Type>http://portablecontacts.net/spec/1.0</Type>
          <URI>http://example.com/portablecontacts/</URI>
        </Service>
      </XRD>
    </XRDS>


Available Authorization Methods
-------------------------------

When accessing a Portable Contacts endpoint, if sufficient authorization 
credentials are not provided, the Service Provider SHOULD return a 401 
Unauthorized response, and SHOULD provide the available Authorization 
mechanisms available by including WWW-Authenticate headers in the response 
for each type of Authorization method supported (as defined in [RFC2616] 
(Fielding, R., "Hypertext Transfer Protocol -- HTTP/1.1," .), section 14.47. 
Consumers will then be able to recognize that the API is a protected resource 
and initiate the proper Authorization process needed to obtain the appropriate 
credentials::

    >>> c = Client()
    >>> response = c.get("/portablecontacts/")
    >>> response.status_code
    401
    >>> response._headers['www-authenticate']
    ('WWW-Authenticate', 'WWW-Authenticate: OAuth realm="example.com/portablecontacts/"')


Additional Path Information
---------------------------

A request using the Base URL alone MUST yield a result, assuming that adequate
authorization credentials are provided. In addition, Consumers MAY append
additional path information to the Base URL to request more specific
information. Service Providers MUST recognize the following additional path
information when appended to the Base URL, and MUST return the corresponding
data:

    * ``/@me/@all`` -- Return all contact info (equivalent to providing no 
      additional path info)
    * ``/@me/@all/{id}`` -- Only return contact info for the contact whose id 
      value is equal to the provided {id}, if such a contact exists. In this 
      case, the response format is the same as when requesting all contacts, 
      but any contacts not matching the requested ID MUST be filtered out of 
      the result list by the Service Provider
    * ``/@me/@self`` -- Return contact info for the owner of this information, 
      i.e. the user on whose behalf this request is being made. In this case, 
      the response format is the same as when requesting all contacts, but any 
      contacts not matching the requested ID MUST be filtered out of the 
      result list by the Service Provider.

      >>> c = Client()
      >>> c.login(username='david', password='toto')
      True
      >>> response = c.get("/portablecontacts/@me/@all/")
      >>> response.status_code
      200
      >>> print response.content
      {
       "itemsPerPage": 100, 
       "startIndex": 0, 
       "totalResults": 3, 
       "entry": [
        {
         "displayName": "Minimal Contact", 
         "id": 1
        }, 
        {
         "relationships": [
          "friend"
         ], 
         "organizations": [
          {
           "name": "Burns Worldwide", 
           "title": "Head Bee Guy"
          }
         ], 
         "phoneNumbers": [
          {
           "type": "work", 
           "value": "KLONDIKE5"
          }, 
          {
           "type": "mobile", 
           "value": "650-123-4567"
          }
         ], 
         "displayName": "Mork Hashimoto", 
         "name": {
          "givenName": "Mork", 
          "familyName": "Hashimoto"
         }, 
         "tags": [
          "plaxo guy"
         ], 
         "emails": [
          {
           "type": "work", 
           "primary": true, 
           "value": "mhashimoto-04@plaxo.com"
          }, 
          {
           "type": "home", 
           "value": "mhashimoto-04@plaxo.com"
          }
         ], 
         "photos": [
          {
           "type": "thumbnail", 
           "value": "http://sample.site.org/photos/12345.jpg"
          }
         ], 
         "ims": [
          {
           "type": "aim", 
           "value": "plaxodev8"
          }
         ], 
         "accounts": [
          {
           "domain": "plaxo.com", 
           "userid": "2706"
          }
         ], 
         "urls": [
          {
           "type": "work", 
           "value": "http://www.seeyellow.com"
          }, 
          {
           "type": "home", 
           "value": "http://www.angryalien.com"
          }
         ], 
         "id": 2, 
         "addresses": [
          {
           "locality": "Springfield", 
           "country": "USA", 
           "region": "VT", 
           "formatted": "742 Evergreen Terrace\nSuite 123\nSpringfield VT 12345 USA", 
           "streetAddress": "742 Evergreen Terrace\nSuite 123", 
           "postalCode": "12345", 
           "type": "home"
          }
         ]
        }, 
        {
         "preferredUsername": "david", 
         "displayName": "David Larlet", 
         "id": 3, 
         "name": {
          "givenName": "David", 
          "familyName": "Larlet"
         }
        }
       ]
      }
      >>> response = c.get("/portablecontacts/@me/@all/1/")
      >>> response.status_code
      200
      >>> print response.content
      {
       "entry": [
        {
         "displayName": "Minimal Contact", 
         "id": 1
        }
       ]
      }
      >>> response = c.get("/portablecontacts/@me/@self/")
      >>> response.status_code
      200
      >>> print response.content
      {
       "entry": {
        "preferredUsername": "david", 
        "displayName": "David Larlet", 
        "name": {
         "givenName": "David", 
         "familyName": "Larlet"
        }, 
        "id": 3
       }
      }

Presentations
-------------

fields, if non-empty, each contact returned SHALL contain only the fields 
explicitly requested. Service Provider MAY return a subset of the requested 
fields if they are not supported. This field is used for efficiency when the 
client only wishes to access a subset of the fields normally returned in 
results. Value is a comma separated list of top level field names 
(e.g. id,name,emails,photos) and defaults to an empty list which means it's up 
to the Provider which fields to return. Consumers may request all available 
fields to be returned by using the special value @all::

      >>> response = c.get("/portablecontacts/@me/@all/", {'fields': 'id,familyName'})
      >>> response.status_code
      200
      >>> print response.content
      {
       "itemsPerPage": 100, 
       "startIndex": 0, 
       "totalResults": 3, 
       "entry": [
        {
         "id": 1
        }, 
        {
         "id": 2, 
         "name": {
          "familyName": "Hashimoto"
         }
        }, 
        {
         "id": 3, 
         "name": {
          "familyName": "Larlet"
         }
        }
       ]
      }


format, specifies the format in which the response data is returned. 
Service Providers MUST support the values json for JSON (http://json.org) and 
xml for XML (http://www.w3.org/XML/) and MAY support additional formats if 
desired. The format defaults to json if no format is specified. The data 
structure returned is equivalent in both formats; the only difference is in 
the encoding of the data. Singular Fields are encoded as string key/value 
pairs in JSON and tags with text content in XML, e.g. "field": "value" and 
<field>value</field> respectively. Plural Fields and Plural Bundles are 
encoded as arrays in JSON and repeated tags in XML, e.g. "fields": 
[ "value1", "value2" ] and <fields>value1</field><fields<value2</field> 
respectively. Nodes with multiple sub-nodes are represented as objects in 
JSON and tags with sub-tags in XML, e.g. "field": { "subfield1": "value1", 
"subfield2": "value2" } and <field><subfield1><value1></subfield1><subfield2>
value2</subfield2></field> respectively::

      >>> response = c.get("/portablecontacts/@me/@all/", {'format': 'xml'})
      >>> response.status_code
      200
      >>> print response.content
      <?xml version="1.0" encoding="utf-8"?>
      <response>
       <startIndex>0</startIndex>
       <itemsPerPage>100</itemsPerPage>
       <totalResults>3</totalResults>
       <entry>
        <id>1</id>
        <displayName>Minimal Contact</displayName>
       </entry>
       <entry>
        <id>2</id>
        <displayName>Mork Hashimoto</displayName>
        <name>
         <familyName>Hashimoto</familyName>
         <givenName>Mork</givenName>
        </name>
        <emails>
         <value>mhashimoto-04@plaxo.com</value>
         <type>work</type>
         <primary>true</primary>
        </emails>
        <emails>
         <value>mhashimoto-04@plaxo.com</value>
         <type>home</type>
        </emails>
        <urls>
         <value>http://www.seeyellow.com</value>
         <type>work</type>
        </urls>
        <urls>
         <value>http://www.angryalien.com</value>
         <type>home</type>
        </urls>
        <phoneNumbers>
         <value>KLONDIKE5</value>
         <type>work</type>
        </phoneNumbers>
        <phoneNumbers>
         <value>650-123-4567</value>
         <type>mobile</type>
        </phoneNumbers>
        <ims>
         <value>plaxodev8</value>
         <type>aim</type>
        </ims>
        <photos>
         <value>http://sample.site.org/photos/12345.jpg</value>
         <type>thumbnail</type>
        </photos>
        <tags>
         <value>plaxo guy</value>
        </tags>
        <relationships>
         <value>friend</value>
        </relationships>
        <addresses>
         <formatted>742 Evergreen Terrace
      Suite 123
      Springfield VT 12345 USA</formatted>
         <type>home</type>
         <streetAddress>742 Evergreen Terrace
      Suite 123</streetAddress>
         <locality>Springfield</locality>
         <region>VT</region>
         <postalCode>12345</postalCode>
         <country>USA</country>
        </addresses>
        <organizations>
         <name>Burns Worldwide</name>
         <title>Head Bee Guy</title>
        </organizations>
        <accounts>
         <domain>plaxo.com</domain>
         <userid>2706</userid>
        </accounts>
       </entry>
       <entry>
        <id>3</id>
        <displayName>David Larlet</displayName>
        <name>
         <familyName>Larlet</familyName>
         <givenName>David</givenName>
        </name>
        <preferredUsername>david</preferredUsername>
       </entry>
      </response>

Sorting
-------

Sorting allows requests to specify the order in which contacts are returned.

sortBy, specifies the field name whose value SHALL be used to order the 
returned Contacts. The sort order is determine by the sortOrder parameter. 
If sortBy is a Singular Field, contacts are sorted according to that field's 
value; if it's a Plural Field, contacts are sorted by the Value 
(or Major Value, if it's a Complex Field) of the field marked with 
"primary": "true", if any, or else the first value in the list, if any, 
or else they are sorted last if the given contact has no data for the given 
field::

    >>> response = c.get("/portablecontacts/@me/@all/", {'sortBy': 'givenName'})
    >>> response.status_code
    200
    >>> print response.content
    {
     "itemsPerPage": 100, 
     "startIndex": 0, 
     "totalResults": 3, 
     "entry": [
      {
       "displayName": "Minimal Contact", 
       "id": 1
      }, 
      {
       "preferredUsername": "david", 
       "displayName": "David Larlet", 
       "id": 3, 
       "name": {
        "givenName": "David", 
        "familyName": "Larlet"
       }
      }, 
      {
       "relationships": [
        "friend"
       ], 
       "organizations": [
        {
         "name": "Burns Worldwide", 
         "title": "Head Bee Guy"
        }
       ], 
       "phoneNumbers": [
        {
         "type": "work", 
         "value": "KLONDIKE5"
        }, 
        {
         "type": "mobile", 
         "value": "650-123-4567"
        }
       ], 
       "displayName": "Mork Hashimoto", 
       "name": {
        "givenName": "Mork", 
        "familyName": "Hashimoto"
       }, 
       "tags": [
        "plaxo guy"
       ], 
       "emails": [
        {
         "type": "work", 
         "primary": true, 
         "value": "mhashimoto-04@plaxo.com"
        }, 
        {
         "type": "home", 
         "value": "mhashimoto-04@plaxo.com"
        }
       ], 
       "photos": [
        {
         "type": "thumbnail", 
         "value": "http://sample.site.org/photos/12345.jpg"
        }
       ], 
       "ims": [
        {
         "type": "aim", 
         "value": "plaxodev8"
        }
       ], 
       "accounts": [
        {
         "domain": "plaxo.com", 
         "userid": "2706"
        }
       ], 
       "urls": [
        {
         "type": "work", 
         "value": "http://www.seeyellow.com"
        }, 
        {
         "type": "home", 
         "value": "http://www.angryalien.com"
        }
       ], 
       "id": 2, 
       "addresses": [
        {
         "locality": "Springfield", 
         "country": "USA", 
         "region": "VT", 
         "formatted": "742 Evergreen Terrace\nSuite 123\nSpringfield VT 12345 USA", 
         "streetAddress": "742 Evergreen Terrace\nSuite 123", 
         "postalCode": "12345", 
         "type": "home"
        }
       ]
      }
     ]
    }

sortOrder, the order in which the sortBy parameter is applied. Allowed values 
are ascending and descending. If a value for sortBy is provided and no 
sortOrder is specifies, the sortOrder SHALL default to ascending. Sort order 
is expected to be case-insensitive Unicode alphabetic sort order, with no 
specific locale implied::

    >>> response = c.get("/portablecontacts/@me/@all/", {'sortBy': 'givenName', 'sortOrder': 'descending'})
    >>> response.status_code
    200
    >>> print response.content
    {
     "itemsPerPage": 100, 
     "startIndex": 0, 
     "totalResults": 3, 
     "entry": [
      {
       "relationships": [
        "friend"
       ], 
       "organizations": [
        {
         "name": "Burns Worldwide", 
         "title": "Head Bee Guy"
        }
       ], 
       "phoneNumbers": [
        {
         "type": "work", 
         "value": "KLONDIKE5"
        }, 
        {
         "type": "mobile", 
         "value": "650-123-4567"
        }
       ], 
       "displayName": "Mork Hashimoto", 
       "name": {
        "givenName": "Mork", 
        "familyName": "Hashimoto"
       }, 
       "tags": [
        "plaxo guy"
       ], 
       "emails": [
        {
         "type": "work", 
         "primary": true, 
         "value": "mhashimoto-04@plaxo.com"
        }, 
        {
         "type": "home", 
         "value": "mhashimoto-04@plaxo.com"
        }
       ], 
       "photos": [
        {
         "type": "thumbnail", 
         "value": "http://sample.site.org/photos/12345.jpg"
        }
       ], 
       "ims": [
        {
         "type": "aim", 
         "value": "plaxodev8"
        }
       ], 
       "accounts": [
        {
         "domain": "plaxo.com", 
         "userid": "2706"
        }
       ], 
       "urls": [
        {
         "type": "work", 
         "value": "http://www.seeyellow.com"
        }, 
        {
         "type": "home", 
         "value": "http://www.angryalien.com"
        }
       ], 
       "id": 2, 
       "addresses": [
        {
         "locality": "Springfield", 
         "country": "USA", 
         "region": "VT", 
         "formatted": "742 Evergreen Terrace\nSuite 123\nSpringfield VT 12345 USA", 
         "streetAddress": "742 Evergreen Terrace\nSuite 123", 
         "postalCode": "12345", 
         "type": "home"
        }
       ]
      }, 
      {
       "preferredUsername": "david", 
       "displayName": "David Larlet", 
       "id": 3, 
       "name": {
        "givenName": "David", 
        "familyName": "Larlet"
       }
      }, 
      {
       "displayName": "Minimal Contact", 
       "id": 1
      }
     ]
    }


Pagination
----------

The pagination parameters can be used together to "page through" a large 
number of results in manageable chunks.

For instance, on an initial query, specifying startIndex=0&count=1 will return 
only the first result. The total number of possible results is indicated by 
the totalResults field of results, so the client knows how many "pages" of 
results exist::

    >>> response = c.get("/portablecontacts/@me/@all/", {'startIndex':0, 'count':1})
    >>> response.status_code
    200
    >>> print response.content
    {
     "itemsPerPage": 1, 
     "startIndex": 0, 
     "totalResults": 3, 
     "entry": [
      {
       "displayName": "Minimal Contact", 
       "id": 1
      }
     ]
    }

A subsequent query of startIndex=2&count=1 will return the next next result, 
and so on::

    >>> response = c.get("/portablecontacts/@me/@all/", {'startIndex':2, 'count':1})
    >>> response.status_code
    200
    >>> print response.content
    {
     "itemsPerPage": 1, 
     "startIndex": 2, 
     "totalResults": 3, 
     "entry": [
      {
       "preferredUsername": "david", 
       "displayName": "David Larlet", 
       "id": 3, 
       "name": {
        "givenName": "David", 
        "familyName": "Larlet"
       }
      }
     ]
    }

}}}

Updated

Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.