From b69f8c47852babc9bb3a469a45f768dcff5f2152 Mon Sep 17 00:00:00 2001 From: Mark Goodrich Date: Wed, 3 Apr 2024 12:50:09 -0400 Subject: [PATCH] HTML-841: Errors with Form Namespace and Path --- .../htmlformentry/FormSubmissionActions.java | 81 +------- .../htmlformentry/ObsGroupComponent.java | 71 +++---- .../FormNamespaceAndPathRegressionTest.java | 184 ++++++++++++++++++ .../doubleObsGroupFormWithControlId.xml | 14 ++ .../singleObsGroupFormWithControlId.xml | 10 - 5 files changed, 242 insertions(+), 118 deletions(-) create mode 100644 api/src/test/resources/org/openmrs/module/htmlformentry/include/doubleObsGroupFormWithControlId.xml delete mode 100644 api/src/test/resources/org/openmrs/module/htmlformentry/include/singleObsGroupFormWithControlId.xml diff --git a/api/src/main/java/org/openmrs/module/htmlformentry/FormSubmissionActions.java b/api/src/main/java/org/openmrs/module/htmlformentry/FormSubmissionActions.java index b2fcd3f3a..d74cdcf2c 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/FormSubmissionActions.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/FormSubmissionActions.java @@ -381,7 +381,7 @@ public Obs createObs(Concept concept, Object value, Date datetime, String access * @param comment comment for the obs */ public void modifyObs(Obs existingObs, Concept concept, Object newValue, Date newDatetime, String accessionNumber, - String comment) { + String comment, String controlFormPath) { if (newValue == null || "".equals(newValue)) { // we want to delete the existing obs if (log.isDebugEnabled()) @@ -397,6 +397,7 @@ public void modifyObs(Obs existingObs, Concept concept, Object newValue, Date ne return; } Obs newObs = HtmlFormEntryUtil.createObs(concept, newValue, newDatetime, accessionNumber); + // random note... it is possible that the existing obs has changed even though the value as string is the same (probably not?) String oldString = existingObs.getValueAsString(Context.getLocale()); String newString = newObs.getValueAsString(Context.getLocale()); if (log.isDebugEnabled() && concept != null) { @@ -418,7 +419,7 @@ public void modifyObs(Obs existingObs, Concept concept, Object newValue, Date ne // TODO: really the voided obs should link to the new one, but this is a pain to implement due to the dreaded error: org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session obsToVoid.add(existingObs); - newObs = createObs(concept, newValue, newDatetime, accessionNumber, comment); + newObs = createObs(concept, newValue, newDatetime, accessionNumber, comment, controlFormPath); newObs.setPreviousVersion(existingObs); } else { if (existingObs != null && StringUtils.isNotBlank(comment)) @@ -430,79 +431,13 @@ public void modifyObs(Obs existingObs, Concept concept, Object newValue, Date ne } } - /** - * Legacy modifyObs methods without the comment argument - */ - public void modifyObs(Obs existingObs, Concept concept, Object newValue, Date newDatetime, String accessionNumber) { - modifyObs(existingObs, concept, newValue, newDatetime, accessionNumber, null); + public void modifyObs(Obs existingObs, Concept concept, Object newValue, Date newDatetime, String accessionNumber, + String comment) { + modifyObs(existingObs, concept, newValue, newDatetime, accessionNumber, comment, null); } - /** - * Modifies an existing Obs. - *

- * This method works by adding the current Obs to a list of Obs to void, and then adding the new Obs - * to a list of Obs to create. Note that this method does not commit the changes to the - * database--the changes are applied elsewhere in the framework. - * - * @param existingObs the Obs to modify - * @param concept concept associated with the Obs - * @param newValue the new value of the Obs - * @param newDatetime the new date information for the Obs - * @param accessionNumber new accession number for the Obs - * @param comment comment for the obs - */ - public void modifyObs(Obs existingObs, Concept concept, Object newValue, Date newDatetime, String accessionNumber, - String comment, String controlFormPath) { - if (newValue == null || "".equals(newValue)) { - // we want to delete the existing obs - if (log.isDebugEnabled()) { - log.debug("VOID: " + printObsHelper(existingObs)); - } - obsToVoid.add(existingObs); - return; - } - if (concept == null) { - // we want to delete the existing obs - if (log.isDebugEnabled()) - log.debug("VOID: " + printObsHelper(existingObs)); - obsToVoid.add(existingObs); - return; - } - Obs newObs = HtmlFormEntryUtil.createObs(concept, newValue, newDatetime, accessionNumber); - if (controlFormPath != null) { - newObs.setFormField(FORM_NAMESPACE, controlFormPath); - } - String oldString = existingObs.getValueAsString(Context.getLocale()); - String newString = newObs.getValueAsString(Context.getLocale()); - if (log.isDebugEnabled() && concept != null) { - log.debug("For concept " + concept.getName(Context.getLocale()) + ": " + oldString + " -> " + newString); - } - boolean valueChanged = !newString.equals(oldString); - // TODO: handle dates that may equal encounter date - boolean dateChanged = dateChangedHelper(existingObs.getObsDatetime(), newObs.getObsDatetime()); - boolean accessionNumberChanged = accessionNumberChangedHelper(existingObs.getAccessionNumber(), - newObs.getAccessionNumber()); - boolean conceptsHaveChanged = false; - if (!existingObs.getConcept().getConceptId().equals(concept.getConceptId())) { - conceptsHaveChanged = true; - } - if (valueChanged || dateChanged || accessionNumberChanged || conceptsHaveChanged) { - if (log.isDebugEnabled()) { - log.debug("CHANGED: " + printObsHelper(existingObs)); - } - // TODO: really the voided obs should link to the new one, but this is a pain to implement due to the dreaded error: org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session - obsToVoid.add(existingObs); - - newObs = createObs(concept, newValue, newDatetime, accessionNumber, comment); - newObs.setPreviousVersion(existingObs); - } else { - if (existingObs != null && StringUtils.isNotBlank(comment)) - existingObs.setComment(comment); - - if (log.isDebugEnabled()) { - log.debug("SAME: " + printObsHelper(existingObs)); - } - } + public void modifyObs(Obs existingObs, Concept concept, Object newValue, Date newDatetime, String accessionNumber) { + modifyObs(existingObs, concept, newValue, newDatetime, accessionNumber, null, null); } /** diff --git a/api/src/main/java/org/openmrs/module/htmlformentry/ObsGroupComponent.java b/api/src/main/java/org/openmrs/module/htmlformentry/ObsGroupComponent.java index 944cb412a..c26a76d16 100644 --- a/api/src/main/java/org/openmrs/module/htmlformentry/ObsGroupComponent.java +++ b/api/src/main/java/org/openmrs/module/htmlformentry/ObsGroupComponent.java @@ -30,6 +30,8 @@ public class ObsGroupComponent { private Drug answerDrug; + private String controlId = null; + private Boolean partOfSet = false; private Boolean lastInSet = false; @@ -40,21 +42,25 @@ public class ObsGroupComponent { public ObsGroupComponent() { } - public ObsGroupComponent(Concept question, Concept answer) { + public ObsGroupComponent(Concept question, Concept answer, String controlId) { this.question = question; this.answer = answer; + this.controlId = controlId; } - public ObsGroupComponent(Concept question, Concept answer, Drug answerDrug) { + public ObsGroupComponent(Concept question, Concept answer, Drug answerDrug, String controlId) { this.question = question; this.answer = answer; this.answerDrug = answerDrug; + this.controlId = controlId; } - public ObsGroupComponent(Concept question, Concept answer, Drug answerDrug, Boolean partOfSet, Boolean lastInSet) { + public ObsGroupComponent(Concept question, Concept answer, Drug answerDrug, String controlId, Boolean partOfSet, + Boolean lastInSet) { this.question = question; this.answer = answer; this.answerDrug = answerDrug; + this.controlId = controlId; this.partOfSet = partOfSet; this.lastInSet = lastInSet; } @@ -87,31 +93,7 @@ public void setAnswerDrug(Drug answerDrug) { this.answerDrug = answerDrug; } - @Deprecated - public static boolean supports(List questionsAndAnswers, Obs parentObs, Set group) { - for (Obs obs : group) { - boolean match = false; - for (ObsGroupComponent test : questionsAndAnswers) { - boolean questionMatches = test.getQuestion().getConceptId().equals(obs.getConcept().getConceptId()); - boolean answerMatches = test.getAnswer() == null || (obs.getValueCoded() != null - && test.getAnswer().getConceptId().equals(obs.getValueCoded().getConceptId())); - - if (questionMatches && !answerMatches) { - match = false; - break; - } - - if (questionMatches && answerMatches) { - match = true; - } - } - if (!match) { - return false; - } - } - return true; - } - + // TODO update the supporting rank to take form field path into account and prioritize public static int supportingRank(List obsGroupComponents, Set obsSet) { int rank = 0; @@ -121,6 +103,14 @@ public static int supportingRank(List obsGroupComponents, Set // iterate though all form obs elements for obs group we are testing for a match against for (ObsGroupComponent obsGroupComponent : obsGroupComponents) { + ; + + // if any one of the obs group components have a control id, and it matches the obs control id, return an insurmountable ranking + if (obsGroupComponent.getControlId() != null + && obsGroupComponent.getControlId().equals(HtmlFormEntryUtil.getControlId(obs))) { + return 1000; + } + Concept groupComponentQuestion = obsGroupComponent.getQuestion(); if (groupComponentQuestion == null) { // The correct error should be thrown with useful contextual information from ObsSubmissionElement:174 @@ -180,7 +170,7 @@ public static List findQuestionsAndAnswersForGroup(String par Pair hiddenObs, Node node) { List ret = new ArrayList(); if (hiddenObs != null) { // consider the hidden obs when making a match - ret.add(new ObsGroupComponent(hiddenObs.getKey(), hiddenObs.getValue(), null, false, false)); + ret.add(new ObsGroupComponent(hiddenObs.getKey(), hiddenObs.getValue(), null, null, false, false)); } findQuestionsAndAnswersForGroupHelper(parentGroupingConceptId, node, ret); return ret; @@ -195,6 +185,7 @@ private static void findQuestionsAndAnswersForGroupHelper(String parentGroupingC Concept answer = null; Drug answerDrug = null; List answersList = null; + String controlId = null; NamedNodeMap attrs = node.getAttributes(); try { String questionsStr = attrs.getNamedItem("conceptIds").getNodeValue(); @@ -250,6 +241,12 @@ private static void findQuestionsAndAnswersForGroupHelper(String parentGroupingC } } } + try { + controlId = attrs.getNamedItem("controlId").getNodeValue(); + } + catch (Exception ex) { + // this is fine + } //determine whether or not the obs group parent of this obs is the obsGroup obs that we're looking at. boolean thisObsInThisGroup = false; @@ -278,17 +275,17 @@ private static void findQuestionsAndAnswersForGroupHelper(String parentGroupingC while (i.hasNext()) { Concept c = i.next(); // add all the answers as separate obs group components, flagging them all as part of a set, and flagging the last one as the last one in the set - obsGroupComponents.add(new ObsGroupComponent(question, c, null, true, !i.hasNext())); + obsGroupComponents.add(new ObsGroupComponent(question, c, null, controlId, true, !i.hasNext())); } } else if (questions != null && questions.size() > 0) { Iterator i = questions.iterator(); while (i.hasNext()) { Concept c = i.next(); // add all the questions as separate obs group components, flagging them all as part of a set, and flagging the last one as the last one in the set - obsGroupComponents.add(new ObsGroupComponent(c, answer, null, true, !i.hasNext())); + obsGroupComponents.add(new ObsGroupComponent(c, answer, null, controlId, true, !i.hasNext())); } } else { - addToObsGroupComponentList(obsGroupComponents, question, answer, answerDrug); + addToObsGroupComponentList(obsGroupComponents, question, answer, answerDrug, controlId); } } } else if ("obsgroup".equals(node.getNodeName())) { @@ -296,7 +293,7 @@ private static void findQuestionsAndAnswersForGroupHelper(String parentGroupingC NamedNodeMap attrs = node.getAttributes(); attrs.getNamedItem("groupingConceptId").getNodeValue(); obsGroupComponents.add(new ObsGroupComponent( - HtmlFormEntryUtil.getConcept(attrs.getNamedItem("groupingConceptId").getNodeValue()), null)); + HtmlFormEntryUtil.getConcept(attrs.getNamedItem("groupingConceptId").getNodeValue()), null, null)); } catch (Exception ex) { throw new RuntimeException("Unable to get groupingConcept out of obsgroup tag."); @@ -310,7 +307,7 @@ private static void findQuestionsAndAnswersForGroupHelper(String parentGroupingC // see: https://issues.openmrs.org/browse/HTML-806 private static void addToObsGroupComponentList(List list, Concept question, Concept answer, - Drug answerDrug) { + Drug answerDrug, String controlId) { boolean isSet = false; // if there are any existing components with the same question, make sure they are flagged as part of set, but *not* the last one in the set for (ObsGroupComponent component : list) { @@ -321,7 +318,7 @@ private static void addToObsGroupComponentList(List list, Con } } // if existing components with the same question were found, flag this new component as part of a set, *and* the last element in the set - list.add(new ObsGroupComponent(question, answer, answerDrug, isSet, isSet)); + list.add(new ObsGroupComponent(question, answer, answerDrug, controlId, isSet, isSet)); } /** @@ -378,4 +375,8 @@ public Boolean isLastInSet() { public void setLastInSet(Boolean lastInSet) { this.lastInSet = lastInSet; } + + public String getControlId() { + return controlId; + } } diff --git a/api/src/test/java/org/openmrs/module/htmlformentry/FormNamespaceAndPathRegressionTest.java b/api/src/test/java/org/openmrs/module/htmlformentry/FormNamespaceAndPathRegressionTest.java index 4b69bf00d..38bcfc081 100644 --- a/api/src/test/java/org/openmrs/module/htmlformentry/FormNamespaceAndPathRegressionTest.java +++ b/api/src/test/java/org/openmrs/module/htmlformentry/FormNamespaceAndPathRegressionTest.java @@ -2,16 +2,21 @@ import org.junit.Assert; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.openmrs.Encounter; import org.openmrs.Obs; +import org.openmrs.api.APIException; +import org.openmrs.api.context.Context; import org.springframework.mock.web.MockHttpServletRequest; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.util.Date; import java.util.Iterator; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; public class FormNamespaceAndPathRegressionTest extends BaseHtmlFormEntryTest { @@ -132,6 +137,185 @@ public void testResults(SubmissionResults results) { }.run(); } + @Test + public void testDoubleObsGroupFormWithControlIdSavesNamespaceAndPath() throws Exception { + final Date date = new Date(); + new RegressionTestHelper() { + + @Override + public String getFormName() { + return "doubleObsGroupFormWithControlId"; + } + + @Override + public String[] widgetLabels() { + return new String[] { "Date:", "Location:", "Provider:", "Allergy 1:", "Allergy 1 Date:", "Allergy 2:", + "Allergy 2 Date:" }; + } + + @Override + public void setupRequest(MockHttpServletRequest request, Map widgets) { + request.addParameter(widgets.get("Date:"), dateAsString(date)); + request.addParameter(widgets.get("Location:"), "2"); + request.addParameter(widgets.get("Provider:"), "502"); + request.addParameter(widgets.get("Allergy 1:"), "Bee stings"); + request.addParameter(widgets.get("Allergy 1 Date:"), dateAsString(date)); + request.addParameter(widgets.get("Allergy 2:"), "Lactose"); + request.addParameter(widgets.get("Allergy 2 Date:"), dateAsString(date)); + } + + @Override + public void testResults(SubmissionResults results) { + results.assertNoErrors(); + Encounter encounter = results.getEncounterCreated(); + Assert.assertEquals(4, encounter.getObs().size()); + + List formFieldPaths = encounter.getObs().stream().map(Obs::getFormFieldPath) + .collect(Collectors.toList()); + + // note that "FakeForm and 2.0" are just hardcoded into the Regression Test Helper context setup + Assert.assertTrue(formFieldPaths.contains("FakeForm.2.0/allergy-1-0")); + Assert.assertTrue(formFieldPaths.contains("FakeForm.2.0/allergy-2-0")); + Assert.assertTrue(formFieldPaths.contains("FakeForm.2.0/allergy-date-1-0")); + Assert.assertTrue(formFieldPaths.contains("FakeForm.2.0/allergy-date-2-0")); + + } + }.run(); + } + + @Test + public void testDoubleObsGroupFormWithControlIdCorrectlyReloadsObs() throws Exception { + final Date date = new Date(); + new RegressionTestHelper() { + + @Override + public String getFormName() { + return "doubleObsGroupFormWithControlId"; + } + + @Override + public Encounter getEncounterToView() throws Exception { + Encounter e = new Encounter(); + e.setPatient(getPatient()); + Date date = Context.getDateFormat().parse("01/02/2003"); + e.setDateCreated(new Date()); + e.setEncounterType(Context.getEncounterService().getEncounterType(1)); + e.setEncounterDatetime(date); + e.setLocation(Context.getLocationService().getLocation(2)); + e.addProvider(Context.getEncounterService().getEncounterRole(1), + Context.getProviderService().getProvider(1)); + + // first create the second allergies obs group + Obs allergy2Parent = TestUtil.createObs(e, 70000, null, date); + e.addObs(allergy2Parent); + Obs allergy2 = TestUtil.createObs(e, 80000, "Lactose", date); + allergy2.setFormField(null, "FakeForm.2.0/allergy-2-0"); + allergy2Parent.addGroupMember(allergy2); + + // then create the first allergies obs group + Obs allergy1Parent = TestUtil.createObs(e, 70000, null, date); + e.addObs(allergy1Parent); + Obs allergy1 = TestUtil.createObs(e, 80000, "Bee stings", date); + allergy1.setFormField(null, "FakeForm.2.0/allergy-1-0"); + allergy1Parent.addGroupMember(allergy1); + + e = Context.getEncounterService().saveEncounter(e); + return e; + } + + @Override + public void testViewingEncounter(Encounter encounter, String html) { + TestUtil.assertFuzzyContains("Allergy 1:Bee stings", html); + TestUtil.assertFuzzyContains("Allergy 2:Lactose", html); + } + }.run(); + } + + @Test + public void testDoubleObsGroupFormWithControlIdCorrectlyEditsObs() throws Exception { + final Date date = new Date(); + new RegressionTestHelper() { + + @Override + public String getFormName() { + return "doubleObsGroupFormWithControlId"; + } + + @Override + public Encounter getEncounterToEdit() { + Encounter e = new Encounter(); + e.setPatient(getPatient()); + Date date; + try { + date = Context.getDateFormat().parse("01/02/2003"); + } + catch (Exception ex) { + throw new APIException(); + } + + e.setDateCreated(new Date()); + e.setEncounterType(Context.getEncounterService().getEncounterType(1)); + e.setEncounterDatetime(date); + e.setLocation(Context.getLocationService().getLocation(2)); + e.addProvider(Context.getEncounterService().getEncounterRole(1), + Context.getProviderService().getProvider(1)); + + // first create the second allergies obs group + Obs allergy2Parent = TestUtil.createObs(e, 70000, null, date); + e.addObs(allergy2Parent); + Obs allergy2 = TestUtil.createObs(e, 80000, "Lactose", date); + allergy2.setFormField(null, "FakeForm.2.0/allergy-2-0"); + allergy2Parent.addGroupMember(allergy2); + + // then create the first allergies obs group + Obs allergy1Parent = TestUtil.createObs(e, 70000, null, date); + e.addObs(allergy1Parent); + Obs allergy1 = TestUtil.createObs(e, 80000, "Bee stings", date); + allergy1.setFormField(null, "FakeForm.2.0/allergy-1-0"); + allergy1Parent.addGroupMember(allergy1); + + e = Context.getEncounterService().saveEncounter(e); + return e; + } + + @Override + public boolean doEditEncounter() { + return true; + } + + @Override + public String[] widgetLabelsForEdit() { + return new String[] { "Allergy 1:", "Allergy 2:" }; + } + + @Override + public void setupEditRequest(MockHttpServletRequest request, Map widgets) { + request.setParameter(widgets.get("Allergy 1:"), "Wasp stings"); + request.setParameter(widgets.get("Allergy 2:"), "Milk"); + } + + @Override + public void testEditedResults(SubmissionResults results) { + results.assertNoErrors(); + Encounter encounter = results.getEncounterCreated(); + Assert.assertEquals(2, encounter.getObs().size()); + + List obsValues = encounter.getObs().stream().map(Obs::getValueText).collect(Collectors.toList()); + Assert.assertTrue(obsValues.contains("Wasp stings")); + Assert.assertTrue(obsValues.contains("Milk")); + + List formFieldPaths = encounter.getObs().stream().map(Obs::getFormFieldPath) + .collect(Collectors.toList()); + + // testing that the form field paths were preserved, see https://openmrs.atlassian.net/browse/HTML-841 + // note that "FakeForm and 2.0" are just hardcoded into the Regression Test Helper context setup + Assert.assertTrue(formFieldPaths.contains("FakeForm.2.0/allergy-1-0")); + Assert.assertTrue(formFieldPaths.contains("FakeForm.2.0/allergy-2-0")); + } + + }.run(); + } + private BufferedImage createImage() { int width = 10; int height = 10; diff --git a/api/src/test/resources/org/openmrs/module/htmlformentry/include/doubleObsGroupFormWithControlId.xml b/api/src/test/resources/org/openmrs/module/htmlformentry/include/doubleObsGroupFormWithControlId.xml new file mode 100644 index 000000000..ff39e095b --- /dev/null +++ b/api/src/test/resources/org/openmrs/module/htmlformentry/include/doubleObsGroupFormWithControlId.xml @@ -0,0 +1,14 @@ + + Date: + Location: + Provider: + + Allergy 1: + Allergy 1 Date: + + + Allergy 2: + Allergy 2 Date: + + + diff --git a/api/src/test/resources/org/openmrs/module/htmlformentry/include/singleObsGroupFormWithControlId.xml b/api/src/test/resources/org/openmrs/module/htmlformentry/include/singleObsGroupFormWithControlId.xml deleted file mode 100644 index ca45d9dc5..000000000 --- a/api/src/test/resources/org/openmrs/module/htmlformentry/include/singleObsGroupFormWithControlId.xml +++ /dev/null @@ -1,10 +0,0 @@ - - Date: - Location: - Provider: - - Allergy: - Allergy Date: - - -