HOWTO: write an Indivo app using Python

From Indivo

Jump to: navigation, search

This is a walkthrough on writing an Indivo X application that is embedded within Indivo X. In this example, the application will help to manage the user's medical problems/conditions (i.e. arthritis, hypertension, ...)

Contents

[edit] Getting Started

An Indivo App is just a web site that:

  • presents a web interface to the user, framed within the Indivo user interface
  • connects to Indivo X on the backend to fetch and store data

The Indivo API defines how to call the Indivo X server on the backend.

This document explains the details of how to build an Indivo app, using Python. Of course, any other programming language / web platform can be used following the same principles.

[edit] Scope

Our sample application will help track a user's medical problem list, using coded-value lookups to fill in the problem name.

[edit] Download the Sample Application

Download the Sample PHA.

[edit] Install

To install and run the sample application v0.1, you'll need

  • Python 2.5 or later in the 2.x series
  • Django 1.0.x (1.0.4 is the latest and works well)
  • a web-accessible installation of Indivo X pre-alpha 1 with user-app credentials to that installation (the Indivo staging server will do.)
  • Linux or Mac OS (we suspect this can work on Windows, but we haven't tried it.)

Untar the application in a directory of your choosing, which we'll assume is APPHOME for the remainder of the instructions.

[edit] Update Settings

in APPHOME/settings.py, make sure the settings at the top of the file are set appropriately for your installation and for the Indivo X server you wish to connect to.

[edit] Run the Django dev server

in APPHOME/, run the django dev server as follows:

 python manage.py runserver HOSTNAME:PORT

typically, if you're running on localhost and, say, port 8001, you'll just need to do:

 python manage.py runserver 8001

[edit] General Architecture and User Flow

A user will add the Indivo Problems application to their record on demand, or it may be added for them by an administrative application. User Authentication is entirely performed via Indivo.

Indivo Problems will store all of its data in Indivo X, using the Indivo Document Schema: Problem. Notes added to the problem will be stored as standard annotations [link]. Thus, Indivo Problems does not require anything other than application logic: no database, no authentication mechanism, just HTML serving and access to the Indivo X API.

[edit] Authentication

The steps involved in adding an application to an Indivo record are:

  • the user clicks "Add Application" in Indivo
  • Indivo lists the available applications, including Indivo Problems, which the user selects
  • Indivo opens up an IFRAME onto the app's start_url, with the specific record_id as parameter.
  • The app begins an oAuth authorization protocol with the Indivo backend server.
  • The IFRAME is redirected to the Indivo authorization screen
  • when the user approves the app, the IFRAME is redirected to the app's post_auth URL, which can now complete the oAuth process and access Indivo.

Thus, our application needs:

  • a start_url
/start_auth?record_id={record_id}
  • a post_auth URL that complies with the oAuth protocol (v1.0a):
/after_auth?oauth_token={oauth_token}&oauth_verifier={oauth_verifier}

IMPORTANT: the Indivo system is split into a UI server, which presents the HTML user interface, and a BACKEND server, which communicates only in XML. The oAuth process involves both servers: oAuth calls are made against the BACKEND server, while the browser is redirected to the UI server.

[edit] Authentication Logic

Let's build the logic for these two entry points.

[edit] Start

We start with a python function declaration:

def start_auth(request):
   """
   begin the oAuth protocol with the server
   """

and now we need access to Indivo to get our oAuth request token from the backend server.:

   client = IndivoClient(settings.INDIVO_SERVER_OAUTH['consumer_key'], settings.INDIVO_SERVER_OAUTH['consumer_secret'], settings.INDIVO_SERVER_LOCATION)

Note how we're pulling the credentials from the Django settings, which are stored in settings.py Also, in the actual codebase, we've modularized this to a get_indivo_client function.

Next, we check to see if we were passed a record_id parameter, which is what happens when Indivo opens up an IFRAME onto our start URL, since it knows exactly what record is currently being accessed. We use this record_id to set up our oAuth parameters, and then get ourselves a request token:

   # do we have a record_id?
   record_id = request.GET.get('record_id', None)

   # prepare request token parameters
   params = {'oauth_callback':'oob'}
   if record_id:
       params['indivo_record_id'] = record_id

   # request a request token
   request_token = parse_token_from_response(client.post_request_token(data=params))

The parse_token_from_response function is included in the source code for this sample app. Its only purpose is to parse the parameters embedded in the response body and return a Python dictionary.

Now that we have this request token, it's time to store it in the web session for later and send the user to Indivo for authorization:

   # store the request token in the session for when we return from auth
   request.session['request_token'] = request_token
   
   # redirect to the UI server
   return HttpResponseRedirect(settings.INDIVO_UI_SERVER_BASE + '/oauth/authorize?oauth_token=%s' % request_token['oauth_token'])

Note how the redirect is now to the UI server, which is different from the backend server.

And that's it, we're finished with half of the code needed to connect an app with Indivo X for authentication and medical-record connectivity!

[edit] Post Auth

Once the user has approved the application for addition, Indivo X will redirect the user to the post_auth URL at our Problems App web server, and now it's time for us to complete the authentication process by converting our request token into an access token. We start with a new Python function:

def after_auth(request):
   """
   after Indivo authorization, exchange the request token for an access token and store it in the web session.
   """

Then, we retrieve the request token we stored in the session, as well as the token string and oauth verifier we receive as URL parameters:

   # get the token and verifier from the URL parameters
   oauth_token, oauth_verifier = request.GET['oauth_token'], request.GET['oauth_verifier']

   # retrieve request token stored in the session
   token_in_session = request.session['request_token']

We quickly check that the token in the URL parameter matches the web session, just to be extra safe:

   # is this the right token?
   if token_in_session['oauth_token'] != oauth_token:
       return HttpResponse("uh oh bad token")

Then we connect to Indivo using the consumer secret but also the request-token details to exchange the request token for a long-lived access token:

   # get the indivo client and use the request token as the token for the exchange
   client =  IndivoClient(settings.INDIVO_SERVER_OAUTH['consumer_key'], settings.INDIVO_SERVER_OAUTH['consumer_secret'], settings.INDIVO_SERVER_LOCATION)
   client.update_token(token_in_session)

   # create the client
   params = {'oauth_verifier' : oauth_verifier}
   access_token = parse_token_from_response(client.post_access_token(data=params))

Once again, in the actual code, we've modularized the client creation to the get_indivo_client function.

And that's it, we're fully connected! We now store the access token details in the web session for later use, and redirect to the app's homepage:

   # store stuff in the session
   request.session['access_token'] = access_token
   request.session['record_id'] = access_token['xoauth_indivo_record_id']
   # go to list of problems
   return HttpResponseRedirect("/")

Notice how the access token came back with an extra parameter that indicates the identifier of the Indivo record we just managed to bind.

[edit] URL handlers

We build URL handlers in Django's urls.py:

from views import start_auth, after_auth

urlpatterns = patterns(' ',
  # authentication
  (r'^start_auth', start_auth),
  (r'^after_auth', after_auth),

[edit] Recording and Displaying Problems

The rest of the application is a standard web app that displays a list of problems and lets the user add a new one. The generic web components are best explained by the existing Django documentation. Here, we cover briefly the Indivo-specific touchpoints.

[edit] Getting information from Indivo

Every call to the Indivo Problem List app requires information from Indivo. Thus, in every call, it is useful to set up the client front-end to Indivo as:

   client = IndivoClient(settings.INDIVO_SERVER_OAUTH['consumer_key'], settings.INDIVO_SERVER_OAUTH['consumer_secret'], settings.INDIVO_SERVER_LOCATION)
   client.update_token(request.session['access_token'])

In the Indivo Problem List code, this is packaged as get_indivo_client in the utils.py file.

[edit] Reading a list of Problems

Though each problem is its own Indivo document, problems might come from a CCR, from a list of problems in another schema, etc... Thus, it is always best to access the Problems Report when listing problems, which will list all of the reports processed from all input documents.

   client = get_indivo_client(request)
   record_id = request.session['record_id']

   problems_xml = client.read_problems(record_id = record_id, parameters={'order_by': '-date_onset'}).response['response_data']

Note how the record_id is needed here, and is easily obtained from the session data since we stored it there earlier.

Once we have our list of problems in XML, it's time to parse it. In our Python application, we use ElementTree.

The returned XML looks like:

<Reports>
  <Report>
    <Meta>
       <Document id="...">
          ...
       </Document>
    </Meta>
    <Item>
       <Problem xmlns="...">
       </Problem>
   </Item>
 </Report>
 
 <Report>
   ...
 </Report>

  ...
</Reports>

where the Document metadata is the Indivo Document Metadata Schema, and Problem schema is the Indivo Document Schema: Problem.

[edit] Creating a Document

To create a document, one must first put together the necessary XML. The way we do this in our sample application is to use Django's templating system to interpolate values into the XML template for Problem:

       params = {'code_abbrev':, 'coding_system': 'umls-snomed', 'date_onset': request.POST['date_onset'], 'date_resolution': request.POST['date_resolution'], 'code_fullname': request.POST['code_fullname'], 'code': request.POST['code'], 'diagnosed_by' : request.POST['diagnosed_by'], 'comments' : request.POST['comments']}
       problem_xml = render_raw('problem', params, type='xml')

Then, we submit this as a new document:

       client.post_document(record_id = request.session['record_id'], data=problem_xml)

[edit] Coded Values

In the course of creating a document, one needs to access coded values, for example SNOMED codes. Indivo makes coded values available via its API, e.g.:

  client.lookup_code(coding_system='umls-snomed', parameters= {'q' : query}).response['response_data']

which will return a JSON list of codes, each with properties abbrev, code, full_name, umls_code, and consumer_value.

In our sample application, we take this return value and format it for the jQuery Autocomplete Plugin:

   query = request.GET['query']

   codes = simplejson.loads(client.lookup_code(coding_system='umls-snomed', parameters= {'q' : query}).response['response_data'])

   formatted_codes = {'query': query, 'suggestions': [c['full_value'] for c in codes], 'data': codes}

   return HttpResponse(simplejson.dumps(formatted_codes), mimetype="text/plain")

[edit] Reading a single Document

From the report, we can get the document_id from which each problem is extracted. Using this document_id, it's easy to get the original document itself. In this case, the document is the same thing as what was found inside the report, but oftentimes the document will contain more detail than the high-level, summary points.

    doc_xml = client.read_document(record_id= record_id, document_id = problem_document_id).response['response_data']

[edit] Notifying the Record

Sometimes, a PHA needs to notify a record of some action.

    client.record_notify(record_id = request.session['record_id'], data={'content':'a new problem has been added to your problem list'})
Personal tools
Navigation