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

Add result and condition filters to test results list #6682

Merged
merged 42 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
5775897
Refactor results table - each multiplex result on own row
nathancrtr Sep 5, 2023
c986570
Rename TestResultsList test titles to reflect refactor; remove obsole…
nathancrtr Sep 6, 2023
ca0c17f
Merge branch 'main' into nac/6104-refactor-results-table-addtl-diseases
nathancrtr Sep 8, 2023
d85a7a3
Add skeleton structure for returning Results by facility & organization
nathancrtr Sep 18, 2023
3fb4d9f
Add result service methods to return results with various filters app…
nathancrtr Sep 25, 2023
8985b84
Adds support for GraphQL query to filter and return granular test result
nathancrtr Sep 29, 2023
018b787
Defines query operation to retrieve filtered test results from server…
nathancrtr Sep 29, 2023
360dc13
Fix page & result count display; Tidy comments & un-used code
nathancrtr Oct 2, 2023
056d15c
Merge branch 'main' into nac/6303-results-table-add-condition-result-…
nathancrtr Oct 3, 2023
85ef1e2
Remove reference to non-existent generated GraphQL method
nathancrtr Oct 3, 2023
3fcb9df
Test changes - update many, many mocks and fudge some typing to get t…
nathancrtr Oct 4, 2023
9ac8f73
Fix reference to test id in mock
nathancrtr Oct 4, 2023
bd83caa
Update some more test results mocks
nathancrtr Oct 4, 2023
cbae0e4
Merge branch 'main' into nac/6303-results-table-add-condition-result-…
nathancrtr Oct 4, 2023
672eb13
Update sooooo many mocks, other test tweaks
nathancrtr Oct 5, 2023
83ca095
Merge branch 'main' into nac/6303-results-table-add-condition-result-…
nathancrtr Oct 5, 2023
543031a
comment out some junk so I can build
nathancrtr Oct 5, 2023
bfe6c5e
Broke out new ResultService tests into nested block to keep setup sep…
nathancrtr Oct 6, 2023
791eb0b
Correct mocks for ResultsTable unit tests
nathancrtr Oct 11, 2023
861e88d
Merge branch 'main' into nac/6303-results-table-add-condition-result-…
nathancrtr Oct 11, 2023
5b5b151
Update test result CSV download mock
nathancrtr Oct 11, 2023
ca25a6e
Actually correct mock response for CSV results download
nathancrtr Oct 11, 2023
d98c65b
Cleanup....
nathancrtr Oct 12, 2023
7c2fd17
Add ResultService test for date range filter; clean up some code smells
nathancrtr Oct 17, 2023
c221c12
Remove unused file
nathancrtr Oct 17, 2023
169a96a
More code smell cleanup, on the frontend this time
nathancrtr Oct 17, 2023
e2f9607
Remove unread local variables in unit test file
nathancrtr Oct 17, 2023
db26d6e
Add some (not super-useful) unit tests for ResultResolver
nathancrtr Oct 18, 2023
6b86674
Get patient link via GraphQL query for test result row in e2e tests
nathancrtr Oct 18, 2023
55fad84
Merge branch 'main' into nac/6303-results-table-add-condition-result-…
nathancrtr Oct 18, 2023
cba21bd
Un-comment things from testing
nathancrtr Oct 18, 2023
93a470a
Run login hooks for patient link e2e test - needed for one-off GraphQ…
nathancrtr Oct 18, 2023
a101869
Add and employ a GraphQL query called from Cypress to get patient lin…
nathancrtr Oct 18, 2023
526d4ee
Try to get the right envvar for the SR frontend...
nathancrtr Oct 19, 2023
2b1be0b
Ditto
nathancrtr Oct 19, 2023
eab7484
once more, with feeling
nathancrtr Oct 19, 2023
fbdd80c
Get rid of some stuff that should not be there
nathancrtr Oct 19, 2023
34346c5
Remove some commented-out code and unused test result item properties
nathancrtr Oct 20, 2023
cc7f1bf
Remove unnecessary assertion in TestResultsList unit tests
nathancrtr Oct 20, 2023
224a1e9
Codegen
nathancrtr Oct 20, 2023
76e3733
Remove some unused properties on mock objects for ResultsTable
nathancrtr Oct 20, 2023
8152475
Merge branch 'main' into nac/6303-results-table-add-condition-result-…
nathancrtr Oct 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package gov.cdc.usds.simplereport.api.testresult;

