Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EA-207: Implement an API for Fetching Visits with Notes and Diagnosis #250

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
14 changes: 14 additions & 0 deletions api/src/main/java/org/openmrs/module/emrapi/db/EmrApiDAO.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.openmrs.module.emrapi.db;

import org.openmrs.Patient;
import org.openmrs.module.emrapi.visit.VisitWithDiagnoses;

import java.util.List;
import java.util.Map;

Expand All @@ -8,4 +11,15 @@ public interface EmrApiDAO {
<T> List<T> executeHql(String queryString, Map<String, Object> parameters, Class<T> clazz);

<T> List<T> executeHqlFromResource(String resource, Map<String, Object> parameters, Class<T> clazz);

/**
* Fetches visits with note encounters and diagnoses of a patient.
*
* @param patient the patient
* @param startIndex the start index of the number of visits to fetch
* @param limit the limit of the number of visits to fetch
*
* @return a list of visits that has note encounters and diagnoses
*/
List<VisitWithDiagnoses> getVisitsWithNotesAndDiagnosesByPatient(Patient patient, int startIndex, int limit);
}
74 changes: 68 additions & 6 deletions api/src/main/java/org/openmrs/module/emrapi/db/EmrApiDAOImpl.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,41 @@
package org.openmrs.module.emrapi.db;

