Skip to content

Commit

Permalink
[JN-417] Implement basic kit management UI (#477)
Browse files Browse the repository at this point in the history
Co-authored-by: Matt Bemis <[email protected]>
  • Loading branch information
breilly2 and MatthewBemis authored Aug 2, 2023
1 parent c855e6b commit 9ff8b5d
Show file tree
Hide file tree
Showing 44 changed files with 1,218 additions and 144 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import bio.terra.pearl.api.admin.api.EnrolleeApi;
import bio.terra.pearl.api.admin.service.AuthUtilService;
import bio.terra.pearl.api.admin.service.enrollee.EnrolleeExtService;
import bio.terra.pearl.core.model.EnvironmentName;
import bio.terra.pearl.core.model.admin.AdminUser;
import bio.terra.pearl.core.model.kit.KitRequest;
import bio.terra.pearl.core.model.participant.Enrollee;
Expand Down Expand Up @@ -79,5 +80,17 @@ public ResponseEntity<Object> getKitRequests(
return ResponseEntity.ok(kitRequests);
}

@Override
public ResponseEntity<Object> enrolleesWithKits(
String portalShortcode, String studyShortcode, String envName) {
AdminUser adminUser = authUtilService.requireAdminUser(request);
EnvironmentName environmentName = EnvironmentName.valueOfCaseInsensitive(envName);

var enrollees =
enrolleeExtService.findForKitManagement(
adminUser, portalShortcode, studyShortcode, environmentName);
return ResponseEntity.ok(enrollees);
}

public record WithdrawnResponse(UUID withdrawnEnrolleeId) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package bio.terra.pearl.api.admin.controller.kit;

import bio.terra.pearl.api.admin.api.KitApi;
import bio.terra.pearl.api.admin.service.AuthUtilService;
import bio.terra.pearl.api.admin.service.kit.KitExtService;
import bio.terra.pearl.core.model.EnvironmentName;
import bio.terra.pearl.core.model.admin.AdminUser;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;

@Controller
public class KitController implements KitApi {
private final AuthUtilService authUtilService;
private final KitExtService kitExtService;
private final HttpServletRequest request;

public KitController(
AuthUtilService authUtilService, KitExtService kitExtService, HttpServletRequest request) {
this.authUtilService = authUtilService;
this.kitExtService = kitExtService;
this.request = request;
}

@Override
public ResponseEntity<Object> kitsByStudyEnvironment(
String portalShortcode, String studyShortcode, String envName) {
AdminUser adminUser = authUtilService.requireAdminUser(request);
EnvironmentName environmentName = EnvironmentName.valueOfCaseInsensitive(envName);

var kits =
kitExtService.getKitRequestsByStudyEnvironment(
adminUser, portalShortcode, studyShortcode, environmentName);

return ResponseEntity.ok(kits);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ public List<EnrolleeSearchResult> search(
return enrolleeSearchService.search(studyShortcode, environmentName, facets);
}

public List<Enrollee> findForKitManagement(
AdminUser user,
String portalShortcode,
String studyShortcode,
EnvironmentName environmentName) {
authUtilService.authUserToStudy(user, portalShortcode, studyShortcode);
return enrolleeService.findForKitManagement(studyShortcode, environmentName);
}

public Enrollee findWithAdminLoad(AdminUser user, String enrolleeShortcode) {
Enrollee enrollee = authUtilService.authAdminUserToEnrollee(user, enrolleeShortcode);
return enrolleeService.loadForAdminView(enrollee);
Expand Down Expand Up @@ -90,6 +99,6 @@ public KitRequest requestKit(AdminUser adminUser, String enrolleeShortcode, Stri

public Collection<KitRequest> getKitRequests(AdminUser adminUser, String enrolleeShortcode) {
Enrollee enrollee = authUtilService.authAdminUserToEnrollee(adminUser, enrolleeShortcode);
return kitRequestService.getKitRequests(adminUser, enrollee);
return kitRequestService.getKitRequests(enrollee);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package bio.terra.pearl.api.admin.service.kit;

import bio.terra.pearl.api.admin.service.AuthUtilService;
import bio.terra.pearl.core.model.EnvironmentName;
import bio.terra.pearl.core.model.admin.AdminUser;
import bio.terra.pearl.core.model.kit.KitRequest;
import bio.terra.pearl.core.model.study.StudyEnvironment;
import bio.terra.pearl.core.service.kit.KitRequestService;
import bio.terra.pearl.core.service.study.StudyEnvironmentService;
import java.util.Collection;
import org.springframework.stereotype.Service;

@Service
public class KitExtService {
private final AuthUtilService authUtilService;
private final KitRequestService kitRequestService;
private final StudyEnvironmentService studyEnvironmentService;

public KitExtService(
AuthUtilService authUtilService,
KitRequestService kitRequestService,
StudyEnvironmentService studyEnvironmentService) {
this.authUtilService = authUtilService;
this.kitRequestService = kitRequestService;
this.studyEnvironmentService = studyEnvironmentService;
}

public Collection<KitRequest> getKitRequestsByStudyEnvironment(
AdminUser adminUser,
String portalShortcode,
String studyShortcode,
EnvironmentName environmentName) {
authUtilService.authUserToStudy(adminUser, portalShortcode, studyShortcode);

StudyEnvironment studyEnvironment =
studyEnvironmentService.findByStudy(studyShortcode, environmentName).get();

return kitRequestService.getSampleKitsByStudyEnvironment(studyEnvironment);
}
}
31 changes: 31 additions & 0 deletions api-admin/src/main/resources/api/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,37 @@ paths:
format: binary
'500':
$ref: '#/components/responses/ServerError'
/api/portals/v1/{portalShortcode}/studies/{studyShortcode}/env/{envName}/enrolleesWithKits:
get:
summary: Gets a list of enrollees with tasks and kit requests
tags: [ enrollee ]
operationId: enrolleesWithKits
parameters:
- { name: portalShortcode, in: path, required: true, schema: { type: string } }
- { name: studyShortcode, in: path, required: true, schema: { type: string } }
- { name: envName, in: path, required: true, schema: { type: string } }
responses:
'200':
description: enrollee list
content: { application/json: { schema: { type: object } } }
'500':
$ref: '#/components/responses/ServerError'
/api/portals/v1/{portalShortcode}/studies/{studyShortcode}/env/{envName}/kits:
get:
summary: Gets a list of kits for a study
tags: [ kit ]
operationId: kitsByStudyEnvironment
parameters:
- { name: portalShortcode, in: path, required: true, schema: { type: string } }
- { name: studyShortcode, in: path, required: true, schema: { type: string } }
- { name: envName, in: path, required: true, schema: { type: string } }
responses:
'200':
description: kit list
content: { application/json: { schema: { type: object } } }
'500':
$ref: '#/components/responses/ServerError'

components:
responses:
SystemStatusResponse:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package bio.terra.pearl.api.admin.service.kit;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import bio.terra.pearl.api.admin.BaseSpringBootTest;
import bio.terra.pearl.api.admin.service.AuthUtilService;
import bio.terra.pearl.core.model.EnvironmentName;
import bio.terra.pearl.core.model.admin.AdminUser;
import bio.terra.pearl.core.service.exception.PermissionDeniedException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.transaction.annotation.Transactional;

public class KitExtServiceTests extends BaseSpringBootTest {
@Autowired private KitExtService kitExtService;

@Test
@Transactional
public void testGetKitRequestsForStudyEnvironmentRequiresAdmin() {
when(mockAuthUtilService.authUserToStudy(any(), any(), any()))
.thenThrow(new PermissionDeniedException(""));
AdminUser adminUser = new AdminUser();
Assertions.assertThrows(
PermissionDeniedException.class,
() ->
kitExtService.getKitRequestsByStudyEnvironment(
adminUser, "someportal", "somestudy", EnvironmentName.sandbox));
}

@MockBean private AuthUtilService mockAuthUtilService;
}
24 changes: 24 additions & 0 deletions core/src/main/java/bio/terra/pearl/core/dao/BaseJdbiDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.jdbi.v3.core.Jdbi;
Expand Down Expand Up @@ -248,6 +250,15 @@ protected List<T> findAllByProperty(String columnName, Object columnValue) {
);
}

protected Stream<T> streamAllByProperty(String columnName, Object columnValue) {
return jdbi.withHandle(handle ->
handle.createQuery("select * from " + tableName + " where " + columnName + " = :columnValue;")
.bind("columnValue", columnValue)
.mapTo(clazz)
.stream()
);
}

protected List<T> findAllByPropertyCollection(String columnName, Collection<?> columnValues) {
if (columnValues.isEmpty()) {
// short circuit this case because bindList errors if list is empty
Expand All @@ -261,6 +272,19 @@ protected List<T> findAllByPropertyCollection(String columnName, Collection<?> c
);
}

protected Stream<T> streamAllByPropertyCollection(String columnName, Collection<?> columnValues) {
if (columnValues.isEmpty()) {
// short circuit this case because bindList errors if list is empty
return Stream.empty();
}
return jdbi.withHandle(handle ->
handle.createQuery("select * from " + tableName + " where " + columnName + " IN (<columnValues>);")
.bindList("columnValues", columnValues)
.mapTo(clazz)
.stream()
);
}

protected List<T> findAllByPropertySorted(String columnName, Object columnValue, String sortProperty, String sortDir) {
return jdbi.withHandle(handle ->
handle.createQuery("select * from " + tableName + " where " + columnName + " = :columnValue"
Expand Down
38 changes: 29 additions & 9 deletions core/src/main/java/bio/terra/pearl/core/dao/kit/KitRequestDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,55 @@
import org.jdbi.v3.core.Jdbi;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.UUID;
import java.util.*;
import java.util.stream.Collectors;

@Component
public class KitRequestDao extends BaseMutableJdbiDao<KitRequest> {
public KitRequestDao(Jdbi jdbi) {
super(jdbi);
}

private final String BASE_QUERY_BY_STUDY =
" select " + prefixedGetQueryColumns("kit") + " from " + tableName + " kit "
+ " join enrollee on kit.enrollee_id = enrollee.id "
+ " join study_environment on enrollee.study_environment_id = study_environment.id "
+ " where study_environment.id = :studyEnvironmentId ";

@Override
protected Class<KitRequest> getClazz() { return KitRequest.class; }

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

public Map<UUID, List<KitRequest>> findByEnrolleeIds(Collection<UUID> enrolleeIds) {
return streamAllByPropertyCollection("enrollee_id", enrolleeIds)
.collect(Collectors.groupingBy(KitRequest::getEnrolleeId, Collectors.toList()));
}

/**
* Find all kits that are not complete (or errored) for a study.
* This represents the set of in-flight kits that we want to keep an eye on in Pepper.
*/
public List<KitRequest> findIncompleteKits(UUID studyEnvironmentId) {
public List<KitRequest> findByStatus(UUID studyEnvironmentId, List<KitRequestStatus> statuses) {
return jdbi.withHandle(handle ->
handle.createQuery(BASE_QUERY_BY_STUDY +
" and kit.status in (<kitStatuses>) ")
.bind("studyEnvironmentId", studyEnvironmentId)
.bindList("kitStatuses", statuses)
.mapTo(clazz)
.list()
);
}

/**
* Find all kits for a study (environment).
*/
public List<KitRequest> findByStudyEnvironment(UUID studyEnvironmentId) {
return jdbi.withHandle(handle ->
handle.createQuery(" select " + prefixedGetQueryColumns("kit") + " from " + tableName + " kit " +
" join enrollee on kit.enrollee_id = enrollee.id " +
" join study_environment on enrollee.study_environment_id = study_environment.id " +
" where study_environment.id = :studyEnvironmentId " +
" and kit.status in (<kitStatuses>) ")
handle.createQuery(BASE_QUERY_BY_STUDY)
.bind("studyEnvironmentId", studyEnvironmentId)
.bindList("kitStatuses", KitRequestStatus.NON_TERMINAL_STATES)
.mapTo(clazz)
.list()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
import bio.terra.pearl.core.dao.survey.SurveyResponseDao;
import bio.terra.pearl.core.model.kit.KitRequest;
import bio.terra.pearl.core.model.participant.Enrollee;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jdbi.v3.core.Jdbi;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class EnrolleeDao extends BaseMutableJdbiDao<Enrollee> {
Expand Down Expand Up @@ -58,6 +62,11 @@ public List<Enrollee> findByStudyEnvironmentId(UUID studyEnvironmentId) {
return findAllByProperty("study_environment_id", studyEnvironmentId);
}

@Transactional
public Stream<Enrollee> streamByStudyEnvironmentId(UUID studyEnvironmentId) {
return streamAllByProperty("study_environment_id", studyEnvironmentId);
}

public List<Enrollee> findAllByShortcodes(List<String> shortcodes) {
return findAllByPropertyCollection("shortcode", shortcodes);
}
Expand Down Expand Up @@ -117,6 +126,40 @@ public Enrollee loadForAdminView(Enrollee enrollee) {
return enrollee;
}

/**
* Fetches enrollees, loading all details needed for the kit management view -- currently tasks and kits.
* Reduces database round-trips by fetching entities from each table and performing in-memory joins.
* Uses Streams to reduce the number of iterations over collections of entities:
* - Streams enrollees into two lists: enrollees and enrollee IDs
* - avoids separately collecting IDs from entities
* - retains order of results (not otherwise guaranteed when using something like Collectors.toMap())
* - Streams tasks and kits into maps grouped by enrollee ID
* - avoids separate iteration to build these maps
* All that remains is a single traversal through the enrollee list to attach their tasks and kits.
*/
@Transactional
public List<Enrollee> findForKitManagement(UUID studyEnvironmentId) {
var enrolleesAndIds = streamByStudyEnvironmentId(studyEnvironmentId).collect(Collectors.teeing(
Collectors.toList(),
Collectors.mapping(Enrollee::getId, Collectors.toList()),
Pair::of
));

var enrollees = enrolleesAndIds.getFirst();
var enrolleeIds = enrolleesAndIds.getSecond();

var tasksByEnrolleeId = participantTaskDao.findByEnrolleeIds(enrolleeIds);
var kitsByEnrolleeId = kitRequestDao.findByEnrolleeIds(enrolleeIds);

enrollees.forEach(enrollee -> {
// Be sure to set empty collections to indicate that they are empty instead of not initialized
enrollee.setParticipantTasks(tasksByEnrolleeId.getOrDefault(enrollee.getId(), Collections.emptySet()));
enrollee.setKitRequests(kitsByEnrolleeId.getOrDefault(enrollee.getId(), Collections.emptyList()));
});

return enrollees;
}

public int countByStudyEnvironment(UUID studyEnvironmentId) {
return countByProperty("study_environment_id", studyEnvironmentId);
}
Expand Down
Loading

0 comments on commit 9ff8b5d

Please sign in to comment.