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;