Skip to content

Commit 9ff8b5d

Browse files
[JN-417] Implement basic kit management UI (#477)
Co-authored-by: Matt Bemis <[email protected]>
1 parent c855e6b commit 9ff8b5d

File tree

44 files changed

+1218
-144
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1218
-144
lines changed

api-admin/src/main/java/bio/terra/pearl/api/admin/controller/enrollee/EnrolleeController.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import bio.terra.pearl.api.admin.api.EnrolleeApi;
44
import bio.terra.pearl.api.admin.service.AuthUtilService;
55
import bio.terra.pearl.api.admin.service.enrollee.EnrolleeExtService;
6+
import bio.terra.pearl.core.model.EnvironmentName;
67
import bio.terra.pearl.core.model.admin.AdminUser;
78
import bio.terra.pearl.core.model.kit.KitRequest;
89
import bio.terra.pearl.core.model.participant.Enrollee;
@@ -79,5 +80,17 @@ public ResponseEntity<Object> getKitRequests(
7980
return ResponseEntity.ok(kitRequests);
8081
}
8182

83+
@Override
84+
public ResponseEntity<Object> enrolleesWithKits(
85+
String portalShortcode, String studyShortcode, String envName) {
86+
AdminUser adminUser = authUtilService.requireAdminUser(request);
87+
EnvironmentName environmentName = EnvironmentName.valueOfCaseInsensitive(envName);
88+
89+
var enrollees =
90+
enrolleeExtService.findForKitManagement(
91+
adminUser, portalShortcode, studyShortcode, environmentName);
92+
return ResponseEntity.ok(enrollees);
93+
}
94+
8295
public record WithdrawnResponse(UUID withdrawnEnrolleeId) {}
8396
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package bio.terra.pearl.api.admin.controller.kit;
2+
3+
import bio.terra.pearl.api.admin.api.KitApi;
4+
import bio.terra.pearl.api.admin.service.AuthUtilService;
5+
import bio.terra.pearl.api.admin.service.kit.KitExtService;
6+
import bio.terra.pearl.core.model.EnvironmentName;
7+
import bio.terra.pearl.core.model.admin.AdminUser;
8+
import javax.servlet.http.HttpServletRequest;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.stereotype.Controller;
11+
12+
@Controller
13+
public class KitController implements KitApi {
14+
private final AuthUtilService authUtilService;
15+
private final KitExtService kitExtService;
16+
private final HttpServletRequest request;
17+
18+
public KitController(
19+
AuthUtilService authUtilService, KitExtService kitExtService, HttpServletRequest request) {
20+
this.authUtilService = authUtilService;
21+
this.kitExtService = kitExtService;
22+
this.request = request;
23+
}
24+
25+
@Override
26+
public ResponseEntity<Object> kitsByStudyEnvironment(
27+
String portalShortcode, String studyShortcode, String envName) {
28+
AdminUser adminUser = authUtilService.requireAdminUser(request);
29+
EnvironmentName environmentName = EnvironmentName.valueOfCaseInsensitive(envName);
30+
31+
var kits =
32+
kitExtService.getKitRequestsByStudyEnvironment(
33+
adminUser, portalShortcode, studyShortcode, environmentName);
34+
35+
return ResponseEntity.ok(kits);
36+
}
37+
}

api-admin/src/main/java/bio/terra/pearl/api/admin/service/enrollee/EnrolleeExtService.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ public List<EnrolleeSearchResult> search(
6363
return enrolleeSearchService.search(studyShortcode, environmentName, facets);
6464
}
6565

66+
public List<Enrollee> findForKitManagement(
67+
AdminUser user,
68+
String portalShortcode,
69+
String studyShortcode,
70+
EnvironmentName environmentName) {
71+
authUtilService.authUserToStudy(user, portalShortcode, studyShortcode);
72+
return enrolleeService.findForKitManagement(studyShortcode, environmentName);
73+
}
74+
6675
public Enrollee findWithAdminLoad(AdminUser user, String enrolleeShortcode) {
6776
Enrollee enrollee = authUtilService.authAdminUserToEnrollee(user, enrolleeShortcode);
6877
return enrolleeService.loadForAdminView(enrollee);
@@ -90,6 +99,6 @@ public KitRequest requestKit(AdminUser adminUser, String enrolleeShortcode, Stri
9099

91100
public Collection<KitRequest> getKitRequests(AdminUser adminUser, String enrolleeShortcode) {
92101
Enrollee enrollee = authUtilService.authAdminUserToEnrollee(adminUser, enrolleeShortcode);
93-
return kitRequestService.getKitRequests(adminUser, enrollee);
102+
return kitRequestService.getKitRequests(enrollee);
94103
}
95104
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package bio.terra.pearl.api.admin.service.kit;
2+
3+
import bio.terra.pearl.api.admin.service.AuthUtilService;
4+
import bio.terra.pearl.core.model.EnvironmentName;
5+
import bio.terra.pearl.core.model.admin.AdminUser;
6+
import bio.terra.pearl.core.model.kit.KitRequest;
7+
import bio.terra.pearl.core.model.study.StudyEnvironment;
8+
import bio.terra.pearl.core.service.kit.KitRequestService;
9+
import bio.terra.pearl.core.service.study.StudyEnvironmentService;
10+
import java.util.Collection;
11+
import org.springframework.stereotype.Service;
12+
13+
@Service
14+
public class KitExtService {
15+
private final AuthUtilService authUtilService;
16+
private final KitRequestService kitRequestService;
17+
private final StudyEnvironmentService studyEnvironmentService;
18+
19+
public KitExtService(
20+
AuthUtilService authUtilService,
21+
KitRequestService kitRequestService,
22+
StudyEnvironmentService studyEnvironmentService) {
23+
this.authUtilService = authUtilService;
24+
this.kitRequestService = kitRequestService;
25+
this.studyEnvironmentService = studyEnvironmentService;
26+
}
27+
28+
public Collection<KitRequest> getKitRequestsByStudyEnvironment(
29+
AdminUser adminUser,
30+
String portalShortcode,
31+
String studyShortcode,
32+
EnvironmentName environmentName) {
33+
authUtilService.authUserToStudy(adminUser, portalShortcode, studyShortcode);
34+
35+
StudyEnvironment studyEnvironment =
36+
studyEnvironmentService.findByStudy(studyShortcode, environmentName).get();
37+
38+
return kitRequestService.getSampleKitsByStudyEnvironment(studyEnvironment);
39+
}
40+
}

api-admin/src/main/resources/api/openapi.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,37 @@ paths:
888888
format: binary
889889
'500':
890890
$ref: '#/components/responses/ServerError'
891+
/api/portals/v1/{portalShortcode}/studies/{studyShortcode}/env/{envName}/enrolleesWithKits:
892+
get:
893+
summary: Gets a list of enrollees with tasks and kit requests
894+
tags: [ enrollee ]
895+
operationId: enrolleesWithKits
896+
parameters:
897+
- { name: portalShortcode, in: path, required: true, schema: { type: string } }
898+
- { name: studyShortcode, in: path, required: true, schema: { type: string } }
899+
- { name: envName, in: path, required: true, schema: { type: string } }
900+
responses:
901+
'200':
902+
description: enrollee list
903+
content: { application/json: { schema: { type: object } } }
904+
'500':
905+
$ref: '#/components/responses/ServerError'
906+
/api/portals/v1/{portalShortcode}/studies/{studyShortcode}/env/{envName}/kits:
907+
get:
908+
summary: Gets a list of kits for a study
909+
tags: [ kit ]
910+
operationId: kitsByStudyEnvironment
911+
parameters:
912+
- { name: portalShortcode, in: path, required: true, schema: { type: string } }
913+
- { name: studyShortcode, in: path, required: true, schema: { type: string } }
914+
- { name: envName, in: path, required: true, schema: { type: string } }
915+
responses:
916+
'200':
917+
description: kit list
918+
content: { application/json: { schema: { type: object } } }
919+
'500':
920+
$ref: '#/components/responses/ServerError'
921+
891922
components:
892923
responses:
893924
SystemStatusResponse:
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package bio.terra.pearl.api.admin.service.kit;
2+
3+
import static org.mockito.ArgumentMatchers.any;
4+
import static org.mockito.Mockito.when;
5+
6+
import bio.terra.pearl.api.admin.BaseSpringBootTest;
7+
import bio.terra.pearl.api.admin.service.AuthUtilService;
8+
import bio.terra.pearl.core.model.EnvironmentName;
9+
import bio.terra.pearl.core.model.admin.AdminUser;
10+
import bio.terra.pearl.core.service.exception.PermissionDeniedException;
11+
import org.junit.jupiter.api.Assertions;
12+
import org.junit.jupiter.api.Test;
13+
import org.springframework.beans.factory.annotation.Autowired;
14+
import org.springframework.boot.test.mock.mockito.MockBean;
15+
import org.springframework.transaction.annotation.Transactional;
16+
17+
public class KitExtServiceTests extends BaseSpringBootTest {
18+
@Autowired private KitExtService kitExtService;
19+
20+
@Test
21+
@Transactional
22+
public void testGetKitRequestsForStudyEnvironmentRequiresAdmin() {
23+
when(mockAuthUtilService.authUserToStudy(any(), any(), any()))
24+
.thenThrow(new PermissionDeniedException(""));
25+
AdminUser adminUser = new AdminUser();
26+
Assertions.assertThrows(
27+
PermissionDeniedException.class,
28+
() ->
29+
kitExtService.getKitRequestsByStudyEnvironment(
30+
adminUser, "someportal", "somestudy", EnvironmentName.sandbox));
31+
}
32+
33+
@MockBean private AuthUtilService mockAuthUtilService;
34+
}

core/src/main/java/bio/terra/pearl/core/dao/BaseJdbiDao.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import java.time.LocalDate;
99
import java.util.*;
1010
import java.util.stream.Collectors;
11+
import java.util.stream.Stream;
12+
1113
import lombok.Getter;
1214
import org.apache.commons.lang3.StringUtils;
1315
import org.jdbi.v3.core.Jdbi;
@@ -248,6 +250,15 @@ protected List<T> findAllByProperty(String columnName, Object columnValue) {
248250
);
249251
}
250252

253+
protected Stream<T> streamAllByProperty(String columnName, Object columnValue) {
254+
return jdbi.withHandle(handle ->
255+
handle.createQuery("select * from " + tableName + " where " + columnName + " = :columnValue;")
256+
.bind("columnValue", columnValue)
257+
.mapTo(clazz)
258+
.stream()
259+
);
260+
}
261+
251262
protected List<T> findAllByPropertyCollection(String columnName, Collection<?> columnValues) {
252263
if (columnValues.isEmpty()) {
253264
// short circuit this case because bindList errors if list is empty
@@ -261,6 +272,19 @@ protected List<T> findAllByPropertyCollection(String columnName, Collection<?> c
261272
);
262273
}
263274

275+
protected Stream<T> streamAllByPropertyCollection(String columnName, Collection<?> columnValues) {
276+
if (columnValues.isEmpty()) {
277+
// short circuit this case because bindList errors if list is empty
278+
return Stream.empty();
279+
}
280+
return jdbi.withHandle(handle ->
281+
handle.createQuery("select * from " + tableName + " where " + columnName + " IN (<columnValues>);")
282+
.bindList("columnValues", columnValues)
283+
.mapTo(clazz)
284+
.stream()
285+
);
286+
}
287+
264288
protected List<T> findAllByPropertySorted(String columnName, Object columnValue, String sortProperty, String sortDir) {
265289
return jdbi.withHandle(handle ->
266290
handle.createQuery("select * from " + tableName + " where " + columnName + " = :columnValue"

core/src/main/java/bio/terra/pearl/core/dao/kit/KitRequestDao.java

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,55 @@
66
import org.jdbi.v3.core.Jdbi;
77
import org.springframework.stereotype.Component;
88

9-
import java.util.List;
10-
import java.util.UUID;
9+
import java.util.*;
10+
import java.util.stream.Collectors;
1111

1212
@Component
1313
public class KitRequestDao extends BaseMutableJdbiDao<KitRequest> {
1414
public KitRequestDao(Jdbi jdbi) {
1515
super(jdbi);
1616
}
1717

18+
private final String BASE_QUERY_BY_STUDY =
19+
" select " + prefixedGetQueryColumns("kit") + " from " + tableName + " kit "
20+
+ " join enrollee on kit.enrollee_id = enrollee.id "
21+
+ " join study_environment on enrollee.study_environment_id = study_environment.id "
22+
+ " where study_environment.id = :studyEnvironmentId ";
23+
1824
@Override
1925
protected Class<KitRequest> getClazz() { return KitRequest.class; }
2026

2127
public List<KitRequest> findByEnrollee(UUID enrolleeId) {
2228
return super.findAllByProperty("enrollee_id", enrolleeId);
2329
}
2430

31+
public Map<UUID, List<KitRequest>> findByEnrolleeIds(Collection<UUID> enrolleeIds) {
32+
return streamAllByPropertyCollection("enrollee_id", enrolleeIds)
33+
.collect(Collectors.groupingBy(KitRequest::getEnrolleeId, Collectors.toList()));
34+
}
35+
2536
/**
2637
* Find all kits that are not complete (or errored) for a study.
2738
* This represents the set of in-flight kits that we want to keep an eye on in Pepper.
2839
*/
29-
public List<KitRequest> findIncompleteKits(UUID studyEnvironmentId) {
40+
public List<KitRequest> findByStatus(UUID studyEnvironmentId, List<KitRequestStatus> statuses) {
41+
return jdbi.withHandle(handle ->
42+
handle.createQuery(BASE_QUERY_BY_STUDY +
43+
" and kit.status in (<kitStatuses>) ")
44+
.bind("studyEnvironmentId", studyEnvironmentId)
45+
.bindList("kitStatuses", statuses)
46+
.mapTo(clazz)
47+
.list()
48+
);
49+
}
50+
51+
/**
52+
* Find all kits for a study (environment).
53+
*/
54+
public List<KitRequest> findByStudyEnvironment(UUID studyEnvironmentId) {
3055
return jdbi.withHandle(handle ->
31-
handle.createQuery(" select " + prefixedGetQueryColumns("kit") + " from " + tableName + " kit " +
32-
" join enrollee on kit.enrollee_id = enrollee.id " +
33-
" join study_environment on enrollee.study_environment_id = study_environment.id " +
34-
" where study_environment.id = :studyEnvironmentId " +
35-
" and kit.status in (<kitStatuses>) ")
56+
handle.createQuery(BASE_QUERY_BY_STUDY)
3657
.bind("studyEnvironmentId", studyEnvironmentId)
37-
.bindList("kitStatuses", KitRequestStatus.NON_TERMINAL_STATES)
3858
.mapTo(clazz)
3959
.list()
4060
);

core/src/main/java/bio/terra/pearl/core/dao/participant/EnrolleeDao.java

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@
88
import bio.terra.pearl.core.dao.survey.SurveyResponseDao;
99
import bio.terra.pearl.core.model.kit.KitRequest;
1010
import bio.terra.pearl.core.model.participant.Enrollee;
11-
import java.util.List;
12-
import java.util.Optional;
13-
import java.util.UUID;
11+
12+
import java.util.*;
13+
import java.util.stream.Collectors;
14+
import java.util.stream.Stream;
15+
1416
import org.jdbi.v3.core.Jdbi;
17+
import org.springframework.data.util.Pair;
1518
import org.springframework.stereotype.Component;
19+
import org.springframework.transaction.annotation.Transactional;
1620

1721
@Component
1822
public class EnrolleeDao extends BaseMutableJdbiDao<Enrollee> {
@@ -58,6 +62,11 @@ public List<Enrollee> findByStudyEnvironmentId(UUID studyEnvironmentId) {
5862
return findAllByProperty("study_environment_id", studyEnvironmentId);
5963
}
6064

65+
@Transactional
66+
public Stream<Enrollee> streamByStudyEnvironmentId(UUID studyEnvironmentId) {
67+
return streamAllByProperty("study_environment_id", studyEnvironmentId);
68+
}
69+
6170
public List<Enrollee> findAllByShortcodes(List<String> shortcodes) {
6271
return findAllByPropertyCollection("shortcode", shortcodes);
6372
}
@@ -117,6 +126,40 @@ public Enrollee loadForAdminView(Enrollee enrollee) {
117126
return enrollee;
118127
}
119128

129+
/**
130+
* Fetches enrollees, loading all details needed for the kit management view -- currently tasks and kits.
131+
* Reduces database round-trips by fetching entities from each table and performing in-memory joins.
132+
* Uses Streams to reduce the number of iterations over collections of entities:
133+
* - Streams enrollees into two lists: enrollees and enrollee IDs
134+
* - avoids separately collecting IDs from entities
135+
* - retains order of results (not otherwise guaranteed when using something like Collectors.toMap())
136+
* - Streams tasks and kits into maps grouped by enrollee ID
137+
* - avoids separate iteration to build these maps
138+
* All that remains is a single traversal through the enrollee list to attach their tasks and kits.
139+
*/
140+
@Transactional
141+
public List<Enrollee> findForKitManagement(UUID studyEnvironmentId) {
142+
var enrolleesAndIds = streamByStudyEnvironmentId(studyEnvironmentId).collect(Collectors.teeing(
143+
Collectors.toList(),
144+
Collectors.mapping(Enrollee::getId, Collectors.toList()),
145+
Pair::of
146+
));
147+
148+
var enrollees = enrolleesAndIds.getFirst();
149+
var enrolleeIds = enrolleesAndIds.getSecond();
150+
151+
var tasksByEnrolleeId = participantTaskDao.findByEnrolleeIds(enrolleeIds);
152+
var kitsByEnrolleeId = kitRequestDao.findByEnrolleeIds(enrolleeIds);
153+
154+
enrollees.forEach(enrollee -> {
155+
// Be sure to set empty collections to indicate that they are empty instead of not initialized
156+
enrollee.setParticipantTasks(tasksByEnrolleeId.getOrDefault(enrollee.getId(), Collections.emptySet()));
157+
enrollee.setKitRequests(kitsByEnrolleeId.getOrDefault(enrollee.getId(), Collections.emptyList()));
158+
});
159+
160+
return enrollees;
161+
}
162+
120163
public int countByStudyEnvironment(UUID studyEnvironmentId) {
121164
return countByProperty("study_environment_id", studyEnvironmentId);
122165
}

0 commit comments

Comments
 (0)