Monday, September 21, 2009

Extending the OpenMRS Reporting API: Cohort Queries

Introduction
At the Cape Town meeting last week, we demo'd (if you could call it that) the new reporting framework and some of the early versions of tools that we've built on top of it. Some of those tools have been described in recent entries and others are still undocumented. That will change soon. The UI and tool set for the reporting framework is still immature, but we're happy with what we've come up with from an API standpoint. As a developer, you *should* be able to do most of what you need with what exists. However, there will be instances where you need to extend the framework to do what you want.

NOTE: If anyone knows a good plugin to include code snippets in Blogger, please let me know.

Extending the Cohort Query
Here's a quick example of how to use the API to evaluate a cohort definition. This will return you a list of patients that can be used as input into another piece of code.

// Define the cohort definition
GenderCohortDefinition gcd = new GenderCohortDefinition();
gcd.setGender("M");

// Evaluate the cohort definition
Cohort males = Context.getService(CohortDefinitionService.class).evaluate(gcd, new EvaluationContext());


This is a pretty trivial example, but there are lots of cohort definitions in the system that will allow you to do more complex queries. With that said, there will likely be a time when you need to implement a cohort query that is not currently supported within the framework.

For instance, let's say that the Ministry of Health within the country you work calls your mobile and says "Hey, Justin ... we need to report on the number of patients that have had the XYZ form completed on a certain date or in the past month/year".

In order to extend the system to support this query, we need to do three things: (1) write a new cohort definition to define this query, (2) write the evaluator to evaluate the cohort definition and return a cohort.

0) Write a JUnit test that tests what your new cohort definition should do



public class FormCohortDefinitionEvaluatorTest extends BaseModuleContextSensitiveTest {

private Log log = LogFactory.getLog(this.getClass());

@Test
public void evaluate_shouldReturnPatientsWhoHadFormCompletedOnDate() throws Exception {
FormCohortDefinition fcd = new FormCohortDefinition();
fcd.setForms(Context.getFormService().getAllForms());
Calendar newYears = Calendar.getInstance();
newYears.set(2009, 0, 1);
EvaluationContext evalContext = new EvaluationContext();
evalContext.addParameterValue("sinceDate", newYears.getTime());
evalContext.addParameterValue("untilDate", newYears.getTime());

Cohort cohort = Context.getService(CohortDefinitionService.class).evaluate(fcd, evalContext);
Assert.assertEquals(1, cohort.getSize());
}
}


1) Write the cohort definition
So the first step is to define the cohort definition. We just need to extend the DateRangeCohortDefinition to allow dates to be specified as properties or parameters and add a "forms" property that will be used to allow a user to select forms in the system.


public class FormCohortDefinition extends DateRangeCohortDefinition {

private static final long serialVersionUID = 1L;

@ConfigurationProperty(required=false)
private List<Form> forms;

public FormCohortDefinition() {
super();
}

public List<Form> getForms() {
return forms;
}

public void setForms(List<Form> forms) {
this.forms = forms;
}
}


2) Write the cohort definition evaluator

@Handler(supports={FormCohortDefinition.class})
public class FormCohortDefinitionEvaluator implements CohortDefinitionEvaluator {

/**
* Default Constructor
*/
public FormCohortDefinitionEvaluator() {}

/**
* @see CohortDefinitionEvaluator#evaluateCohort(CohortDefinition, EvaluationContext)
*/
public Cohort evaluate(CohortDefinition cohortDefinition, EvaluationContext context) {
FormCohortDefinition fcd = (FormCohortDefinition) cohortDefinition;
Date fromDate = fcd.getCalculatedFromDate(context);
Date toDate = fcd.getCalculatedToDate(context);
Cohort cohort = new Cohort();

PatientSetService pss = Context.getPatientSetService();
for (Form form : fcd.getForms()) {
List types = null;
Cohort tempCohort = pss.getPatientsHavingEncounters(types, null, form, fromDate, toDate, null, null);
cohort = Cohort.union(cohort, tempCohort);
}
return cohort;
}
}


I want to use the API (instead of writing my own DAO, so we have to do something slightly stupid here. The PatientSetService.getPatientsHavingEncounters(...) method only takes a single form as a parameter, therefore I need to iterate over the forms in the cohort definition and call the API each time. If performance was an issue (and if I had more time) I would write a DAO that does the actually query I want. But neither of those statements are true, so this is a perfectly good solution.

3.) Now, let's can go back and run our JUnit test ... and hopefully see green.

INFO - LoggingAdvice.invoke(102) |2009-09-21 17:13:46,399| In method CohortDefinitionService.evaluate. Arguments: CohortDefinition=null, EvaluationContext=org.openmrs.module.evaluation.EvaluationContext@1d742a1,
INFO - LoggingAdvice.invoke(127) |2009-09-21 17:13:46,528| Exiting method evaluate
INFO - FormCohortDefinitionEvaluatorTest.evaluate_shouldEvaluate(51) |2009-09-21 17:13:46,536| # of patients: 1