Skip to content

Commit 8be6e7e

Browse files
Merge pull request #13152 from SORMAS-Foundation/#13150-sutomatic-manual-case-classification-ustomization-per-disease
#13150 automatic manual case classification customization per disease
2 parents bde6b27 + 32c6b7d commit 8be6e7e

File tree

9 files changed

+198
-32
lines changed

9 files changed

+198
-32
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* SORMAS® - Surveillance Outbreak Response Management & Analysis System
3+
* Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
* You should have received a copy of the GNU General Public License
13+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
14+
*/
15+
16+
package de.symeda.sormas.api;
17+
18+
public enum CaseClassificationCalculationMode {
19+
20+
DISABLED(false, false),
21+
MANUAL(true, false),
22+
AUTOMATIC(false, true),
23+
MANUAL_AND_AUTOMATIC(true, true);
24+
25+
private final boolean manualEnabled;
26+
private final boolean automaticEnabled;
27+
28+
CaseClassificationCalculationMode(boolean manualEnabled, boolean automaticEnabled) {
29+
this.manualEnabled = manualEnabled;
30+
this.automaticEnabled = automaticEnabled;
31+
}
32+
33+
public boolean isManualEnabled() {
34+
return manualEnabled;
35+
}
36+
37+
public boolean isAutomaticEnabled() {
38+
return automaticEnabled;
39+
}
40+
}

