Skip to content

Commit

Permalink
Merge pull request #13152 from SORMAS-Foundation/#13150-sutomatic-man…
Browse files Browse the repository at this point in the history
…ual-case-classification-ustomization-per-disease

#13150 automatic manual case classification customization per disease
  • Loading branch information
leventegal-she authored Oct 14, 2024
2 parents bde6b27 + 32c6b7d commit 8be6e7e
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* SORMAS® - Surveillance Outbreak Response Management & Analysis System
* Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package de.symeda.sormas.api;

public enum CaseClassificationCalculationMode {

DISABLED(false, false),
MANUAL(true, false),
AUTOMATIC(false, true),
MANUAL_AND_AUTOMATIC(true, true);

private final boolean manualEnabled;
private final boolean automaticEnabled;

CaseClassificationCalculationMode(boolean manualEnabled, boolean automaticEnabled) {
this.manualEnabled = manualEnabled;
this.automaticEnabled = automaticEnabled;
}

public boolean isManualEnabled() {
return manualEnabled;
}

public boolean isAutomaticEnabled() {
return automaticEnabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ public interface ConfigFacade {

String getSormasStatsUrl();

boolean isFeatureAutomaticCaseClassification();

String getEmailSenderAddress();

String getEmailSenderName();
Expand Down Expand Up @@ -159,4 +157,8 @@ public interface ConfigFacade {
void resetRequestContext();

String[] getAllowedFileExtensions();

CaseClassificationCalculationMode getCaseClassificationCalculationMode(Disease disease);

boolean isAnyCaseClassificationCalculationEnabled();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2066,7 +2066,7 @@ private void updateTasksOnCaseChanged(Case newCase, CaseDataDto existingCase) {
@PermitAll
public void onCaseSampleChanged(Case associatedCase) {
// Update case classification if the feature is enabled
if (configFacade.isFeatureAutomaticCaseClassification()) {
if (configFacade.getCaseClassificationCalculationMode(associatedCase.getDisease()).isAutomaticEnabled()) {
if (associatedCase.getCaseClassification() != CaseClassification.NO_CASE) {
Long pathogenTestsCount = pathogenTestService.countByCase(associatedCase);
if (pathogenTestsCount == 0) {
Expand Down Expand Up @@ -2263,7 +2263,7 @@ private void handleClassificationOnCaseChange(CaseDataDto existingDto, Case save
// Update case classification if the feature is enabled
CaseClassification classification = null;
boolean setClassificationInfo = true;
if (configFacade.isFeatureAutomaticCaseClassification()) {
if (configFacade.getCaseClassificationCalculationMode(savedCase.getDisease()).isAutomaticEnabled()) {
if (savedCase.getCaseClassification() != CaseClassification.NO_CASE
|| configFacade.isConfiguredCountry(CountryHelper.COUNTRY_CODE_LUXEMBOURG)) {
// calculate classification
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@

import com.google.common.collect.Lists;

import de.symeda.sormas.api.CaseClassificationCalculationMode;
import de.symeda.sormas.api.ConfigFacade;
import de.symeda.sormas.api.Disease;
import de.symeda.sormas.api.Language;
import de.symeda.sormas.api.RequestContextHolder;
import de.symeda.sormas.api.RequestContextTO;
Expand Down Expand Up @@ -86,7 +88,8 @@ public class ConfigFacadeEjb implements ConfigFacade {
public static final String APP_URL = "app.url";
public static final String APP_LEGACY_URL = "app.legacy.url";

public static final String FEATURE_AUTOMATIC_CASE_CLASSIFICATION = "feature.automaticcaseclassification";
public static final String CASE_CLASSIFICATION_CALCULATION_PREFIX = "caseClassification.";
public static final String CASE_CLASSIFICATION_CALCULATION_ALL = CASE_CLASSIFICATION_CALCULATION_PREFIX + "ALL";

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

protected <T extends Enum<T>> T getEnumValue(String name, Class<T> enumType, T defaultValue) {
return parseProperty(name, defaultValue, value -> Enum.valueOf(enumType, value));
}

@Override
public String getCountryName() {

Expand Down Expand Up @@ -404,8 +411,37 @@ public String getRScriptExecutable() {
}

@Override
public boolean isFeatureAutomaticCaseClassification() {
return getBoolean(FEATURE_AUTOMATIC_CASE_CLASSIFICATION, true);
public CaseClassificationCalculationMode getCaseClassificationCalculationMode(Disease disease) {
CaseClassificationCalculationMode diseaseConfig =
getEnumValue(CASE_CLASSIFICATION_CALCULATION_PREFIX + disease.name(), CaseClassificationCalculationMode.class, null);
if (diseaseConfig == null) {
diseaseConfig = getCaseClassificationCalculationModeForAllDiseases();
}

return diseaseConfig;
}

private CaseClassificationCalculationMode getCaseClassificationCalculationModeForAllDiseases() {
return getEnumValue(
CASE_CLASSIFICATION_CALCULATION_ALL,
CaseClassificationCalculationMode.class,
CaseClassificationCalculationMode.MANUAL_AND_AUTOMATIC);
}

@Override
public boolean isAnyCaseClassificationCalculationEnabled() {
if (getCaseClassificationCalculationModeForAllDiseases() != CaseClassificationCalculationMode.DISABLED) {
return true;
}

List<String> classificationCalculationProps =
props.stringPropertyNames().stream().filter(p -> p.startsWith(CASE_CLASSIFICATION_CALCULATION_PREFIX)).collect(Collectors.toList());

if (classificationCalculationProps.isEmpty()) {
return true;
}

return classificationCalculationProps.stream().anyMatch(p -> !CaseClassificationCalculationMode.DISABLED.name().equals(props.getProperty(p)));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
import java.util.List;

import org.jboss.weld.exceptions.UnsupportedOperationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import de.symeda.sormas.api.CaseClassificationCalculationMode;
import de.symeda.sormas.api.CountryHelper;
import de.symeda.sormas.api.Disease;
import de.symeda.sormas.api.caze.CaseClassification;
Expand Down Expand Up @@ -62,11 +62,6 @@

public class CaseClassificationLogicTest extends AbstractBeanTest {

@BeforeEach
public void enableAutomaticCaseClassification() {
MockProducer.getProperties().setProperty(ConfigFacadeEjb.FEATURE_AUTOMATIC_CASE_CLASSIFICATION, "true");
}

@Test
public void testAutomaticClassificationForEVD() {

Expand Down Expand Up @@ -1180,6 +1175,46 @@ public void testCalculationReferenceDefinitionNonGermanServer() {
assertEquals(null, caze.getCaseReferenceDefinition());
}

@Test
public void testCalculationByDisease() {
MockProducer.getProperties()
.setProperty(ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_ALL, CaseClassificationCalculationMode.DISABLED.name());
CaseDataDto caze = buildSuspectCase(Disease.CORONAVIRUS);
caze = getCaseFacade().save(caze);
caze = getCaseFacade().getCaseDataByUuid(caze.getUuid());
assertEquals(CaseClassification.NOT_CLASSIFIED, caze.getCaseClassification());

MockProducer.getProperties()
.setProperty(
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS.getName(),
CaseClassificationCalculationMode.AUTOMATIC.name());
caze = getCaseFacade().save(buildSuspectCase(Disease.CORONAVIRUS));
assertEquals(CaseClassification.SUSPECT, caze.getCaseClassification());

MockProducer.getProperties()
.setProperty(
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS.getName(),
CaseClassificationCalculationMode.MANUAL_AND_AUTOMATIC.name());
caze = getCaseFacade().save(buildSuspectCase(Disease.CORONAVIRUS));
assertEquals(CaseClassification.SUSPECT, caze.getCaseClassification());

MockProducer.getProperties()
.setProperty(
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS.getName(),
CaseClassificationCalculationMode.DISABLED.name());
caze = getCaseFacade().save(buildSuspectCase(Disease.CORONAVIRUS));
assertEquals(CaseClassification.NOT_CLASSIFIED, caze.getCaseClassification());

MockProducer.getProperties()
.setProperty(ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_ALL, CaseClassificationCalculationMode.AUTOMATIC.name());
MockProducer.getProperties()
.setProperty(
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS.getName(),
CaseClassificationCalculationMode.DISABLED.name());
caze = getCaseFacade().save(buildSuspectCase(Disease.CORONAVIRUS));
assertEquals(CaseClassification.NOT_CLASSIFIED, caze.getCaseClassification());
}

/**
* Sets all symptoms with the SymptomState type to YES.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import de.symeda.sormas.api.CaseClassificationCalculationMode;
import de.symeda.sormas.api.ConfigFacade;
import de.symeda.sormas.api.Disease;
import de.symeda.sormas.api.utils.InfoProvider;
import de.symeda.sormas.backend.AbstractBeanTest;
import de.symeda.sormas.backend.MockProducer;
Expand Down Expand Up @@ -185,4 +187,51 @@ public void testPatientDiaryConfigTokenLifetime() {
MockProducer.getProperties().setProperty(ConfigFacadeEjb.INTERFACE_PATIENT_DIARY_TOKEN_LIFETIME, "666");
assertThat(getConfigFacade().getPatientDiaryConfig().getTokenLifetime(), equalTo(Duration.ofSeconds(666L)));
}

@Test
public void testHasAnyCaseClassificationCalculationEnabled() {
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(true));

MockProducer.getProperties()
.setProperty(ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_ALL, CaseClassificationCalculationMode.DISABLED.name());
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(false));

MockProducer.getProperties()
.setProperty(
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS,
CaseClassificationCalculationMode.MANUAL.name());
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(true));

MockProducer.getProperties()
.setProperty(
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS,
CaseClassificationCalculationMode.AUTOMATIC.name());
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(true));

MockProducer.getProperties()
.setProperty(
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS,
CaseClassificationCalculationMode.MANUAL_AND_AUTOMATIC.name());
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(true));

MockProducer.getProperties()
.setProperty(
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS,
CaseClassificationCalculationMode.DISABLED.name());
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(false));

MockProducer.getProperties()
.setProperty(
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CHOLERA,
CaseClassificationCalculationMode.AUTOMATIC.name());
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(true));

MockProducer.getProperties().remove(ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_ALL);
MockProducer.getProperties().remove(ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CHOLERA);
MockProducer.getProperties()
.setProperty(
ConfigFacadeEjb.CASE_CLASSIFICATION_CALCULATION_PREFIX + Disease.CORONAVIRUS,
CaseClassificationCalculationMode.DISABLED.name());
assertThat(getConfigFacade().isAnyCaseClassificationCalculationEnabled(), is(true));
}
}
14 changes: 9 additions & 5 deletions sormas-base/setup/sormas.properties
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,15 @@ app.url=
# Possible Values: true, false
#devmode=false

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

# 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.
# default: 90
Expand Down
2 changes: 1 addition & 1 deletion sormas-ui/src/main/java/de/symeda/sormas/ui/AboutView.java
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ private List<String> listCustomDocumentsFiles() {
}

private boolean shouldShowClassificationDocumentLink() {
return FacadeProvider.getConfigFacade().isFeatureAutomaticCaseClassification()
return FacadeProvider.getConfigFacade().isAnyCaseClassificationCalculationEnabled()
&& FacadeProvider.getFeatureConfigurationFacade().isFeatureEnabled(FeatureType.CASE_SURVEILANCE);
}

Expand Down
26 changes: 13 additions & 13 deletions sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java
Original file line number Diff line number Diff line change
Expand Up @@ -498,19 +498,6 @@ protected void addFields() {
ComboBox caseReferenceDefinition = addField(CaseDataDto.CASE_REFERENCE_DEFINITION, ComboBox.class);
caseReferenceDefinition.setReadOnly(true);

if (diseaseClassificationExists()) {
Button caseClassificationCalculationButton = ButtonHelper.createButton(Captions.caseClassificationCalculationButton, e -> {
CaseClassification classification = FacadeProvider.getCaseClassificationFacade().getClassification(getValue());
((Field<CaseClassification>) getField(CaseDataDto.CASE_CLASSIFICATION)).setValue(classification);
}, ValoTheme.BUTTON_PRIMARY, FORCE_CAPTION);

getContent().addComponent(caseClassificationCalculationButton, CASE_CLASSIFICATION_CALCULATE_BTN_LOC);

if (!UiUtil.permitted(UserRight.CASE_CLASSIFY)) {
caseClassificationCalculationButton.setEnabled(false);
}
}

//if(cbCaseClassification.getCaption())
addField(CaseDataDto.NOT_A_CASE_REASON_NEGATIVE_TEST, CheckBox.class);
addField(CaseDataDto.NOT_A_CASE_REASON_PHYSICIAN_INFORMATION, CheckBox.class);
Expand All @@ -536,6 +523,19 @@ protected void addFields() {
caseClassificationGroup.removeItem(CaseClassification.CONFIRMED_UNKNOWN_SYMPTOMS);
}

if (diseaseClassificationExists() && FacadeProvider.getConfigFacade().getCaseClassificationCalculationMode(disease).isManualEnabled()) {
Button caseClassificationCalculationButton = ButtonHelper.createButton(Captions.caseClassificationCalculationButton, e -> {
CaseClassification classification = FacadeProvider.getCaseClassificationFacade().getClassification(getValue());
((Field<CaseClassification>) getField(CaseDataDto.CASE_CLASSIFICATION)).setValue(classification);
}, ValoTheme.BUTTON_PRIMARY, FORCE_CAPTION);

getContent().addComponent(caseClassificationCalculationButton, CASE_CLASSIFICATION_CALCULATE_BTN_LOC);

if (!UiUtil.permitted(UserRight.CASE_CLASSIFY)) {
caseClassificationCalculationButton.setEnabled(false);
}
}

boolean extendedClassification = FacadeProvider.getDiseaseConfigurationFacade().usesExtendedClassification(disease);

if (extendedClassification) {
Expand Down

0 comments on commit 8be6e7e

Please sign in to comment.