diff --git a/api/src/main/java/org/openmrs/module/legacyui/GeneralUtils.java b/api/src/main/java/org/openmrs/module/legacyui/GeneralUtils.java index e54e934f..90bd0448 100644 --- a/api/src/main/java/org/openmrs/module/legacyui/GeneralUtils.java +++ b/api/src/main/java/org/openmrs/module/legacyui/GeneralUtils.java @@ -11,6 +11,7 @@ import org.openmrs.Concept; import org.openmrs.api.context.Context; +import org.openmrs.util.OpenmrsConstants; public class GeneralUtils { @@ -68,4 +69,23 @@ public static Concept getConcept(String id) { return cpt; } + + /** + * Checks if current version of openmrs is greater or equal to 2.7.0 The aim is to try loading + * ConceptReferenceRange class, which is in version 2.7.0. If the ConceptReferenceRange class is + * loaded, then the current version is greater than or equal to 2.7.0 + * + * @return true if current version is greater or equal to 2.7.0 and false otherwise + * @since 1.17.0 + */ + public static boolean isTwoPointSevenAndAbove() { + try { + Class.forName("org.openmrs.ConceptReferenceRange"); + + return true; + } + catch (ClassNotFoundException exception) { + return false; + } + } } diff --git a/omod/src/main/java/org/openmrs/web/controller/ConceptFormController.java b/omod/src/main/java/org/openmrs/web/controller/ConceptFormController.java index 50fcb110..93abe55e 100644 --- a/omod/src/main/java/org/openmrs/web/controller/ConceptFormController.java +++ b/omod/src/main/java/org/openmrs/web/controller/ConceptFormController.java @@ -9,17 +9,21 @@ */ package org.openmrs.web.controller; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import javax.servlet.ServletException; @@ -54,6 +58,7 @@ import org.openmrs.api.ConceptsLockedException; import org.openmrs.api.DuplicateConceptNameException; import org.openmrs.api.context.Context; +import org.openmrs.module.legacyui.GeneralUtils; import org.openmrs.module.web.extension.ConceptUsageExtension; import org.openmrs.module.web.extension.provider.Link; import org.openmrs.propertyeditor.ConceptAnswersEditor; @@ -69,7 +74,9 @@ import org.openmrs.validator.ValidateUtil; import org.openmrs.web.WebConstants; import org.openmrs.web.attribute.WebAttributeUtil; +import org.openmrs.web.controller.concept.ConceptReferenceRange; import org.openmrs.web.controller.concept.ConceptReferenceTermWebValidator; +import org.openmrs.web.controller.mappper.ConceptFormMapper; import org.springframework.beans.propertyeditors.CustomDateEditor; import org.springframework.beans.propertyeditors.CustomNumberEditor; import org.springframework.context.support.MessageSourceAccessor; @@ -271,6 +278,8 @@ protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse validateConceptUsesPersistedObjects(concept, errors); + new ConceptFormValidator().validateConceptReferenceRange(concept, errors); + if (!errors.hasErrors()) { if (action.equals(msa.getMessage("Concept.cancel"))) { return new ModelAndView(new RedirectView("index.htm")); @@ -406,6 +415,9 @@ protected Map referenceData(HttpServletRequest request) throws E map.put("locale", Context.getLocale()); // should be same string format as conceptNamesByLocale map keys map.put("attributeTypes", cs.getAllConceptAttributeTypes()); + + map.put("canUseConceptReferenceRanges", GeneralUtils.isTwoPointSevenAndAbove()); + return map; } @@ -459,6 +471,8 @@ public class ConceptFormBackingObject { public Collection activeAttributes; + public List referenceRanges; + /** * Default constructor must take in a Concept object to create itself * @@ -517,6 +531,10 @@ public ConceptFormBackingObject(Concept concept) { this.allowDecimal = cn.getAllowDecimal(); this.displayPrecision = cn.getDisplayPrecision(); this.units = cn.getUnits(); + + this.referenceRanges = ListUtils.lazyList( + new ArrayList<>(new ConceptFormMapper().mapToWebReferenceRanges(cn)), + FactoryUtils.instantiateFactory(ConceptReferenceRange.class)); } else if (concept instanceof ConceptComplex) { ConceptComplex complex = (ConceptComplex) concept; this.handlerKey = complex.getHandler(); @@ -662,6 +680,8 @@ public Concept getConceptFromFormData() { cn.setDisplayPrecision(displayPrecision); cn.setUnits(units); + setConceptReferenceRanges(cn); + concept = cn; } else if (concept.getDatatype().getName().equals("Complex")) { @@ -678,6 +698,160 @@ public Concept getConceptFromFormData() { return concept; } + /** + * This method sets reference ranges to concept numeric. + * + * @param cn ConceptNumeric + * @since 1.17.0 + */ + private void setConceptReferenceRanges(ConceptNumeric cn) { + if (this.referenceRanges == null) { + return; + } + for (ConceptReferenceRange referenceRange : this.referenceRanges) { + if (referenceRange == null) { + continue; + } + + if (referenceRange.getId() != null) { + if (referenceRange.getId() <= 0) { + removeReferenceRange(cn, referenceRange); + } else { + updateReferenceRange(cn, referenceRange); + } + } else { + if (referenceRange.getHiAbsolute() != null && referenceRange.getLowAbsolute() != null) { + addReferenceRange(cn, referenceRange); + } + } + } + } + + /** + * This method removes a reference range from conceptNumeric + * + * @param cn conceptNumeric + * @param referenceRange referenceRange + * @since 1.17.0 + */ + private void removeReferenceRange(ConceptNumeric cn, ConceptReferenceRange referenceRange) { + try { + Object platformReferenceRange = new ConceptFormMapper().mapToConceptReferenceRange(referenceRange, cn); + setMethodValue(cn, "removeReferenceRange", platformReferenceRange); + } + catch (Exception exception) { + // Note that openMRS-core version 2.7.0 or higher is required for this functionality to work. + } + } + + /** + * This method adds a new reference range to conceptNumeric + * + * @param cn conceptNumeric + * @param referenceRange referenceRange + * @since 1.17.0 + */ + private void addReferenceRange(ConceptNumeric cn, ConceptReferenceRange referenceRange) { + referenceRange.setConceptNumeric(cn); + try { + Object platformReferenceRange = new ConceptFormMapper().mapToConceptReferenceRange(referenceRange, cn); + setMethodValue(cn, "addReferenceRange", platformReferenceRange); + } + catch (Exception exception) { + // Note that openMRS-core version 2.7.0 or higher is required for this functionality to work. + } + } + + /** + * This method updates concept reference range if a field value has changed. + * + * @param cn ConceptNumeric + * @param referenceRange ConceptReferenceRange + * @since 1.17.0 + */ + public void updateReferenceRange(ConceptNumeric cn, ConceptReferenceRange referenceRange) { + try { + Set existingReferenceRanges = getExistingReferenceRanges(cn); + + for (Object existingRange : existingReferenceRanges) { + Method getIdMethod = existingRange.getClass().getMethod("getId"); + Object idValue = getIdMethod.invoke(existingRange); + + if (Objects.equals(idValue, referenceRange.getId()) && hasReferenceRangeChanged(existingRange, referenceRange)) { + updateReferenceRangeFields(existingRange, referenceRange); + break; + } + } + } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException | + ClassNotFoundException exception) { + // Note that openMRS-core version 2.7.0 or higher is required for this functionality to work. + } catch (Exception exception) { + // Note that openMRS-core version 2.7.0 or higher is required for this functionality to work. + } + } + + /** + * This method gets the existing reference ranges + * + * @param cn ConceptNumeric + * @return a set of reference ranges + * @since 1.17.0 + */ + private Set getExistingReferenceRanges(ConceptNumeric cn) { + try { + Method getReferenceRangesMethod = cn.getClass().getMethod("getReferenceRanges"); + return (Set) getReferenceRangesMethod.invoke(cn); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + // Note that openMRS-core version 2.7.0 or higher is required for this functionality to work. + return Collections.emptySet(); + } + } + + private boolean hasReferenceRangeChanged(Object existingRange, ConceptReferenceRange referenceRange) + throws Exception { + return !Objects.equals(referenceRange.getCriteria(), getMethodValue(existingRange, "getCriteria")) + || !Objects.equals(referenceRange.getHiAbsolute(), getMethodValue(existingRange, "getHiAbsolute")) + || !Objects.equals(referenceRange.getHiCritical(), getMethodValue(existingRange, "getHiCritical")) + || !Objects.equals(referenceRange.getHiNormal(), getMethodValue(existingRange, "getHiNormal")) + || !Objects.equals(referenceRange.getLowAbsolute(), getMethodValue(existingRange, "getLowAbsolute")) + || !Objects.equals(referenceRange.getLowCritical(), getMethodValue(existingRange, "getLowCritical")) + || !Objects.equals(referenceRange.getLowNormal(), getMethodValue(existingRange, "getLowNormal")); + } + + /** + * This method updates reference range fields + * + * @param existingRange existing reference range + * @param referenceRange the updated reference range + * @throws Exception exception + * @since 1.17.0 + */ + private void updateReferenceRangeFields(Object existingRange, ConceptReferenceRange referenceRange) throws Exception { + updateField(existingRange, "setHiAbsolute", referenceRange.getHiAbsolute()); + updateField(existingRange, "setHiCritical", referenceRange.getHiCritical()); + updateField(existingRange, "setHiNormal", referenceRange.getHiNormal()); + updateField(existingRange, "setLowAbsolute", referenceRange.getLowAbsolute()); + updateField(existingRange, "setLowCritical", referenceRange.getLowCritical()); + updateField(existingRange, "setLowNormal", referenceRange.getLowNormal()); + updateField(existingRange, "setCriteria", referenceRange.getCriteria()); + } + + private void updateField(Object obj, String methodName, Object value) throws Exception { + if (value != null) { + setMethodValue(obj, methodName, value); + } + } + + private Object getMethodValue(Object obj, String methodName) throws Exception { + Method method = obj.getClass().getMethod(methodName); + return method.invoke(obj); + } + + private void setMethodValue(Object obj, String methodName, Object value) throws Exception { + Method method = obj.getClass().getMethod(methodName, value.getClass()); + method.invoke(obj, value); + } + /** * Builds a white-space separated list of concept ids belonging to a concept set * @@ -938,6 +1112,36 @@ public List
getFormsInUse() { return Context.getFormService().getFormsContainingConcept(concept); } + /** + * Get reference ranges + * + * @return the referenceRanges + * @since 1.17.0 + */ + public List getReferenceRanges() { + return referenceRanges; + } + + /** + * Sets reference ranges + * + * @param referenceRanges the referenceRanges to set + * @since 1.17.0 + */ + public void setReferenceRanges(List referenceRanges) { + this.referenceRanges = referenceRanges; + } + + /** + * Adds a new reference range to the list of reference ranges + * + * @param referenceRange the referenceRange to add + * @since 1.17.0 + */ + public void addReferenceRange(ConceptReferenceRange referenceRange) { + getReferenceRanges().add(referenceRange); + } + /** * Get the list of extensions/metadata and the specific instances of them that use this * concept. diff --git a/omod/src/main/java/org/openmrs/web/controller/ConceptFormValidator.java b/omod/src/main/java/org/openmrs/web/controller/ConceptFormValidator.java index 8276524b..ad97791d 100644 --- a/omod/src/main/java/org/openmrs/web/controller/ConceptFormValidator.java +++ b/omod/src/main/java/org/openmrs/web/controller/ConceptFormValidator.java @@ -10,16 +10,22 @@ package org.openmrs.web.controller; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.openmrs.Concept; import org.openmrs.ConceptMap; import org.openmrs.ConceptName; +import org.openmrs.ConceptNumeric; import org.openmrs.api.context.Context; import org.openmrs.web.controller.ConceptFormController.ConceptFormBackingObject; +import org.openmrs.web.controller.concept.ConceptReferenceRange; +import org.openmrs.web.controller.mappper.ConceptFormMapper; +import org.springframework.validation.BindException; import org.springframework.validation.Errors; import org.springframework.validation.Validator; @@ -113,4 +119,98 @@ else if (backingObject.getNamesByLocale().get(locale).getConceptNameId() != null } } + /** + * Validates reference range fields + * + * @param concept concept + * @param errors errors + * @since 1.17.0 + */ + public void validateConceptReferenceRange(Concept concept, BindException errors) { + if (concept.isNumeric()) { + ConceptNumeric conceptNumeric = (ConceptNumeric) concept; + + List referenceRanges = new ConceptFormMapper().mapToWebReferenceRanges(conceptNumeric); + + if (referenceRanges == null || referenceRanges.isEmpty()) { + return; + } + + int index = 0; + for (ConceptReferenceRange referenceRange : referenceRanges) { + + if (conceptNumeric.getHiAbsolute() != null && conceptNumeric.getLowAbsolute() != null) { + if (referenceRange.getHiAbsolute() == null) { + setReferenceRangeErrors(errors, index, "hiAbsolute", + "Concept.referenceRanges.error.high.absolute.value.required", + "Concept.referenceRanges.error.absolute.value.required"); + } else { + if (referenceRange.getHiAbsolute() > conceptNumeric.getHiAbsolute()) { + setReferenceRangeErrorsWithValue(errors, index, "hiAbsolute", + "Concept.referenceRanges.error.highAbsolute.value.outOfRange", + referenceRange.getHiAbsolute(), conceptNumeric.getHiAbsolute()); + } else if (referenceRange.getHiAbsolute() < conceptNumeric.getLowAbsolute()) { + setReferenceRangeErrors(errors, index, "hiAbsolute", + "Concept.referenceRanges.error.absolute.value.invalid", + "Concept.referenceRanges.error.absolute.value.invalid"); + } + } + if (referenceRange.getLowAbsolute() == null) { + setReferenceRangeErrors(errors, index, "lowAbsolute", + "Concept.referenceRanges.error.low.absolute.value.required", + "Concept.referenceRanges.error.absolute.value.required"); + } else { + if (referenceRange.getLowAbsolute() < conceptNumeric.getLowAbsolute()) { + setReferenceRangeErrorsWithValue(errors, index, "lowAbsolute", + "Concept.referenceRanges.error.lowAbsolute.value.outOfRange", + referenceRange.getLowAbsolute(), conceptNumeric.getLowAbsolute()); + } else if (referenceRange.getLowAbsolute() > conceptNumeric.getHiAbsolute()) { + setReferenceRangeErrors(errors, index, "lowAbsolute", + "Concept.referenceRanges.error.absolute.value.invalid", + "Concept.referenceRanges.error.absolute.value.invalid"); + } + } + + index++; + } + } + } + } + + /** + * Set Reference Range Errors + * + * @param errors BindException + * @param index index of referenceRange row + * @param field field of the reference range + * @param errorCode error code + * @param defaultMessage default message + * @since 1.17.0 + */ + private void setReferenceRangeErrors(BindException errors, long index, String field, String errorCode, + String defaultMessage) { + errors.pushNestedPath("referenceRanges[" + index + "]"); + errors.rejectValue(field, errorCode, defaultMessage); + errors.popNestedPath(); + } + + /** + * Set Reference Range Errors with values + * + * @param errors BindException + * @param index index of referenceRange row + * @param field field of the reference range + * @param errorCode error code + * @param value current high or low absolute value + * @param thresholdValue high or low absolute value of conceptNumeric + * @since 1.17.0 + */ + private void setReferenceRangeErrorsWithValue(BindException errors, long index, String field, String errorCode, + Double value, Double thresholdValue) { + errors.pushNestedPath("referenceRanges[" + index + "]"); + errors.rejectValue(field, + Context.getMessageSourceService().getMessage(errorCode, new Object[] { value, thresholdValue }, null), + "Concept.referenceRanges.error.absolute.value.invalid"); + errors.popNestedPath(); + } } diff --git a/omod/src/main/java/org/openmrs/web/controller/concept/ConceptReferenceRange.java b/omod/src/main/java/org/openmrs/web/controller/concept/ConceptReferenceRange.java new file mode 100644 index 00000000..b0b95713 --- /dev/null +++ b/omod/src/main/java/org/openmrs/web/controller/concept/ConceptReferenceRange.java @@ -0,0 +1,236 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.web.controller.concept; + +import org.openmrs.ConceptNumeric; + +/** + * This class represents a reference range for a {@link ConceptNumeric}. It is used to store and + * manage reference range values, to be able to allow backward compatibility in openmrs core + * versions. A concept reference range defines the acceptable numeric values/ranges for specific + * factors such as age, gender, e.t.c. + * + * @since 1.17.0 + */ +public class ConceptReferenceRange { + + private Integer conceptReferenceRangeId; + + private String criteria; + + private ConceptNumeric conceptNumeric; + + private String uuid; + + private Double hiAbsolute; + + private Double hiCritical; + + private Double hiNormal; + + private Double lowAbsolute; + + private Double lowCritical; + + private Double lowNormal; + + public ConceptReferenceRange() { + } + + /** + * Gets id of conceptReferenceRange + * + * @return Returns the ConceptReferenceRangeId. + */ + public Integer getConceptReferenceRangeId() { + return conceptReferenceRangeId; + } + + /** + * Sets conceptReferenceRangeId + * + * @param conceptReferenceRangeId The conceptReferenceRangeId to set. + */ + public void setConceptReferenceRangeId(Integer conceptReferenceRangeId) { + this.conceptReferenceRangeId = conceptReferenceRangeId; + } + + /** + * Gets the criteria of conceptReferenceRange + * + * @return criteria + */ + public String getCriteria() { + return this.criteria; + } + + /** + * Sets the criteria of conceptReferenceRange + * + * @param criteria the criteria to set + */ + public void setCriteria(String criteria) { + this.criteria = criteria; + } + + /** + * Gets conceptNumeric of conceptReferenceRange + * + * @return Returns the ConceptNumeric. + */ + public ConceptNumeric getConceptNumeric() { + return conceptNumeric; + } + + /** + * Sets conceptNumeric + * + * @param conceptNumeric concept to set. + */ + public void setConceptNumeric(ConceptNumeric conceptNumeric) { + this.conceptNumeric = conceptNumeric; + } + + /** + * @see org.openmrs.OpenmrsObject#getId() + */ + public Integer getId() { + return getConceptReferenceRangeId(); + } + + /** + * @see org.openmrs.OpenmrsObject#setId(java.lang.Integer) + */ + public void setId(Integer id) { + setConceptReferenceRangeId(id); + } + + /** + * @see org.openmrs.OpenmrsObject#getUuid() + */ + public String getUuid() { + return uuid; + } + + /** + * @see org.openmrs.OpenmrsObject#setUuid(java.lang.String) + */ + public void setUuid(String uuid) { + this.uuid = uuid; + } + + /** + * Gets high absolute value of the referenceRange + * + * @return hiAbsolute the high absolute value + */ + public Double getHiAbsolute() { + return this.hiAbsolute; + } + + /** + * Sets high absolute value of the referenceRange + * + * @param hiAbsolute high absolute value to set + */ + public void setHiAbsolute(Double hiAbsolute) { + this.hiAbsolute = hiAbsolute; + } + + /** + * Gets high critical value of the referenceRange + * + * @return the high critical value + */ + public Double getHiCritical() { + return this.hiCritical; + } + + /** + * Sets high critical value of the referenceRange + * + * @param hiCritical high critical value to set + */ + public void setHiCritical(Double hiCritical) { + this.hiCritical = hiCritical; + } + + /** + * Returns high normal value of the referenceRange + * + * @return the high normal value + */ + public Double getHiNormal() { + return this.hiNormal; + } + + /** + * Sets high normal value of the referenceRange + * + * @param hiNormal high normal value to set + */ + public void setHiNormal(Double hiNormal) { + this.hiNormal = hiNormal; + } + + /** + * Gets low absolute value of the referenceRange + * + * @return the low absolute value + */ + public Double getLowAbsolute() { + return this.lowAbsolute; + } + + /** + * Sets low absolute value of the referenceRange + * + * @param lowAbsolute low absolute value to set + */ + public void setLowAbsolute(Double lowAbsolute) { + this.lowAbsolute = lowAbsolute; + } + + /** + * Gets low critical value of the referenceRange + * + * @return the low critical value + */ + public Double getLowCritical() { + return this.lowCritical; + } + + /** + * Sets low critical value of the referenceRange + * + * @param lowCritical low critical value to set + */ + public void setLowCritical(Double lowCritical) { + this.lowCritical = lowCritical; + } + + /** + * Gets low normal value of the referenceRange + * + * @return the low normal value + */ + public Double getLowNormal() { + return this.lowNormal; + } + + /** + * Sets low normal value of the referenceRange + * + * @param lowNormal low normal value to set + */ + public void setLowNormal(Double lowNormal) { + this.lowNormal = lowNormal; + } +} diff --git a/omod/src/main/java/org/openmrs/web/controller/mappper/ConceptFormMapper.java b/omod/src/main/java/org/openmrs/web/controller/mappper/ConceptFormMapper.java new file mode 100644 index 00000000..60dcbb78 --- /dev/null +++ b/omod/src/main/java/org/openmrs/web/controller/mappper/ConceptFormMapper.java @@ -0,0 +1,103 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.web.controller.mappper; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.openmrs.ConceptNumeric; +import org.openmrs.web.controller.concept.ConceptReferenceRange; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * This class maps the web-based concept attributes to their corresponding internal concepts and + * vice versa. + * + * @since 1.17.0 + */ +public class ConceptFormMapper { + + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * Maps web's reference range to reference range in openMRS core + * + * @param webReferenceRange webReferenceRange + * @param cn ConceptNumeric + * @return reference range + */ + public Object mapToConceptReferenceRange( + ConceptReferenceRange webReferenceRange, + ConceptNumeric cn) { + + try { + Class referenceRangeClass = Class.forName("org.openmrs.ConceptReferenceRange"); + Object referenceRange = referenceRangeClass.getDeclaredConstructor().newInstance(); + + referenceRangeClass.getMethod("setCriteria", String.class).invoke(referenceRange, webReferenceRange.getCriteria()); + referenceRangeClass.getMethod("setConceptNumeric", ConceptNumeric.class).invoke(referenceRange, cn); + referenceRangeClass.getMethod("setUuid", String.class).invoke(referenceRange, webReferenceRange.getUuid()); + referenceRangeClass.getMethod("setHiAbsolute", Double.class).invoke(referenceRange, webReferenceRange.getHiAbsolute()); + referenceRangeClass.getMethod("setHiCritical", Double.class).invoke(referenceRange, webReferenceRange.getHiCritical()); + referenceRangeClass.getMethod("setHiNormal", Double.class).invoke(referenceRange, webReferenceRange.getHiNormal()); + referenceRangeClass.getMethod("setLowAbsolute", Double.class).invoke(referenceRange, webReferenceRange.getLowAbsolute()); + referenceRangeClass.getMethod("setLowCritical", Double.class).invoke(referenceRange, webReferenceRange.getLowCritical()); + referenceRangeClass.getMethod("setLowNormal", Double.class).invoke(referenceRange, webReferenceRange.getLowNormal()); + referenceRangeClass.getMethod("setConceptReferenceRangeId", Integer.class).invoke(referenceRange, webReferenceRange.getConceptReferenceRangeId()); + + return referenceRange; + + } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException | InstantiationException | + ClassNotFoundException exception) { + // Note that openMRS-core version 2.7.* or higher is required for this functionality to work. + } + return null; + } + + /** + * Maps reference ranges in openMRS core to web's reference ranges + * + * @param cn ConceptNumeric + * @return list of reference range + */ + public List mapToWebReferenceRanges(ConceptNumeric cn) { + List webReferenceRanges = new ArrayList<>(); + + try { + Method getReferenceRangesMethod = ConceptNumeric.class.getMethod("getReferenceRanges"); + Object referenceRanges = getReferenceRangesMethod.invoke(cn); + + for (Object referenceRange : (Set) referenceRanges) { + ConceptReferenceRange webReferenceRange = new ConceptReferenceRange(); + + webReferenceRange.setConceptReferenceRangeId((Integer) referenceRange.getClass().getMethod("getConceptReferenceRangeId").invoke(referenceRange)); + webReferenceRange.setUuid((String) referenceRange.getClass().getMethod("getUuid").invoke(referenceRange)); + webReferenceRange.setCriteria((String) referenceRange.getClass().getMethod("getCriteria").invoke(referenceRange)); + webReferenceRange.setHiAbsolute((Double) referenceRange.getClass().getMethod("getHiAbsolute").invoke(referenceRange)); + webReferenceRange.setHiCritical((Double) referenceRange.getClass().getMethod("getHiCritical").invoke(referenceRange)); + webReferenceRange.setHiNormal((Double) referenceRange.getClass().getMethod("getHiNormal").invoke(referenceRange)); + webReferenceRange.setLowAbsolute((Double) referenceRange.getClass().getMethod("getLowAbsolute").invoke(referenceRange)); + webReferenceRange.setLowCritical((Double) referenceRange.getClass().getMethod("getLowCritical").invoke(referenceRange)); + webReferenceRange.setLowNormal((Double) referenceRange.getClass().getMethod("getLowNormal").invoke(referenceRange)); + webReferenceRange.setConceptNumeric((ConceptNumeric) referenceRange.getClass().getMethod("getConceptNumeric").invoke(referenceRange)); + + webReferenceRanges.add(webReferenceRange); + } + } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { + // Note that openMRS-core version 2.7.* or higher is required for this functionality to work. + } + + return webReferenceRanges; + } +} diff --git a/omod/src/main/resources/messages.properties b/omod/src/main/resources/messages.properties new file mode 100644 index 00000000..4f17ef82 --- /dev/null +++ b/omod/src/main/resources/messages.properties @@ -0,0 +1,10 @@ +Concept.referenceRanges=Reference Ranges +Concept.referenceRanges.criteria=Criteria +Concept.referenceRanges.help=Defines reference ranges of this concept with a criteria +Concept.referenceRanges.add=Add a Reference Range +Concept.referenceRanges.error.low.absolute.value.required=Low Absolute cannot be empty +Concept.referenceRanges.error.high.absolute.value.required=High Absolute cannot be empty +Concept.referenceRanges.error.absolute.value.required=Absolute value cannot be empty +Concept.referenceRanges.error.highAbsolute.value.outOfRange=High Absolute {0} cannot exceed concept''s High Absolute {1} +Concept.referenceRanges.error.lowAbsolute.value.outOfRange=Low Absolute {0} cannot be below concept''s Low Absolute {1} +Concept.referenceRanges.error.absolute.value.invalid=Invalid absolute value of reference range \ No newline at end of file diff --git a/omod/src/main/webapp/admin/dictionary/concept.jsp b/omod/src/main/webapp/admin/dictionary/concept.jsp index 7e4883f9..eaaab601 100644 --- a/omod/src/main/webapp/admin/dictionary/concept.jsp +++ b/omod/src/main/webapp/admin/dictionary/concept.jsp @@ -319,6 +319,37 @@ + + + "> + + + + + style="display:none"> + + + + + + + + + + class='evenRow'> + + + + + + + + + +
+ + +
diff --git a/omod/src/main/webapp/admin/dictionary/conceptForm.jsp b/omod/src/main/webapp/admin/dictionary/conceptForm.jsp index 578c6996..340823b0 100644 --- a/omod/src/main/webapp/admin/dictionary/conceptForm.jsp +++ b/omod/src/main/webapp/admin/dictionary/conceptForm.jsp @@ -78,6 +78,11 @@ $j(".hideableEle").hide(); }); + $j(document).ready(function(){ + if(${fn:length(command.referenceRanges)} == 0) + $j(".referenceRangeHeader").hide(); + }); + @@ -815,6 +820,65 @@ + + + + "/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ + + ${status.errorMessage} + +
+ +
+ + +
"/> @@ -987,6 +1051,16 @@ function resetNewTermForm(){ $j('#newTermErrorMsg').hide(); $j('#successMsg').hide(); } + +document.addEventListener("DOMContentLoaded", function() { + var conceptSetCheckbox = document.getElementById("conceptSet"); + changeSetStatus(conceptSetCheckbox); + }); + + function changeSetStatus(checkbox) { + var conceptSetRow = document.getElementById("conceptSetRow"); + conceptSetRow.style.display = checkbox.checked ? "" : "none"; + } diff --git a/omod/src/main/webapp/dictionary/concept.jsp b/omod/src/main/webapp/dictionary/concept.jsp index 73692904..3ba17778 100644 --- a/omod/src/main/webapp/dictionary/concept.jsp +++ b/omod/src/main/webapp/dictionary/concept.jsp @@ -322,6 +322,37 @@ + + + "> + + + + + style="display:none"> + + + + + + + + + + class='evenRow'> + + + + + + + + + +
+ + +
diff --git a/omod/src/main/webapp/dictionary/conceptForm.jsp b/omod/src/main/webapp/dictionary/conceptForm.jsp index fe7835bd..586667e8 100644 --- a/omod/src/main/webapp/dictionary/conceptForm.jsp +++ b/omod/src/main/webapp/dictionary/conceptForm.jsp @@ -78,6 +78,11 @@ $j(".hideableEle").hide(); }); + $j(document).ready(function(){ + if(${fn:length(command.referenceRanges)} == 0) + $j(".referenceRangeHeader").hide(); + }); + @@ -789,7 +794,7 @@ - @@ -815,6 +820,65 @@ + + + + "/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ + + ${status.errorMessage} + +
+ +
+ + +
@@ -1008,6 +1072,16 @@ function resetNewTermForm(){ $j('#newTermErrorMsg').hide(); $j('#successMsg').hide(); } + +document.addEventListener("DOMContentLoaded", function() { + var conceptSetCheckbox = document.getElementById("conceptSet"); + changeSetStatus(conceptSetCheckbox); + }); + + function changeSetStatus(checkbox) { + var conceptSetRow = document.getElementById("conceptSetRow"); + conceptSetRow.style.display = checkbox.checked ? "" : "none"; + } diff --git a/omod/src/main/webapp/resources/scripts/dictionary/conceptForm.js b/omod/src/main/webapp/resources/scripts/dictionary/conceptForm.js index 1fc7aa8a..12ee568e 100644 --- a/omod/src/main/webapp/resources/scripts/dictionary/conceptForm.js +++ b/omod/src/main/webapp/resources/scripts/dictionary/conceptForm.js @@ -169,13 +169,23 @@ customDatatypes.push("coded"); customDatatypes.push("complex"); function changeDatatype(obj) { + var selectedType = obj[obj.selectedIndex].text.toLowerCase(); + for (var i=0; i < customDatatypes.length; i++) { var row = document.getElementById(customDatatypes[i] + "DatatypeRow"); - if (obj[obj.selectedIndex].text.toLowerCase() == customDatatypes[i]) + if (selectedType == customDatatypes[i]) row.style.display = ""; else row.style.display = "none"; } + + var referenceRangeRow = document.getElementById("referenceRangeRow"); + + if (selectedType == "numeric") { + referenceRangeRow.style.display = ""; + } else { + referenceRangeRow.style.display = "none"; + } } function listKeyPress(from, to, delim, event) { @@ -433,7 +443,7 @@ function addConceptMapping(initialSizeOfClonedSiblings){ input.id = 'term[' + index + '].name'; } } - + //find the select element and set the name attribute for the conceptSource and map type for (var i in selects) { var select = selects[i]; @@ -453,6 +463,62 @@ function addConceptMapping(initialSizeOfClonedSiblings){ } } +/** + * Method is invoked to add a new reference range + * @param initialSizeOfReferenceRanges the number of mappings on page load + */ +function addReferenceRanges(initialSizeOfReferenceRanges) { + var inputNamePrefix = 'referenceRanges'; + var newRow = cloneElement('newReferenceRange', initialSizeOfReferenceRanges, inputNamePrefix); + + if(newRow){ + newRow.id = ""; + var index = numberOfClonedElements['newReferenceRange']; + var inputs = newRow.getElementsByTagName("input"); + + for (var x = 0; x < inputs.length; x++) { + var input = inputs[x]; + + if (input && input.type == 'text') { + input.id = inputNamePrefix + '[' + index + '].' + input.name.split('.')[1] + input.name = inputNamePrefix + '[' + index + '].' + input.name.split('.')[1] + } + } + + if(!$j("#headerRow").is(":visible")) + $j(".referenceRangeHeader").show(); + } +} + +/** + * Handles deleting and update visibility of reference range fields + * + * @param btn + * the source object of event + * @param key + * the key object for receiving the save key +*/ +function removeReferenceRangeElement(btn, key) { + removeParentElement(btn.parentNode); + numberOfClonedElements[key]--; +} + +/** + * Handles deleting and updating visibility of reference range fields. + * The removed row is marked for deletion. + * + * @param btn + * the source object of event + * @param index + * the index of the row to be deleted +*/ +function removeReferenceRangeElementByIndex(btn, index) { + removeParentElement(btn.parentNode); + var row = document.getElementById('row-' + index); + + document.getElementById('removeMarker-' + index).value = "0"; +} + function addAutoComplete(displayInputId, sourceSelectElementId, hiddenElementId, nameInputId){ var selectOption = document.getElementById(sourceSelectElementId); // set up the autocomplete on the conceptReferenceTerm input box for the new mapping diff --git a/omod/src/test/java/org/openmrs/web/controller/ConceptFormControllerTest.java b/omod/src/test/java/org/openmrs/web/controller/ConceptFormControllerTest.java index 5c7e5ecd..3cf5685a 100644 --- a/omod/src/test/java/org/openmrs/web/controller/ConceptFormControllerTest.java +++ b/omod/src/test/java/org/openmrs/web/controller/ConceptFormControllerTest.java @@ -19,8 +19,11 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.Collection; import java.util.Locale; +import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -49,6 +52,8 @@ import org.openmrs.util.LocaleUtility; import org.openmrs.util.OpenmrsConstants; import org.openmrs.web.controller.ConceptFormController.ConceptFormBackingObject; +import org.openmrs.web.controller.concept.ConceptReferenceRange; +import org.openmrs.web.controller.mappper.ConceptFormMapper; import org.openmrs.web.test.BaseModuleWebContextSensitiveTest; import org.openmrs.web.test.WebTestHelper; import org.openmrs.web.test.WebTestHelper.Response; @@ -1314,4 +1319,121 @@ public void shouldSetAttributesToVoidIfTheValueIsNotSet() throws Exception { Assert.assertTrue(((ConceptAttribute) (concept.getAttributes().toArray()[0])).getVoided()); Assert.assertFalse(errors.hasErrors()); } + + /** + * To be uncommented when the openmrs-core version in this module is ultimately changed to 2.7.0 + * or above // /** // * @see ConceptFormController#onSubmit(HttpServletRequest, + * HttpServletResponse, Object, // * BindException) // / + * + * @Test public void onSubmit_shouldAddANewReferenceRangeToAnExistingConceptNumeric() throws + * Exception { ConceptService cs = Context.getConceptService(); ConceptNumeric + * conceptNumeric = cs.getConceptNumeric(4090); assertNotNull(conceptNumeric); int + * initialConceptMappingCount = + * getReferenceRangesFromConceptNumeric(conceptNumeric).size(); ConceptFormController + * conceptFormController = (ConceptFormController) + * applicationContext.getBean("conceptForm"); MockHttpServletRequest mockRequest = new + * MockHttpServletRequest(); MockHttpServletResponse response = new + * MockHttpServletResponse(); mockRequest.setMethod("POST"); + * mockRequest.setParameter("referenceRanges[0].hiAbasolute", "120"); + * mockRequest.setParameter("referenceRanges[0].lowAbsolute", "100"); + * mockRequest.setParameter("referenceRanges[0].criteria", "$fn.getAge() > 3"); + * ModelAndView mav = conceptFormController.handleRequest(mockRequest, response); + * assertNotNull(mav); assertTrue(mav.getModel().isEmpty()); + * assertEquals(initialConceptMappingCount + 1, getReferenceRangesByConceptId(cs, + * conceptNumeric.getConceptId())); } // /** // * @see + * ConceptFormController#onSubmit(HttpServletRequest, HttpServletResponse, Object, // * + * BindException) // / + * @Test public void onSubmit_shouldAddANewReferenceRangeWhenCreatingAConceptNumeric() throws + * Exception { ConceptService cs = Context.getConceptService(); final String conceptName = + * "new concept"; // make sure the concept doesn't already exist Concept newConcept = + * cs.getConceptByName(conceptName); assertNull(newConcept); ConceptFormController + * conceptFormController = (ConceptFormController) + * applicationContext.getBean("conceptForm"); MockHttpServletRequest mockRequest = new + * MockHttpServletRequest(); MockHttpServletResponse response = new + * MockHttpServletResponse(); mockRequest.setMethod("POST"); + * mockRequest.setParameter("action", ""); + * mockRequest.setParameter("namesByLocale[en_GB].name", conceptName); + * mockRequest.setParameter("descriptionsByLocale[en_GB].description", + * "some description"); mockRequest.setParameter("concept.datatype", "1"); + * mockRequest.setParameter("referenceRanges[0].hiAbasolute", "120"); + * mockRequest.setParameter("referenceRanges[0].lowAbsolute", "100"); + * mockRequest.setParameter("referenceRanges[0].criteria", "$fn.getAge() > 3"); + * ModelAndView mav = conceptFormController.handleRequest(mockRequest, response); + * assertNotNull(mav); assertTrue(mav.getModel().isEmpty()); Concept createdConcept = + * cs.getConceptByName(conceptName); assertNotNull(createdConcept); + * assertTrue(createdConcept instanceof ConceptNumeric); Class referenceRangeClass = + * Class.forName("org.openmrs.ConceptReferenceRange"); Method getReferenceRangesMethod = + * ConceptNumeric.class.getMethod("getReferenceRanges", referenceRangeClass); Set + * listOfReferenceRanges = (Set) getReferenceRangesMethod.invoke(createdConcept, null); + * Assert.assertEquals(1, listOfReferenceRanges.size()); } // /** // * @see + * ConceptFormController#onSubmit(HttpServletRequest, HttpServletResponse, Object, // * + * BindException) // / + * @Test public void onSubmit_shouldIgnoreNewConceptReferenceRowIfTheUserDidNotEnterAnyData() + * throws Exception { ConceptService cs = Context.getConceptService(); int conceptId = + * 4090; ConceptNumeric conceptNumeric = cs.getConceptNumeric(conceptId); + * assertNotNull(conceptNumeric); int initialConceptMappingCount = + * getReferenceRangesFromConceptNumeric(conceptNumeric).size(); ConceptFormController + * conceptFormController = (ConceptFormController) + * applicationContext.getBean("conceptForm"); MockHttpServletRequest mockRequest = new + * MockHttpServletRequest(); MockHttpServletResponse response = new + * MockHttpServletResponse(); mockRequest.setMethod("POST"); + * mockRequest.setParameter("action", ""); mockRequest.setParameter("conceptId", + * conceptNumeric.getConceptId().toString()); + * mockRequest.setParameter("referenceRanges[0].hiAbasolute", "120"); + * mockRequest.setParameter("referenceRanges[0].lowAbsolute", "100"); + * mockRequest.setParameter("referenceRanges[0].criteria", "$fn.getAge() > 3"); + * ModelAndView mav = conceptFormController.handleRequest(mockRequest, response); + * assertNotNull(mav); assertTrue(mav.getModel().isEmpty()); + * assertEquals(initialConceptMappingCount, getReferenceRangesByConceptId(cs, + * conceptId).size()); } // /** // * @see + * ConceptFormController#onSubmit(HttpServletRequest, HttpServletResponse, Object, // * + * BindException) // / + * @Test public void onSubmit_shouldRemoveAReferenceRangeFromAnExistingConceptNumeric() throws + * Exception { ConceptService cs = Context.getConceptService(); int conceptId = 4090; // + * make sure the concept already exists and has some concept mappings ConceptNumeric + * conceptNumeric = cs.getConceptNumeric(conceptId); assertNotNull(conceptNumeric); int + * initialConceptMappingCount = + * getReferenceRangesFromConceptNumeric(conceptNumeric).size(); + * assertTrue(initialConceptMappingCount > 0); ConceptFormController conceptFormController + * = (ConceptFormController) applicationContext.getBean("conceptForm"); + * MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + * MockHttpServletResponse response = new MockHttpServletResponse(); + * mockRequest.setMethod("POST"); mockRequest.setParameter("action", ""); + * mockRequest.setParameter("conceptId", conceptNumeric.getConceptId().toString()); + * mockRequest.setParameter("referenceRanges[0].id", "0"); ModelAndView mav = + * conceptFormController.handleRequest(mockRequest, response); assertNotNull(mav); + * assertTrue(mav.getModel().isEmpty()); assertEquals(initialConceptMappingCount - 1, + * getReferenceRangesByConceptId(cs, conceptId).size()); } // /** // * @see + * ConceptFormValidator#validateConceptReferenceRange(Concept, BindException) // / + * @Test public void + * validateReferenceRangeAbsolutes_shouldAddErrorIfAbsolutesAreOutsideConceptAbsoluteBound + * () { ConceptNumeric conceptNumeric = new ConceptNumeric(); + * conceptNumeric.setHiAbsolute(100.0); conceptNumeric.setLowAbsolute(80.0); + * ConceptReferenceRange referenceRange = new ConceptReferenceRange(); + * referenceRange.setHiAbsolute(1100.0); referenceRange.setLowAbsolute(1.0); + * updateConceptReferenceRange(referenceRange, conceptNumeric); BindException errors = new + * BindException(conceptNumeric, "conceptNumeric"); new + * ConceptFormValidator().validateConceptReferenceRange(conceptNumeric, errors); + * Assert.assertEquals(1, errors.getErrorCount()); + * assertTrue(errors.hasFieldErrors("referenceRanges[0].hiAbsolute")); + * assertTrue(errors.hasFieldErrors("referenceRanges[0].lowAbsolute")); } private void + * updateConceptReferenceRange( ConceptReferenceRange webReferenceRange, ConceptNumeric + * cn) { try { Class referenceRangeClass = + * Class.forName("org.openmrs.ConceptReferenceRange"); Object referenceRange = new + * ConceptFormMapper().mapToConceptReferenceRange(webReferenceRange, cn, + * referenceRangeClass); Method addReferenceRangeMethod = + * ConceptNumeric.class.getMethod("addReferenceRange", referenceRangeClass); + * addReferenceRangeMethod.invoke(cn, referenceRange); } catch (InvocationTargetException + * | NoSuchMethodException | IllegalAccessException | ClassNotFoundException exception) { + * logger.error("Failed to add reference range: Exception: " + exception.getMessage(), + * exception); } } private static Set getReferenceRangesByConceptId(ConceptService cs, int + * conceptId) throws NoSuchMethodException, IllegalAccessException, + * InvocationTargetException { Method getConceptReferenceRangesByConceptIdMethod = + * ConceptService.class.getMethod( "getConceptReferenceRangesByConceptId", Integer.class); + * return (Set) getConceptReferenceRangesByConceptIdMethod.invoke(cs, conceptId); } + * private static Set getReferenceRangesFromConceptNumeric(ConceptNumeric conceptNumeric) + * throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + * Method getReferenceRangesMethod = ConceptNumeric.class.getMethod("getReferenceRanges"); + * return (Set) getReferenceRangesMethod.invoke(conceptNumeric); } + */ } diff --git a/omod/src/test/java/org/openmrs/web/controller/concept/ConceptFormControllerTest.java b/omod/src/test/java/org/openmrs/web/controller/concept/ConceptFormControllerTest.java index 0a52c71e..d4447b41 100644 --- a/omod/src/test/java/org/openmrs/web/controller/concept/ConceptFormControllerTest.java +++ b/omod/src/test/java/org/openmrs/web/controller/concept/ConceptFormControllerTest.java @@ -56,4 +56,5 @@ public void ConceptFormBackingObject_shouldCopyNumericAttributes() { org.junit.Assert.assertEquals("ml", conceptFormBackingObject.getUnits()); } + }