import lombok.Setter;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Query;
import org.openmrs.Diagnosis;
import org.openmrs.EncounterType;
import org.openmrs.Patient;
import org.openmrs.Visit;
import org.openmrs.api.db.hibernate.DbSessionFactory;
import org.openmrs.module.emrapi.EmrApiProperties;
import org.openmrs.module.emrapi.visit.VisitWithDiagnoses;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class EmrApiDAOImpl implements EmrApiDAO {

protected final Log log = LogFactory.getLog(getClass());

@Setter
private DbSessionFactory sessionFactory;

public void setSessionFactory(DbSessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}

@Override

@Setter
private EmrApiProperties emrApiProperties;

@Override
@SuppressWarnings("unchecked")
public <T> List<T> executeHql(String queryString, Map<String, Object> parameters, Class<T> clazz) {
Query query = sessionFactory.getCurrentSession().createQuery(queryString);
Expand Down Expand Up @@ -55,4 +67,54 @@ public <T> List<T> executeHqlFromResource(String resource, Map<String, Object> p
}
return executeHql(hql, parameters, clazz);
}

public List<VisitWithDiagnoses> getVisitsWithNotesAndDiagnosesByPatient(Patient patient, int startIndex, int limit) {

String visitNoteEncounterTypeUuid = emrApiProperties.getVisitNoteEncounterType().getUuid();

String hqlVisit="SELECT DISTINCT v FROM Visit v " +
"LEFT JOIN FETCH v.encounters enc " +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I wasn't aware of this FETCH syntax.... this is saying when fetching the encounters associated with the visit, limit to those which are notes? ie, although this query only returns visits, you can fetch the encounters via visit.encounters, and those will be limited to notes? @jayasanka-sack

Copy link
Member Author

@jayasanka-sack jayasanka-sack Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you’re right!

P.S.
I used the FETCH syntax to load encounters eagerly instead of lazily.

"LEFT JOIN enc.encounterType et " +
"WHERE v.patient.id = :patientId " +
"AND (et.uuid = :encounterTypeUuid OR enc IS NULL) " +
"ORDER BY v.startDatetime DESC";

Query visitQuery = sessionFactory.getCurrentSession().createQuery(hqlVisit);

visitQuery.setParameter("patientId", patient.getId());
visitQuery.setParameter("encounterTypeUuid", visitNoteEncounterTypeUuid);
visitQuery.setFirstResult(startIndex);
visitQuery.setMaxResults(limit);

List<Visit> visits = visitQuery.list();

String hqlDiagnosis = "SELECT DISTINCT diag FROM Diagnosis diag " +
"JOIN diag.encounter e " +
"WHERE e.visit.id IN :visitIds";

List<Integer> visitIds = visits.stream()
.map(Visit::getId)
.collect(Collectors.toList());

List<Diagnosis> diagnoses = sessionFactory.getCurrentSession()
.createQuery(hqlDiagnosis)
.setParameterList("visitIds", visitIds)
.list();

Map<Visit, Set<Diagnosis>> visitToDiagnosesMap = new HashMap<>();
for (Diagnosis diagnosis : diagnoses) {
Visit visit = diagnosis.getEncounter().getVisit();
visitToDiagnosesMap
.computeIfAbsent(visit, v -> new HashSet<>())
.add(diagnosis);
}

List<VisitWithDiagnoses> visitWithDiagnoses = visits.stream()
.sorted(Comparator.comparing(Visit::getStartDatetime).reversed())
.map(visit -> new VisitWithDiagnoses(visit, visitToDiagnosesMap.getOrDefault(visit, new HashSet<>())))
.collect(Collectors.toList());

return visitWithDiagnoses;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.openmrs.module.emrapi.visit;

import java.util.List;

public interface EmrApiVisitService {
/**
* Fetches visits with note encounters and diagnoses of a patient by patient ID.
*
* @param patientUuid the UUID of the patient
* @return a list of visits that has note encounters and diagnoses
*/
List<VisitWithDiagnoses> getVisitsWithNotesAndDiagnosesByPatient(String patientUuid, int startIndex, int limit);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.openmrs.module.emrapi.visit;

import lombok.Setter;
import org.hibernate.ObjectNotFoundException;
import org.openmrs.Patient;
import org.openmrs.api.PatientService;
import org.openmrs.api.impl.BaseOpenmrsService;
import org.openmrs.module.emrapi.db.EmrApiDAO;
import org.springframework.stereotype.Service;

import java.util.List;

@Setter
@Service
public class EmrApiVisitServiceImpl extends BaseOpenmrsService implements EmrApiVisitService {

PatientService patientService;
EmrApiDAO emrApiDAO;

@Override
public List<VisitWithDiagnoses> getVisitsWithNotesAndDiagnosesByPatient(String patientUuid, int startIndex, int limit) {

Patient patient = patientService.getPatientByUuid(patientUuid);

if (patient == null) {
throw new ObjectNotFoundException("No patient found with uuid " + patientUuid, Patient.class.getName());
}

return emrApiDAO.getVisitsWithNotesAndDiagnosesByPatient(patient, startIndex, limit);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.openmrs.module.emrapi.visit;

import lombok.Getter;
import lombok.Setter;
import org.openmrs.Diagnosis;
import org.openmrs.Visit;

import java.util.Set;

@Setter
@Getter
public class VisitWithDiagnoses {

private Visit visit;
private Set<Diagnosis> diagnoses;

public VisitWithDiagnoses(Visit visit, Set<Diagnosis> diagnoses) {
jayasanka-sack marked this conversation as resolved.
Show resolved Hide resolved
this.visit = visit;
this.diagnoses = diagnoses;
}

}
9 changes: 9 additions & 0 deletions api/src/main/resources/moduleApplicationContext.xml
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,19 @@
<constructor-arg ref="encounterService"/>
</bean>


<bean id="emrApiDAOImpl" class="org.openmrs.module.emrapi.db.EmrApiDAOImpl">
<property name="sessionFactory">
<ref bean="dbSessionFactory"/>
</property>
<property name="emrApiProperties">
<ref bean="emrApiProperties"/>
</property>
</bean>

<bean id="emrApiVisitService" class="org.openmrs.module.emrapi.visit.EmrApiVisitServiceImpl">
<property name="emrApiDAO" ref="emrApiDAOImpl"/>
<property name="patientService" ref="patientService"/>
</bean>

<bean id="visitResponseMapper" class="org.openmrs.module.emrapi.visit.VisitResponseMapper">
Expand Down
90 changes: 90 additions & 0 deletions api/src/test/java/org/openmrs/module/emrapi/db/EmrApiDaoTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package org.openmrs.module.emrapi.db;

import org.junit.Before;
import org.junit.Test;
import org.openmrs.Diagnosis;
import org.openmrs.Encounter;
import org.openmrs.Patient;
import org.openmrs.module.emrapi.EmrApiContextSensitiveTest;
import org.openmrs.module.emrapi.visit.VisitWithDiagnoses;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;
import java.util.Set;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class EmrApiDaoTest extends EmrApiContextSensitiveTest {

@Autowired
private EmrApiDAO emrApiDAO;

@Before
public void setup() {
executeDataSet("baseTestDataset.xml");
executeDataSet("pastVisitSetup.xml");
}

@Test
public void shouldFetchVisitsWithNotesAndDiagnosesByPatientId() {
String visitNoteEncounterTypeUuid = "d7151f82-c1f3-4152-a605-2f9ea7414a79";

Patient patient = new Patient();
patient.setPatientId(109);

List<VisitWithDiagnoses> visits = emrApiDAO.getVisitsWithNotesAndDiagnosesByPatient(patient,0,10);
assertNotNull(visits);
assert visits.size() == 3;

VisitWithDiagnoses firstVisit = visits.get(2);
Set<Encounter> firstVisitEncounters = firstVisit.getVisit().getEncounters();
Set<Diagnosis> firstVisitDiagnoses = firstVisit.getDiagnoses();

assert firstVisit.getVisit().getId() == 1014;
assert firstVisit.getVisit().getPatient().getPatientId() == 109;
assert firstVisitEncounters.size() == 2;
assert firstVisitDiagnoses.size() == 3;

for (Encounter encounter : firstVisitEncounters) {
assert encounter.getEncounterType().getUuid().equals(visitNoteEncounterTypeUuid);
}

VisitWithDiagnoses secondVisit = visits.get(1);
Set<Encounter> secondVisitEncounters = secondVisit.getVisit().getEncounters();
Set<Diagnosis> secondVisitDiagnoses = secondVisit.getDiagnoses();

assert secondVisit.getVisit().getId() == 1015;
assert secondVisit.getVisit().getPatient().getPatientId() == 109;
assert secondVisitEncounters.size() == 1;
assert secondVisitDiagnoses.size() == 2;

for (Encounter encounter : secondVisitEncounters) {
assert encounter.getEncounterType().getUuid().equals(visitNoteEncounterTypeUuid);
}

VisitWithDiagnoses thirdVisit = visits.get(0);
Set<Encounter> thirdVisitEncounters = thirdVisit.getVisit().getEncounters();
Set<Diagnosis> thirdVisitDiagnoses = thirdVisit.getDiagnoses();

assert thirdVisit.getVisit().getId() == 1017;
assert thirdVisit.getVisit().getPatient().getPatientId() == 109;
assert thirdVisitEncounters.isEmpty();
assert thirdVisitDiagnoses.isEmpty();

}

@Test
public void shouldFetchVisitsWithNotesAndDiagnosesWithPagination() {
Patient patient = new Patient();
patient.setPatientId(109);

List<VisitWithDiagnoses> visits = emrApiDAO.getVisitsWithNotesAndDiagnosesByPatient(patient,0,1);
assertNotNull(visits);
assert visits.size() == 1;

VisitWithDiagnoses mostRecentVisit = visits.get(0);
assert mostRecentVisit.getVisit().getId() == 1017;
}

}
29 changes: 29 additions & 0 deletions api/src/test/resources/metadata/pastVisitSetup.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version='1.0' encoding='UTF-8'?>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this class being used, or is everything handled in the other "pastVisitSetup" class below?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both files are used in tests in both modules.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the distinction between them?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the organization is a little weird. It looks like there may have been some convention for all the metadata to go into a file like this and the data into a file like the other one, but both of them mix data and metadata, so it's confusing why we need the two.


<dataset>

<encounter_type encounter_type_id="1003" name="Visit Note" description="Visit note" creator="1" date_created="2023-10-15 10:00:00.0" retired="false" uuid="d7151f82-c1f3-4152-a605-2f9ea7414a79"/>
<encounter_type encounter_type_id="1004" name="Vitals" description="For capturing vital signs" creator="1" date_created="2023-10-15 10:00:00.0" retired="false" uuid="d92ca5dc-7fd0-4fa2-b726-ab16fbc53fc1"/>

<!-- Person (Patient) Information -->
<person person_id="109" gender="M" dead="false" creator="1" birthdate_estimated="0"
date_created="2023-10-15 10:30:00.0" voided="false" void_reason=""
uuid="8604d42e-3ca8-11e3-bf2b-0d0c09861e97"/>
<person_name person_name_id="109" person_id="109" preferred="true" prefix="" given_name="Scott" middle_name=""
family_name="Clark" family_name_suffix="" creator="1" date_created="2023-10-15 10:31:00.0"
voided="false" void_reason="" uuid="8f9e3a7b-6482-487d-9abe-c07b123c4f08"/>

<!-- Patient Record -->
<patient patient_id="109" creator="1" date_created="2023-10-15 10:32:00.0" voided="false" />

<!-- Location Information -->
<location location_id="1901" name="Location tagged to visit" creator="1" date_created="2023-10-15 10:35:00.0" retired="false" uuid="f1771d8e-bf1f-4dc5-957f-9d40a5eebf08"/>

<!-- Visit Information -->
<visit visit_id="1014" patient_id="109" visit_type_id="1" date_started="2023-10-20 09:00:00.0" date_stopped="2023-10-20 09:30:00.0" location_id="1901" creator="1" date_created="2023-10-20 09:05:00.0" voided="0" uuid="1esd5218-6b78-11e0-93c3-18a905e044dc" />

<!-- Encounter Information (Visit Note) -->
<encounter encounter_id="2001" visit_id="1014" encounter_type="1003" patient_id="109" location_id="1901" form_id="1" encounter_datetime="2023-10-20 09:10:00.0" creator="1" date_created="2023-10-20 09:12:00.0" voided="false" uuid="a123d653-444b-4118-9c83-a3715b82d4ac"/>
<encounter encounter_id="2002" visit_id="1014" encounter_type="1004" patient_id="109" location_id="1901" form_id="1" encounter_datetime="2023-10-20 09:10:00.0" creator="1" date_created="2023-10-20 09:12:00.0" voided="false" uuid="a9a41017-2593-442a-bb88-c80358692d1e"/>

</dataset>
Loading
Loading