Skip to content

Commit ad3a0b5

Browse files
authored
Merge pull request #13990 from SORMAS-Foundation/feature/13948-filters
Filter cases and the sample dashboard by pathogen test result, serogr…
2 parents dc03030 + 531a6ef commit ad3a0b5

15 files changed

Lines changed: 650 additions & 74 deletions

File tree

sormas-api/src/main/java/de/symeda/sormas/api/caze/CaseCriteria.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import de.symeda.sormas.api.person.PersonReferenceDto;
3636
import de.symeda.sormas.api.person.PresentCondition;
3737
import de.symeda.sormas.api.person.SymptomJournalStatus;
38+
import de.symeda.sormas.api.sample.PathogenTestResultType;
3839
import de.symeda.sormas.api.share.ExternalShareCriteria;
3940
import de.symeda.sormas.api.survey.SurveyReferenceDto;
4041
import de.symeda.sormas.api.user.UserReferenceDto;
@@ -92,10 +93,14 @@ public class CaseCriteria extends CriteriaWithDateType implements ExternalShareC
9293
public static final String SURVEY_ASSIGNED_TO = "surveyAssignedTo";
9394
public static final String SURVEY_RESPONSE_STATUS = "surveyResponseStatus";
9495
public static final String SURVEY = "survey";
96+
public static final String PATHOGEN_TEST_RESULT = "pathogenTestResult";
97+
public static final String SEROGROUP = "serogroup";
9598

9699
private UserRoleReferenceDto reportingUserRole;
97100
private Disease disease;
98101
private DiseaseVariant diseaseVariant;
102+
private PathogenTestResultType pathogenTestResult;
103+
private String serogroup;
99104
private CaseOutcome outcome;
100105
private CaseClassification caseClassification;
101106
private InvestigationStatus investigationStatus;
@@ -235,6 +240,33 @@ public DiseaseVariant getDiseaseVariant() {
235240
return diseaseVariant;
236241
}
237242

243+
public void setPathogenTestResult(PathogenTestResultType pathogenTestResult) {
244+
this.pathogenTestResult = pathogenTestResult;
245+
}
246+
247+
public CaseCriteria pathogenTestResult(PathogenTestResultType pathogenTestResult) {
248+
setPathogenTestResult(pathogenTestResult);
249+
return this;
250+
}
251+
252+
public PathogenTestResultType getPathogenTestResult() {
253+
return pathogenTestResult;
254+
}
255+
256+
public void setSerogroup(String serogroup) {
257+
this.serogroup = serogroup;
258+
}
259+
260+
public CaseCriteria serogroup(String serogroup) {
261+
setSerogroup(serogroup);
262+
return this;
263+
}
264+
265+
@IgnoreForUrl
266+
public String getSerogroup() {
267+
return serogroup;
268+
}
269+
238270
public void setJurisdictionType(CaseJurisdictionType jurisdictionType) {
239271
this.jurisdictionType = jurisdictionType;
240272
}

sormas-api/src/main/java/de/symeda/sormas/api/dashboard/SampleDashboardCriteria.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515

1616
package de.symeda.sormas.api.dashboard;
1717

18+
import de.symeda.sormas.api.disease.DiseaseVariant;
1819
import de.symeda.sormas.api.environment.environmentsample.EnvironmentSampleMaterial;
20+
import de.symeda.sormas.api.sample.PathogenTestResultType;
1921
import de.symeda.sormas.api.sample.SampleDashboardFilterDateType;
2022
import de.symeda.sormas.api.sample.SampleMaterial;
2123

@@ -24,6 +26,9 @@ public class SampleDashboardCriteria extends BaseDashboardCriteria<SampleDashboa
2426
private SampleDashboardFilterDateType sampleDateType;
2527
private SampleMaterial sampleMaterial;
2628
private EnvironmentSampleMaterial environmentSampleMaterial;
29+
private PathogenTestResultType pathogenTestResult;
30+
private String serogroup;
31+
private DiseaseVariant diseaseVariant;
2732

