diff --git a/nbri_ehr/resources/queries/nbri_ehr/Conception.js b/nbri_ehr/resources/queries/nbri_ehr/Conception.js index 72d57b2..9dd9a9c 100644 --- a/nbri_ehr/resources/queries/nbri_ehr/Conception.js +++ b/nbri_ehr/resources/queries/nbri_ehr/Conception.js @@ -1 +1,45 @@ -require("ehr/triggers").initScript(this); \ No newline at end of file +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +require("ehr/triggers").initScript(this); + +var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); + +// the shared trigger collects modified participants from row.Id, which this table does not have, so announce the dams +// here or their cached activeConceptions keeps a stale Pregnant value +var damsModified = []; + +function addDam(dam) { + if (dam && damsModified.indexOf(dam) === -1) { + damsModified.push(dam); + } +} + +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'nbri_ehr', 'Conception', function(event, helper){ + // the script scope can outlive a single save, so never inherit dams from a prior one + damsModified = []; +}); + +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_UPSERT, 'nbri_ehr', 'Conception', function(helper, scriptErrors, row, oldRow) { + if (helper.isValidateOnly()) + return; + + addDam(row.Dam); + + // a re-pointed conception frees the dam it used to belong to + addDam(oldRow ? oldRow.Dam : null); +}); + +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_DELETE, 'nbri_ehr', 'Conception', function(helper, scriptErrors, row) { + // the row LabKey passes for a delete can carry keys only, and the record is still readable at this point + addDam(row.Dam || triggerHelper.getConceptionDam(row.ConceptId)); +}); + +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.COMPLETE, 'nbri_ehr', 'Conception', function(event, errors, helper){ + if (damsModified.length) { + triggerHelper.reportDataChange('nbri_ehr', 'Conception', damsModified); + damsModified = []; + } +}); diff --git a/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml b/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml index 32e2b50..df0d1ca 100644 --- a/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml +++ b/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml @@ -2,6 +2,7 @@ + Conception Records diff --git a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql b/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql index 68b07b4..599a1ed 100644 --- a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql +++ b/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql @@ -9,17 +9,19 @@ SELECT c.ConceptDate, c.Estimated, c.Sire, + c.isActive, CASE + WHEN c.isActive = true THEN 'Unknown' WHEN b.conceptId IS NOT NULL THEN 'Live Birth' - WHEN po.conceptId IS NOT NULL THEN COALESCE(po.result, 'Unknown') - ELSE 'Unknown' + ELSE COALESCE(po.result, 'Unknown') END AS conceptionOutcome, b.offspring, c.Remark, c.QCState AS qcstate FROM Conception c --- a conception yields at most one birth; the aggregate only guards against duplicates the birth trigger warns about but does not block -LEFT JOIN (SELECT b.conceptId, MAX(b.Id) AS offspring FROM study.birth b WHERE b.conceptId IS NOT NULL GROUP BY b.conceptId) b +-- Both joins match isActive: a record claims its conception unless its QC state is explicitly non-public, so a null state counts as public. +-- The birth trigger blocks a duplicate conceptId, but ETL imports skip that check, so the aggregate guards against one. +LEFT JOIN (SELECT b.conceptId, MAX(b.Id) AS offspring FROM study.birth b WHERE b.conceptId IS NOT NULL AND (b.qcstate IS NULL OR b.qcstate.publicdata = true) GROUP BY b.conceptId) b ON b.conceptId = c.ConceptId -LEFT JOIN (SELECT p.conceptId, MAX(p.result.title) AS result FROM study.pregnancy p WHERE p.conceptId IS NOT NULL GROUP BY p.conceptId) po +LEFT JOIN (SELECT p.conceptId, MAX(p.result.title) AS result FROM study.pregnancy p WHERE p.conceptId IS NOT NULL AND (p.qcstate IS NULL OR p.qcstate.publicdata = true) GROUP BY p.conceptId) po ON po.conceptId = c.ConceptId diff --git a/nbri_ehr/resources/queries/study/activeConceptions.sql b/nbri_ehr/resources/queries/study/activeConceptions.sql new file mode 100644 index 0000000..3fd6f1f --- /dev/null +++ b/nbri_ehr/resources/queries/study/activeConceptions.sql @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +SELECT + c.Dam AS Id, + c.ConceptId, + c.ConceptDate +FROM nbri_ehr.Conception c +WHERE c.isActive = true AND c.Dam IS NOT NULL diff --git a/nbri_ehr/resources/queries/study/birth.js b/nbri_ehr/resources/queries/study/birth.js index 6531bc9..85b64b9 100644 --- a/nbri_ehr/resources/queries/study/birth.js +++ b/nbri_ehr/resources/queries/study/birth.js @@ -9,6 +9,10 @@ EHR.Server.Utils = require("ehr/utils").EHR.Server.Utils; var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); var idsToSync = []; +// dams whose conception this save closes. The birth row announces the newborn, so the dam's cached Pregnant value +// would otherwise keep listing a conception that is no longer open. +var damsToSync = []; + // conception ids claimed by the rows of this save that have already been validated. Rows entered together are not in // study.birth yet when each one is checked, so this is the only way the one-birth-per-conception rule can see them. var conceptIdsInSave = []; @@ -34,6 +38,17 @@ function createAssignment(scriptErrors, dataset, fieldName, value, row) { } } +// resolves the dam of a conception so the birth can announce her; the conception carries the dam, the birth row does not +function addConceptionDam(conceptId) { + if (!conceptId) + return; + + var dam = triggerHelper.getConceptionDam(conceptId); + if (dam && damsToSync.indexOf(dam) === -1) { + damsToSync.push(dam); + } +} + function onInit(event, helper){ helper.setScriptOptions({ allowAnyId: true, @@ -50,6 +65,7 @@ function onInit(event, helper){ // the script scope can outlive a single save, so never inherit ids from a prior one idsToSync = []; conceptIdsInSave = []; + damsToSync = []; helper.decodeExtraContextProperty('birthsInTransaction'); } @@ -65,6 +81,16 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even } idsToSync = []; } + + if (damsToSync.length) { + triggerHelper.reportDataChange('nbri_ehr', 'Conception', damsToSync); + damsToSync = []; + } +}); + +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_DELETE, 'study', 'birth', function(helper, scriptErrors, row) { + // deleting the birth reopens its conception + addConceptionDam(row.conceptId); }); EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_UPSERT, 'study', 'birth', function(helper, scriptErrors, row, oldRow) { @@ -96,6 +122,13 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even } } + if (!helper.isETL() && !helper.isValidateOnly()) { + addConceptionDam(row.conceptId); + + // a re-pointed birth reopens the conception it used to claim + addConceptionDam(oldRow ? oldRow.conceptId : null); + } + if (!helper.isETL()) { if (row.QCStateLabel) { diff --git a/nbri_ehr/resources/queries/study/pregnancy.js b/nbri_ehr/resources/queries/study/pregnancy.js index ab4dc7b..c989278 100644 --- a/nbri_ehr/resources/queries/study/pregnancy.js +++ b/nbri_ehr/resources/queries/study/pregnancy.js @@ -8,6 +8,26 @@ EHR.Server.Utils = require("ehr/utils").EHR.Server.Utils; var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); +// dams whose conception this save claims or frees. The outcome announces its own Id, which is the dam only when the +// record was entered against her, so resolve the dam from the conception instead of trusting row.Id. +var damsToSync = []; + +// resolves the dam of a conception so the outcome can announce her; the conception carries the dam, the outcome row does not +function addConceptionDam(conceptId) { + if (!conceptId) + return; + + var dam = triggerHelper.getConceptionDam(conceptId); + if (dam && damsToSync.indexOf(dam) === -1) { + damsToSync.push(dam); + } +} + +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'pregnancy', function(event, helper){ + // the script scope can outlive a single save, so never inherit dams from a prior one + damsToSync = []; +}); + EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_UPSERT, 'study', 'pregnancy', function(helper, scriptErrors, row, oldRow) { if (!helper.isETL() && row.conceptId) { if (triggerHelper.totalRecords('nbri_ehr', 'Conception', 'ConceptId', row.conceptId) === 0) { @@ -24,6 +44,26 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even EHR.Server.Utils.addError(scriptErrors, 'conceptId', 'This conception Id is already used by a birth record', 'INFO'); } } + + // validation never reaches COMPLETE, so resolving dams during it is a wasted query per row + if (!helper.isETL() && !helper.isValidateOnly()) { + addConceptionDam(row.conceptId); + + // a re-pointed or cleared outcome reopens the conception it used to claim + addConceptionDam(oldRow ? oldRow.conceptId : null); + } +}); + +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_DELETE, 'study', 'pregnancy', function(helper, scriptErrors, row) { + // deleting the outcome reopens its conception + addConceptionDam(row.conceptId); +}); + +EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.COMPLETE, 'study', 'pregnancy', function(event, errors, helper){ + if (damsToSync.length) { + triggerHelper.reportDataChange('nbri_ehr', 'Conception', damsToSync); + damsToSync = []; + } }); EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.ON_BECOME_PUBLIC, 'study', 'pregnancy', function(scriptErrors, helper, row, oldRow) { @@ -36,4 +76,4 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even } triggerHelper.sendPregnancyOutcomeNotification(row.Id, outcomeRec); } -}); \ No newline at end of file +}); diff --git a/nbri_ehr/resources/web/nbri_ehr/panel/AnimalDetailsPanel.js b/nbri_ehr/resources/web/nbri_ehr/panel/AnimalDetailsPanel.js index e22d7e5..36ef139 100644 --- a/nbri_ehr/resources/web/nbri_ehr/panel/AnimalDetailsPanel.js +++ b/nbri_ehr/resources/web/nbri_ehr/panel/AnimalDetailsPanel.js @@ -138,6 +138,9 @@ Ext4.define('NBRI_EHR.panel.AnimalDetailsPanel', { },{ fieldLabel: 'Cagemates', name: 'cagemates' + },{ + fieldLabel: 'Pregnant', + name: 'pregnant' },{ fieldLabel: 'Weight', name: 'weights' diff --git a/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js b/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js index 0ee4d92..9bcccb7 100644 --- a/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js +++ b/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js @@ -85,10 +85,6 @@ Ext4.define('NBRI_EHR.panel.SnapshotPanel', { xtype: 'displayfield', fieldLabel: 'Source', name: 'source' - },{ - xtype: 'displayfield', - fieldLabel: 'Prev Id', - name: 'prev_id' }] },{ xtype: 'container', @@ -133,6 +129,10 @@ Ext4.define('NBRI_EHR.panel.SnapshotPanel', { xtype: 'displayfield', fieldLabel: 'Last TB', name: 'lastTB' + },{ + xtype: 'displayfield', + fieldLabel: 'Pregnant', + name: 'pregnant' },{ xtype: 'displayfield', fieldLabel: 'Weights', @@ -391,4 +391,32 @@ Ext4.define('NBRI_EHR.panel.SnapshotPanel', { toSet['parents'] = 'No data'; } }, + + appendDataResults: function(toSet, results, id){ + this.callParent(arguments); + this.appendPregnancy(toSet, results); + }, + + appendPregnancy: function(toSet, results){ + var records = results ? results.getData()['activeConceptions'] : null; + // getEHRContext returns null when the study container property is unset; fall back to the current container + var ctx = EHR.Utils.getEHRContext() || {}; + var values = []; + + if (Ext4.isArray(records)){ + Ext4.each(records, function(record){ + var conceptId = record['ConceptId']; + if (conceptId){ + var url = LABKEY.ActionURL.buildURL('query', 'executeQuery', ctx['EHRStudyContainer'], { + schemaName: 'nbri_ehr', + 'query.queryName': 'Conception', + 'query.ConceptId~eq': conceptId + }); + values.push('' + LABKEY.Utils.encodeHtml(conceptId) + ''); + } + }, this); + } + + toSet['pregnant'] = values.length ? values.join('
') : 'No'; + }, }); \ No newline at end of file diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java b/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java index 0b87214..06c078e 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java @@ -47,6 +47,7 @@ import org.labkey.nbri_ehr.dataentry.form.*; import org.labkey.nbri_ehr.demographics.ActiveAssignmentsDemographicsProvider; import org.labkey.nbri_ehr.demographics.ActiveCasesDemographicsProvider; +import org.labkey.nbri_ehr.demographics.ActiveConceptionsDemographicsProvider; import org.labkey.nbri_ehr.demographics.ActiveFlagsDemographicsProvider; import org.labkey.nbri_ehr.demographics.ActiveTreatmentsDemographicsProvider; import org.labkey.nbri_ehr.demographics.CagematesDemographicsProvider; @@ -135,6 +136,7 @@ protected void doStartupAfterSpringConfig(ModuleContext moduleContext) ehrService.registerDemographicsProvider(new ActiveTreatmentsDemographicsProvider(this)); ehrService.registerDemographicsProvider(new SourceDemographicsProvider(this)); ehrService.registerDemographicsProvider(new NecropsyStatusDemographicsProvider(this)); + ehrService.registerDemographicsProvider(new ActiveConceptionsDemographicsProvider(this)); EHRService.get().registerHistoryDataSource(new AnimalGroupsDataSource(this)); EHRService.get().registerHistoryDataSource(new AnimalGroupsEndDataSource(this)); diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/demographics/ActiveConceptionsDemographicsProvider.java b/nbri_ehr/src/org/labkey/nbri_ehr/demographics/ActiveConceptionsDemographicsProvider.java new file mode 100644 index 0000000..b27b347 --- /dev/null +++ b/nbri_ehr/src/org/labkey/nbri_ehr/demographics/ActiveConceptionsDemographicsProvider.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.nbri_ehr.demographics; + +import org.labkey.api.data.Sort; +import org.labkey.api.ehr.demographics.AbstractListDemographicsProvider; +import org.labkey.api.module.Module; +import org.labkey.api.query.FieldKey; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class ActiveConceptionsDemographicsProvider extends AbstractListDemographicsProvider +{ + public ActiveConceptionsDemographicsProvider(Module module) + { + super(module, "study", "activeConceptions", "activeConceptions"); + // isActive already excludes non-public conceptions, and the query exposes no QCState column for the inherited filter to use + _supportsQCState = false; + } + + @Override + public boolean requiresRecalc(String schema, String query) + { + return ("study".equalsIgnoreCase(schema) && ("birth".equalsIgnoreCase(query) || "pregnancy".equalsIgnoreCase(query))) || + ("nbri_ehr".equalsIgnoreCase(schema) && "Conception".equalsIgnoreCase(query)); + } + + @Override + protected Collection getFieldKeys() + { + Set keys = new HashSet<>(); + keys.add(FieldKey.fromString("Id")); + keys.add(FieldKey.fromString("ConceptId")); + keys.add(FieldKey.fromString("ConceptDate")); + + return keys; + } + + @Override + protected Sort getSort() + { + return new Sort("-ConceptDate"); + } +} diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java b/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java index b1ed437..d5c01d8 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/query/NBRI_EHRTriggerHelper.java @@ -829,6 +829,18 @@ public long totalRecords(String schemaName, String queryName, String columnName, return ts.getRowCount(); } + // The Conception table has no Id column, so its trigger cannot announce a modified participant on its own + public String getConceptionDam(String conceptId) + { + if (conceptId == null) + return null; + + TableInfo ti = getTableInfo("nbri_ehr", "Conception"); + SimpleFilter filter = new SimpleFilter(FieldKey.fromString("ConceptId"), conceptId); + + return new TableSelector(ti, Collections.singleton("Dam"), filter, null).getObject(String.class); + } + public boolean canCloseCase() { return _container.hasPermission(_user, EHRVeterinarianPermission.class); diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java index 7f4406b..ca8a01a 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java @@ -62,9 +62,11 @@ import org.labkey.nbri_ehr.dataentry.form.NBRIClinicalObservationsFormType; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashSet; +import java.util.List; import java.util.Set; public class NBRI_EHRCustomizer extends AbstractTableCustomizer @@ -644,6 +646,10 @@ public void doTableSpecificCustomizations(AbstractTableInfo ti) { addIsActiveForProject(ti, EHRService.EndingOption.activeAfterMidnightTonight); } + if (matches(ti, "nbri_ehr", "Conception")) + { + addIsActiveForConception(ti); + } if (matches(ti, "study", "protocolAssignment")) { EHRService.get().addIsActiveCol(ti, false, EHRService.EndingOption.activeAfterMidnightTonight, EHRService.EndingOption.activeAfterMidnightTonight); @@ -685,6 +691,64 @@ private void addIsActiveForProject(AbstractTableInfo ti, EHRService.EndingOption } } + private void addIsActiveForConception(AbstractTableInfo ti) + { + String name = "isActive"; + // both columns back the expression below, so neither may be missing + if (ti.getColumn(name) != null || ti.getColumn("conceptid") == null || ti.getColumn("qcstate") == null) + return; + + UserSchema us = ti.getUserSchema(); + Container ehrContainer = us == null ? null : EHRService.get().getEHRStudyContainer(us.getContainer()); + if (ehrContainer == null) + return; + + String birthTable = getDatasetStorageTableName(ehrContainer, "birth"); + String pregnancyTable = getDatasetStorageTableName(ehrContainer, "pregnancy"); + if (birthTable == null || pregnancyTable == null) + return; + + String alias = ExprColumn.STR_TABLE_ALIAS; + String isFalse = ti.getSqlDialect().getBooleanFALSE(); + + // ConceptId is globally unique, so the subqueries need no container filter + SQLFragment sql = new SQLFragment("(CASE WHEN (" + + isPublicSql(alias, isFalse) + + " AND NOT EXISTS (SELECT 1 FROM studydataset." + birthTable + " b WHERE b.conceptid = " + alias + ".conceptid AND " + isPublicSql("b", isFalse) + ")" + + " AND NOT EXISTS (SELECT 1 FROM studydataset." + pregnancyTable + " p WHERE p.conceptid = " + alias + ".conceptid AND " + isPublicSql("p", isFalse) + ")" + + ") THEN " + ti.getSqlDialect().getBooleanTRUE() + + " ELSE " + isFalse + + " END)"); + + ExprColumn col = new ExprColumn(ti, name, sql, JdbcType.BOOLEAN, ti.getColumn("conceptid"), ti.getColumn("qcstate")); + col.setLabel("Is Active?"); + col.setDescription("No birth or pregnancy outcome record has claimed this conception Id."); + ti.addColumn(col); + + // Customizers run after the query XML column reorder, so listing isActive there does nothing and it lands last + List visible = new ArrayList<>(ti.getDefaultVisibleColumns()); + visible.remove(col.getFieldKey()); + int sireIndex = visible.indexOf(FieldKey.fromParts("Sire")); + visible.add(sireIndex < 0 ? visible.size() : sireIndex + 1, col.getFieldKey()); + ti.setDefaultVisibleColumns(visible); + } + + // A null QCState means none was assigned, which LabKey treats as visible, so only an explicitly non-public state hides a row + private String isPublicSql(String tableAlias, String isFalse) + { + return "NOT EXISTS (SELECT 1 FROM core.datastates ds WHERE ds.rowid = " + tableAlias + ".qcstate AND ds.publicdata = " + isFalse + ")"; + } + + private String getDatasetStorageTableName(Container c, String datasetName) + { + StudyService studyService = StudyService.get(); + if (studyService == null) + return null; + + Dataset dataset = studyService.getDataset(c, studyService.getDatasetIdByName(c, datasetName)); + return dataset != null && dataset.getDomain() != null ? dataset.getDomain().getStorageTableName() : null; + } + public void doSharedCustomization(AbstractTableInfo ti) { for (var col : ti.getMutableColumns()) diff --git a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java index b93f92e..f1619ec 100644 --- a/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java +++ b/nbri_ehr/test/src/org.labkey.test/tests.nbri_ehr/NBRI_EHRTest.java @@ -788,6 +788,9 @@ public void testBirthForm() throws Exception conception.addRow(Map.of("ConceptId", conceptId, "ConceptDate", now.minusDays(160), "Dam", damId, "Sire", sireId)); conception.execute(getApiHelper().getConnection(), getContainerPath()); + log("Verifying the dam's Animal Details reports the open conception before the birth"); + assertEquals("Animal Details did not report the open conception", conceptId, getSnapshotFieldValue(damId, "Pregnant")); + gotoEnterData(); waitAndClickAndWait(Locator.linkWithText("Birth")); lockForm(); @@ -895,6 +898,11 @@ public void testBirthForm() throws Exception Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList(damId), report.getRowDataAsText(0, "Id")); Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList("Live Birth"), report.getRowDataAsText(0, "conceptionOutcome")); Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList(bornAnimal), report.getRowDataAsText(0, "offspring")); + Assert.assertEquals("A conception claimed by a birth should not be active", + Arrays.asList("false"), report.getRowDataAsText(0, "isActive")); + + log("Verifying the birth cleared the dam's pregnancy"); + assertEquals("Animal Details still reports a conception a birth has closed", "No", getSnapshotFieldValue(damId, "Pregnant")); } @Test @@ -1096,6 +1104,13 @@ public void testPregnancyForm() throws IOException, CommandException report.setFilter("ConceptId", "Equals", conceptId); Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList(animalId), report.getRowDataAsText(0, "Id")); Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList(result), report.getRowDataAsText(0, "conceptionOutcome")); + Assert.assertEquals("A conception claimed by a pregnancy outcome should not be active", + Arrays.asList("false"), report.getRowDataAsText(0, "isActive")); + + log("Verifying the pregnancy outcome cleared the dam's pregnancy"); + // this dam carries other conceptions from sibling tests, so assert only that this one is gone + Assert.assertFalse("Animal Details still reports a conception a pregnancy outcome has closed", + getSnapshotFieldValue(animalId, "Pregnant").contains(conceptId)); } @Test @@ -1140,6 +1155,30 @@ public void testConceptionForm() report.setFilter("ConceptId", "Equals", conceptId); Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList(damId), report.getRowDataAsText(0, "Id")); Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList("Unknown"), report.getRowDataAsText(0, "conceptionOutcome")); + Assert.assertEquals("A conception with no birth or pregnancy outcome should be active", + Arrays.asList("true"), report.getRowDataAsText(0, "isActive")); + + log("Verifying the dam's Animal Details links to the open conception"); + // this dam carries other conceptions from sibling tests, so assert only that this one is listed + Assert.assertTrue("Animal Details did not report the open conception", + getSnapshotFieldValue(damId, "Pregnant").contains(conceptId)); + } + + /** + * Reads one field from the Animal Details snapshot panel, which renders Ext4 displayfields rather than a grid, so + * there is no page object to read through. The value arrives from the demographics cache after the page settles, + * so an empty field means not-yet-loaded rather than no value. + */ + private String getSnapshotFieldValue(String animalId, String fieldLabel) + { + ParticipantViewPage.beginAt(this, animalId); + Locator field = Locator.xpath("//*[contains(@class,'x4-form-item')][.//label[starts-with(normalize-space(.),'" + + fieldLabel + "')]]//div[contains(@class,'x4-form-display-field')]"); + waitForElement(field); + waitFor(() -> !field.findElement(getDriver()).getText().trim().isEmpty(), + "Animal Details did not populate the " + fieldLabel + " field", WAIT_FOR_JAVASCRIPT); + + return field.findElement(getDriver()).getText().trim(); } @Test