sormas-api/src/main/java/de/symeda/sormas/api/ConfigFacade.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ public interface ConfigFacade {
4040

4141
String getSormasStatsUrl();
4242

43-
boolean isFeatureAutomaticCaseClassification();
44-
4543
String getEmailSenderAddress();
4644

4745
String getEmailSenderName();
@@ -159,4 +157,8 @@ public interface ConfigFacade {
159157
void resetRequestContext();
160158

161159
String[] getAllowedFileExtensions();
160+
161+
CaseClassificationCalculationMode getCaseClassificationCalculationMode(Disease disease);
162+
163+
boolean isAnyCaseClassificationCalculationEnabled();
162164
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2066,7 +2066,7 @@ private void updateTasksOnCaseChanged(Case newCase, CaseDataDto existingCase) {
20662066
@PermitAll
20672067
public void onCaseSampleChanged(Case associatedCase) {
20682068
// Update case classification if the feature is enabled
2069-
if (configFacade.isFeatureAutomaticCaseClassification()) {
2069+
if (configFacade.getCaseClassificationCalculationMode(associatedCase.getDisease()).isAutomaticEnabled()) {
20702070
if (associatedCase.getCaseClassification() != CaseClassification.NO_CASE) {
20712071
Long pathogenTestsCount = pathogenTestService.countByCase(associatedCase);
20722072
if (pathogenTestsCount == 0) {
@@ -2263,7 +2263,7 @@ private void handleClassificationOnCaseChange(CaseDataDto existingDto, Case save
22632263
// Update case classification if the feature is enabled
22642264
CaseClassification classification = null;
22652265
boolean setClassificationInfo = true;
2266-
if (configFacade.isFeatureAutomaticCaseClassification()) {
2266+
if (configFacade.getCaseClassificationCalculationMode(savedCase.getDisease()).isAutomaticEnabled()) {
22672267
if (savedCase.getCaseClassification() != CaseClassification.NO_CASE
22682268
|| configFacade.isConfiguredCountry(CountryHelper.COUNTRY_CODE_LUXEMBOURG)) {
22692269
// calculate classification

sormas-backend/src/main/java/de/symeda/sormas/backend/common/ConfigFacadeEjb.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535

3636
import com.google.common.collect.Lists;
3737

38+
import de.symeda.sormas.api.CaseClassificationCalculationMode;
3839
import de.symeda.sormas.api.ConfigFacade;
40+
import de.symeda.sormas.api.Disease;
3941
import de.symeda.sormas.api.Language;
4042
import de.symeda.sormas.api.RequestContextHolder;
4143
import de.symeda.sormas.api.RequestContextTO;
@@ -86,7 +88,8 @@ public class ConfigFacadeEjb implements ConfigFacade {
8688
public static final String APP_URL = "app.url";
8789
public static final String APP_LEGACY_URL = "app.legacy.url";
8890

89-
public static final String FEATURE_AUTOMATIC_CASE_CLASSIFICATION = "feature.automaticcaseclassification";
91+
public static final String CASE_CLASSIFICATION_CALCULATION_PREFIX = "caseClassification.";
92+
public static final String CASE_CLASSIFICATION_CALCULATION_ALL = CASE_CLASSIFICATION_CALCULATION_PREFIX + "ALL";
9093

9194
public static final String DOCUMENT_FILES_PATH = "documents.path";
9295
public static final String TEMP_FILES_PATH = "temp.path";
@@ -235,6 +238,10 @@ protected long getLong(String name, long defaultValue) {
235238
return parseProperty(name, defaultValue, Long::parseLong);
236239
}
237240

241+
protected <T extends Enum<T>> T getEnumValue(String name, Class<T> enumType, T defaultValue) {
242+
return parseProperty(name, defaultValue, value -> Enum.valueOf(enumType, value));
243+
}
244+
238245
@Override
239246
public String getCountryName() {
240247

@@ -404,8 +411,37 @@ public String getRScriptExecutable() {
404411
}
405412

406413
@Override
407-
public boolean isFeatureAutomaticCaseClassification() {
408-
return getBoolean(FEATURE_AUTOMATIC_CASE_CLASSIFICATION, true);
414+
public CaseClassificationCalculationMode getCaseClassificationCalculationMode(Disease disease) {
415+
CaseClassificationCalculationMode diseaseConfig =
416+
getEnumValue(CASE_CLASSIFICATION_CALCULATION_PREFIX + disease.name(), CaseClassificationCalculationMode.class, null);
417+
if (diseaseConfig == null) {
418+
diseaseConfig = getCaseClassificationCalculationModeForAllDiseases();
419+
}
420+
421+
return diseaseConfig;
422+
}
423+
424+
private CaseClassificationCalculationMode getCaseClassificationCalculationModeForAllDiseases() {
425+
return getEnumValue(
426+
CASE_CLASSIFICATION_CALCULATION_ALL,
427+
CaseClassificationCalculationMode.class,
428+
CaseClassificationCalculationMode.MANUAL_AND_AUTOMATIC);
429+
}
430+
431+
@Override
432+
public boolean isAnyCaseClassificationCalculationEnabled() {
433+
if (getCaseClassificationCalculationModeForAllDiseases() != CaseClassificationCalculationMode.DISABLED) {
434+
return true;
435+
}
436+
437+
List<String> classificationCalculationProps =
438+
props.stringPropertyNames().stream().filter(p -> p.startsWith(CASE_CLASSIFICATION_CALCULATION_PREFIX)).collect(Collectors.toList());
439+
440+
if (classificationCalculationProps.isEmpty()) {
441+
return true;
442+
}
443+
444+
return classificationCalculationProps.stream().anyMatch(p -> !CaseClassificationCalculationMode.DISABLED.name().equals(props.getProperty(p)));
409445
}
410446

411447
@Override

sormas-backend/src/test/java/de/symeda/sormas/backend/caze/CaseClassificationLogicTest.java

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
import java.util.List;
2626

2727
import org.jboss.weld.exceptions.UnsupportedOperationException;
28-
import org.junit.jupiter.api.BeforeEach;
2928
import org.junit.jupiter.api.Test;
3029

30+
import de.symeda.sormas.api.CaseClassificationCalculationMode;
3131
import de.symeda.sormas.api.CountryHelper;
3232
import de.symeda.sormas.api.Disease;
3333
import de.symeda.sormas.api.caze.CaseClassification;
@@ -62,11 +62,6 @@
6262

6363
public class CaseClassificationLogicTest extends AbstractBeanTest {
6464

65-
@BeforeEach
66-
public void enableAutomaticCaseClassification() {
67-
MockProducer.getProperties().setProperty(ConfigFacadeEjb.FEATURE_AUTOMATIC_CASE_CLASSIFICATION, "true");
68-
}
69-
7065
@Test
7166
public void testAutomaticClassificationForEVD() {
7267

@@ -1180,6 +1175,46 @@ public void testCalculationReferenceDefinitionNonGermanServer() {
11801175
assertEquals(null, caze.getCaseReferenceDefinition());
11811176
}
11821177

1178+
@Test
1179+
public void testCalculationByDisease() {
1180+
MockProducer.getProperties()
1181+
.setProperty(ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_ALL, CaseClassificationCalculationMode.DISABLED.name());
1182+
CaseDataDto caze = buildSuspectCase(Disease.CORONAVIRUS);
1183+
caze = getCaseFacade().save(caze);
1184+
caze = getCaseFacade().getCaseDataByUuid(caze.getUuid());
1185+
assertEquals(CaseClassification.NOT_CLASSIFIED, caze.getCaseClassification());
1186+
1187+
MockProducer.getProperties()
1188+
.setProperty(
1189+
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS.getName(),
1190+
CaseClassificationCalculationMode.AUTOMATIC.name());
1191+
caze = getCaseFacade().save(buildSuspectCase(Disease.CORONAVIRUS));
1192+
assertEquals(CaseClassification.SUSPECT, caze.getCaseClassification());
1193+
1194+
MockProducer.getProperties()
1195+
.setProperty(
1196+
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS.getName(),
1197+
CaseClassificationCalculationMode.MANUAL_AND_AUTOMATIC.name());
1198+
caze = getCaseFacade().save(buildSuspectCase(Disease.CORONAVIRUS));
1199+
assertEquals(CaseClassification.SUSPECT, caze.getCaseClassification());
1200+
1201+
MockProducer.getProperties()
1202+
.setProperty(
1203+
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS.getName(),
1204+
CaseClassificationCalculationMode.DISABLED.name());
1205+
caze = getCaseFacade().save(buildSuspectCase(Disease.CORONAVIRUS));
1206+
assertEquals(CaseClassification.NOT_CLASSIFIED, caze.getCaseClassification());
1207+
1208+
MockProducer.getProperties()
1209+
.setProperty(ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_ALL, CaseClassificationCalculationMode.AUTOMATIC.name());
1210+
MockProducer.getProperties()
1211+
.setProperty(
1212+
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS.getName(),
1213+
CaseClassificationCalculationMode.DISABLED.name());
1214+
caze = getCaseFacade().save(buildSuspectCase(Disease.CORONAVIRUS));
1215+
assertEquals(CaseClassification.NOT_CLASSIFIED, caze.getCaseClassification());
1216+
}
1217+
11831218
/**
11841219
* Sets all symptoms with the SymptomState type to YES.
11851220
*/

sormas-backend/src/test/java/de/symeda/sormas/backend/common/ConfigFacadeEjbTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
import org.junit.jupiter.api.Test;
3131
import org.mockito.Mockito;
3232

33+
import de.symeda.sormas.api.CaseClassificationCalculationMode;
3334
import de.symeda.sormas.api.ConfigFacade;
35+
import de.symeda.sormas.api.Disease;
3436
import de.symeda.sormas.api.utils.InfoProvider;
3537
import de.symeda.sormas.backend.AbstractBeanTest;
3638
import de.symeda.sormas.backend.MockProducer;
@@ -185,4 +187,51 @@ public void testPatientDiaryConfigTokenLifetime() {
185187
MockProducer.getProperties().setProperty(ConfigFacadeEjb.INTERFACE_PATIENT_DIARY_TOKEN_LIFETIME, "666");
186188
assertThat(getConfigFacade().getPatientDiaryConfig().getTokenLifetime(), equalTo(Duration.ofSeconds(666L)));
187189
}
190+
191+
@Test
192+
public void testHasAnyCaseClassificationCalculationEnabled() {
193+
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(true));
194+
195+
MockProducer.getProperties()
196+
.setProperty(ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_ALL, CaseClassificationCalculationMode.DISABLED.name());
197+
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(false));
198+
199+
MockProducer.getProperties()
200+
.setProperty(
201+
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS,
202+
CaseClassificationCalculationMode.MANUAL.name());
203+
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(true));
204+
205+
MockProducer.getProperties()
206+
.setProperty(
207+
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS,
208+
CaseClassificationCalculationMode.AUTOMATIC.name());
209+
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(true));
210+
211+
MockProducer.getProperties()
212+
.setProperty(
213+
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS,
214+
CaseClassificationCalculationMode.MANUAL_AND_AUTOMATIC.name());
215+
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(true));
216+
217+
MockProducer.getProperties()
218+
.setProperty(
219+
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS,
220+
CaseClassificationCalculationMode.DISABLED.name());
221+
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(false));
222+
223+
MockProducer.getProperties()
224+
.setProperty(
225+
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CHOLERA,
226+
CaseClassificationCalculationMode.AUTOMATIC.name());
227+
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(true));
228+
229+
MockProducer.getProperties().remove(ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_ALL);
230+
MockProducer.getProperties().remove(ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CHOLERA);
231+
MockProducer.getProperties()
232+
.setProperty(
233+
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS,
234+
CaseClassificationCalculationMode.DISABLED.name());
235+
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(true));
236+
}
188237
}

sormas-base/setup/sormas.properties

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,15 @@ app.url=
127127
# Possible Values: true, false
128128
#devmode=false
129129

130-
# Determines whether cases are automatically classified according to a specific ruleset based on their disease.
131-
# Please note that automatic case classification is not necessarily available for every disease.
132-
# Default: true
133-
# Possible Values: true, false
134-
#feature.automaticcaseclassification=true
130+
# Configuration for case classification calculation for all diseases.
131+
# Default: MANUAL_AND_AUTOMATIC
132+
# Possible values: DISABLED, MANUAL, AUTOMATIC, MANUAL_AND_AUTOMATIC
133+
#caseClassification.ALL=AUTOMATIC
134+
# Configuration for case classification calculation for specific diseases.
135+
# Default: taken from caseClassification.ALL
136+
# Possible values: DISABLED, MANUAL, AUTOMATIC, MANUAL_AND_AUTOMATIC
137+
#caseClassification.CORONAVIRUS=MANUAL_AND_AUTOMATIC
138+
#caseClassification.CHOLERA=DISABLED
135139

136140
# Number of days after which system events are deleted from the database. An example for a system event is the last date at which data from an external service was pulled.
137141
# default: 90

sormas-ui/src/main/java/de/symeda/sormas/ui/AboutView.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ private List<String> listCustomDocumentsFiles() {
333333
}
334334

335335
private boolean shouldShowClassificationDocumentLink() {
336-
return FacadeProvider.getConfigFacade().isFeatureAutomaticCaseClassification()
336+
return FacadeProvider.getConfigFacade().isAnyCaseClassificationCalculationEnabled()
337337
&& FacadeProvider.getFeatureConfigurationFacade().isFeatureEnabled(FeatureType.CASE_SURVEILANCE);
338338
}
339339

sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -498,19 +498,6 @@ protected void addFields() {
498498
ComboBox caseReferenceDefinition = addField(CaseDataDto.CASE_REFERENCE_DEFINITION, ComboBox.class);
499499
caseReferenceDefinition.setReadOnly(true);
500500

501-
if (diseaseClassificationExists()) {
502-
Button caseClassificationCalculationButton = ButtonHelper.createButton(Captions.caseClassificationCalculationButton, e -> {
503-
CaseClassification classification = FacadeProvider.getCaseClassificationFacade().getClassification(getValue());
504-
((Field<CaseClassification>) getField(CaseDataDto.CASE_CLASSIFICATION)).setValue(classification);
505-
}, ValoTheme.BUTTON_PRIMARY, FORCE_CAPTION);
506-
507-
getContent().addComponent(caseClassificationCalculationButton, CASE_CLASSIFICATION_CALCULATE_BTN_LOC);
508-
509-
if (!UiUtil.permitted(UserRight.CASE_CLASSIFY)) {
510-
caseClassificationCalculationButton.setEnabled(false);
511-
}
512-
}
513-
514501
//if(cbCaseClassification.getCaption())
515502
addField(CaseDataDto.NOT_A_CASE_REASON_NEGATIVE_TEST, CheckBox.class);
516503
addField(CaseDataDto.NOT_A_CASE_REASON_PHYSICIAN_INFORMATION, CheckBox.class);
@@ -536,6 +523,19 @@ protected void addFields() {
536523
caseClassificationGroup.removeItem(CaseClassification.CONFIRMED_UNKNOWN_SYMPTOMS);
537524
}
538525

526+
if (diseaseClassificationExists() && FacadeProvider.getConfigFacade().getCaseClassificationCalculationMode(disease).isManualEnabled()) {
527+
Button caseClassificationCalculationButton = ButtonHelper.createButton(Captions.caseClassificationCalculationButton, e -> {
528+
CaseClassification classification = FacadeProvider.getCaseClassificationFacade().getClassification(getValue());
529+
((Field<CaseClassification>) getField(CaseDataDto.CASE_CLASSIFICATION)).setValue(classification);
530+
}, ValoTheme.BUTTON_PRIMARY, FORCE_CAPTION);
531+
532+
getContent().addComponent(caseClassificationCalculationButton, CASE_CLASSIFICATION_CALCULATE_BTN_LOC);
533+
534+
if (!UiUtil.permitted(UserRight.CASE_CLASSIFY)) {
535+
caseClassificationCalculationButton.setEnabled(false);
536+
}
537+
}
538+
539539
boolean extendedClassification = FacadeProvider.getDiseaseConfigurationFacade().usesExtendedClassification(disease);
540540

541541
if (extendedClassification) {

0 commit comments

Comments
 (0)