2833
private Boolean withNoDisease;
2934

@@ -60,12 +65,44 @@ public SampleDashboardCriteria withNoDisease(Boolean withNoDisease) {
6065

6166
return self;
6267
}
68+
6369
public EnvironmentSampleMaterial getEnvironmentSampleMaterial() {
6470
return environmentSampleMaterial;
6571
}
72+
6673
public SampleDashboardCriteria environmentSampleMaterial(EnvironmentSampleMaterial environmentSampleMaterial) {
6774
this.environmentSampleMaterial = environmentSampleMaterial;
6875

6976
return self;
7077
}
78+
79+
public PathogenTestResultType getPathogenTestResult() {
80+
return pathogenTestResult;
81+
}
82+
83+
public SampleDashboardCriteria pathogenTestResult(PathogenTestResultType pathogenTestResult) {
84+
this.pathogenTestResult = pathogenTestResult;
85+
86+
return self;
87+
}
88+
89+
public String getSerogroup() {
90+
return serogroup;
91+
}
92+
93+
public SampleDashboardCriteria serogroup(String serogroup) {
94+
this.serogroup = serogroup;
95+
96+
return self;
97+
}
98+
99+
public DiseaseVariant getDiseaseVariant() {
100+
return diseaseVariant;
101+
}
102+
103+
public SampleDashboardCriteria diseaseVariant(DiseaseVariant diseaseVariant) {
104+
this.diseaseVariant = diseaseVariant;
105+
106+
return self;
107+
}
71108
}

