Skip to content

Commit 531a6ef

Browse files
committed
Polish test entries and verify developer-mode sample generation
1 parent 0805869 commit 531a6ef

3 files changed

Lines changed: 167 additions & 28 deletions

File tree

sormas-ui/src/main/java/de/symeda/sormas/ui/configuration/DevModeView.java

Lines changed: 95 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,19 @@
9090
import de.symeda.sormas.api.person.Sex;
9191
import de.symeda.sormas.api.sample.AdditionalTestDto;
9292
import de.symeda.sormas.api.sample.AdditionalTestType;
93+
import de.symeda.sormas.api.sample.PathogenTestDto;
94+
import de.symeda.sormas.api.sample.PathogenTestResultType;
9395
import de.symeda.sormas.api.sample.PathogenTestType;
96+
import de.symeda.sormas.api.sample.ResultValueType;
9497
import de.symeda.sormas.api.sample.SampleDto;
9598
import de.symeda.sormas.api.sample.SampleMaterial;
9699
import de.symeda.sormas.api.sample.SamplePurpose;
97100
import de.symeda.sormas.api.sample.SimpleTestResultType;
98101
import de.symeda.sormas.api.sample.SpecimenCondition;
102+
import de.symeda.sormas.api.user.UserDto;
99103
import de.symeda.sormas.api.user.UserReferenceDto;
100104
import de.symeda.sormas.api.utils.DateHelper;
105+
import de.symeda.sormas.api.utils.Diseases;
101106
import de.symeda.sormas.api.utils.SortProperty;
102107
import de.symeda.sormas.api.utils.UtilDate;
103108
import de.symeda.sormas.api.utils.ValidationRuntimeException;
@@ -126,6 +131,9 @@ public class DevModeView extends AbstractConfigurationView {
126131

127132
private static Random randomGenerator;
128133

134+
private static final List<PathogenTestResultType> PATHOGEN_TEST_RESULT_POOL =
135+
Arrays.asList(PathogenTestResultType.POSITIVE, PathogenTestResultType.NEGATIVE, PathogenTestResultType.PENDING);
136+
129137
private TextField seedField;
130138
private CheckBox useManualSeedCheckbox;
131139
private static boolean useManualSeed = false;
@@ -195,9 +203,7 @@ private HorizontalLayout createDevButtonsLayout() {
195203
Button btnClearCaseVaccinationStatuses = ButtonHelper.createButton(
196204
"clearCaseVaccinationStatuses",
197205
"Clear case vaccination statuses",
198-
e -> showClearVaccinationStatusesPopup(
199-
"Clear case vaccination statuses",
200-
FacadeProvider.getCaseFacade()::deleteVaccinationStatuses));
206+
e -> showClearVaccinationStatusesPopup("Clear case vaccination statuses", FacadeProvider.getCaseFacade()::deleteVaccinationStatuses));
201207
horizontalLayout.addComponent(btnClearCaseVaccinationStatuses);
202208

203209
Button btnClearContactVaccinationStatuses = ButtonHelper.createButton(
@@ -1160,6 +1166,19 @@ private void generateSamples(SampleGenerationConfig sampleGenerationConfig) {
11601166

11611167
UserReferenceDto user = UiUtil.getUserReference();
11621168

1169+
// Resolve the disease once, up front: pick a random one if none was selected and freeze it for the whole
1170+
// run. This must happen BEFORE the case query so the cases are actually scoped to this disease — otherwise
1171+
// the generated pathogen tests would carry a testedDisease that contradicts their case.
1172+
Disease disease = sampleGenerationConfig.getDisease();
1173+
if (disease == null) {
1174+
disease = random(FacadeProvider.getDiseaseConfigurationFacade().getAllDiseases(true, true, true));
1175+
sampleGenerationConfig.setDisease(disease);
1176+
}
1177+
1178+
// The pickable test types only depend on the (now fixed) disease, so compute them once instead of per sample.
1179+
List<PathogenTestType> pickablePathogenTestTypes =
1180+
sampleGenerationConfig.isRequestPathogenTestsToBePerformed() ? getPickablePathogenTestTypes(disease) : Collections.emptyList();
1181+
11631182
List<CaseReferenceDto> cases = FacadeProvider.getCaseFacade()
11641183
.getRandomCaseReferences(
11651184
new CaseCriteria().region(sampleGenerationConfig.getRegion())
@@ -1168,18 +1187,11 @@ private void generateSamples(SampleGenerationConfig sampleGenerationConfig) {
11681187
sampleGenerationConfig.getEntityCountAsNumber() * 2,
11691188
random());
11701189

1171-
if (nonNull(cases)) {
1190+
if (nonNull(cases) && !cases.isEmpty()) {
11721191
for (int i = 0; i < sampleGenerationConfig.getEntityCountAsNumber(); i++) {
11731192

11741193
CaseReferenceDto caseReference = random(cases);
11751194

1176-
List<Disease> diseases = FacadeProvider.getDiseaseConfigurationFacade().getAllDiseases(true, true, true);
1177-
Disease disease = sampleGenerationConfig.getDisease();
1178-
if (disease == null) {
1179-
disease = random(diseases);
1180-
sampleGenerationConfig.setDisease(disease);
1181-
}
1182-
11831195
LocalDateTime referenceDateTime = getReferenceDateTime(
11841196
i,
11851197
sampleGenerationConfig.getEntityCountAsNumber(),
@@ -1203,16 +1215,6 @@ private void generateSamples(SampleGenerationConfig sampleGenerationConfig) {
12031215

12041216
sample.setLab(sampleGenerationConfig.getLaboratory());
12051217

1206-
if (sampleGenerationConfig.isRequestPathogenTestsToBePerformed()) {
1207-
Set pathogenTestTypes = new HashSet<PathogenTestType>();
1208-
int until = randomInt(1, PathogenTestType.values().length);
1209-
for (int j = 0; j < until; j++) {
1210-
pathogenTestTypes.add(PathogenTestType.values()[j]);
1211-
}
1212-
sample.setPathogenTestingRequested(true);
1213-
sample.setRequestedPathogenTests(pathogenTestTypes);
1214-
}
1215-
12161218
if (sampleGenerationConfig.isRequestAdditionalTestsToBePerformed()) {
12171219
Set additionalTestTypes = new HashSet<AdditionalTestType>();
12181220
int until = randomInt(1, AdditionalTestType.values().length);
@@ -1238,6 +1240,10 @@ private void generateSamples(SampleGenerationConfig sampleGenerationConfig) {
12381240

12391241
SampleDto sampleDto = FacadeProvider.getSampleFacade().saveSample(sample);
12401242

1243+
if (sampleGenerationConfig.isRequestPathogenTestsToBePerformed()) {
1244+
createPathogenTests(sampleDto, disease, date, pickablePathogenTestTypes);
1245+
}
1246+
12411247
if (sampleGenerationConfig.isRequestAdditionalTestsToBePerformed()) {
12421248
createAdditionalTest(sampleDto, date);
12431249
}
@@ -1254,7 +1260,7 @@ private void generateSamples(SampleGenerationConfig sampleGenerationConfig) {
12541260
logger.info(msg);
12551261
Notification.show("", msg, Notification.Type.TRAY_NOTIFICATION);
12561262
} else {
1257-
String msg = "No Sample has been generated because cases is null ";
1263+
String msg = "No samples generated: no cases found for the selected region/district/disease.";
12581264
logger.info(msg);
12591265
Notification.show("", msg, Notification.Type.TRAY_NOTIFICATION);
12601266
}
@@ -1294,6 +1300,73 @@ private void createAdditionalTest(SampleDto sample, Date date) {
12941300
FacadeProvider.getAdditionalTestFacade().saveAdditionalTest(additionalTestDto);
12951301
}
12961302

1303+
/**
1304+
* The pathogen test types that are valid for the given disease and selectable for new tests. Antibiotic
1305+
* susceptibility is excluded because it requires a separate drug-susceptibility result. Mirrors the rules the
1306+
* real pathogen test form uses to populate its test-type combo.
1307+
*/
1308+
private List<PathogenTestType> getPickablePathogenTestTypes(Disease disease) {
1309+
return Diseases.DiseasesConfiguration.getVisibleValues(PathogenTestType.class, disease)
1310+
.stream()
1311+
.filter(PathogenTestType::isSelectableForNewTests)
1312+
.filter(type -> type != PathogenTestType.ANTIBIOTIC_SUSCEPTIBILITY)
1313+
.collect(Collectors.toList());
1314+
}
1315+
1316+
private void createPathogenTests(SampleDto sample, Disease disease, Date date, List<PathogenTestType> pickableTypes) {
1317+
1318+
if (pickableTypes.isEmpty()) {
1319+
return;
1320+
}
1321+
1322+
final UserDto currentUser = UiUtil.getUser();
1323+
if (currentUser == null) {
1324+
return;
1325+
}
1326+
1327+
int count = randomInt(1, 4);
1328+
for (int i = 0; i < count; i++) {
1329+
PathogenTestDto test = PathogenTestDto.build(sample, currentUser);
1330+
test.setTestedDisease(disease);
1331+
// build() already resolves the lab from the current user, falling back to the sample's lab. Don't
1332+
// override it here (that would discard the user lab and re-introduce a null lab when the sample has none).
1333+
test.setTestType(random(pickableTypes));
1334+
test.setTestResult(random(PATHOGEN_TEST_RESULT_POOL));
1335+
test.setTestDateTime(date);
1336+
1337+
// Populate a quantitative value for half the tests so the inline quantitative display has data.
1338+
if (randomPercent(50)) {
1339+
Set<ResultValueType> valueTypes = PathogenTestType.getResultValueTypes(test.getTestType());
1340+
if (valueTypes.contains(ResultValueType.NUMERIC)) {
1341+
test.setQuantitativeValue(random().nextFloat() * 1000);
1342+
test.setQuantitativeUnit("copies/mL");
1343+
} else if (valueTypes.contains(ResultValueType.TEXT)) {
1344+
test.setQuantitativeText("Generated result");
1345+
}
1346+
}
1347+
1348+
// Populate free-text result details for some tests so the result-details indicator has data to surface.
1349+
if (randomPercent(40)) {
1350+
test.setTestResultText("Generated result details for testing the lab results view.");
1351+
}
1352+
if (randomPercent(30)) {
1353+
test.setPerformedByReferenceLaboratory(true);
1354+
}
1355+
if (randomPercent(20)) {
1356+
test.setRetestRequested(true);
1357+
}
1358+
1359+
try {
1360+
// Note: each save triggers the full pathogen-test cascade (sample/case result derivation,
1361+
// case reclassification, lab-result notifications, and Sormas-to-Sormas share sync when configured).
1362+
// That is acceptable for a dev-data tool.
1363+
FacadeProvider.getPathogenTestFacade().savePathogenTest(test);
1364+
} catch (RuntimeException e) {
1365+
logger.warn("Failed to generate a pathogen test for sample {}: {}", sample.getUuid(), e.getMessage());
1366+
}
1367+
}
1368+
}
1369+
12971370
private void generateContacts() {
12981371
ContactGenerationConfig contactGenerationConfig = contactGeneratorConfigBinder.getBean();
12991372
boolean valid = true;

sormas-ui/src/main/java/de/symeda/sormas/ui/samples/pathogentestlink/PathogenTestListEntry.java

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,19 @@ public PathogenTestListEntry(PathogenTestDto pathogenTest, boolean showTestResul
6060
topLabelLayout.addComponent(labelTopLeft);
6161

6262
if (Boolean.TRUE.equals(pathogenTest.getTestResultVerified())) {
63-
Label labelTopRight = new Label(VaadinIcons.CHECK_CIRCLE.getHtml(), ContentMode.HTML);
64-
labelTopRight.setSizeUndefined();
65-
labelTopRight.addStyleName(CssStyles.LABEL_LARGE);
66-
labelTopRight.setDescription(I18nProperties.getPrefixCaption(PathogenTestDto.I18N_PREFIX, PathogenTestDto.TEST_RESULT_VERIFIED));
67-
topLabelLayout.addComponent(labelTopRight);
68-
topLabelLayout.setComponentAlignment(labelTopRight, Alignment.TOP_RIGHT);
63+
addTopRightIcon(topLabelLayout, VaadinIcons.CHECK_CIRCLE, PathogenTestDto.TEST_RESULT_VERIFIED);
64+
}
65+
66+
// At-a-glance lab indicators: reference laboratory, retest, and result details
67+
// (the latter only when the full result text is not already shown below).
68+
if (shouldShowRefLabIcon(pathogenTest)) {
69+
addTopRightIcon(topLabelLayout, VaadinIcons.INSTITUTION, PathogenTestDto.PERFORMED_BY_REFERENCE_LABORATORY);
70+
}
71+
if (shouldShowRetestIcon(pathogenTest)) {
72+
addTopRightIcon(topLabelLayout, VaadinIcons.REFRESH, PathogenTestDto.RETEST_REQUESTED);
73+
}
74+
if (shouldShowResultDetailsIcon(pathogenTest, showTestResultText)) {
75+
addTopRightIcon(topLabelLayout, VaadinIcons.INFO_CIRCLE, PathogenTestDto.TEST_RESULT_TEXT);
6976
}
7077

7178
if (showTestResultText && !DataHelper.isNullOrEmpty(pathogenTest.getTestResultText())) {
@@ -184,6 +191,31 @@ static String formatQuantitativeResult(PathogenTestDto test) {
184191
return parts.isEmpty() ? null : String.join(": ", parts);
185192
}
186193

194+
private static void addTopRightIcon(HorizontalLayout topLabelLayout, VaadinIcons icon, String captionProperty) {
195+
Label iconLabel = new Label(icon.getHtml(), ContentMode.HTML);
196+
iconLabel.setSizeUndefined();
197+
iconLabel.addStyleNames(CssStyles.LABEL_LARGE, CssStyles.HSPACE_LEFT_4);
198+
iconLabel.setDescription(I18nProperties.getPrefixCaption(PathogenTestDto.I18N_PREFIX, captionProperty));
199+
topLabelLayout.addComponent(iconLabel);
200+
topLabelLayout.setComponentAlignment(iconLabel, Alignment.TOP_RIGHT);
201+
}
202+
203+
static boolean shouldShowRefLabIcon(PathogenTestDto test) {
204+
return Boolean.TRUE.equals(test.getPerformedByReferenceLaboratory());
205+
}
206+
207+
static boolean shouldShowRetestIcon(PathogenTestDto test) {
208+
return Boolean.TRUE.equals(test.getRetestRequested());
209+
}
210+
211+
/**
212+
* The result-details cue is only shown in the compact view (when the full result text is not rendered),
213+
* to avoid a redundant icon next to text that is already visible.
214+
*/
215+
static boolean shouldShowResultDetailsIcon(PathogenTestDto test, boolean showTestResultText) {
216+
return !showTestResultText && !DataHelper.isNullOrEmpty(test.getTestResultText());
217+
}
218+
187219
@Nullable
188220
private static String getDiseaseOrPathogenCaption(PathogenTestDto pathogenTest) {
189221
final String diseaseOrPathogen;

sormas-ui/src/test/java/de/symeda/sormas/ui/samples/pathogentestlink/PathogenTestListEntryTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
package de.symeda.sormas.ui.samples.pathogentestlink;
1919

2020
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertFalse;
2122
import static org.junit.jupiter.api.Assertions.assertNull;
23+
import static org.junit.jupiter.api.Assertions.assertTrue;
2224

2325
import org.junit.jupiter.api.Test;
2426

@@ -133,4 +135,36 @@ public void allFieldsPopulated_orderedInterpretationGradeBooleanNumericText() {
133135
"free text");
134136
assertEquals(expected, PathogenTestListEntry.formatQuantitativeResult(t));
135137
}
138+
139+
@Test
140+
public void refLabIcon_shownOnlyWhenTrue() {
141+
assertFalse(PathogenTestListEntry.shouldShowRefLabIcon(test()));
142+
PathogenTestDto t = test();
143+
t.setPerformedByReferenceLaboratory(false);
144+
assertFalse(PathogenTestListEntry.shouldShowRefLabIcon(t));
145+
t.setPerformedByReferenceLaboratory(true);
146+
assertTrue(PathogenTestListEntry.shouldShowRefLabIcon(t));
147+
}
148+
149+
@Test
150+
public void retestIcon_shownOnlyWhenTrue() {
151+
assertFalse(PathogenTestListEntry.shouldShowRetestIcon(test()));
152+
PathogenTestDto t = test();
153+
t.setRetestRequested(false);
154+
assertFalse(PathogenTestListEntry.shouldShowRetestIcon(t));
155+
t.setRetestRequested(true);
156+
assertTrue(PathogenTestListEntry.shouldShowRetestIcon(t));
157+
}
158+
159+
@Test
160+
public void resultDetailsIcon_shownOnlyInCompactViewWhenTextPresent() {
161+
PathogenTestDto t = test();
162+
t.setTestResultText("Details");
163+
// Compact view (full text not shown) → icon surfaces the cue.
164+
assertTrue(PathogenTestListEntry.shouldShowResultDetailsIcon(t, false));
165+
// Full-text view → icon would be redundant.
166+
assertFalse(PathogenTestListEntry.shouldShowResultDetailsIcon(t, true));
167+
// No details → no icon.
168+
assertFalse(PathogenTestListEntry.shouldShowResultDetailsIcon(test(), false));
169+
}
136170
}

0 commit comments

Comments
 (0)