Child pages
  • Adding a New Kind of Data to the Patient Summary
Skip to end of metadata
Go to start of metadata

Background

This tutorial describes how you would add a new kind of data about the patient. In this example we add "Patient Relationships" which was not previously part of OpenMRS 2.x, adding this to the clinician-facing patient summary (TO DO), as well as adding a detailed screen to manage those relationships.

We assume that you have a 2.x Development Environment set up already, and are familiar with OpenMRS and Angular-JS development.

Step 0 - Create a REST web service API to your data

In this example, Relationship and RelationshipType already exists in the OpenMRS data model, and there are resources for them in the webservices.rest module: RelationshipResource1_8RelationshipTypeResource1_8.

If you are developing a new kind of data, you will need to create a Java and REST API (which is not covered here).

Step 1 - Create angular services to access your web service resources

AngularJS has an ngResource plugin that makes it very easy to interact with RESTful web services. Although it is possible to access your web services directly via HTTP calls, we strongly recommend using $resource to encapsulate this access in a service, to keep your code clean and simple.

If you want to use a core OpenMRS REST resource, check in the UI Commons module's services folder to see if it already has an associated service. (At time of writing, we only have a very few, so if the one you want is missing, you should feel free to implement the one you want, and contribute it back!). If you are using a module's REST resource, you should put the resource in that module.

Here is "relationshipService.js", which shows a pattern you can copy-and-paste from. (You should copy from the latest version in the services folder linked above, rather than using my annotated version here. Also note that when you copy-paste-edit a new one of these, that you also copy the associated javascript test!)

relationshipService.js (in the uicommons module)
// Each service should be its own angular module, so that a consuming page can include just the ones it needs.
// This module declares that it requires the 'ngResource' module, and the 'uicommons.common' module. (This provides
// important default behaviors, including suppressing the browser's basic authentication popup, and redirecting to the
// login screen if an HTTP call returns a 401 or 403.
angular.module('relationshipService', ['ngResource', 'uicommons.common'])
 
    // This is a lower-level API that the consumer will probably not use directly.
    // Relationship exposes get, query, save, and delete functions: see https://docs.angularjs.org/api/ngResource/service/$resource
    .factory('Relationship', function($resource) {
        return $resource("/" + OPENMRS_CONTEXT_PATH  + "/ws/rest/v1/relationship/:uuid", { // the url template for this resource
            uuid: '@uuid' // tells $resource that the uuid property of an object maps to :uuid in the url template
        }, {
            // Override default $resource behavior, since OpenMRS REST resources return { "results": [...] },
            // whereas $resource expects a plain [...] array.
            query: { method:'GET', isArray:false }
        });
    })
 
    // This is the higher-level API that we want consumers to actually use
    .factory('RelationshipService', function(Relationship) {
        return {

            /**
             * Fetches Relationships
             *
             * @param params to search against
             * @returns $promise of array of matching Relationships (REST ref representation by default)
             */
            getRelationships: function(params) {
                return Relationship.query(params).$promise.then(function(res) {
                    return res.results;
                });
            },

            /**
             * Creates a new relationship
             *
             * @param relationship
             * @returns {Relationship}
             */
            createRelationship: function(relationship) {
                var created = new Relationship(relationship);
                created.$save();
                return created;
            },

            /**
             * Soft-deletes a relationship
             * @param relationship must have a uuid property, but may be a minimal representation
             */
            deleteRelationship: function(relationship) {
                var toDelete = new Relationship({ uuid: relationship.uuid });
                toDelete.$delete();
            }
        }
    });

 

Note that this is a new pattern for OpenMRS, so we welcome feedback about how to improve it!

The latest version of this code (with less commentary) lives at relationshipService.js.

Step 2 - Create a new screen to manage your data

The OpenMRS 2.x framework is very flexible, so you can implement this screen using any technology, as long as it's accessible via a URL.

The easiest approach is to use the UI Framework to create a GSP page which lets you easily include that patient header and breadcrumbs, and do the majority of i18n.

