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_8, RelationShipTypeResource1_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!)
// 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!
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:
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:
<% ui.decorateWith("appui", "standardEmrPage") ui.includeJavascript("uicommons", "angular.js") ui.includeJavascript("uicommons", "angular-resource.min.js") ui.includeJavascript("uicommons", "angular-common.js") ui.includeJavascript("uicommons", "angular-ui/ui-bootstrap-tpls-0.6.0.min.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") ui.includeJavascript("coreapps", "relationships/relationships.js") %> <script type="text/javascript"> var breadcrumbs = [ { icon: "icon-home", link: '/' + OPENMRS_CONTEXT_PATH + '/index.htm' }, { label: "${ ui.format(patient.patient.familyName) }, ${ ui.format(patient.patient.givenName) }" , link: '${ui.pageLink("coreapps", "clinicianfacing/patient", [patientId: patient.patient.id])}'}, { label: "${ ui.escapeJs(ui.message("coreapps.task.relationships.label")) }" } ] </script> <style type="text/css"> .relationship { position: relative; border: 1px solid #DADADA; padding: 5px 25px 5px 5px; } .relationship i { position: absolute; right: 0px; cursor: pointer; } .dialog { position: absolute; z-index: 999; } .add-confirm-spacer { min-height: 110px; } </style> ${ ui.includeFragment("coreapps", "patientHeader", [ patient: patient.patient ]) } <h2>${ ui.message("coreapps.task.relationships.label") }</h2> <div id="relationships-app" ng-controller="PersonRelationshipsCtrl" ng-init="init('${ patient.patient.uuid }')"> <div ng-show="addDialogMode" class="dialog" id="add-relationship-dialog"> <div class="dialog-header"> <%= ui.message("coreapps.relationships.add.header", "{{ addDialogOtherLabel }}") %> </div> <div class="dialog-content"> <div> <label> <%= ui.message("coreapps.relationships.add.choose", "{{ addDialogOtherLabel }}") %> </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>{{ addDialogThisLabel }}: ${ ui.format(patient.patient) } ${ ui.message("coreapps.relationships.add.thisPatient") }</p> <p>{{ addDialogOtherLabel }}: {{ otherPerson.display }}</p> </div> </div> <div> <button class="confirm right" ng-disabled="!otherPerson" ng-click="doAddRelationship()">${ ui.message("uicommons.save") }</button> <button class="cancel" ng-click="cancelAddRelationship()">${ ui.message("uicommons.cancel") }</button> </div> </div> </div> <div ng-show="relationshipToDelete" class="dialog" id="delete-relationship-dialog"> <div class="dialog-header">${ ui.message("coreapps.relationships.delete.header") }</div> <div class="dialog-content"> <form> ${ ui.message("coreapps.relationships.delete.title") } <p> <label>{{ relType(relationshipToDelete).aIsToB }}</label> {{ relationshipToDelete.personA.display }} </p> <p> <label>{{ relType(relationshipToDelete).bIsToA }}</label> {{ relationshipToDelete.personB.display }} </p> <button class="confirm right" ng-click="doDeleteRelationship(relationshipToDelete)">${ ui.message("uicommons.delete") }</button> <button class="cancel" ng-click="cancelDeleteRelationship(relationshipToDelete)">${ ui.message("uicommons.cancel") }</button> </form> </div> </div> <form id="existing-relationships"> <div ng-repeat="relType in relationshipTypes"> <p> <label>{{ relType.aIsToB }}</label> <span ng-repeat="rel in relationshipsByType(relType, 'A')" class="relationship"> {{ rel.personA.display }} <a ng-click="showDeleteDialog(rel)"> <i class="small icon-remove"></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="small icon-remove"></i> </a> </span> <a ng-click="showAddDialog(relType, 'A')"> <i class="small icon-plus-sign"></i> </a> </p> <p ng-hide="relType.aIsToB == relType.bIsToA"> <label>{{ relType.bIsToA }}</label> <span ng-repeat="rel in relationshipsByType(relType, 'B')" class="relationship"> {{ rel.personB.display }} <a ng-click="showDeleteDialog(rel)"> <i class="small icon-remove"></i> </a> </span> <a ng-click="showAddDialog(relType, 'B')"> <i class="small icon-plus-sign"></i> </a> </p> </div> </form> </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>
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.
[ ... { "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