diff --git a/nbri_ehr/resources/data/death_reason.tsv b/nbri_ehr/resources/data/death_reason.tsv deleted file mode 100644 index 0b39f78..0000000 --- a/nbri_ehr/resources/data/death_reason.tsv +++ /dev/null @@ -1,6 +0,0 @@ -value title -1 Euthanasia (clinical) -2 Euthaniasia (project) -3 Morbid -4 Natural Death -5 Program Management \ No newline at end of file diff --git a/nbri_ehr/resources/data/death_type.tsv b/nbri_ehr/resources/data/death_type.tsv index 700a88d..61853c9 100644 --- a/nbri_ehr/resources/data/death_type.tsv +++ b/nbri_ehr/resources/data/death_type.tsv @@ -1,14 +1,14 @@ -value title sort_order -A Experimental 1 +value title sort_order date_disabled +A Experimental 1 2026-09-03 D Spontaneous/Normal 2 -F Fetal 3 -FD Fetal Death 4 -FL Fetal Live 5 -FN Fetal found at necropsy 6 -FX Fetal experimental 7 +F Fetal 3 2026-09-03 +FD Fetal Death 4 2026-09-03 +FL Fetal Live 5 2026-09-03 +FN Fetal found at necropsy 6 2026-09-03 +FX Fetal experimental 7 2026-09-03 K Cull (scheduled) 8 M Medical cull (non-scheduled) 9 -ND Non-vaginal (C-section) dead 10 -NT Not pregnant at assessment 11 +ND Non-vaginal (C-section) dead 10 2026-09-03 +NT Not pregnant at assessment 11 2026-09-03 S Cull 12 X Experimental 13 \ No newline at end of file diff --git a/nbri_ehr/resources/data/editable_lookups.tsv b/nbri_ehr/resources/data/editable_lookups.tsv index c12dbd0..0d76eba 100644 --- a/nbri_ehr/resources/data/editable_lookups.tsv +++ b/nbri_ehr/resources/data/editable_lookups.tsv @@ -40,7 +40,6 @@ ehr_lookups country Colony Management Country ehr_lookups country_category Colony Management Country Category ehr_lookups daily_enrich_codes Behavior Daily enrichment codes. ehr_lookups data_category Clinical Data Categories Used in datasets. -ehr_lookups death_reason Colony Management Death Reason ehr_lookups death_type Colony Management Death Type Death type codes. ehr_lookups delivery_mode Colony Management Delivery Mode ehr_lookups delivery_state Colony Management Delivery State diff --git a/nbri_ehr/resources/data/lookup_sets.tsv b/nbri_ehr/resources/data/lookup_sets.tsv index 6cd8cba..c265799 100644 --- a/nbri_ehr/resources/data/lookup_sets.tsv +++ b/nbri_ehr/resources/data/lookup_sets.tsv @@ -31,7 +31,6 @@ country Country value title country_category Country Category value title daily_enrich_codes Daily Enrichment Codes value data_category Data Category Field Values value -death_reason Death Reason value death_type Death Type value title delivery_mode Delivery Mode value title delivery_state Delivery State value title diff --git a/nbri_ehr/resources/data/lookupsManifest.tsv b/nbri_ehr/resources/data/lookupsManifest.tsv index 19aa5a4..cd41275 100644 --- a/nbri_ehr/resources/data/lookupsManifest.tsv +++ b/nbri_ehr/resources/data/lookupsManifest.tsv @@ -35,7 +35,6 @@ country country_category daily_enrich_codes data_category -death_reason death_type delivery_mode delivery_state diff --git a/nbri_ehr/resources/data/lookupsManifestTest.tsv b/nbri_ehr/resources/data/lookupsManifestTest.tsv index 8d028e6..7f1db9d 100644 --- a/nbri_ehr/resources/data/lookupsManifestTest.tsv +++ b/nbri_ehr/resources/data/lookupsManifestTest.tsv @@ -35,7 +35,6 @@ country country_category daily_enrich_codes data_category -death_reason death_type delivery_mode delivery_state diff --git a/nbri_ehr/resources/queries/study/arrival.js b/nbri_ehr/resources/queries/study/arrival.js index d374944..9ca4acf 100644 --- a/nbri_ehr/resources/queries/study/arrival.js +++ b/nbri_ehr/resources/queries/study/arrival.js @@ -8,6 +8,11 @@ require("ehr/triggers").initScript(this); var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); var idsToSync = []; +// generation 0 is a real value, so emptiness cannot be tested by truthiness the way the other demographics fields test it +function isBlankGeneration(value) { + return value === null || value === undefined || value === ''; +} + // opens one assignment record against the animal being entered; each dataset carries the assignment under its own field function createAssignment(scriptErrors, dataset, fieldName, value, row) { if (!value) @@ -46,6 +51,11 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even EHR.Server.Utils.addError(scriptErrors, 'Id', 'Animal Id ' + row.Id + ' is already in use. Please use a different Id.', 'ERROR'); } + // only a form entry can be held to a generation: the form seeds it to 0, while a study import has no such column to carry + if (!row.rearrival && !helper.isETL() && helper.isEHRDataEntry() && helper.getEvent() == 'insert' && isBlankGeneration(row['Id/demographics/generation'])) { + EHR.Server.Utils.addError(scriptErrors, 'Id/demographics/generation', 'Generation is required', 'ERROR'); + } + if (row.eventDate) { row.date = row.eventDate; } @@ -69,6 +79,7 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even row.gender = row['Id/demographics/gender'] || null; row.geographic_origin = row['Id/demographics/geographic_origin'] || null; row.socialCode = row['Id/demographics/socialCode'] || null; + row.generation = isBlankGeneration(row['Id/demographics/generation']) ? null : parseInt(row['Id/demographics/generation'], 10); if (row.QCStateLabel) { row.qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId(); @@ -152,6 +163,12 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even hasUpdates = true; } + if (!isBlankGeneration(row.generation) && row.generation !== data.generation) + { + obj.generation = row.generation; + hasUpdates = true; + } + if (row.sire && row.sire !== data.sire) { obj.sire = row.sire; diff --git a/nbri_ehr/resources/queries/study/birth.js b/nbri_ehr/resources/queries/study/birth.js index 6531bc9..fec63c3 100644 --- a/nbri_ehr/resources/queries/study/birth.js +++ b/nbri_ehr/resources/queries/study/birth.js @@ -13,6 +13,11 @@ var idsToSync = []; // 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 = []; +// generation 0 is a real value, so emptiness cannot be tested by truthiness the way the other demographics fields test it +function isBlankGeneration(value) { + return value === null || value === undefined || value === ''; +} + // opens one assignment record against the animal being entered; each dataset carries the assignment under its own field function createAssignment(scriptErrors, dataset, fieldName, value, row) { if (!value) @@ -146,6 +151,7 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even birth: row.date || null, gender: row['Id/demographics/gender'] || null, socialCode: row['Id/demographics/socialCode'] || null, + generation: isBlankGeneration(row['Id/demographics/generation']) ? null : parseInt(row['Id/demographics/generation'], 10), taskid: row.taskid, remark: row.remark, QCStateLabel: row.QCStateLabel, @@ -161,6 +167,19 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even obj.species = helper.getJavaHelper().getSpecies(obj.dam); } + // the conception window fills this in, so a blank one means a save that bypassed the form + if (isBlankGeneration(obj.generation)) { + var damGeneration = obj.dam ? triggerHelper.getGeneration(obj.dam) : null; + if (damGeneration === null) { + var generationWarning = obj.dam + ? 'No generation is recorded for dam ' + obj.dam + ', so this birth was recorded as generation 1' + : 'This birth record has no dam, so it was recorded as generation 1'; + EHR.Server.Utils.addError(scriptErrors, 'Id/demographics/generation', generationWarning, 'WARN'); + } + + obj.generation = (damGeneration === null ? 0 : damGeneration) + 1; + } + if (!oldRow) { //if not already present, we insert into demographics helper.getJavaHelper().createDemographicsRecord(row.Id, obj, extraDemographicsFieldMappings); @@ -198,6 +217,11 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even hasUpdates = true; } + if (!isBlankGeneration(obj.generation) && obj.generation !== data.generation) { + record.generation = obj.generation; + hasUpdates = true; + } + if (obj.performedby && obj.performedby !== data.performedby) { record.performedby = obj.performedby; hasUpdates = true; diff --git a/nbri_ehr/resources/queries/study/deathNotification.sql b/nbri_ehr/resources/queries/study/deathNotification.sql index 86f8800..51f5906 100644 --- a/nbri_ehr/resources/queries/study/deathNotification.sql +++ b/nbri_ehr/resources/queries/study/deathNotification.sql @@ -8,6 +8,5 @@ SELECT Id, date, taskid, - performedBy.DisplayName AS performedBy, - reason.title AS reason + performedBy.DisplayName AS performedBy FROM study.deaths \ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/deaths.query.xml b/nbri_ehr/resources/queries/study/deaths.query.xml index e28e2f0..1cb8b02 100644 --- a/nbri_ehr/resources/queries/study/deaths.query.xml +++ b/nbri_ehr/resources/queries/study/deaths.query.xml @@ -16,15 +16,6 @@ title - - Disposition - - ehr_lookups - death_reason - value - title - - Death Weight (kg) diff --git a/nbri_ehr/resources/queries/study/deaths/.qview.xml b/nbri_ehr/resources/queries/study/deaths/.qview.xml index 56eecec..eb7c293 100644 --- a/nbri_ehr/resources/queries/study/deaths/.qview.xml +++ b/nbri_ehr/resources/queries/study/deaths/.qview.xml @@ -7,7 +7,6 @@ - diff --git a/nbri_ehr/resources/queries/study/demographics.query.xml b/nbri_ehr/resources/queries/study/demographics.query.xml index 0323b4a..6ca4511 100644 --- a/nbri_ehr/resources/queries/study/demographics.query.xml +++ b/nbri_ehr/resources/queries/study/demographics.query.xml @@ -100,6 +100,9 @@ title + + Generation + CITES diff --git a/nbri_ehr/resources/queries/study/demographics/.qview.xml b/nbri_ehr/resources/queries/study/demographics/.qview.xml index 5f80b01..1fcb392 100644 --- a/nbri_ehr/resources/queries/study/demographics/.qview.xml +++ b/nbri_ehr/resources/queries/study/demographics/.qview.xml @@ -13,6 +13,7 @@ + diff --git a/nbri_ehr/resources/queries/study/pregnancy.query.xml b/nbri_ehr/resources/queries/study/pregnancy.query.xml index a97139a..5ee2a9f 100644 --- a/nbri_ehr/resources/queries/study/pregnancy.query.xml +++ b/nbri_ehr/resources/queries/study/pregnancy.query.xml @@ -29,6 +29,16 @@ ConceptId + + Delivery Mode + false + + ehr_lookups + delivery_mode + value + title + + diff --git a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml index eed9f6a..b036dfa 100644 --- a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml +++ b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml @@ -509,6 +509,9 @@ varchar + + integer + varchar @@ -561,9 +564,6 @@ http://cpas.labkey.com/Study#VisitDate http://cpas.labkey.com/Study#VisitDate - - varchar - double @@ -1367,7 +1367,6 @@ http://cpas.labkey.com/Study#VisitDate - Type varchar diff --git a/nbri_ehr/resources/views/necropsy.html b/nbri_ehr/resources/views/necropsy.html index 9952555..7e7c544 100644 --- a/nbri_ehr/resources/views/necropsy.html +++ b/nbri_ehr/resources/views/necropsy.html @@ -62,7 +62,7 @@ schemaName: 'study', queryName: 'deaths', filterArray: filterArray, - columns: 'Id,Id/demographics/species,Id/demographics/gender,date,reason,deathWeight,Id/lastProtocol/protocol,Id/lastProject/project', + columns: 'Id,Id/demographics/species,Id/demographics/gender,date,deathWeight,Id/lastProtocol/protocol,Id/lastProject/project', }, title: 'Death', renderTo: 'animalDeath', diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js index 9883d37..af420c2 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js @@ -20,7 +20,8 @@ EHR.model.DataModelManager.registerMetadata('Arrival', { byQuery: { 'study.arrival': { 'cage': { - // allowBlank: false, + allowBlank: false, + nullable: false, columnConfig: { fixed: true, width: 200 @@ -55,6 +56,21 @@ EHR.model.DataModelManager.registerMetadata('Arrival', { width: 200 } }, + // an arriving animal establishes its own lineage, so it starts at generation 0 + 'Id/demographics/generation': { + allowBlank: false, + nullable: false, + getInitialValue: function(v) { + return Ext4.isEmpty(v) ? 0 : v; + }, + editorConfig: { + minValue: 0 + }, + columnConfig: { + fixed: true, + width: 120 + } + }, // an animal joins the colony already assigned to a project, a protocol and a group; the trigger script // opens the matching assignment record for each one project: { diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js index bed0ab7..a961152 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js @@ -59,7 +59,8 @@ EHR.model.DataModelManager.registerMetadata('Birth', { } }, 'cage': { - // allowBlank: false, + allowBlank: false, + nullable: false, columnConfig: { fixed: true, width: 200 @@ -145,6 +146,17 @@ EHR.model.DataModelManager.registerMetadata('Birth', { columnConfig: { width: 200 } + }, + // derived from the dam by the conception window, but left editable so it can be corrected by hand + 'Id/demographics/generation': { + allowBlank: false, + nullable: false, + editorConfig: { + minValue: 0 + }, + columnConfig: { + width: 120 + } } } } diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js index 84eb22a..39b7865 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js @@ -36,11 +36,9 @@ EHR.model.DataModelManager.registerMetadata('Death', { nullable: false, columnConfig: { width: 160 - } - }, - reason: { - columnConfig: { - width: 160 + }, + lookup: { + filterArray: [LABKEY.Filter.create('date_disabled', null, LABKEY.Filter.Types.ISBLANK)] } }, remark: { diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Pregnancy.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Pregnancy.js index c22335d..39df64e 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Pregnancy.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Pregnancy.js @@ -12,9 +12,6 @@ EHR.model.DataModelManager.registerMetadata('Pregnancy', { project: { hidden: true, }, - type: { - hidden: true, - }, diagnosis: { hidden: true, }, @@ -26,9 +23,18 @@ EHR.model.DataModelManager.registerMetadata('Pregnancy', { nullable: false, }, conceptId: { + allowBlank: false, + nullable: false, columnConfig: { width: 150 } + }, + // shares the delivery_mode lookup with study.birth, but is optional here: an outcome can be recorded + // before the delivery mode is known + type: { + columnConfig: { + width: 200 + } } }, diff --git a/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js b/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js index 9c33be5..3557aa7 100644 --- a/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js +++ b/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js @@ -89,8 +89,8 @@ Ext4.define('NBRI_EHR.window.StartWithConceptionWindow', { var sire = record.get('Sire'); btn.disable(); - this.getSpecies(dam, function(species, speciesError){ - this.applyConception(conceptId, dam, sire, species); + this.getDamAttributes(dam, function(damAttributes, speciesError){ + this.applyConception(conceptId, dam, sire, damAttributes.species, damAttributes.generation); btn.enable(); this.close(); @@ -102,47 +102,60 @@ Ext4.define('NBRI_EHR.window.StartWithConceptionWindow', { }, this); }, - // the species of the offspring is inferred from the dam of the conception. When it cannot be determined the - // callback receives a message explaining why, rather than a null that is indistinguishable from an unset field. - getSpecies: function(dam, callback, scope){ + // the species and generation of the offspring are both inferred from the dam of the conception. A species that + // cannot be determined comes back with a message explaining why, rather than a null that is indistinguishable from + // an unset field; a dam with no generation is not an error and simply leaves the offspring at generation 1. + getDamAttributes: function(dam, callback, scope){ if (!dam){ - callback.call(scope, null, 'The conception record has no dam, so the species could not be determined.'); + callback.call(scope, {species: null, generation: this.nextGeneration(null)}, + 'The conception record has no dam, so the species could not be determined.'); return; } LABKEY.Query.selectRows({ schemaName: 'study', queryName: 'demographics', - columns: 'Id,species', + columns: 'Id,species,generation', filterArray: [LABKEY.Filter.create('Id', dam, LABKEY.Filter.Types.EQUAL)], scope: this, success: function(results){ var rows = (results && results.rows) || []; if (!rows.length){ - callback.call(scope, null, 'No demographics record was found for dam ' + dam + '.'); + callback.call(scope, {species: null, generation: this.nextGeneration(null)}, + 'No demographics record was found for dam ' + dam + '.'); return; } + var generation = this.nextGeneration(rows[0].generation); + if (!rows[0].species){ - callback.call(scope, null, 'No species is recorded on the demographics record for dam ' + dam + '.'); + callback.call(scope, {species: null, generation: generation}, + 'No species is recorded on the demographics record for dam ' + dam + '.'); return; } - callback.call(scope, rows[0].species); + callback.call(scope, {species: rows[0].species, generation: generation}); }, failure: function(error){ console.error(error); - callback.call(scope, null, 'Unable to look up the species of dam ' + dam + ': ' + ((error && error.exception) || 'the query failed') + '.'); + callback.call(scope, {species: null, generation: this.nextGeneration(null)}, + 'Unable to look up the species of dam ' + dam + ': ' + ((error && error.exception) || 'the query failed') + '.'); } }); }, - applyConception: function(conceptId, dam, sire, species){ + // a founder dam carries no generation of her own, so her offspring start the count at 1 + nextGeneration: function(damGeneration){ + return (damGeneration == null ? 0 : damGeneration) + 1; + }, + + applyConception: function(conceptId, dam, sire, species, generation){ var values = { conceptId: conceptId, 'Id/demographics/dam': dam, 'Id/demographics/sire': sire, - 'Id/demographics/species': species + 'Id/demographics/species': species, + 'Id/demographics/generation': generation }; // only the fields the conception owns are written, so anything already entered on the row survives diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java index 5707656..46d0204 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIArrivalFormSection.java @@ -55,8 +55,10 @@ protected List getFieldKeys(TableInfo ti) keys.add(indexOf(keys, "project") + 1, FieldKey.fromString("Id/demographics/geographic_origin")); - // the social code sits beside Initial Location - keys.add(indexOf(keys, "cage") + 1, FieldKey.fromString("Id/demographics/socialCode")); + // the social code and generation sit beside Initial Location + keys.addAll(indexOf(keys, "cage") + 1, List.of( + FieldKey.fromString("Id/demographics/socialCode"), + FieldKey.fromString("Id/demographics/generation"))); return keys; } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java index 8088c16..12907ca 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java @@ -38,6 +38,7 @@ public class NBRIBirthFormSection extends NewAnimalFormSection FieldKey.fromString("Id/demographics/sire"), FieldKey.fromString("cage"), FieldKey.fromString("Id/demographics/socialCode"), + FieldKey.fromString("Id/demographics/generation"), FieldKey.fromString("project"), FieldKey.fromString("birthProtocol"), FieldKey.fromString("groupId"), diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/history/DeathDataSource.java b/nbri_ehr/src/org/labkey/nbri_ehr/history/DeathDataSource.java index 1e00fcd..ae3fc77 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/history/DeathDataSource.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/history/DeathDataSource.java @@ -35,7 +35,7 @@ public DeathDataSource(Module module) @Override protected Set getColumnNames() { - return PageFlowUtil.set("Id", "date", "reason/title", "remark"); + return PageFlowUtil.set("Id", "date", "remark"); } @Override @@ -43,16 +43,6 @@ protected String getHtml(Container c, Results rs, boolean redacted) throws SQLEx { StringBuilder sb = new StringBuilder(); - if(rs.hasColumn(FieldKey.fromString("reason/title")) && rs.getObject(FieldKey.fromString("reason/title")) != null) - { - sb.append(safeAppend(rs, "Disposition", "reason/title")); - } - else - { - sb.append("Disposition: Unknown"); - sb.append("\n"); - } - if(rs.hasColumn(FieldKey.fromString("remark")) && rs.getObject(FieldKey.fromString("remark")) != null) sb.append(safeAppend(rs, "Remark", "remark")); 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..6f2cce2 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 @@ -300,6 +300,21 @@ public boolean animalIdExists(String id) return ts.exists(); } + /** + * Null both when the animal has no demographics record and when it has one carrying no generation; birth.js treats + * the two the same, so the caller never needs to tell them apart. + */ + public Integer getGeneration(String id) + { + TableInfo ti = getTableInfo("study", "demographics"); + if (null == ti.getColumn("generation")) + throw new IllegalStateException("The demographics dataset has no 'generation' column. Import the reference study to add it."); + + TableSelector ts = new TableSelector(ti, PageFlowUtil.set("generation"), new SimpleFilter(FieldKey.fromString("Id"), id), null); + + return ts.getObject(Integer.class); + } + public boolean birthExists(String id) { TableInfo ti = getTableInfo("study", "birth"); @@ -584,11 +599,10 @@ public void sendDeathNotification(final String animalId) //get death info TableInfo deaths = getTableInfo("study", "deathNotification"); - TableSelector deathsTs = new TableSelector(deaths, PageFlowUtil.set("Id", "date", "taskid", "performedBy", "reason"), new SimpleFilter(FieldKey.fromString("Id"), animalId), null); + TableSelector deathsTs = new TableSelector(deaths, PageFlowUtil.set("Id", "date", "taskid", "performedBy"), new SimpleFilter(FieldKey.fromString("Id"), animalId), null); final Mutable deathDate = new MutableObject<>(); final Mutable taskId = new MutableObject<>(); final Mutable performedBy = new MutableObject<>(); - final Mutable disposition = new MutableObject<>(); deathsTs.forEach(rs -> { if (rs.getString("date") != null) { @@ -596,7 +610,6 @@ public void sendDeathNotification(final String animalId) deathDate.setValue(date); taskId.setValue(rs.getString("taskid")); performedBy.setValue(rs.getString("performedBy")); - disposition.setValue(rs.getString("reason")); } }); @@ -609,8 +622,7 @@ public void sendDeathNotification(final String animalId) return; } html.append("Animal '").append(PageFlowUtil.filter(animalId)).append("' has been declared dead on '").append(_dateFormat.format(deathDate.get())).append("'.
"); - html.append("Performed By: ").append(PageFlowUtil.filter(performedBy.get())).append("
"); - html.append("Disposition: ").append(PageFlowUtil.filter(disposition.get())).append("