import gov.cdc.usds.simplereport.api.model.ApiFacility;
import gov.cdc.usds.simplereport.db.model.auxiliary.TestResultsListItem;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;

@Controller
public class ResultDataResolver {

@SchemaMapping(typeName = "Result", field = "facility")
public ApiFacility getFacility(TestResultsListItem result) {
return new ApiFacility(result.getFacility());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package gov.cdc.usds.simplereport.api.testresult;

import gov.cdc.usds.simplereport.api.Translators;
import gov.cdc.usds.simplereport.db.model.SupportedDisease;
import gov.cdc.usds.simplereport.db.model.auxiliary.TestResultsListItem;
import gov.cdc.usds.simplereport.service.DiseaseService;
import gov.cdc.usds.simplereport.service.ResultService;
import gov.cdc.usds.simplereport.service.TestOrderService;
import java.util.Date;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;

@Controller
@RequiredArgsConstructor
public class ResultResolver {

private final ResultService service;
private final DiseaseService diseaseService;

@QueryMapping
public Page<TestResultsListItem> resultsPage(
@Argument UUID facilityId,
@Argument UUID patientId,
@Argument String result,
@Argument String role,
@Argument String disease,
@Argument Date startDate,
@Argument Date endDate,
@Argument int pageNumber,
@Argument int pageSize) {

if (pageNumber < 0) {
pageNumber = TestOrderService.DEFAULT_PAGINATION_PAGEOFFSET;
}

if (pageSize < 1) {
pageSize = TestOrderService.DEFAULT_PAGINATION_PAGESIZE;
}

SupportedDisease supportedDisease =
disease != null ? diseaseService.getDiseaseByName(disease) : null;

if (facilityId == null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

remove if statement and only use getFacilityResults

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a clean way to move this null conditional into the service instead of having it in the resolver? This was related to our other comment about combining getOrganizationResults and getFacilityResults, although not sure whether the different auth requirements complicates this then

Copy link
Contributor Author

@nathancrtr nathancrtr Oct 23, 2023

Choose a reason for hiding this comment

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

I think having this conditional in the resolver makes sense for the auth reasons discussed elsewhere.

Fwiw, this pattern (and the associated auth requirements) are lifted one-to-one from the existing TestResultResolver and TestOrderService classes, which these changes largely supplant:

if (facilityId == null) {
return tos.getOrganizationTestEventsResults(
patientId,
Translators.parseTestResult(result),
Translators.parsePersonRole(role, true),
startDate,
endDate,
pageNumber,
pageSize);
}
return tos.getFacilityTestEventsResults(
facilityId,
patientId,
Translators.parseTestResult(result),
Translators.parsePersonRole(role, true),
startDate,
endDate,
pageNumber,
pageSize);
}

return service
.getOrganizationResults(
patientId,
Translators.parseTestResult(result),
Translators.parsePersonRole(role, true),
supportedDisease,
startDate,
endDate,
pageNumber,
pageSize)
.map(TestResultsListItem::new);
}

return service
.getFacilityResults(
facilityId,
patientId,
Translators.parseTestResult(result),
Translators.parsePersonRole(role, true),
supportedDisease,
startDate,
endDate,
pageNumber,
pageSize)
.map(TestResultsListItem::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package gov.cdc.usds.simplereport.db.model.auxiliary;

import gov.cdc.usds.simplereport.db.model.ApiUser;
import gov.cdc.usds.simplereport.db.model.DeviceType;
import gov.cdc.usds.simplereport.db.model.Facility;
import gov.cdc.usds.simplereport.db.model.Person;
import gov.cdc.usds.simplereport.db.model.Result;
import java.util.Date;
import java.util.UUID;
import lombok.Getter;

@Getter
public class TestResultsListItem {
private final UUID id;
private final Facility facility;
private final Person patient;
private final Date dateAdded;
private final Date dateUpdated;
private final DeviceType deviceType;
private final String disease;
private final TestResult testResult;
private final Date dateTested;
private final TestCorrectionStatus correctionStatus;
private final String reasonForCorrection;
private final ApiUser createdBy;

public TestResultsListItem(Result result) {
this.id = result.getTestEvent().getInternalId();
this.facility = result.getTestEvent().getFacility();
this.patient = result.getTestEvent().getPatient();
this.dateAdded = result.getTestEvent().getDateTested();
this.dateUpdated = result.getUpdatedAt();
this.deviceType = result.getTestEvent().getDeviceType();
this.disease = result.getDisease().getName();
this.testResult = result.getTestResult();
this.dateTested = result.getTestEvent().getDateTested();
this.correctionStatus = result.getTestEvent().getCorrectionStatus();
this.reasonForCorrection = result.getTestEvent().getReasonForCorrection();
this.createdBy = result.getTestEvent().getCreatedBy();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
import gov.cdc.usds.simplereport.db.model.TestOrder;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;

public interface ResultRepository extends EternalAuditedEntityRepository<Result> {

Page<Result> findAll(Specification<Result> searchSpec, Pageable p);

List<Result> findAllByTestEvent(TestEvent testEvent);

List<Result> findAllByTestOrder(TestOrder testOrder);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
package gov.cdc.usds.simplereport.service;

import gov.cdc.usds.simplereport.config.AuthorizationConfiguration;
import gov.cdc.usds.simplereport.db.model.AuditedEntity_;
import gov.cdc.usds.simplereport.db.model.BaseTestInfo_;
import gov.cdc.usds.simplereport.db.model.IdentifiedEntity_;
import gov.cdc.usds.simplereport.db.model.Person;
import gov.cdc.usds.simplereport.db.model.Person_;
import gov.cdc.usds.simplereport.db.model.Result;
import gov.cdc.usds.simplereport.db.model.Result_;
import gov.cdc.usds.simplereport.db.model.SupportedDisease;
import gov.cdc.usds.simplereport.db.model.TestEvent;
import gov.cdc.usds.simplereport.db.model.TestEvent_;
import gov.cdc.usds.simplereport.db.model.TestOrder;
import gov.cdc.usds.simplereport.db.model.auxiliary.PersonRole;
import gov.cdc.usds.simplereport.db.model.auxiliary.TestResult;
import gov.cdc.usds.simplereport.db.repository.ResultRepository;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Predicate;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -16,6 +35,140 @@
@Transactional
public class ResultService {
private final ResultRepository resultRepository;
private final DiseaseService diseaseService;
private final OrganizationService organizationService;

private Specification<Result> buildResultSearchFilter(
UUID facilityId,
UUID patientId,
TestResult result,
PersonRole role,
SupportedDisease disease,
Date startDate,
Date endDate) {
return (root, query, cb) -> {
Join<Result, TestEvent> testEventJoin = root.join(Result_.testEvent);
Join<TestEvent, Person> personJoin = testEventJoin.join(TestEvent_.patient);
Predicate p = cb.conjunction();

query.orderBy(cb.desc(root.get(AuditedEntity_.createdAt)));
query.distinct(true);

if (facilityId != null) {
p =
cb.and(
p,
cb.equal(
testEventJoin.get(BaseTestInfo_.facility).get(IdentifiedEntity_.internalId),
facilityId));
} else {
final UUID finalOrgId = organizationService.getCurrentOrganization().getInternalId();

p =
cb.and(
p,
cb.equal(
testEventJoin.get(BaseTestInfo_.organization).get(IdentifiedEntity_.internalId),
finalOrgId));
}

if (patientId != null) {
p =
cb.and(
p,
cb.equal(
testEventJoin.get(BaseTestInfo_.patient).get(IdentifiedEntity_.internalId),
patientId));
}

if (result != null) {
p = cb.and(p, cb.equal(root.get(Result_.testResult), result));
}

if (role != null) {
p = cb.and(p, cb.equal(personJoin.get(Person_.role), role));
}

if (disease != null) {
p = cb.and(p, cb.equal(root.get(Result_.disease), disease));
}

if (startDate != null) {
p =
cb.and(
p,
cb.or(
cb.and(
cb.isNotNull(testEventJoin.get(BaseTestInfo_.dateTestedBackdate)),
cb.greaterThanOrEqualTo(
testEventJoin.get(BaseTestInfo_.dateTestedBackdate), startDate)),
cb.and(
cb.isNull(testEventJoin.get(BaseTestInfo_.dateTestedBackdate)),
cb.greaterThanOrEqualTo(
testEventJoin.get(AuditedEntity_.createdAt), startDate))));
}

if (endDate != null) {
p =
cb.and(
p,
cb.or(
cb.and(
cb.isNotNull(testEventJoin.get(BaseTestInfo_.dateTestedBackdate)),
cb.lessThanOrEqualTo(
testEventJoin.get(BaseTestInfo_.dateTestedBackdate), endDate)),
cb.and(
cb.isNull(testEventJoin.get(BaseTestInfo_.dateTestedBackdate)),
cb.lessThanOrEqualTo(
testEventJoin.get(AuditedEntity_.createdAt), endDate))));
}

return p;
};
}

@Transactional(readOnly = true)
@AuthorizationConfiguration.RequirePermissionViewAllFacilityResults
public Page<Result> getOrganizationResults(
nathancrtr marked this conversation as resolved.
Show resolved Hide resolved
UUID patientId,
TestResult result,
PersonRole role,
SupportedDisease supportedDisease,
Date startDate,
Date endDate,
int pageOffset,
int pageSize) {

PageRequest pageRequest =
PageRequest.of(pageOffset, pageSize, Sort.by("createdAt").descending());

return resultRepository.findAll(
buildResultSearchFilter(
null, patientId, result, role, supportedDisease, startDate, endDate),
pageRequest);
}

@Transactional(readOnly = true)
@AuthorizationConfiguration.RequirePermissionReadResultListAtFacility
public Page<Result> getFacilityResults(
UUID facilityId,
UUID patientId,
TestResult result,
PersonRole role,
SupportedDisease supportedDisease,
Date startDate,
Date endDate,
int pageOffset,
int pageSize) {

PageRequest pageRequest =
PageRequest.of(pageOffset, pageSize, Sort.by("createdAt").descending());

return resultRepository.findAll(
buildResultSearchFilter(
facilityId, patientId, result, role, supportedDisease, startDate, endDate),
pageRequest);
}

public TestEvent addResultsToTestEvent(TestEvent testEvent, Collection<Result> results) {
if (testEvent == null || results == null || results.isEmpty()) {
Expand Down
37 changes: 35 additions & 2 deletions backend/src/main/resources/graphql/main.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,9 @@ type TestOrder {
id: ID!
internalId: ID! @deprecated(reason: "alias for 'id'")
patient: Patient!
facility: Facility!
dateAdded: String!
dateUpdated: DateTime
pregnancy: String
noSymptoms: Boolean
symptoms: String
Expand Down Expand Up @@ -288,9 +290,29 @@ type TestResultsPage {
content: [TestResult]
}

type Result {
id: ID!
facility: Facility!
patient: Patient!
dateAdded: String!
dateUpdated: DateTime
deviceType: DeviceType!
disease: String!
testResult: String!
dateTested: DateTime!
correctionStatus: String
reasonForCorrection: String
createdBy: ApiUser
}

type ResultsPage {
totalElements: Int
content: [Result]
}

type MultiplexResult {
disease: SupportedDisease
testResult: String
disease: SupportedDisease!
testResult: String!
}

input MultiplexResultInput {
Expand Down Expand Up @@ -466,6 +488,17 @@ type Query {
): Int @requiredPermissions(allOf: ["READ_RESULT_LIST"])
testResult(id: ID!): TestResult
@requiredPermissions(allOf: ["READ_RESULT_LIST"])
resultsPage(
facilityId: ID
patientId: ID
result: String
role: String
disease: String
startDate: DateTime
endDate: DateTime
pageNumber: Int = 0
pageSize: Int = 5000
): ResultsPage @requiredPermissions(allOf: ["READ_RESULT_LIST"])
organizationLevelDashboardMetrics(
startDate: DateTime!
endDate: DateTime!
Expand Down
Loading
Loading