HOWTO: write an Indivo app using Python
From Indivo
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, ...)
This document pertains to Indivo X Alpha 3.
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.6 or later in the 2.x series
- Django 1.1.x
- a web-accessible installation of Indivo X alpha 2 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 8002, you'll just need to do:
python manage.py runserver 8002
[edit] Having Problems Loading the App?
Check that your settings.py file, your server, and your port all match up with the entry for the problems app in indivo_data.xml in the /utils directory of the Indivo Backend Server instance to which you are connecting.
[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 either record_id or carenet_id as a URL query 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}&carenet_id={carenet_id}
Note that only one of those two variables will be filled in.
- 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] Understanding Record-Based vs. Carenet-Based
When an app is invoked in Indivo, it is either invoked at the record level, or at the carenet level. A record-level app launch means that the app is launched by the record-owner, and can access all of that user's data. A carenet-level app launch means that the app is launched by a guest of the record-owner, and can see only limited data within that specific carenet. In general record-level apps display additional logic that carenet-level apps do not, e.g. the sharing controls.
[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 or carenet_id parameter, which is what happens when Indivo opens up an IFRAME onto our start URL, since it knows exactly what record (or carenet) is currently being accessed. We use this record_id or carenet_id to set up our oAuth parameters, and then get ourselves a request token:
# do we have a record_id or carenet_id?
record_id = request.GET.get('record_id', None)
carenet_id = request.GET.get('carenet_id', None)
# prepare request token parameters
params = {'oauth_callback':'oob'}
if record_id:
params['indivo_record_id'] = record_id
if carenet_id:
params['indivo_carenet_id'] = carenet_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 an 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
# depending on whether we get a record or carenet id back.
if access_token.has_key('xoauth_indivo_record_id'):
request.session['record_id'] = access_token['xoauth_indivo_record_id']
else:
request.session['carenet_id'] = access_token['xoauth_indivo_carenet_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, or of the carenet.
[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.
The call is slightly different depending on whether this is a record or carenet (eventually, Indivo may provide a single API call to make this easier, but for now we must differentiate):
client = get_indivo_client(request)
if request.session.has_key('record_id'):
record_id = request.session['record_id']
problems_xml = client.read_problems(record_id = record_id, parameters={'order_by': '-date_onset'}).response['response_data']
else:
carenet_id = request.session['carenet_id']
problems_xml = client.read_carenet_problems(carenet_id = carenet_id, parameters={'order_by': '-date_onset'}).response['response_data']
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.
Again, we must be conscious of whether this is within a record or carenet:
record_id = request.session.get('record_id', None)
if record_id:
doc_xml = client.read_document(record_id= record_id, document_id = problem_id).response['response_data']
else:
carenet_id = request.session['carenet_id']
doc_xml = client.get_carenet_document(carenet_id= carenet_id, document_id = problem_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'})
[edit] Adding UI Widgets
Indivo X, as of alpha 2, supports UI widgets that an app can easily integrate into its interface. The first such widget is "Sharing and Audit", which lets a user modify the sharing preferences and quickly view the audit log for a particular document. This sharing widget should really only be displayed when the app is record-level.
[edit] Getting SURL Credentials
To invoke a widget, an app must first generate SURL credentials, i.e. credentials that will allow it to generate Signed URL. Signed URLs ensure that only authorized apps can embed a specific widget. Fortunately, the Indivo client provides a simple built-in method for generating these SURL credentials:
surl_credentials = client.get_surl_credentials()
[edit] Setting up the JavaScript
Once SURL credentials have been generated, it's time to load the widget JavaScript and initialize it. This is done in the HTML template:
<script src="{INDIVO_UI_SERVER_BASE}/lib/widgets.js"></script>
then:
<script>
Indivo.setup('{INDIVO_UI_SERVER_BASE}');
</script>
and:
<script>
Indivo.Auth.setToken("{surl_credentials.token}","{surl_credentials.secret}");
</script>
[edit] Adding the Widget
Finally, it's time to add the widget:
{% if record_id %}
<script>
Indivo.Widget.DocumentAccess.add('{record_id}', '{problem_id}');
</script>
{% endif %}
Note how this widget is only added if there is a record_id, since a carenet-level app should not display the sharing widget. And, in fact, if it tried, it wouldn't know the record_id needed, and if it guessed it correctly it would not have the right permissions to do so.