"); + html.append("Performed By: ").append(PageFlowUtil.filter(performedBy.get())).append("

"); //append animal details appendAnimalDetails(html, animalId, container); 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..a241bb4 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 @@ -696,6 +696,12 @@ public void testArrivalForm() throws IOException, CommandException waitForFormError("The field: Social Code is required"); arrivals.setGridCell(1, "Id/demographics/socialCode", socialCode); + log("Verifying Generation is seeded to 0 and is required"); + assertEquals("An arriving animal should start at generation 0", "0", String.valueOf(arrivals.getFieldValue(1, "Id/demographics/generation"))); + arrivals.setGridCellJS(1, "Id/demographics/generation", null); + waitForFormError("The field: Generation is required"); + arrivals.setGridCellJS(1, "Id/demographics/generation", 0); + // the animal's opening project, protocol and group are entered on the arrival row itself; the trigger script // opens the matching assignment record for each one arrivals.setGridCell(1, "project", "640991"); @@ -754,6 +760,8 @@ public void testArrivalForm() throws IOException, CommandException table.setFilter("Id", "Equals", arrivedAnimal); Assert.assertEquals("Social code entered on the arrival form did not reach demographics", Arrays.asList(socialCode), table.getRowDataAsText(0, "socialCode")); + Assert.assertEquals("Generation seeded by the arrival form did not reach demographics", + Arrays.asList("0"), table.getRowDataAsText(0, "generation")); log("Verifying the birth date reached demographics and agrees with the birth record"); String arrivalBirthDay = now.minusDays(7).format(_dateFormat); @@ -774,6 +782,7 @@ public void testBirthForm() throws Exception String damSpecies = "Brown-Tufted Capuchin"; String conceptId = "TESTCONCEPT1"; String breedingType = "Time-Mated"; + int damGeneration = 2; // demographics.socialCode holds an ehr_lookups.social_code code; the grids display its title String socialCode = "Mother-rearing (for indoors)"; // the group is an ehr_lookups.breeding_type code, carried to animal_group_members; the grids display its title @@ -781,7 +790,7 @@ public void testBirthForm() throws Exception LocalDateTime now = LocalDateTime.now(); log("Creating the dam and sire of the conception"); - createBreedingPair(damId, sireId, damSpeciesCode); + createBreedingPair(damId, sireId, damSpeciesCode, damGeneration); log("Creating conception record"); InsertRowsCommand conception = new InsertRowsCommand("nbri_ehr", "Conception"); @@ -803,6 +812,8 @@ public void testBirthForm() throws Exception assertEquals("Dam was not copied from the conception", damId, births.getFieldValue(1, "Id/demographics/dam")); assertEquals("Sire was not copied from the conception", sireId, births.getFieldValue(1, "Id/demographics/sire")); assertEquals("Species was not copied from the dam of the conception", damSpeciesCode, births.getFieldValue(1, "Id/demographics/species")); + assertEquals("Generation was not derived from the dam of the conception", String.valueOf(damGeneration + 1), + String.valueOf(births.getFieldValue(1, "Id/demographics/generation"))); log("Verifying Conception Id is required"); births.setGridCellJS(1, "conceptId", null); @@ -821,6 +832,11 @@ public void testBirthForm() throws Exception waitForFormError("The field: Social Code is required"); births.setGridCell(1, "Id/demographics/socialCode", socialCode); + log("Verifying Generation is required"); + births.setGridCellJS(1, "Id/demographics/generation", null); + waitForFormError("The field: Generation is required"); + births.setGridCellJS(1, "Id/demographics/generation", damGeneration + 1); + // the animal's opening project, protocol and group are entered on the birth row itself; the trigger script // opens the matching assignment record for each one births.setGridCell(1, "project", "795644"); @@ -859,6 +875,8 @@ public void testBirthForm() throws Exception Assert.assertEquals("Invalid demographics record", Arrays.asList(damSpecies), table.getRowDataAsText(0, "species")); Assert.assertEquals("Social code entered on the birth form did not reach demographics", Arrays.asList(socialCode), table.getRowDataAsText(0, "socialCode")); + Assert.assertEquals("Generation derived from the dam did not reach demographics", + Arrays.asList(String.valueOf(damGeneration + 1)), table.getRowDataAsText(0, "generation")); goToSchemaBrowser(); table = viewQueryData("study", "assignment"); @@ -931,9 +949,11 @@ public void testDuplicateConceptionRejected() throws Exception log("Entering two births that both claim the first conception"); startWithConception(births, firstConcept, 1); - fillBirthRow(births, 1, firstAnimal, now.minusDays(1), socialCode, animalGroup); + assertEquals("A dam with no generation of her own should leave the birth at generation 1", "1", + String.valueOf(births.getFieldValue(1, "Id/demographics/generation"))); + fillBirthRow(births, 1, firstAnimal, now.minusDays(1), socialCode, animalGroup, CAGE_IN_R2); startWithConception(births, firstConcept, 2); - fillBirthRow(births, 2, secondAnimal, now.minusDays(1), socialCode, animalGroup); + fillBirthRow(births, 2, secondAnimal, now.minusDays(1), socialCode, animalGroup, CAGE_IN_R3); // Live validation only sends the row that just changed, so the rows of one form entry first reach the // server together on submit. A rule that compares them therefore reports by refusing the save rather than @@ -976,6 +996,9 @@ public void testConceptionPickedFromGridCell() throws Exception // different species on each pair, so the copy from the newly picked dam is visible String firstSpeciesCode = "CAP"; String secondSpeciesCode = "MMU"; + // different generations on each dam, so the re-derivation from the newly picked dam is visible + int firstDamGeneration = 2; + int secondDamGeneration = 5; String firstConcept = "TESTCONCEPT6"; String secondConcept = "TESTCONCEPT7"; String socialCode = "Mother-rearing (for indoors)"; @@ -983,8 +1006,8 @@ public void testConceptionPickedFromGridCell() throws Exception LocalDateTime now = LocalDateTime.now(); log("Creating a breeding pair and a conception for each"); - createBreedingPair(firstDam, firstSire, firstSpeciesCode); - createBreedingPair(secondDam, secondSire, secondSpeciesCode); + createBreedingPair(firstDam, firstSire, firstSpeciesCode, firstDamGeneration); + createBreedingPair(secondDam, secondSire, secondSpeciesCode, secondDamGeneration); InsertRowsCommand conceptions = new InsertRowsCommand("nbri_ehr", "Conception"); conceptions.addRow(Map.of("ConceptId", firstConcept, "ConceptDate", now.minusDays(200), "Dam", firstDam, "Sire", firstSire)); @@ -997,7 +1020,7 @@ public void testConceptionPickedFromGridCell() throws Exception Ext4GridRef births = _helper.getExt4GridForFormSection("Births"); startWithConception(births, firstConcept, 1); - fillBirthRow(births, 1, bornAnimal, now.minusDays(1), socialCode, animalGroup); + fillBirthRow(births, 1, bornAnimal, now.minusDays(1), socialCode, animalGroup, CAGE_IN_R1); births.setGridCell(1, "breedingType", "Time-Mated"); // the codes behind these lookups are not spelled out in the test, so remember what the row carries and @@ -1028,6 +1051,8 @@ public void testConceptionPickedFromGridCell() throws Exception assertEquals("Dam was not replaced from the picked conception", secondDam, births.getFieldValue(1, "Id/demographics/dam")); assertEquals("Sire was not replaced from the picked conception", secondSire, births.getFieldValue(1, "Id/demographics/sire")); assertEquals("Species was not replaced from the dam of the picked conception", secondSpeciesCode, births.getFieldValue(1, "Id/demographics/species")); + assertEquals("Generation was not re-derived from the dam of the picked conception", String.valueOf(secondDamGeneration + 1), + String.valueOf(births.getFieldValue(1, "Id/demographics/generation"))); log("Verifying nothing else on the row was touched"); assertEquals("Animal Id should have been left alone", bornAnimal, births.getFieldValue(1, "Id")); @@ -1064,6 +1089,7 @@ public void testPregnancyForm() throws IOException, CommandException String conceptId = "TESTCONCEPT2"; // a non-live outcome, so ConceptionsByDam reports it rather than falling through to 'Live Birth' String result = "Fetal Death"; + String deliveryMode = "Vaginal"; LocalDateTime now = LocalDateTime.now(); log("Creating conception record"); @@ -1081,6 +1107,7 @@ public void testPregnancyForm() throws IOException, CommandException outcomes.setGridCell(1, "Id", animalId); outcomes.setGridCell(1, "result", result); outcomes.setGridCell(1, "conceptId", conceptId); + outcomes.setGridCell(1, "type", deliveryMode); submitForm("Submit Final", "Finalize"); goToSchemaBrowser(); @@ -1089,6 +1116,7 @@ public void testPregnancyForm() throws IOException, CommandException Assert.assertEquals("Invalid Pregnancy Outcome record", Arrays.asList(animalId), table.getRowDataAsText(0, "Id")); Assert.assertEquals("Invalid Pregnancy Outcome record", Arrays.asList(result), table.getRowDataAsText(0, "result")); Assert.assertEquals("Invalid Pregnancy Outcome record", Arrays.asList(conceptId), table.getRowDataAsText(0, "conceptId")); + Assert.assertEquals("Invalid Pregnancy Outcome record", Arrays.asList(deliveryMode), table.getRowDataAsText(0, "type")); log("Verifying conception outcome in ConceptionsByDam"); goToSchemaBrowser(); @@ -1619,7 +1647,7 @@ public void createSubjectsForDeathForm() throws IOException, CommandException log("Marking an animal dead"); InsertRowsCommand deaths = new InsertRowsCommand("study", "deaths"); - deaths.addRow(Map.of("Id", deadAnimalId, "date", LocalDateTime.now().minusDays(10), "reason", "4", "performedby", 1004)); + deaths.addRow(Map.of("Id", deadAnimalId, "date", LocalDateTime.now().minusDays(10), "performedby", 1004)); deaths.execute(getApiHelper().getConnection(), getContainerPath()); log("Marking an animal departed"); @@ -1661,7 +1689,6 @@ public void testDeathNecropsyForm() throws IOException, CommandException setFormElement(Locator.name("Id"), aliveAnimalId); _ext4Helper.selectComboBoxItem("Death Type:", "Spontaneous/Normal"); - _ext4Helper.selectComboBoxItem("Disposition:", "Euthaniasia (project)"); waitForElement(Locator.name("deathWeight")); setFormElement(Locator.name("deathWeight"), "23"); Assert.assertFalse(isElementPresent(Locator.linkWithText("Submit Necropsy for Review"))); @@ -1671,8 +1698,8 @@ public void testDeathNecropsyForm() throws IOException, CommandException log("Verify a second death insert is rejected with a validation error, not a unique constraint violation"); SimplePostCommand duplicateDeath = getApiHelper().prepareInsertCommand("study", "deaths", "lsid", - new String[]{"Id", "date", "reason", "performedby"}, - new Object[][]{{aliveAnimalId, LocalDateTime.now(), "4", 1004}}); + new String[]{"Id", "date", "performedby"}, + new Object[][]{{aliveAnimalId, LocalDateTime.now(), 1004}}); CommandException duplicateError = getApiHelper().doSaveRowsExpectingError(DATA_ADMIN.getEmail(), duplicateDeath, getExtraContext()); Map> duplicateErrors = getApiHelper().extractErrors(duplicateError.getProperties()); Assert.assertTrue("Expected duplicate death validation error, got: " + duplicateErrors, @@ -1815,7 +1842,7 @@ public void testDeathDeleteRestoresDepartedStatus() throws Exception // the death has to be recorded before the departure: the deaths trigger rejects an animal that has shipped log("Recording the death"); InsertRowsCommand deaths = new InsertRowsCommand("study", "deaths"); - deaths.addRow(Map.of("Id", animalId, "date", now.minusDays(10), "reason", "4", "QCStateLabel", "Completed", "performedby", 1004)); + deaths.addRow(Map.of("Id", animalId, "date", now.minusDays(10), "QCStateLabel", "Completed", "performedby", 1004)); deaths.execute(getApiHelper().getConnection(), getContainerPath()); assertEquals("Demographics death date does not match the death record", @@ -2321,12 +2348,14 @@ private void startWithConception(Ext4GridRef births, String conceptId, int expec } // Fills in everything a birth row needs beyond what the conception supplies, so the form can be submitted. - // Birth Location is left blank on purpose: it is optional, and skipping it keeps housing out of these tests. + // Generation is not set here: the conception supplies it from the dam. Each row takes its own cage so that a + // co-housing or capacity rule can never be what fails these tests. private void fillBirthRow(Ext4GridRef births, int rowIdx, String animalId, LocalDateTime birthDate, - String socialCode, String animalGroup) + String socialCode, String animalGroup, String cage) { births.setGridCellJS(rowIdx, "date", birthDate.format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); births.setGridCell(rowIdx, "Id", animalId); + births.setGridCell(rowIdx, "cage", cage); births.setGridCell(rowIdx, "Id/demographics/gender", "Female"); births.setGridCell(rowIdx, "Id/demographics/socialCode", socialCode); births.setGridCell(rowIdx, "project", "795644"); @@ -2336,10 +2365,16 @@ private void fillBirthRow(Ext4GridRef births, int rowIdx, String animalId, Local private void createBreedingPair(String damId, String sireId, String species) throws Exception { - String[] fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; + createBreedingPair(damId, sireId, species, null); + } + + // A null damGeneration leaves the dam with no generation of her own, which is what makes her offspring generation 1. + private void createBreedingPair(String damId, String sireId, String species, Integer damGeneration) throws Exception + { + String[] fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby", "generation"}; Object[][] data = new Object[][]{ - {damId, species, (new Date()).toString(), getFemale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {sireId, species, (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} + {damId, species, (new Date()).toString(), getFemale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004, damGeneration}, + {sireId, species, (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004, null} }; SimplePostCommand insertCommand = getApiHelper().prepareInsertCommand("study", "demographics", "lsid", fields, data); getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", damId + ";" + sireId, Filter.Operator.IN)); @@ -2351,7 +2386,8 @@ private void createBreedingPair(String damId, String sireId, String species) thr private void verifyBirthColumnOrder(Ext4GridRef births) { List expectedOrder = List.of("Id", "date", "conceptId", "Id/demographics/species", "Id/demographics/gender", - "Id/demographics/dam", "Id/demographics/sire", "cage", "Id/demographics/socialCode", "project", + "Id/demographics/dam", "Id/demographics/sire", "cage", "Id/demographics/socialCode", + "Id/demographics/generation", "project", "birthProtocol", "groupId", "type", "breedingType", "remark", "performedby"); int previousIdx = 0;