My current preferred approach beyond that is to implement a single-page AngularJS app that lets us do sophisticated client-side behaviors, while keeping the code clean and organized. 

Since we're going to do all the work on the client-side via AngularJS, our page's controller only needs to fetch the patient. Thus we can just extends a base patient controller class:

ListPageController - Located in your module eg: org.openmrs.module.yourmodule.page.controller
package org.openmrs.module.coreapps.page.controller.relationships;

import org.openmrs.module.coreapps.helper.SimplePatientPageController;

public class ListPageController extends SimplePatientPageController {
    // GET defined in superclass
}

 

The view for this page is large, and will require some AngularJS knowledge to understand (TO DO describe this better):

relationships/list.gsp (in coreapps module)
<%
    // Nearly every page you write should do this, to get the standard reference application decoration and includes
    ui.decorateWith("appui", "standardEmrPage")
 
    // include angular and plugins
    ui.includeJavascript("uicommons", "angular.js")
    ui.includeJavascript("uicommons", "angular-resource.min.js")
    ui.includeJavascript("uicommons", "angular-ui/ui-bootstrap-tpls-0.6.0.min.js")
    ui.includeJavascript("uicommons", "ngDialog/ngDialog.js")
    ui.includeCss("uicommons", "ngDialog/ngDialog.min.css")
 
    // we use the following OpenMRS angular services and directives
    ui.includeJavascript("uicommons", "angular-common.js")
    ui.includeJavascript("uicommons", "services/relationshipService.js")
    ui.includeJavascript("uicommons", "services/relationshipTypeService.js")
    ui.includeJavascript("uicommons", "services/personService.js")
    ui.includeJavascript("uicommons", "directives/select-person.js")
 
    // custom JS and CSS for this page
    ui.includeJavascript("coreapps", "relationships/relationships.js")
    ui.includeCss("coreapps", "relationships/list.css")
%>
 
<!-- Configuring breadcrumbs; you'd typically copy-paste this from another page and change the last item in the list -->
<script type="text/javascript">
    var breadcrumbs = [
        { icon: "icon-home", link: '/' + OPENMRS_CONTEXT_PATH + '/index.htm' },
        { label: "${ ui.escapeJs(patient.patient.familyName) }, ${ ui.escapeJs(patient.patient.givenName) }" ,
            link: '${ui.pageLink("coreapps", "clinicianfacing/patient", [patientId: patient.patient.id])}'},
        { label: "${ ui.escapeJs(ui.message("coreapps.task.relationships.label")) }" }
    ]
</script>
 
<!-- Include the standard patient header before we get to the specifics of this page -->
${ ui.includeFragment("coreapps", "patientHeader", [ patient: patient.patient ]) }
 
<!-- Your page should have a title -->
<h3>${ ui.message("coreapps.task.relationships.label") }</h3>
 