sormas-api/src/main/java/de/symeda/sormas/api/i18n/Captions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2894,6 +2894,7 @@ public interface Captions {
28942894
String selfReportDeletedEnvironments = "selfReportDeletedEnvironments";
28952895
String selfReportProcess = "selfReportProcess";
28962896
String selfReportSelfReportsList = "selfReportSelfReportsList";
2897+
String serogroup = "serogroup";
28972898
String sex = "sex";
28982899
String showPlacesOnMap = "showPlacesOnMap";
28992900
String singleDayEventDate = "singleDayEventDate";

sormas-api/src/main/resources/captions.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ epiWeekTo=To Epi Week
3232
facilityType=Facility type
3333
facilityTypeGroup=Facility category
3434
featureConfiguration=Feature Configuration
35+
serogroup=Serogroup
3536
firstName=First name
3637
sex=Sex
3738
nationalHealthId=National health ID

sormas-backend/src/main/java/de/symeda/sormas/backend/caze/CaseService.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
import de.symeda.sormas.backend.location.Location;
159159
import de.symeda.sormas.backend.person.Person;
160160
import de.symeda.sormas.backend.person.PersonQueryContext;
161+
import de.symeda.sormas.backend.sample.PathogenTest;
161162
import de.symeda.sormas.backend.sample.Sample;
162163
import de.symeda.sormas.backend.sample.SampleJoins;
163164
import de.symeda.sormas.backend.sample.SampleService;
@@ -706,6 +707,28 @@ public <T extends AbstractDomainObject> Predicate createCriteriaFilter(CaseCrite
706707
filter =
707708
CriteriaBuilderHelper.and(cb, filter, cb.equal(from.get(Case.DISEASE_VARIANT_VALUE), caseCriteria.getDiseaseVariant().getValue()));
708709
}
710+
if (caseCriteria.getPathogenTestResult() != null) {
711+
Subquery<Long> resultSubquery = cq.subquery(Long.class);
712+
Root<Sample> sampleRoot = resultSubquery.from(Sample.class);
713+
resultSubquery.select(sampleRoot.get(AbstractDomainObject.ID));
714+
resultSubquery.where(
715+
cb.equal(sampleRoot.get(Sample.ASSOCIATED_CASE), from),
716+
cb.equal(sampleRoot.get(Sample.PATHOGEN_TEST_RESULT), caseCriteria.getPathogenTestResult()),
717+
cb.isFalse(sampleRoot.get(DeletableAdo.DELETED)));
718+
filter = CriteriaBuilderHelper.and(cb, filter, cb.exists(resultSubquery));
719+
}
720+
if (StringUtils.isNotBlank(caseCriteria.getSerogroup())) {
721+
Subquery<Long> serogroupSubquery = cq.subquery(Long.class);
722+
Root<Sample> sampleRoot = serogroupSubquery.from(Sample.class);
723+
Join<Sample, PathogenTest> pathogenTestJoin = sampleRoot.join(Sample.PATHOGENTESTS, JoinType.INNER);
724+
serogroupSubquery.select(sampleRoot.get(AbstractDomainObject.ID));
725+
serogroupSubquery.where(
726+
cb.equal(sampleRoot.get(Sample.ASSOCIATED_CASE), from),
727+
CriteriaBuilderHelper.unaccentedIlike(cb, pathogenTestJoin.get(PathogenTest.SEROTYPE_TEXT), caseCriteria.getSerogroup().trim()),
728+
cb.isFalse(sampleRoot.get(DeletableAdo.DELETED)),
729+
cb.isFalse(pathogenTestJoin.get(DeletableAdo.DELETED)));
730+
filter = CriteriaBuilderHelper.and(cb, filter, cb.exists(serogroupSubquery));
731+
}
709732
if (caseCriteria.getOutcome() != null) {
710733
filter = CriteriaBuilderHelper.and(cb, filter, cb.equal(from.get(Case.OUTCOME), caseCriteria.getOutcome()));
711734
}
@@ -2045,7 +2068,7 @@ private Predicate getSimilarityFilters(CaseSimilarityCriteria criteria, Criteria
20452068

20462069
/**
20472070
* Performance: May be slow when there are 10000s of cases with similar report date in the same region.
2048-
*
2071+
*
20492072
* @param limit
20502073
* null: no limit
20512074
*/
@@ -2338,7 +2361,7 @@ private List<Case> getCasesSetAsDuplicate(Long caseId) {
23382361
/**
23392362
* Updates the vaccination status of all cases of the specified person and disease using
23402363
* vaccination status data derived from immunization records.
2341-
*
2364+
*
23422365
* @param personId
23432366
* The ID of the case person whose cases should be updated
23442367
* @param disease
@@ -2352,7 +2375,7 @@ public void updateVaccinationStatuses(Long personId, Disease disease, Vaccinatio
23522375

23532376
/**
23542377
* Updates vaccination statuses using the enhanced determination logic (determined mode).
2355-
*
2378+
*
23562379
* <p>
23572380
* This method updates cases with sophisticated vaccination statuses derived from immunization data.
23582381
* It uses {@link ImmunizationService#deriveVaccinationStatus} to compute statuses that reflect:
@@ -2363,12 +2386,12 @@ public void updateVaccinationStatuses(Long personId, Disease disease, Vaccinatio
23632386
* <li>Other immunity sources (OTHER status)</li>
23642387
* <li>Immunization validity periods (validFrom/validUntil dates)</li>
23652388
* </ul>
2366-
*
2389+
*
23672390
* <p>
23682391
* The method only updates cases where the derived status differs from the current status,
23692392
* and automatically updates the change date timestamp.
23702393
* </p>
2371-
*
2394+
*
23722395
* @param personId
23732396
* The ID of the person whose cases should be updated
23742397
* @param disease

sormas-backend/src/main/java/de/symeda/sormas/backend/dashboard/sample/SampleDashboardService.java

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,15 @@
4545
import javax.persistence.criteria.Selection;
4646
import javax.persistence.criteria.Subquery;
4747

48-
import de.symeda.sormas.api.environment.environmentsample.EnvironmentSampleMaterial;
48+
import org.apache.commons.lang3.StringUtils;
4949
import org.apache.commons.lang3.tuple.Pair;
5050
import org.jetbrains.annotations.NotNull;
5151

5252
import de.symeda.sormas.api.dashboard.SampleDashboardCriteria;
5353
import de.symeda.sormas.api.dashboard.sample.MapSampleDto;
5454
import de.symeda.sormas.api.dashboard.sample.SampleShipmentStatus;
5555
import de.symeda.sormas.api.environment.environmentsample.EnvironmentSampleCriteria;
56+
import de.symeda.sormas.api.environment.environmentsample.EnvironmentSampleMaterial;
5657
import de.symeda.sormas.api.sample.PathogenTestResultType;
5758
import de.symeda.sormas.api.sample.SampleAssociationType;
5859
import de.symeda.sormas.api.sample.SampleCriteria;
@@ -150,15 +151,17 @@ public Map<SpecimenCondition, Long> getSampleCountsBySpecimenCondition(SampleDas
150151

151152
public Map<SpecimenCondition, Long> getEnvironmentalSampleCountsBySpecimenCondition(SampleDashboardCriteria dashboardCriteria) {
152153
return getEnvironmentalSampleCountsBySpecimenCondition(
153-
EnvironmentSample.SPECIMEN_CONDITION,
154-
SpecimenCondition.class,
155-
dashboardCriteria,
156-
null);
154+
EnvironmentSample.SPECIMEN_CONDITION,
155+
SpecimenCondition.class,
156+
dashboardCriteria,
157+
null);
157158
}
158159

159-
private Map<SpecimenCondition, Long> getEnvironmentalSampleCountsBySpecimenCondition(String property, Class<SpecimenCondition> propertyType,
160-
SampleDashboardCriteria dashboardCriteria,
161-
BiFunction<CriteriaBuilder, Root<EnvironmentSample>, Predicate> additionalFilters) {
160+
private Map<SpecimenCondition, Long> getEnvironmentalSampleCountsBySpecimenCondition(
161+
String property,
162+
Class<SpecimenCondition> propertyType,
163+
SampleDashboardCriteria dashboardCriteria,
164+
BiFunction<CriteriaBuilder, Root<EnvironmentSample>, Predicate> additionalFilters) {
162165
final CriteriaBuilder cb = em.getCriteriaBuilder();
163166
final CriteriaQuery<Tuple> cq = cb.createTupleQuery();
164167
final Root<EnvironmentSample> sample = cq.from(EnvironmentSample.class);
@@ -173,9 +176,9 @@ private Map<SpecimenCondition, Long> getEnvironmentalSampleCountsBySpecimenCondi
173176
cq.groupBy(groupingProperty);
174177

175178
return QueryHelper.getResultList(em, cq, null, null, Function.identity())
176-
.stream()
177-
.filter(t -> t.get(0) != null)
178-
.collect(Collectors.toMap(t -> propertyType.cast(t.get(0)), t -> (Long) t.get(1)));
179+
.stream()
180+
.filter(t -> t.get(0) != null)
181+
.collect(Collectors.toMap(t -> propertyType.cast(t.get(0)), t -> (Long) t.get(1)));
179182
}
180183

181184
private <T extends Enum<?>> Map<T, Long> getSampleCountsByEnumProperty(
@@ -219,8 +222,8 @@ public Map<SampleShipmentStatus, Long> getSampleCountsByShipmentStatus(SampleDas
219222
cq.groupBy(shipped, received);
220223

221224
return QueryHelper.getResultList(em, cq, null, null, Function.identity())
222-
.stream()
223-
.collect(Collectors.toMap(t -> getSampleShipmentStatusByFlags((Boolean) t.get(0), (Boolean) t.get(1)), t -> (Long) t.get(2), Long::sum));
225+
.stream()
226+
.collect(Collectors.toMap(t -> getSampleShipmentStatusByFlags((Boolean) t.get(0), (Boolean) t.get(1)), t -> (Long) t.get(2), Long::sum));
224227
}
225228

226229
public Map<SampleShipmentStatus, Long> getEnvironmentalSampleCountsByShipmentStatus(SampleDashboardCriteria dashboardCriteria) {
@@ -238,8 +241,8 @@ public Map<SampleShipmentStatus, Long> getEnvironmentalSampleCountsByShipmentSta
238241
cq.groupBy(shipped, received);
239242

240243
return QueryHelper.getResultList(em, cq, null, null, Function.identity())
241-
.stream()
242-
.collect(Collectors.toMap(t -> getSampleShipmentStatusByFlags((Boolean) t.get(0), (Boolean) t.get(1)), t -> (Long) t.get(2), Long::sum));
244+
.stream()
245+
.collect(Collectors.toMap(t -> getSampleShipmentStatusByFlags((Boolean) t.get(0), (Boolean) t.get(1)), t -> (Long) t.get(2), Long::sum));
243246
}
244247

245248
private SampleShipmentStatus getSampleShipmentStatusByFlags(Boolean shipped, Boolean received) {
@@ -255,7 +258,10 @@ public Map<PathogenTestResultType, Long> getTestResultCountsByResultType(SampleD
255258

256259
cq.multiselect(pathogenTestResult, cb.count(pathogenTestJoin));
257260

258-
final Predicate criteriaFilter = createSampleFilter(new SampleQueryContext(cb, cq, sample), dashboardCriteria);
261+
Predicate criteriaFilter = createSampleFilter(new SampleQueryContext(cb, cq, sample), dashboardCriteria);
262+
if (dashboardCriteria.getPathogenTestResult() != null) {
263+
criteriaFilter = CriteriaBuilderHelper.and(cb, criteriaFilter, cb.equal(pathogenTestResult, dashboardCriteria.getPathogenTestResult()));
264+
}
259265
cq.where(criteriaFilter);
260266

261267
cq.groupBy(pathogenTestResult);
@@ -286,9 +292,9 @@ public Map<PathogenTestResultType, Long> getEnvironmentalTestResultCountsByResul
286292
cq.groupBy(pathogenTestResult);
287293

288294
return QueryHelper.getResultList(em, cq, null, null, Function.identity())
289-
.stream()
290-
.filter(t -> t.get(0) != null)
291-
.collect(Collectors.toMap(t -> (PathogenTestResultType) t.get(0), t -> (Long) t.get(1)));
295+
.stream()
296+
.filter(t -> t.get(0) != null)
297+
.collect(Collectors.toMap(t -> (PathogenTestResultType) t.get(0), t -> (Long) t.get(1)));
292298
}
293299

294300
/**
@@ -304,13 +310,14 @@ public Map<EnvironmentSampleMaterial, Long> getEnvironmentalSampleCounts(SampleD
304310
final Root<EnvironmentSample> sample = cq.from(EnvironmentSample.class);
305311
final Path<Object> groupingProperty = sample.get(EnvironmentSample.SAMPLE_MATERIAL);
306312
cq.multiselect(groupingProperty, cb.count(sample));
307-
final Predicate criteriaFilter = createEnvironmentSampleFilter(new EnvironmentSampleQueryContext(cb, cq, sample, new EnvironmentSampleJoins(sample)), dashboardCriteria);
313+
final Predicate criteriaFilter =
314+
createEnvironmentSampleFilter(new EnvironmentSampleQueryContext(cb, cq, sample, new EnvironmentSampleJoins(sample)), dashboardCriteria);
308315
cq.where(criteriaFilter);
309316
cq.groupBy(groupingProperty);
310317
return QueryHelper.getResultList(em, cq, null, null, Function.identity())
311-
.stream()
312-
.filter(t -> t.get(0) != null)
313-
.collect(Collectors.toMap(t -> (EnvironmentSampleMaterial) t.get(0), t -> (Long) t.get(1)));
318+
.stream()
319+
.filter(t -> t.get(0) != null)
320+
.collect(Collectors.toMap(t -> (EnvironmentSampleMaterial) t.get(0), t -> (Long) t.get(1)));
314321
}
315322

316323
private static <J extends ISampleJoins> List<Selection<?>> getCoordinatesSelection(
@@ -510,6 +517,32 @@ private <T extends AbstractDomainObject> Predicate createSampleFilter(SampleQuer
510517
filter = CriteriaBuilderHelper.and(cb, filter, cb.equal(sampleRoot.get(Sample.SAMPLE_MATERIAL), criteria.getSampleMaterial()));
511518
}
512519

520+
// Test result is denormalized on the sample (final lab result) → direct equality, no join.
521+
if (criteria.getPathogenTestResult() != null) {
522+
filter = CriteriaBuilderHelper.and(cb, filter, cb.equal(sampleRoot.get(Sample.PATHOGEN_TEST_RESULT), criteria.getPathogenTestResult()));
523+
}
524+
525+
if (StringUtils.isNotBlank(criteria.getSerogroup())) {
526+
Subquery<Long> serogroupSubquery = cq.subquery(Long.class);
527+
Root<PathogenTest> pathogenTestRoot = serogroupSubquery.from(PathogenTest.class);
528+
serogroupSubquery.select(pathogenTestRoot.get(PathogenTest.ID));
529+
serogroupSubquery.where(
530+
cb.equal(pathogenTestRoot.get(PathogenTest.SAMPLE), sampleRoot),
531+
CriteriaBuilderHelper.unaccentedIlike(cb, pathogenTestRoot.get(PathogenTest.SEROTYPE_TEXT), criteria.getSerogroup().trim()),
532+
cb.isFalse(pathogenTestRoot.get(PathogenTest.DELETED)));
533+
filter = CriteriaBuilderHelper.and(cb, filter, cb.exists(serogroupSubquery));
534+
}
535+
if (criteria.getDiseaseVariant() != null) {
536+
Subquery<Long> variantSubquery = cq.subquery(Long.class);
537+
Root<PathogenTest> pathogenTestRoot = variantSubquery.from(PathogenTest.class);
538+
variantSubquery.select(pathogenTestRoot.get(PathogenTest.ID));
539+
variantSubquery.where(
540+
cb.equal(pathogenTestRoot.get(PathogenTest.SAMPLE), sampleRoot),
541+
cb.equal(pathogenTestRoot.get(PathogenTest.TESTED_DISEASE_VARIANT_VALUE), criteria.getDiseaseVariant().getValue()),
542+
cb.isFalse(pathogenTestRoot.get(PathogenTest.DELETED)));
543+
filter = CriteriaBuilderHelper.and(cb, filter, cb.exists(variantSubquery));
544+
}
545+
513546
if (Boolean.TRUE.equals(criteria.getWithNoDisease())) {
514547
filter = CriteriaBuilderHelper.and(cb, filter, cb.isNotNull(joins.getEventParticipant()), cb.isNull(joins.getEvent().get(Event.DISEASE)));
515548
} else if (Boolean.FALSE.equals(criteria.getWithNoDisease())) {
@@ -573,7 +606,8 @@ private Predicate createEnvironmentSampleFilter(EnvironmentSampleQueryContext qu
573606
}
574607

575608
if (criteria.getEnvironmentSampleMaterial() != null) {
576-
filter = CriteriaBuilderHelper.and(cb, filter, cb.equal(sampleRoot.get(EnvironmentSample.SAMPLE_MATERIAL), criteria.getEnvironmentSampleMaterial()));
609+
filter = CriteriaBuilderHelper
610+
.and(cb, filter, cb.equal(sampleRoot.get(EnvironmentSample.SAMPLE_MATERIAL), criteria.getEnvironmentSampleMaterial()));
577611
}
578612

579613
return CriteriaBuilderHelper.and(

sormas-backend/src/main/java/de/symeda/sormas/backend/sample/PathogenTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public class PathogenTest extends DeletableAdo {
8989
public static final String TEST_RESULT_VERIFIED = "testResultVerified";
9090
public static final String FOUR_FOLD_INCREASE_ANTIBODY_TITER = "fourFoldIncreaseAntibodyTiter";
9191
public static final String SEROTYPE = "serotype";
92+
public static final String SEROTYPE_TEXT = "serotypeText";
9293
public static final String CQ_VALUE = "cqValue";
9394
public static final String CT_VALUE_E = "ctValueE";
9495
public static final String CT_VALUE_N = "ctValueN";

0 commit comments

Comments
 (0)