Skip to content

Commit 52b07df

Browse files
authored
Merge pull request #14129 from SORMAS-Foundation/bugfix-customizable_field_default_value_handling
#14084 - Ensure default values for customizable fields
2 parents 5f02486 + aed177c commit 52b07df

9 files changed

Lines changed: 538 additions & 20 deletions

File tree

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1784,12 +1784,24 @@ public void syncSharesAsync(ShareTreeCriteria criteria) {
17841784

17851785
private void doSave(Case caze, boolean handleChanges, CaseDataDto existingCaseDto, boolean syncShares) {
17861786
service.ensurePersisted(caze);
1787+
ensureDefaultCustomizableFieldValues(caze, existingCaseDto);
17871788
if (handleChanges) {
17881789
updateCaseVisitAssociations(existingCaseDto, caze);
17891790
onCaseChanged(existingCaseDto, caze, syncShares);
17901791
}
17911792
}
17921793

1794+
private void ensureDefaultCustomizableFieldValues(Case caze, CaseDataDto existingCaseDto) {
1795+
if (existingCaseDto == null || existingCaseDto.getUuid() == null) {
1796+
customizableFieldValueService.ensureDefaultValuesForEntity(caze.getUuid(), CustomizableFieldContext.CASE);
1797+
}
1798+
1799+
epiDataFacade.ensureDefaultCustomizableFieldValues(
1800+
caze.getEpiData(),
1801+
existingCaseDto != null ? existingCaseDto.getEpiData() : null,
1802+
caze.getDisease());
1803+
}
1804+
17931805
private void updateCaseVisitAssociations(CaseDataDto existingCase, Case caze) {
17941806

17951807
if (existingCase != null

sormas-backend/src/main/java/de/symeda/sormas/backend/contact/ContactFacadeEjb.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,10 @@ public ContactDto save(ContactDto dto, boolean handleChanges, boolean handleCase
419419

420420
Contact entity = fillOrBuildEntity(dto, existingContact, checkChangeDate);
421421
service.ensurePersisted(entity);
422+
epiDataFacade.ensureDefaultCustomizableFieldValues(
423+
entity.getEpiData(),
424+
existingContactDto != null ? existingContactDto.getEpiData() : null,
425+
entity.getDisease());
422426

423427
if (existingContact == null && featureConfigurationFacade.isTaskGenerationFeatureEnabled(TaskType.CONTACT_INVESTIGATION)) {
424428
createInvestigationTask(entity);

sormas-backend/src/main/java/de/symeda/sormas/backend/customizablefield/CustomizableFieldMetadataFacadeEjb.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ protected CustomizableFieldMetadataDto toDto(CustomizableFieldMetadata source) {
287287
return target;
288288
}
289289

290-
private static CustomizableFieldVisibilityRestrictions parseVisibilityRestrictions(String json) {
290+
public static CustomizableFieldVisibilityRestrictions parseVisibilityRestrictions(String json) {
291291
if (StringUtils.isBlank(json)) {
292292
return null;
293293
}

sormas-backend/src/main/java/de/symeda/sormas/backend/customizablefield/CustomizableFieldValueService.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616
package de.symeda.sormas.backend.customizablefield;
1717

1818
import java.util.HashMap;
19+
import java.util.HashSet;
1920
import java.util.List;
2021
import java.util.Map;
22+
import java.util.Set;
2123

24+
import javax.ejb.EJB;
2225
import javax.ejb.LocalBean;
2326
import javax.ejb.Stateless;
2427
import javax.persistence.criteria.CriteriaBuilder;
@@ -27,10 +30,14 @@
2730
import javax.persistence.criteria.Predicate;
2831
import javax.persistence.criteria.Root;
2932

33+
import org.apache.commons.lang3.StringUtils;
34+
3035
import de.symeda.sormas.api.common.DeletableEntityType;
3136
import de.symeda.sormas.api.common.DeletionDetails;
3237
import de.symeda.sormas.api.customizablefield.CustomizableFieldContext;
3338
import de.symeda.sormas.api.customizablefield.CustomizableFieldValueDto;
39+
import de.symeda.sormas.api.customizablefield.CustomizableFieldVisibilityContext;
40+
import de.symeda.sormas.api.customizablefield.CustomizableFieldVisibilityRestrictions;
3441
import de.symeda.sormas.backend.common.AbstractCoreAdoService;
3542
import de.symeda.sormas.backend.common.AbstractDomainObject;
3643
import de.symeda.sormas.backend.common.DeletableAdo;
@@ -42,6 +49,9 @@
4249
@LocalBean
4350
public class CustomizableFieldValueService extends AbstractCoreAdoService<CustomizableFieldValue, CustomizableFieldValueJoins> {
4451

52+
@EJB
53+
private CustomizableFieldMetadataService customizableFieldMetadataService;
54+
4555
public CustomizableFieldValueService() {
4656
super(CustomizableFieldValue.class, DeletableEntityType.CUSTOMIZABLE_FIELD_VALUE);
4757
}
@@ -98,6 +108,59 @@ public void saveEntityValues(
98108
}
99109
}
100110

111+
public void ensureDefaultValuesForEntity(String entityUuid, CustomizableFieldContext contextClass) {
112+
ensureDefaultValuesForEntity(entityUuid, contextClass, null);
113+
}
114+
115+
/**
116+
* Ensures default values for customizable fields for the given entity.
117+
* <p>
118+
* Only fields that are visible for the given visibility context will have their default values
119+
* persisted. Fields with specific visibility restrictions will be skipped
120+
* if the visiblity context is not matched.
121+
*
122+
* @param entityUuid
123+
* the UUID of the entity
124+
* @param contextClass
125+
* the context class for the customizable fields
126+
* @param visibilityContext
127+
* the visibility context to check against field restrictions; may be null to skip
128+
* visibility checks
129+
*/
130+
public void ensureDefaultValuesForEntity(
131+
String entityUuid,
132+
CustomizableFieldContext contextClass,
133+
CustomizableFieldVisibilityContext visibilityContext) {
134+
if (StringUtils.isBlank(entityUuid) || contextClass == null) {
135+
return;
136+
}
137+
138+
Map<CustomizableFieldMetadata, CustomizableFieldValue> existing = getValuesForEntity(entityUuid, contextClass);
139+
Set<String> existingMetadataUuids = new HashSet<>();
140+
for (CustomizableFieldMetadata metadata : existing.keySet()) {
141+
existingMetadataUuids.add(metadata.getUuid());
142+
}
143+
144+
for (CustomizableFieldMetadata metadata : customizableFieldMetadataService.getActiveFieldsForContext(contextClass)) {
145+
if (existingMetadataUuids.contains(metadata.getUuid()) || StringUtils.isBlank(metadata.getDefaultValue())) {
146+
continue;
147+
}
148+
149+
// Skip fields that are not visible for the given context
150+
if (visibilityContext != null && metadata.getVisibilityRestrictions() != null && !metadata.getVisibilityRestrictions().isBlank()) {
151+
CustomizableFieldVisibilityRestrictions metadataVisibilityRestrictions =
152+
CustomizableFieldMetadataFacadeEjb.parseVisibilityRestrictions(metadata.getVisibilityRestrictions());
153+
if (!metadataVisibilityRestrictions.matches(visibilityContext)) {
154+
continue;
155+
}
156+
}
157+
158+
CustomizableFieldValue value = createNewValue(metadata, entityUuid, contextClass);
159+
value.setValue(metadata.getDefaultValue());
160+
ensurePersisted(value);
161+
}
162+
}
163+
101164
public void deleteValuesForEntity(String entityUuid, CustomizableFieldContext contextClass) {
102165
Map<CustomizableFieldMetadata, CustomizableFieldValue> values = getValuesForEntity(entityUuid, contextClass);
103166
for (CustomizableFieldValue value : values.values()) {

sormas-backend/src/main/java/de/symeda/sormas/backend/epidata/EpiDataFacadeEjb.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@
2828
import javax.ejb.LocalBean;
2929
import javax.ejb.Stateless;
3030

31+
import de.symeda.sormas.api.Disease;
3132
import de.symeda.sormas.api.activityascase.ActivityAsCaseDto;
3233
import de.symeda.sormas.api.common.DeletionDetails;
3334
import de.symeda.sormas.api.customizablefield.CustomizableFieldContext;
35+
import de.symeda.sormas.api.customizablefield.CustomizableFieldVisibilityContext;
3436
import de.symeda.sormas.api.epidata.EpiDataDto;
3537
import de.symeda.sormas.api.epidata.EpiDataFacade;
3638
import de.symeda.sormas.api.exposure.ExposureDto;
@@ -79,6 +81,35 @@ public void softDeleteCustomizableFieldValues(EpiData epiData, DeletionDetails d
7981
}
8082
}
8183

84+
public void ensureDefaultCustomizableFieldValues(EpiData epiData, EpiDataDto existingEpiData, Disease disease) {
85+
if (epiData == null || epiData.getUuid() == null) {
86+
return;
87+
}
88+
89+
// Build visibility context from the provided disease
90+
CustomizableFieldVisibilityContext visibilityContext = disease != null ? new CustomizableFieldVisibilityContext().withDisease(disease) : null;
91+
92+
if (existingEpiData == null || existingEpiData.getUuid() == null) {
93+
customizableFieldValueService.ensureDefaultValuesForEntity(epiData.getUuid(), CustomizableFieldContext.EPIDATA, visibilityContext);
94+
}
95+
96+
HashSet<String> existingExposureUuids = new HashSet<>();
97+
if (existingEpiData != null && existingEpiData.getExposures() != null) {
98+
for (ExposureDto existingExposure : existingEpiData.getExposures()) {
99+
if (existingExposure.getUuid() != null) {
100+
existingExposureUuids.add(existingExposure.getUuid());
101+
}
102+
}
103+
}
104+
105+
for (Exposure exposure : epiData.getExposures()) {
106+
if (exposure.getUuid() != null && !existingExposureUuids.contains(exposure.getUuid())) {
107+
// Exposure inherits disease from parent EpiData's disease
108+
customizableFieldValueService.ensureDefaultValuesForEntity(exposure.getUuid(), CustomizableFieldContext.EXPOSURE, visibilityContext);
109+
}
110+
}
111+
}
112+
82113
public EpiData fillOrBuildEntity(EpiDataDto source, EpiData target, boolean checkChangeDate) {
83114
if (source == null) {
84115
return null;

sormas-backend/src/test/java/de/symeda/sormas/backend/customizablefield/CustomizableFieldFacadeEjbTest.java

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import de.symeda.sormas.api.caze.CaseClassification;
3333
import de.symeda.sormas.api.caze.CaseDataDto;
3434
import de.symeda.sormas.api.caze.InvestigationStatus;
35+
import de.symeda.sormas.api.contact.ContactDto;
3536
import de.symeda.sormas.api.customizablefield.CustomizableFieldContext;
3637
import de.symeda.sormas.api.customizablefield.CustomizableFieldGroup;
3738
import de.symeda.sormas.api.customizablefield.CustomizableFieldMetadataDto;
@@ -69,12 +70,7 @@ void testCaseRelatedCustomizableFields(CustomizableFieldContext context) {
6970
CustomizableFieldMetadataFacade metadataFacade = getBean(CustomizableFieldMetadataFacadeEjb.CustomizableFieldMetadataFacadeEjbLocal.class);
7071
CustomizableFieldValueFacade valueFacade = getBean(CustomizableFieldValueFacadeEjb.CustomizableFieldValueFacadeEjbLocal.class);
7172

72-
CustomizableFieldMetadataDto metadata = new CustomizableFieldMetadataDto();
73-
metadata.setName("customField_" + context.name().toLowerCase());
74-
metadata.setFieldType(CustomizableFieldType.TEXT);
75-
metadata.setContextClass(context);
76-
metadata.setUiGroup(CustomizableFieldGroup.getGroupsForContext(context).stream().findFirst().orElse(null));
77-
metadata.setUiLinePosition(1);
73+
CustomizableFieldMetadataDto metadata = buildMetadata(context, "customField_" + context.name().toLowerCase());
7874

7975
CustomizableFieldMetadataDto savedMetadata = metadataFacade.save(metadata);
8076
assertThat(savedMetadata.getUuid(), is(notNullValue()));
@@ -114,6 +110,79 @@ void testCaseRelatedCustomizableFields(CustomizableFieldContext context) {
114110
assertThat(values.get(savedMetadata).getValue(), is(equalTo("custom-value")));
115111
}
116112

113+
@ParameterizedTest
114+
@EnumSource(value = CustomizableFieldContext.class,
115+
names = {
116+
"CASE",
117+
"EPIDATA",
118+
"EXPOSURE" })
119+
void testCaseRelatedDefaultValuesArePersistedOnCreate(CustomizableFieldContext context) {
120+
CustomizableFieldMetadataFacade metadataFacade = getBean(CustomizableFieldMetadataFacadeEjb.CustomizableFieldMetadataFacadeEjbLocal.class);
121+
CustomizableFieldValueFacade valueFacade = getBean(CustomizableFieldValueFacadeEjb.CustomizableFieldValueFacadeEjbLocal.class);
122+
123+
CustomizableFieldMetadataDto metadata = buildMetadata(context, "defaultField_" + context.name().toLowerCase());
124+
metadata.setDefaultValue("default-" + context.name().toLowerCase());
125+
CustomizableFieldMetadataDto savedMetadata = metadataFacade.save(metadata);
126+
127+
PersonDto person = creator.createPerson("Default", "Case", Sex.MALE, 1981, 2, 2);
128+
CaseDataDto caze = creator.createCase(
129+
surveillanceSupervisor.toReference(),
130+
person.toReference(),
131+
Disease.EVD,
132+
CaseClassification.PROBABLE,
133+
InvestigationStatus.PENDING,
134+
new java.util.Date(),
135+
rdcf);
136+
137+
if (context == CustomizableFieldContext.EXPOSURE) {
138+
caze.getEpiData().getExposures().add(ExposureDto.build(ExposureType.TRAVEL));
139+
caze = getCaseFacade().save(caze);
140+
}
141+
142+
Map<CustomizableFieldMetadataDto, CustomizableFieldValueDto> values =
143+
valueFacade.getValuesForEntity(getEntityUuidForContext(context, caze), context);
144+
145+
assertThat(values, hasKey(savedMetadata));
146+
assertThat(values.get(savedMetadata).getValue(), is(equalTo(savedMetadata.getDefaultValue())));
147+
}
148+
149+
@ParameterizedTest
150+
@EnumSource(value = CustomizableFieldContext.class,
151+
names = {
152+
"EPIDATA",
153+
"EXPOSURE" })
154+
void testContactRelatedDefaultValuesArePersistedOnCreate(CustomizableFieldContext context) {
155+
CustomizableFieldMetadataFacade metadataFacade = getBean(CustomizableFieldMetadataFacadeEjb.CustomizableFieldMetadataFacadeEjbLocal.class);
156+
CustomizableFieldValueFacade valueFacade = getBean(CustomizableFieldValueFacadeEjb.CustomizableFieldValueFacadeEjbLocal.class);
157+
158+
CustomizableFieldMetadataDto metadata = buildMetadata(context, "contactDefaultField_" + context.name().toLowerCase());
159+
metadata.setDefaultValue("contact-default-" + context.name().toLowerCase());
160+
CustomizableFieldMetadataDto savedMetadata = metadataFacade.save(metadata);
161+
162+
PersonDto person = creator.createPerson("Default", "Contact", Sex.FEMALE, 1982, 3, 3);
163+
ContactDto contact = creator.createContact(surveillanceSupervisor.toReference(), person.toReference(), Disease.EVD, dto -> {
164+
if (context == CustomizableFieldContext.EXPOSURE) {
165+
dto.getEpiData().getExposures().add(ExposureDto.build(ExposureType.TRAVEL));
166+
}
167+
});
168+
169+
Map<CustomizableFieldMetadataDto, CustomizableFieldValueDto> values =
170+
valueFacade.getValuesForEntity(getEntityUuidForContext(context, contact), context);
171+
172+
assertThat(values, hasKey(savedMetadata));
173+
assertThat(values.get(savedMetadata).getValue(), is(equalTo(savedMetadata.getDefaultValue())));
174+
}
175+
176+
private CustomizableFieldMetadataDto buildMetadata(CustomizableFieldContext context, String name) {
177+
CustomizableFieldMetadataDto metadata = new CustomizableFieldMetadataDto();
178+
metadata.setName(name);
179+
metadata.setFieldType(CustomizableFieldType.TEXT);
180+
metadata.setContextClass(context);
181+
metadata.setUiGroup(CustomizableFieldGroup.getGroupsForContext(context).stream().findFirst().orElse(null));
182+
metadata.setUiLinePosition(1);
183+
return metadata;
184+
}
185+
117186
private String getEntityUuidForContext(CustomizableFieldContext context, CaseDataDto caze) {
118187
switch (context) {
119188
case CASE:
@@ -128,4 +197,17 @@ private String getEntityUuidForContext(CustomizableFieldContext context, CaseDat
128197
throw new IllegalArgumentException("Unhandled context: " + context);
129198
}
130199
}
200+
201+
private String getEntityUuidForContext(CustomizableFieldContext context, ContactDto contact) {
202+
switch (context) {
203+
case EPIDATA:
204+
return contact.getEpiData().getUuid();
205+
case EXPOSURE:
206+
return contact.getEpiData().getExposures() != null && !contact.getEpiData().getExposures().isEmpty()
207+
? contact.getEpiData().getExposures().get(0).getUuid()
208+
: null;
209+
default:
210+
throw new IllegalArgumentException("Unhandled context: " + context);
211+
}
212+
}
131213
}

0 commit comments

Comments
 (0)