<!-- ng-init is the most convenient way to bootstrap the static js code with dynamic data from this gsp -->
<div id="relationships-app" ng-controller="PersonRelationshipsCtrl" ng-init="init('${ patient.patient.uuid }')">

    <!-- This is how you do an inline angular template -->
    <script type="text/ng-template" id="addDialogTemplate">
        <div class="dialog-header">
            <h3><%= ui.message("coreapps.relationships.add.header", "{{ ngDialogData.otherLabel }}") %></h3>
        </div>
        <div class="dialog-content">
            <div>
                <label>
                    <%= ui.message("coreapps.relationships.add.choose", "{{ ngDialogData.otherLabel }}") %>
                </label>
                <select-person ng-model="otherPerson" exclude-person="${ patient.patient.uuid }" />
            </div>
            <div class="add-confirm-spacer">
                <div ng-show="otherPerson" >
                    <h3>${ ui.message("coreapps.relationships.add.confirm") }</h3>
                    <p>{{ ngDialogData.thisLabel }}: ${ ui.format(patient.patient) } ${ ui.message("coreapps.relationships.add.thisPatient") }</p>
                    <p>{{ ngDialogData.otherLabel }}: {{ otherPerson.display }}</p>
                </div>
            </div>
            <div>
                <button class="confirm right" ng-disabled="!otherPerson" ng-click="confirm(otherPerson)">${ ui.message("uicommons.save") }</button>
                <button class="cancel" ng-click="closeThisDialog()">${ ui.message("uicommons.cancel") }</button>
            </div>
        </div>
    </script>
    <script type="text/ng-template" id="deleteDialogTemplate">
        <div class="dialog-header">
            <h3>${ ui.message("coreapps.relationships.delete.header") }</h3>
        </div>
        <div class="dialog-content">
            <form>
                ${ ui.message("coreapps.relationships.delete.title") }
                <p>
                    <label>{{ relType(ngDialogData.relationship).aIsToB }}</label>
                    {{ ngDialogData.relationship.personA.display }}
                </p>
                <p>
                    <label>{{ relType(ngDialogData.relationship).bIsToA }}</label>
                    {{ ngDialogData.relationship.personB.display }}
                </p>
                <button  class="confirm right" ng-click="confirm()">${ ui.message("uicommons.delete") }</button>
                <button class="cancel" ng-click="closeThisDialog()">${ ui.message("uicommons.cancel") }</button>
            </form>
        </div>
    </script>
 
    <div ng-repeat="relType in relationshipTypes">
        <h6>
            {{ relType.aIsToB }}
            <a ng-click="showAddDialog(relType, 'A')">
                <i class="icon-plus-sign edit-action"></i>
            </a>
        </h6>
        <span ng-repeat="rel in relationshipsByType(relType, 'A')" class="relationship">
            {{ rel.personA.display }}
            <a ng-click="showDeleteDialog(rel)">
                <i class="icon-remove delete-action"></i>
            </a>
        </span>
        <span ng-show="relType.aIsToB == relType.bIsToA" ng-repeat="rel in relationshipsByType(relType, 'B')" class="relationship">
            {{ rel.personB.display }}
            <a ng-click="showDeleteDialog(rel)">
                <i class="icon-remove delete-action"></i>
            </a>
        </span>
        <span ng-hide="relType.aIsToB == relType.bIsToA">
            <h6>
                {{ relType.bIsToA }}
                <a ng-click="showAddDialog(relType, 'B')">
                    <i class="icon-plus-sign edit-action"></i>
                </a>
            </h6>
            <span ng-repeat="rel in relationshipsByType(relType, 'B')" class="relationship">
                {{ rel.personB.display }}
                <a ng-click="showDeleteDialog(rel)">
                    <i class="icon-remove delete-action"></i>
                </a>
            </span>
        </span>
    </div>

</div>
<script type="text/javascript">
    // manually bootstrap angular app, in case there are multiple angular apps on a page
    angular.bootstrap('#relationships-app', ['relationships']);
</script>

Finally the JS that controls this page's behavior:

relationships.js (from the coreapps module)
// a new "module" just for this page that depends on various modules from uicommons
angular.module('relationships', ['relationshipTypeService', 'relationshipService', 'personService', 'uicommons.widget.select-person', 'ngDialog' ]).
 
    // this page has a single controller, which gets several dependencies injected 
    controller('PersonRelationshipsCtrl', ['$scope', 'RelationshipTypeService', 'RelationshipService', 'PersonService', '$modal', 'ngDialog',
    function($scope, RelationshipTypeService, RelationshipService, PersonService, $modal, ngDialog) {

        // the model for our view
        $scope.thisPersonUuid = null;
        $scope.relationshipTypes = [];
        $scope.relationships = [];
 
        // this is invoked by ng-init="init($patient.patient.uuid)" in the gsp page, which is how we communicate that specific bit of
        // bootstrapping data to our javascript app
        $scope.init = function(personUuid) {
            $scope.thisPersonUuid = personUuid;
            // these calls (which use $resource under the hood) are asynchronous, so we need to use .then on the promise they return
            RelationshipService.getRelationships({ v: 'default', person: personUuid }).then(function(result) {
                $scope.relationships = result;
            });
            RelationshipTypeService.getRelationshipTypes({ v: 'default' }).then(function(result) {
                $scope.relationshipTypes = result;
            });
        }

        // Helper method, since after saving a relationship, the response object we get back from the OpenMRS REST resource does not
        // include a full rep of its relationshipType, so this helper gets the full rep that we download on init
        $scope.relType = function(relationship) {
            if (!relationship) {
                return null;
            }
            return _.findWhere($scope.relationshipTypes, { uuid: relationship.relationshipType.uuid });
        }
 
        $scope.relationshipsByType = function(relationshipType, whichSide) {
            return _.filter($scope.relationships, function(item) {
                if (item.relationshipType.uuid != relationshipType.uuid) {
                    return false;
                }
                if (whichSide == 'A' && item.personB.uuid == $scope.thisPersonUuid) {
                    return true;
                } else if (whichSide == 'B' && item.personA.uuid == $scope.thisPersonUuid) {
                    return true;
                }
            });
        }
 
        $scope.showAddDialog = function(relationshipType, whichSide) {
            ngDialog.openConfirm({
                showClose: false,
                closeByEscape: true,
                closeByDocument: true,
                data: angular.toJson({
                    otherLabel: whichSide == 'A' ? relationshipType.aIsToB : relationshipType.bIsToA,
                    thisLabel: whichSide == 'A' ? relationshipType.bIsToA : relationshipType.aIsToB
                }),
                template: 'addDialogTemplate'
            }).
            then(function(otherPerson) {
                var relationship = {
                    relationshipType: relationshipType.uuid,
                    personA: whichSide == 'A' ? otherPerson.uuid : $scope.thisPersonUuid,
                    personB: whichSide == 'A' ? $scope.thisPersonUuid : otherPerson.uuid
                };
                var created = RelationshipService.createRelationship(relationship);
                $scope.relationships.push(created);
            });
            angular.element('#select-other-person').focus();
        }

        $scope.showDeleteDialog = function(relationship) {
            ngDialog.openConfirm({
                showClose: false,
                closeByEscape: true,
                closeByDocument: true,
                template: 'deleteDialogTemplate',
                data: angular.toJson({ relationship: relationship }),
                scope: $scope  // need this so the view can call the $scope.relType(relationship) function
            }).
            then(function(relationshipToDelete) {
                RelationshipService.deleteRelationship(relationship);
                $scope.relationships = _.reject($scope.relationships, function(item) {
                    return item.uuid == relationship.uuid;
                });
            });
        }
    }]); 

 

The current, latest version of this code (with fewer comments) can be found at relationships/list.gsp and relationships.js.

Step 3 - Use an Extension to link to your screen from the patient summary

In this example, the piece of data that we're adding (relationships to other persons in the system) is at the level of the patient, and not any particular visit or encounter. Therefore we would like to add a link to it in the "overall actions" sections of the patient summary:

Since relationships are a core part of the system that we want to include out of the box in the reference application, in this example we add this extension to the coreapps module. If you are building add-on functionality, you would want to implement this in your own module.

We add the following to the overallActions_extension.json file in the coreapps module. See App Framework Developer Documentation if you want detailed documentation on this structure.

overallActions_extension.json (in the coreapps module)
[
    ...
    {
        "id": "${project.parent.groupId}.${project.parent.artifactId}.relationships", // this just has to be unique across extensions
        "extensionPointId": "patientDashboard.overallActions", // where we attach our extension
        "type": "link",
        "label": "coreapps.task.relationships.label", // this is a message code, which we need to add to messages.properties for i18n
        "url": "coreapps/relationships/list.page?patientId={{patientId}}", // the url of the page you created in the previous step
        "icon": "icon-group", // see the Style Guide's icons section for available icons
        "order": 23, // currently this is the only way to order the extensions that are attached to a particular point (hacky)
        "requiredPrivilege": "Task: coreapps.relationships" // only display this extension if the user has this privilege
    }
]

 

Step 4 - Create a fragment that displays a summary of your data

TO DO

Step 5 - Include this fragment on the patient summary

TO DO

  • No labels

1 Comment

  1. Good work! It would be useful to include a link to full source code.