diff --git a/nbri_ehr/resources/data/birth_condition.tsv b/nbri_ehr/resources/data/birth_condition.tsv new file mode 100644 index 0000000..bdb4e26 --- /dev/null +++ b/nbri_ehr/resources/data/birth_condition.tsv @@ -0,0 +1,3 @@ +value title sort_order +L Live 1 +D Dead 2 diff --git a/nbri_ehr/resources/data/breeding_type.tsv b/nbri_ehr/resources/data/breeding_type.tsv new file mode 100644 index 0000000..dea0f78 --- /dev/null +++ b/nbri_ehr/resources/data/breeding_type.tsv @@ -0,0 +1,10 @@ +value title sort_order +A Assigned Breeding Protocol 1 +C Cull 2 +H Held from Mating Activity 3 +M Multi-Male 4 +P Project Breeding 5 +Q Testing as Breeder 6 +S Single Male Harem 7 +T Time-Mated 8 +O Not Assigned 9 \ No newline at end of file diff --git a/nbri_ehr/resources/data/death_type.tsv b/nbri_ehr/resources/data/death_type.tsv new file mode 100644 index 0000000..700a88d --- /dev/null +++ b/nbri_ehr/resources/data/death_type.tsv @@ -0,0 +1,14 @@ +value title sort_order +A Experimental 1 +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 +K Cull (scheduled) 8 +M Medical cull (non-scheduled) 9 +ND Non-vaginal (C-section) dead 10 +NT Not pregnant at assessment 11 +S Cull 12 +X Experimental 13 \ No newline at end of file diff --git a/nbri_ehr/resources/data/delivery_mode.tsv b/nbri_ehr/resources/data/delivery_mode.tsv new file mode 100644 index 0000000..cea8a58 --- /dev/null +++ b/nbri_ehr/resources/data/delivery_mode.tsv @@ -0,0 +1,4 @@ +value title +V Vaginal +N Surgical-clinical +NX Surgical experimental \ 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 90cc8af..d0ed33a 100644 --- a/nbri_ehr/resources/data/editable_lookups.tsv +++ b/nbri_ehr/resources/data/editable_lookups.tsv @@ -18,10 +18,12 @@ ehr_lookups bcs_score Clinical Body Condition Score Clinical observation fixed v ehr_lookups behavior_abnormality Clinical Behavior Abnormality Clinical observation fixed values. ehr_lookups behavior_mgmt_codes Behavior Behavior Management Codes Behavior observation fixed values. ehr_lookups behavior_types Behavior Behavior Types Behavior observation fixed values. +ehr_lookups birth_condition Colony Management Birth Condition Birth condition values. ehr_lookups blood_draw_reason Clinical Blood Draw Reason Used in blood draw datasets. ehr_lookups blood_draw_tube_type Clinical Blood Draw Tube Type Used in blood draw datasets. ehr_lookups blood_sample_type Clinical Blood Sample Types Used in blood draw datasets. ehr_lookups blood_tube_volumes Clinical Blood Tube Volumes Used in blood draw datasets. +ehr_lookups breeding_type Colony Management Breeding Type Breeding group assignment codes. ehr_lookups cage_type Colony Management Cage Type Used in cage details. ehr_lookups calculated_status_codes Colony Management Calculated Status Animal status values. ehr_lookups capillary_refill_time Clinical Capillary Refill Times Used clinical observations. @@ -36,8 +38,10 @@ 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 delivery_state Colony Management Delivery State +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 ehr_lookups dental_obs Clinical Dental Observation Types Clinical observation values. ehr_lookups derm_obs Clinical Dermatologic Observation Types Clinical observation values. ehr_lookups digit_amputation Clinical Digit Amputation Clinical observation fixed values. diff --git a/nbri_ehr/resources/data/flag_categories.tsv b/nbri_ehr/resources/data/flag_categories.tsv new file mode 100644 index 0000000..2e815bf --- /dev/null +++ b/nbri_ehr/resources/data/flag_categories.tsv @@ -0,0 +1,11 @@ +Category Description Enforce Single Flag Per Animal? Omit When Displaying Default Flags? Highlight Flags of This Category? Date Disabled +Behavioral Behavioral observations, abnormal-behavior designations and enrichment provided to the animal. false false false +Capture Restraint and handling limitations. false false true +Clinical Clinical conditions, procedures and veterinary designations. false false false +Experimental Experimental treatments, inoculations and implants. false false true +Genetics Genetic background, ancestry, inbreeding and genomic characterization. false true false +Hold Animal held or reserved for a specific project, investigator or shipment. false false false +Reproduction Breeding, contraception, pregnancy, fostering and rearing designations. false false false +Socially housed Social rank, pairing, group-formation and social-housing-exemption designations. false false false +Status Administrative status and availability designations. false false true +Training Training program participation and progress. false false false diff --git a/nbri_ehr/resources/data/flag_values.tsv b/nbri_ehr/resources/data/flag_values.tsv new file mode 100644 index 0000000..8a075a7 --- /dev/null +++ b/nbri_ehr/resources/data/flag_values.tsv @@ -0,0 +1,11 @@ +Category Meaning Description Date Disabled ObjectId +Behavioral Behavioral FLAG 1 Behavioral FLAG 1 +Capture Capture FLAG 1 Capture FLAG 1 +Clinical Clinical FLAG 1 Clinical FLAG 1 +Experimental Experimental FLAG 1 Experimental FLAG 1 +Genetics Genetics FLAG 1 Genetics FLAG 1 +Hold Hold FLAG 1 Hold FLAG 1 +Reproduction Reproduction FLAG 1 Reproduction FLAG 1 +Socially housed Socially housed FLAG 1 Socially housed FLAG 1 +Status Status FLAG 1 Status FLAG 1 +Training Training FLAG 1 Training FLAG 1 diff --git a/nbri_ehr/resources/data/gender_codes.tsv b/nbri_ehr/resources/data/gender_codes.tsv index 01c67fb..a55c8a7 100644 --- a/nbri_ehr/resources/data/gender_codes.tsv +++ b/nbri_ehr/resources/data/gender_codes.tsv @@ -1,4 +1,4 @@ code meaning -1 unknown -2 female -3 male \ No newline at end of file +U Unknown +F Female +M Male \ No newline at end of file diff --git a/nbri_ehr/resources/data/lookup_sets.tsv b/nbri_ehr/resources/data/lookup_sets.tsv index 613eb8e..c465f4a 100644 --- a/nbri_ehr/resources/data/lookup_sets.tsv +++ b/nbri_ehr/resources/data/lookup_sets.tsv @@ -16,8 +16,10 @@ bcs_score BCS Store value title behavior_abnormality Behavior Abnormality value behavior_mgmt_codes Behavior Management Codes value behavior_types Behavior Types value +birth_condition Birth Condition value title blood_draw_reason Blood Draw Reason value blood_sample_type Blood Sample Types value +breeding_type Breeding Type value title cage_type Cage Type value title capillary_refill_time Capillary Refill Time value card_format Card Format value title @@ -31,6 +33,8 @@ 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 dental_obs Dental Observations value derm_obs Dermatologic Observations value @@ -98,6 +102,7 @@ req_order_type Req Order Type value title respiratory_observations Respiratory Observations value title sib_score SIB Score value skin_problem Skin Problem value +status_codes Status Code Field Values value title stool_score Stool Score value stool_types Stool Types value tb_obs_score TB Obs Score value diff --git a/nbri_ehr/resources/data/lookupsManifest.tsv b/nbri_ehr/resources/data/lookupsManifest.tsv index defb466..9f32d12 100644 --- a/nbri_ehr/resources/data/lookupsManifest.tsv +++ b/nbri_ehr/resources/data/lookupsManifest.tsv @@ -18,10 +18,12 @@ bcs_score behavior_abnormality behavior_mgmt_codes behavior_types +birth_condition blood_draw_reason blood_draw_tube_type blood_sample_type blood_tube_volumes +breeding_type cage_type calculated_status_codes capillary_refill_time @@ -35,6 +37,8 @@ country_category daily_enrich_codes data_category death_reason +death_type +delivery_mode delivery_state dental_obs derm_obs @@ -51,6 +55,8 @@ expense_class fecal_score fecal_smear_score feed_assess_types +flag_categories +flag_values gastro_types gender_codes general_obs @@ -108,7 +114,10 @@ routes sib_score source snomed +species +species_codes skin_problem +status_codes stool_score stool_types tb_obs_score diff --git a/nbri_ehr/resources/data/lookupsManifestTest.tsv b/nbri_ehr/resources/data/lookupsManifestTest.tsv index b2006c8..d64c8ba 100644 --- a/nbri_ehr/resources/data/lookupsManifestTest.tsv +++ b/nbri_ehr/resources/data/lookupsManifestTest.tsv @@ -12,15 +12,18 @@ amount_units app_score arrival_type arthritis_types +att_score bandage_observations bcs_score behavior_abnormality behavior_mgmt_codes behavior_types +birth_condition blood_draw_reason blood_draw_tube_type blood_sample_type blood_tube_volumes +breeding_type cage_type calculated_status_codes capillary_refill_time @@ -34,6 +37,8 @@ country_category daily_enrich_codes data_category death_reason +death_type +delivery_mode delivery_state dental_obs derm_obs @@ -50,6 +55,8 @@ expense_class fecal_score fecal_smear_score feed_assess_types +flag_categories +flag_values gastro_types gender_codes general_obs diff --git a/nbri_ehr/resources/data/pregnancy_result.tsv b/nbri_ehr/resources/data/pregnancy_result.tsv index 1c615bc..819182e 100644 --- a/nbri_ehr/resources/data/pregnancy_result.tsv +++ b/nbri_ehr/resources/data/pregnancy_result.tsv @@ -1,3 +1,7 @@ value title -1 Stillborn -2 Abort \ No newline at end of file +NT No Tissue +FD Fetal Death +FN Found at necropsy +FX Live, Term, euthanized at birth +ND Live, Died day of birth (lungs inflated) +FL Fetal Delivery, live in Utero \ No newline at end of file diff --git a/nbri_ehr/resources/data/source.tsv b/nbri_ehr/resources/data/source.tsv index 0d893c4..5c3f18a 100644 --- a/nbri_ehr/resources/data/source.tsv +++ b/nbri_ehr/resources/data/source.tsv @@ -1,108 +1,103 @@ code meaning -1 Adv Bioscience Labs -2 Alamogordo PrimateFaclty -3 Alpha Genesis, Inc. -4 Barton West End Farm -5 Battelle Memorial Inst -6 Baylor Research Inst -7 BIOCULTURE (MTIUS) LTD -8 Bioculture US LLC -9 BIOQUAL, Inc. -10 Boehringer Ingelheim -11 Boston University -12 Buckshire Corporation -13 Caribbean Primate Rsrch -14 CBNC -15 CDC -16 Charles River Laboratory -17 Charles River/Reno -18 Chimp Haven -19 China/Guangxi GF Sci Pri -20 Chiron Corp -21 CiToxLab North America -22 Covance Research Prod -23 CR Rsrch Models Houston -24 Ctr Captive Chimp Care -25 Cynologics Ltd -26 DHMRI -27 Duke Univ Medical Ctr -28 Durham Research Center -29 Emory University YPRC -30 Envigo Global Services -31 Guangdong Landau Biotech -32 Hainan, China -33 Harvard Medical School -34 Indonesia (Pt. W. Sat) -35 Johns Hopkins University -36 LC Preclinical Research -37 Lovelace Biomedical -38 LRRI -39 LSU Health Science BR -40 LSU Health Science NO -41 LSU Health Science Shv -42 Mannheimer Foundation -43 Mass. General Hospital -44 MD Anderson Cancer Ctr. -45 Merck & Co (Rahway) -46 Merck & Co (WP) -47 Merck Research Labs WP -48 Nationwide Children's -49 New England NPRC -50 New York University -51 NIAID (Bioqual) -52 NIAID Morgan Island -53 NIAID NIH Animal Ctr -54 NICHD/NIH -55 NIH -56 NIH Animal Center -57 NINDS NIH Animal Ctr -58 Novartis Pharm. Corp. -59 Novartis Vaccines Srl -60 NYU School of Medicine -61 Oregon NPRC -62 Pfizer -63 Pfizer-Andover -64 Pfizer-Pearl River -65 PreLabs -66 Primate Products -67 Primedica Labs -68 PrimGen -69 Primgen CSP -70 RainForest Adventures -71 Republic of Mauritius -72 Rocky Mountain Lab -73 Save the Chimps -74 Siconbrec Inc -75 Sierra Biomedical -76 SNBL-SRC -77 SRI International -78 St. Jude Childrens Rsrch -79 St. Kitts -80 Stanford Univ School Med -81 Stony Brook University -82 SUNY Downstate Medical -83 SW Found Biomed Rsrch -84 Texas Biomed -85 Three Springs Scientific -86 Tulane NPC (Covington) -87 Tulane Univ Medical Ctr -88 Univ Alabama Birmingham -89 Univ of Illinois -90 Univ of Kansas Med Ctr -91 Univ of Maryland -92 Univ of Nebraska Med Ctr -93 Univ of Pittsburgh -94 Univ of Texas at Austin -95 Univ of Washington NPRC -96 Univ of Wisconsin -97 Univ Tex MD Anderson CC -98 USAMRICD -99 USAMRIID -100 Virginia Commonwealth U -101 WakeForest School of Med -102 WaNPRC -103 Worldwide Primates, Inc. -104 WRAIR -105 Wyeth-Ayerst Rsrch (PR) -106 Yale Univ Sch Medicine -107 Yerkes Regional PRC \ No newline at end of file +AAI Asiatic Animal Imports +BIOQUAL Bioqual, Incorporated +BRANDEIS Brandeis University +CA-DPH Calif Dept Public Health Facilities +CA-DOH California State Department of Health +CPRC Carribean Primate Center +CWRU Case Western Reserve University +UCD-CNS Center for Neuroscience, UCD +CR-KL Charles River - Key Lois +CRL Charles River Labs +CRRP Charles River Research Primates Inc +BROOKFIELD Chicago Zoological Park (Brookfield Zoo) +CHILDRENS Childrens Hospital +CHIMR Christ Hospital Inst. for Medical Res. +CSU Colorado State University +CORNELL Cornell University +COULSTON Coulston Foundation +COVANCE Covance Research Products, Inc. +PRIMGEN CSP-Primgen +CUTTER Cutter Lab +DMT Del Mundo Trading +EPZ El Paso Zoological Gardens +ENVIGO Envigo +HAHNEMANN Hahnemann University +HL Hazelton Laboratories +HLA Hazelton Laboratories America Inc. +UNK Institution Unknown +ISU-VDL Iowa State Univ, Vet Diagnostic Lab +JVL Jan Vacek Limited +JHMC Jewish Hospital & Medical Center +JRI Johnson Research Institute +KNLPC Kunming National Laboratory Primate Ctr. +LABS Laboratory Animal Breeders and Services +LABSINDO Labsindo +LEMSIP LEMSIP, New York Medical Center, RDI +LAIR Letterman Army Res Inst-Presidio +LB Litton Bionetics +LLU Loma Linda University +MSU-MI Michigan State University +MSU-MT Montana State University +MPI MPI Research +NASA-ARC NASA-Ames Research Center +NAMRL Naval Aerospace Medical Research Lab +NEPRC New England Primate Research Center +NIHAC NIH Animal Center +ORPRC Oregon Regional Primate Research Center +PARC-SAF Parc Safari African +PPC Perrine Primate Center +PPP Peruvian Primatological Project +PET-FARM Pet Farm +PRIMLAB Primate Laboratory +PPI Primate Products, Incorporated +PRIVATE Private Party +RIEPT Res. Inst. of Exp. Pathology & Therapy +SALK Salk Institute +SFGH San Francisco General Hospital +SNBL Shin Nippon Biomedical Laboratories +SICONBREC Siconbrec +SBM Sierra Biomedical +SKB Smith, Kline, Beecham +SIU Southern Illinois University +SIU-SOM Southern Illinois University Med School +SORI Southern Research Institute +SFRE Southwest Foundation for Res. & Educ. +SWRF Southwest Research Foundation +SWRI Southwest Research Institute +SOPF Stanford Outdoor Primate Facility +SRI Stanford Research Institute +SUNY-SB State Univ of New York at Stony Brook +SXZ Suzhou Xishan Zhongke Lab Animal Co. +SYNTEX Syntex (USA) Incorporated +TARPON Tarpon Zoo +TTUHSC Texas Tech Health Science Center +BROOKS-AFB Texas, Brooks Air Force Base +TPI The Parkinson's Institute +TNPRC Tulane (Delta) Regional Primate Res Ctr +UCB UC Berkeley +UCLA UC Los Angeles +UCR UC Riverside +UCSD UC San Diego +UCSF UC San Francisco Vivarium +UC-ARS Univ of Calif, Animal Resources Service +GPC Univ of Gottingen Primate Center +UH-RAF Univ of Hawaii, Research Animal Facility +OUHSC Univ of Oklahoma Health Sci. Center +UTHSC-H Univ of Texas, Health Sci Ctr, Houston +UCHSC University of Colorado Health Sciences +UCMC University of Colorado Medical Center +UIC University of Illinois at Chicago +UNV University of Nevada +UNM University of New Mexico +UPR University of Puerto Rico +UTSCC University of Texas System Cancer Center +UNK-CN Unknown Institution, China +VBS Valley Biosystems +WFSM Wake Forest School of Medicine +WANPRC Washington Regional Primate Research Ctr +WHMC Wilford Hall Medical Center +WRPRC Wisconsin Regional Primate Research Ctr +WAI Woodward Asiatic Imports +WWP World Wide Primates, Inc. +YPRC Yemassee Primate Research Center diff --git a/nbri_ehr/resources/data/species.tsv b/nbri_ehr/resources/data/species.tsv index c95e4bd..12d3118 100644 --- a/nbri_ehr/resources/data/species.tsv +++ b/nbri_ehr/resources/data/species.tsv @@ -1,7 +1,15 @@ Common Scientific Name Id Prefix Mhc Prefix Max Blood Draw (mL/kg) Max Percent Blood Drawn Blood Reconstitution Interval (Days) Cites Code Date Disabled USDA Gestation -African Green Monkey Chlorocebus aethiops AGM 3.0000 1.0000 7.0000 -Brown-Tufted Capuchin Cebus apella CAP 3.0000 1.0000 7.0000 -Cynomolgus Macaque Macaca fascicularis CYN 3.0000 1.0000 7.0000 -Marmoset 3.0000 1.0000 7.0000 -Pig-Tailed Macaque Macaca nemestrina PIG 3.0000 1.0000 7.0000 -Rhesus Macaque Macaca mulatta RHM 3.0000 1.0000 7.0000 \ No newline at end of file +Rhesus Macaque Macaca Mulatta 3.0000 1.0000 7.0000 +Pig-Tailed Macaque Macaca Nemestrina 3.0000 1.0000 7.0000 +Bonnet Macaque Macaca Radiata 3.0000 1.0000 7.0000 +Olive Baboon Papio Anubis 3.0000 1.0000 7.0000 +Yellow Baboon Papio Cynocephalus 3.0000 1.0000 7.0000 +Squirrel Monkey Saimiri Sciureus 3.0000 1.0000 7.0000 +African Green Monkey Chlorocebus Aethiops 3.0000 1.0000 7.0000 +Domestic Dog Canis Familiaris 3.0000 1.0000 7.0000 +Formosan Rock Macaque Macaca Cyclopis 3.0000 1.0000 7.0000 +Japanese Macaque Macaca Fuscata 3.0000 1.0000 7.000 +Talapoin Monkey Cercopithecus Talapoin 3.0000 1.0000 7.0000 +Sykes' Monkey Cercopithecus M Albogulari 3.0000 1.0000 7.0000 +Dusky Titi Monkey Callicebus Moloch 3.0000 1.0000 7.0000 +Brown-Tufted Capuchin Cebus Apella 3.0000 1.0000 7.0000 \ No newline at end of file diff --git a/nbri_ehr/resources/data/species_codes.tsv b/nbri_ehr/resources/data/species_codes.tsv index df698e7..feb4100 100644 --- a/nbri_ehr/resources/data/species_codes.tsv +++ b/nbri_ehr/resources/data/species_codes.tsv @@ -1,7 +1,15 @@ Code Scientific Name Common Name Description Genus Species Date Disabled -1 Chlorocebus aethiops AGM African Green Monkey 3 -10 Macaca mulatta RHM Rhesus Macaque 3 -18 Marmoset 3 -4 Cebus apella CAP Brown-Tufted Capuchin 3 -7 Macaca fascicularis CYN Cynomolgus Macaque 3 -9 Macaca nemestrina PIG Pig-Tailed Macaque 3 \ No newline at end of file +MMU Macaca Mulatta Rhesus Macaque +MNE Macaca Nemestrina Pig-Tailed Macaque +MRA Macaca Radiata Bonnet Macaque +PAN Papio Anubis Olive Baboon +PCY Papio Cynocephalus Yellow Baboon +SSC Saimiri Sciureus Squirrel Monkey +CAE Chlorocebus Aethiops African Green / Vervet +DOG Canis Familiaris Domestic Dog +MCY Macaca Cyclopis Formosan Rock Macaque +MFU Macaca Fuscata Japanese Macaque +CTA Cercopithecus Talapoin Talapoin Monkey +CMA Cercopithecus M Albogulari Sykes' Monkey +CMO Callicebus Moloch Dusky Titi Monkey +CAP Cebus Apella Brown-Tufted Capuchin \ No newline at end of file diff --git a/nbri_ehr/resources/data/status_codes.tsv b/nbri_ehr/resources/data/status_codes.tsv new file mode 100644 index 0000000..b131086 --- /dev/null +++ b/nbri_ehr/resources/data/status_codes.tsv @@ -0,0 +1,5 @@ +value title +Alive Alive +Dead Dead +Escaped Escaped +Shipped Shipped diff --git a/nbri_ehr/resources/data/weight_ranges.tsv b/nbri_ehr/resources/data/weight_ranges.tsv index bb2c94f..dca47bd 100644 --- a/nbri_ehr/resources/data/weight_ranges.tsv +++ b/nbri_ehr/resources/data/weight_ranges.tsv @@ -1,3 +1,5 @@ Species Min Weight Max Weight -Cynomolgus 0.0 20.0 -Rhesus 0.0 35.0 \ No newline at end of file +CMO 0.04 2.2 +MCY 0.2 16.0 +MMU 0.2 30.0 +MNE 0.2 30.0 diff --git a/nbri_ehr/resources/queries/ehr/project.query.xml b/nbri_ehr/resources/queries/ehr/project.query.xml index d2db2ca..5b72f21 100644 --- a/nbri_ehr/resources/queries/ehr/project.query.xml +++ b/nbri_ehr/resources/queries/ehr/project.query.xml @@ -47,6 +47,13 @@ true + + + core + Users + UserId + DisplayName + true diff --git a/nbri_ehr/resources/queries/ehr_lookups/buildings.js b/nbri_ehr/resources/queries/ehr_lookups/buildings.js index 3fca085..b5e9890 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/buildings.js +++ b/nbri_ehr/resources/queries/ehr_lookups/buildings.js @@ -7,6 +7,19 @@ var LABKEY = require("labkey"); var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); +// Width of ehr_lookups.buildings.name. The description it is derived from is a wider column, so it can overrun the +// key; reject it here rather than letting the database raise an unreadable error. +var MAX_NAME_LENGTH = 100; + +// 'name' is not user editable, so it is absent from the incoming row map and the value this script derives has +// nowhere to land. Declaring it managed reserves a slot so the derived key is persisted. +function managedColumns() { + return { + insert: ["name"], + update: ["name"], + }; +} + function onUpsert(row, oldRow, errors){ if (extraContext.dataSource != "etl") { if (!row.description) { @@ -25,7 +38,19 @@ function onUpsert(row, oldRow, errors){ return; } - row.name = row.description + '-' + row.area; + if (row.description.length > MAX_NAME_LENGTH) { + errors['description'] = 'Description is too long: it becomes the building key, which cannot exceed ' + MAX_NAME_LENGTH + ' characters.'; + return; + } + + // The description alone identifies the building now that the area is no longer folded in, so a duplicate + // would collide on the key. Say so here instead of surfacing a constraint violation on a hidden column. + if (triggerHelper.totalRecords("ehr_lookups", "buildings", "name", row.description) > 0) { + errors['description'] = 'A building described as ' + row.description + ' already exists. Building descriptions must be unique.'; + return; + } + + row.name = row.description; } } } diff --git a/nbri_ehr/resources/queries/ehr_lookups/cage.js b/nbri_ehr/resources/queries/ehr_lookups/cage.js index e19a2b7..0e71832 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/cage.js +++ b/nbri_ehr/resources/queries/ehr_lookups/cage.js @@ -7,6 +7,19 @@ var LABKEY = require("labkey"); var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); +// Width of ehr_lookups.cage.location. The derived key builds on the room key, which is itself derived, so it can +// overrun the column; reject it here rather than letting the database raise an unreadable error. +var MAX_LOCATION_LENGTH = 100; + +// 'location' is not user editable, so it is absent from the incoming row map and the value this script +// derives has nowhere to land. Declaring it managed reserves a slot so the derived key is persisted. +function managedColumns() { + return { + insert: ["location"], + update: ["location"], + }; +} + function onUpsert(row, oldRow, errors){ if (extraContext.dataSource != "etl") { if (!row.location) { @@ -20,9 +33,16 @@ function onUpsert(row, oldRow, errors){ return; } - row.location = row.room; + let location = row.room; if (row.cage) - row.location += '-' + row.cage; + location += '-' + row.cage; + + if (location.length > MAX_LOCATION_LENGTH) { + errors['cage'] = 'Room and cage are too long: they combine to a ' + location.length + ' character location key, which cannot exceed ' + MAX_LOCATION_LENGTH + '.'; + return; + } + + row.location = location; } } } diff --git a/nbri_ehr/resources/queries/ehr_lookups/cage.query.xml b/nbri_ehr/resources/queries/ehr_lookups/cage.query.xml index 97b1549..3d15c46 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/cage.query.xml +++ b/nbri_ehr/resources/queries/ehr_lookups/cage.query.xml @@ -9,6 +9,11 @@ Room + + ehr_lookups + rooms + room + true diff --git a/nbri_ehr/resources/queries/study/historicalOther.query.xml b/nbri_ehr/resources/queries/ehr_lookups/flag_values.query.xml similarity index 58% rename from nbri_ehr/resources/queries/study/historicalOther.query.xml rename to nbri_ehr/resources/queries/ehr_lookups/flag_values.query.xml index 37e2754..c6531be 100644 --- a/nbri_ehr/resources/queries/study/historicalOther.query.xml +++ b/nbri_ehr/resources/queries/ehr_lookups/flag_values.query.xml @@ -1,13 +1,13 @@ - +
- - Type + + Value
-
\ No newline at end of file + diff --git a/nbri_ehr/resources/queries/ehr_lookups/rooms.js b/nbri_ehr/resources/queries/ehr_lookups/rooms.js index caea28b..18a10a2 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/rooms.js +++ b/nbri_ehr/resources/queries/ehr_lookups/rooms.js @@ -8,6 +8,19 @@ var console = require("console"); var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); +// Width of ehr_lookups.rooms.room. The derived key is built from values the user supplies, so it can overrun the +// column; reject it here rather than letting the database raise an unreadable error. +var MAX_ROOM_LENGTH = 100; + +// 'room' is not user editable, so it is absent from the incoming row map and the value this script derives has +// nowhere to land. Declaring it managed reserves a slot so the derived key is persisted. +function managedColumns() { + return { + insert: ["room"], + update: ["room"], + }; +} + function onUpsert(row, oldRow, errors){ if (extraContext.dataSource != "etl") { if (!row.name) { @@ -15,8 +28,8 @@ function onUpsert(row, oldRow, errors){ return; } - if (!row.floor) { - errors['floor'] = 'Floor is required.'; + if (!row.building) { + errors['building'] = 'Building is required.'; return; } @@ -26,7 +39,13 @@ function onUpsert(row, oldRow, errors){ return; } - row.room = row.name + '-' + row.floor; + let room = row.building + '-' + row.name; + if (room.length > MAX_ROOM_LENGTH) { + errors['name'] = 'Building and room name are too long: they combine to a ' + room.length + ' character room key, which cannot exceed ' + MAX_ROOM_LENGTH + '.'; + return; + } + + row.room = room; } } } diff --git a/nbri_ehr/resources/queries/ehr_lookups/rooms.query.xml b/nbri_ehr/resources/queries/ehr_lookups/rooms.query.xml index dd7b89f..0b522c5 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/rooms.query.xml +++ b/nbri_ehr/resources/queries/ehr_lookups/rooms.query.xml @@ -6,8 +6,10 @@ Rooms - + + + /nbri_ehr/cageDetails.view?room=${room} true false false @@ -17,6 +19,14 @@ false false + + + true + false + false + false + diff --git a/nbri_ehr/resources/queries/ehr_lookups/rooms/.qview.xml b/nbri_ehr/resources/queries/ehr_lookups/rooms/.qview.xml index 0bfbf5b..f8c6e5e 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/rooms/.qview.xml +++ b/nbri_ehr/resources/queries/ehr_lookups/rooms/.qview.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/nbri_ehr/resources/queries/ehr_lookups/weight_ranges.query.xml b/nbri_ehr/resources/queries/ehr_lookups/weight_ranges.query.xml new file mode 100644 index 0000000..b5f2f4d --- /dev/null +++ b/nbri_ehr/resources/queries/ehr_lookups/weight_ranges.query.xml @@ -0,0 +1,19 @@ + + + + + + + Species + + ehr_lookups + species_codes + code + common_name + + + +
+
+
+
diff --git a/nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrder.query.xml b/nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrder.query.xml index 5c829e1..e282d4d 100644 --- a/nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrder.query.xml +++ b/nbri_ehr/resources/queries/nbri_ehr/AnimalReqOrder.query.xml @@ -96,7 +96,7 @@ ehr_lookups rooms room - name + room
diff --git a/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml b/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml index 0eb26a8..7fc6298 100644 --- a/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml +++ b/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml @@ -14,16 +14,28 @@ Conception Date - - Conception Date - Conception Term Date + + Estimated + Conception date is estimated rather than observed + true + + Task Id + ALWAYS_OFF + + ehr + tasks + taskid + rowid + + /ehr/dataEntryFormDetails.view?formType=${TaskId/formtype}&taskid=${TaskId} + Status @@ -31,6 +43,62 @@ qcstate rowid + + + + + + FBEC5D + + + + + + FBEC5D + + + + + + FBEC5D + + + + + + FF0000 + + + + + + FBEC5D + + + + + + FBEC5D + + + + + + FF0000 + + + + + + FBEC5D + + + + + + FBEC5D + + diff --git a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.query.xml b/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.query.xml index 866a7da..b315c2b 100644 --- a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.query.xml +++ b/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.query.xml @@ -11,9 +11,17 @@ Conception Id + + Estimated + Conception date is estimated rather than observed + Conception Outcome + + Offspring + Animal born from this conception, if a birth record references it + diff --git a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql b/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql index 4554f1d..ae7697f 100644 --- a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql +++ b/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql @@ -8,16 +8,19 @@ SELECT c.ConceptId, c.ConceptDate, c.ConceptTermDate, + c.Estimated, c.Sire, CASE WHEN b.conceptId IS NOT NULL THEN 'Live Birth' WHEN po.conceptId IS NOT NULL THEN COALESCE(po.result, 'Unknown') ELSE 'Unknown' END AS conceptionOutcome, + b.offspring, c.Remark, c.QCState AS qcstate FROM Conception c -LEFT JOIN (SELECT DISTINCT b.conceptId FROM study.birth b WHERE b.conceptId IS NOT NULL) b +-- 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 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 ON po.conceptId = c.ConceptId diff --git a/nbri_ehr/resources/queries/study/BehaviorClinRemarks/.qview.xml b/nbri_ehr/resources/queries/study/BehaviorClinRemarks/.qview.xml index 4e381e4..243ffad 100644 --- a/nbri_ehr/resources/queries/study/BehaviorClinRemarks/.qview.xml +++ b/nbri_ehr/resources/queries/study/BehaviorClinRemarks/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/ClinicalClinRemarks/.qview.xml b/nbri_ehr/resources/queries/study/ClinicalClinRemarks/.qview.xml index c3ca1ea..86bc194 100644 --- a/nbri_ehr/resources/queries/study/ClinicalClinRemarks/.qview.xml +++ b/nbri_ehr/resources/queries/study/ClinicalClinRemarks/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/activeBehaviorCases/.qview.xml b/nbri_ehr/resources/queries/study/activeBehaviorCases/.qview.xml index b08797c..6026d97 100644 --- a/nbri_ehr/resources/queries/study/activeBehaviorCases/.qview.xml +++ b/nbri_ehr/resources/queries/study/activeBehaviorCases/.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/activeClinicalCases/.qview.xml b/nbri_ehr/resources/queries/study/activeClinicalCases/.qview.xml index b08797c..6026d97 100644 --- a/nbri_ehr/resources/queries/study/activeClinicalCases/.qview.xml +++ b/nbri_ehr/resources/queries/study/activeClinicalCases/.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/aliases.sql b/nbri_ehr/resources/queries/study/aliases.sql index a1b279f..4ed9962 100644 --- a/nbri_ehr/resources/queries/study/aliases.sql +++ b/nbri_ehr/resources/queries/study/aliases.sql @@ -15,5 +15,4 @@ FROM nbri_ehr.IdHistory UNION SELECT Id, Alias as alias -FROM study.alias where Id.demographics.calculated_status != 'Alive - In Progress' -' \ No newline at end of file +FROM study.alias where Id.demographics.calculated_status != 'Alive - In Progress' \ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/alopecia/.qview.xml b/nbri_ehr/resources/queries/study/alopecia/.qview.xml index f889f92..b514468 100644 --- a/nbri_ehr/resources/queries/study/alopecia/.qview.xml +++ b/nbri_ehr/resources/queries/study/alopecia/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/arrival.js b/nbri_ehr/resources/queries/study/arrival.js index 251c651..2010ffd 100644 --- a/nbri_ehr/resources/queries/study/arrival.js +++ b/nbri_ehr/resources/queries/study/arrival.js @@ -40,7 +40,6 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even row.birth = row['Id/demographics/birth'] || null; row.gender = row['Id/demographics/gender'] || null; row.geographic_origin = row['Id/demographics/geographic_origin'] || null; - row.source = row.sourceFacility || null; if (row.QCStateLabel) { row.qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId(); diff --git a/nbri_ehr/resources/queries/study/arrival.query.xml b/nbri_ehr/resources/queries/study/arrival.query.xml index ffe2108..0955496 100644 --- a/nbri_ehr/resources/queries/study/arrival.query.xml +++ b/nbri_ehr/resources/queries/study/arrival.query.xml @@ -66,7 +66,8 @@ ehr_lookups source - meaning + code + meaning diff --git a/nbri_ehr/resources/queries/study/assignment.query.xml b/nbri_ehr/resources/queries/study/assignment.query.xml index 1cf7a36..df55a2e 100644 --- a/nbri_ehr/resources/queries/study/assignment.query.xml +++ b/nbri_ehr/resources/queries/study/assignment.query.xml @@ -21,6 +21,18 @@ true + + + CoAssignments + false + true + + study + assignmentTotalCoAssigned + lsid + + diff --git a/nbri_ehr/resources/queries/study/behaviorCases/.qview.xml b/nbri_ehr/resources/queries/study/behaviorCases/.qview.xml index b08797c..6026d97 100644 --- a/nbri_ehr/resources/queries/study/behaviorCases/.qview.xml +++ b/nbri_ehr/resources/queries/study/behaviorCases/.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/behaviorObservations/.qview.xml b/nbri_ehr/resources/queries/study/behaviorObservations/.qview.xml index a48d5a5..5a68b93 100644 --- a/nbri_ehr/resources/queries/study/behaviorObservations/.qview.xml +++ b/nbri_ehr/resources/queries/study/behaviorObservations/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/birth.js b/nbri_ehr/resources/queries/study/birth.js index e3e5fd0..cd0cce1 100644 --- a/nbri_ehr/resources/queries/study/birth.js +++ b/nbri_ehr/resources/queries/study/birth.js @@ -38,7 +38,7 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even //when updating a record that already carries this conception id, the existing row accounts for one match var conceptIdThreshold = (oldRow && oldRow.conceptId === row.conceptId) ? 1 : 0; if (triggerHelper.totalRecords('study', 'birth', 'conceptId', row.conceptId) > conceptIdThreshold) { - EHR.Server.Utils.addError(scriptErrors, 'conceptId', 'This conception Id is already used by another birth record', 'INFO'); + EHR.Server.Utils.addError(scriptErrors, 'conceptId', 'This conception Id is already used by another birth record', 'WARN'); } if (triggerHelper.totalRecords('study', 'pregnancy', 'conceptId', row.conceptId) > 0) { diff --git a/nbri_ehr/resources/queries/study/birth.query.xml b/nbri_ehr/resources/queries/study/birth.query.xml index 8eccc34..18a5a92 100644 --- a/nbri_ehr/resources/queries/study/birth.query.xml +++ b/nbri_ehr/resources/queries/study/birth.query.xml @@ -8,11 +8,18 @@ - - + Birth Date + + Conception Id + + nbri_ehr + Conception + ConceptId + + Birth Location 80 @@ -25,6 +32,25 @@ cage + + Delivery Mode + false + + ehr_lookups + delivery_mode + value + title + + + + Birth Condition + + ehr_lookups + birth_condition + value + title + + Project @@ -43,12 +69,13 @@ title - - Conception Id + + Breeding Type - nbri_ehr - Conception - ConceptId + ehr_lookups + breeding_type + value + title diff --git a/nbri_ehr/resources/queries/study/blood/.qview.xml b/nbri_ehr/resources/queries/study/blood/.qview.xml index 831d6b5..c382d83 100644 --- a/nbri_ehr/resources/queries/study/blood/.qview.xml +++ b/nbri_ehr/resources/queries/study/blood/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/breeder/.qview.xml b/nbri_ehr/resources/queries/study/breeder/.qview.xml index 448bd0f..bba85e1 100644 --- a/nbri_ehr/resources/queries/study/breeder/.qview.xml +++ b/nbri_ehr/resources/queries/study/breeder/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/cases/.qview.xml b/nbri_ehr/resources/queries/study/cases/.qview.xml index 340d770..a2f1fbc 100644 --- a/nbri_ehr/resources/queries/study/cases/.qview.xml +++ b/nbri_ehr/resources/queries/study/cases/.qview.xml @@ -3,7 +3,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/cases/Active Behavior Cases.qview.xml b/nbri_ehr/resources/queries/study/cases/Active Behavior Cases.qview.xml index 8333562..7f83fe9 100644 --- a/nbri_ehr/resources/queries/study/cases/Active Behavior Cases.qview.xml +++ b/nbri_ehr/resources/queries/study/cases/Active Behavior Cases.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/cases/Active Clinical Cases.qview.xml b/nbri_ehr/resources/queries/study/cases/Active Clinical Cases.qview.xml index 6081418..7554046 100644 --- a/nbri_ehr/resources/queries/study/cases/Active Clinical Cases.qview.xml +++ b/nbri_ehr/resources/queries/study/cases/Active Clinical Cases.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/cases/All Behavior Cases.qview.xml b/nbri_ehr/resources/queries/study/cases/All Behavior Cases.qview.xml index 261b1bb..25baf96 100644 --- a/nbri_ehr/resources/queries/study/cases/All Behavior Cases.qview.xml +++ b/nbri_ehr/resources/queries/study/cases/All Behavior Cases.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/cases/All Clinical Cases.qview.xml b/nbri_ehr/resources/queries/study/cases/All Clinical Cases.qview.xml index 73fb0b2..e0dd66f 100644 --- a/nbri_ehr/resources/queries/study/cases/All Clinical Cases.qview.xml +++ b/nbri_ehr/resources/queries/study/cases/All Clinical Cases.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/chemistryResults.query.xml b/nbri_ehr/resources/queries/study/chemistryResults.query.xml index caa0e9e..154a0ae 100644 --- a/nbri_ehr/resources/queries/study/chemistryResults.query.xml +++ b/nbri_ehr/resources/queries/study/chemistryResults.query.xml @@ -1,7 +1,7 @@ - +
@@ -15,6 +15,18 @@ Type + + + Ref Range + true + false + + study + chemistryRefRange + lsid + +
diff --git a/nbri_ehr/resources/queries/study/chemistryResults/.qview.xml b/nbri_ehr/resources/queries/study/chemistryResults/.qview.xml index 02467cb..3cc536b 100644 --- a/nbri_ehr/resources/queries/study/chemistryResults/.qview.xml +++ b/nbri_ehr/resources/queries/study/chemistryResults/.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinicalCases/.qview.xml b/nbri_ehr/resources/queries/study/clinicalCases/.qview.xml index b08797c..6026d97 100644 --- a/nbri_ehr/resources/queries/study/clinicalCases/.qview.xml +++ b/nbri_ehr/resources/queries/study/clinicalCases/.qview.xml @@ -4,7 +4,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinicalObservations/.qview.xml b/nbri_ehr/resources/queries/study/clinicalObservations/.qview.xml index 7b1fc1a..0fe5c32 100644 --- a/nbri_ehr/resources/queries/study/clinicalObservations/.qview.xml +++ b/nbri_ehr/resources/queries/study/clinicalObservations/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinical_observations/.qview.xml b/nbri_ehr/resources/queries/study/clinical_observations/.qview.xml index daaa82d..8d99f0c 100644 --- a/nbri_ehr/resources/queries/study/clinical_observations/.qview.xml +++ b/nbri_ehr/resources/queries/study/clinical_observations/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinical_observations/Alopecia Scores.qview.xml b/nbri_ehr/resources/queries/study/clinical_observations/Alopecia Scores.qview.xml index 0a05b82..589eba9 100644 --- a/nbri_ehr/resources/queries/study/clinical_observations/Alopecia Scores.qview.xml +++ b/nbri_ehr/resources/queries/study/clinical_observations/Alopecia Scores.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinical_observations/Behavior.qview.xml b/nbri_ehr/resources/queries/study/clinical_observations/Behavior.qview.xml index f7566f3..6c017a8 100644 --- a/nbri_ehr/resources/queries/study/clinical_observations/Behavior.qview.xml +++ b/nbri_ehr/resources/queries/study/clinical_observations/Behavior.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinical_observations/Clinical.qview.xml b/nbri_ehr/resources/queries/study/clinical_observations/Clinical.qview.xml index adf8cf8..e8cff6b 100644 --- a/nbri_ehr/resources/queries/study/clinical_observations/Clinical.qview.xml +++ b/nbri_ehr/resources/queries/study/clinical_observations/Clinical.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinical_observationsSummary/.qview.xml b/nbri_ehr/resources/queries/study/clinical_observationsSummary/.qview.xml index a48d5a5..5a68b93 100644 --- a/nbri_ehr/resources/queries/study/clinical_observationsSummary/.qview.xml +++ b/nbri_ehr/resources/queries/study/clinical_observationsSummary/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinremarks/.qview.xml b/nbri_ehr/resources/queries/study/clinremarks/.qview.xml index 903f88d..d96725e 100644 --- a/nbri_ehr/resources/queries/study/clinremarks/.qview.xml +++ b/nbri_ehr/resources/queries/study/clinremarks/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinremarks/Behavior.qview.xml b/nbri_ehr/resources/queries/study/clinremarks/Behavior.qview.xml index 24f5a40..007434b 100644 --- a/nbri_ehr/resources/queries/study/clinremarks/Behavior.qview.xml +++ b/nbri_ehr/resources/queries/study/clinremarks/Behavior.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/clinremarks/Clinical.qview.xml b/nbri_ehr/resources/queries/study/clinremarks/Clinical.qview.xml index 6cb3771..3123f8c 100644 --- a/nbri_ehr/resources/queries/study/clinremarks/Clinical.qview.xml +++ b/nbri_ehr/resources/queries/study/clinremarks/Clinical.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/deaths.js b/nbri_ehr/resources/queries/study/deaths.js index 79883b4..abe7bc4 100644 --- a/nbri_ehr/resources/queries/study/deaths.js +++ b/nbri_ehr/resources/queries/study/deaths.js @@ -6,7 +6,6 @@ require("ehr/triggers").initScript(this); var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); -var validIds = []; var idMap = {}; var deathIdMap = {}; @@ -26,9 +25,7 @@ function onInit(event, helper){ return; for(var i=0; i < results.rows.length; i++) { - validIds.push(results.rows[i]["Id"]["value"]) idMap[results.rows[i]["Id"]["value"]] = {calculated_status: results.rows[i]["calculated_status"]["value"], QCStateLabel: results.rows[i]["QCState/Label"]["value"]}; - // console.log(idMap[results.rows[i]["Id"]["value"]]); } }, failure: function (error) { @@ -84,13 +81,27 @@ function onUpsert(helper, scriptErrors, row, oldRow) { //only allow death record to be created if the animal is in the demographics table if (idMap[row.Id]) { + // deathIdMap has no entry for the animal on initial import, and any of these values can be null + var status = idMap[row.Id].calculated_status ? idMap[row.Id].calculated_status.toUpperCase() : null; + var priorDeathQCState = deathIdMap[row.Id] && deathIdMap[row.Id].QCStateLabel ? deathIdMap[row.Id].QCStateLabel.toUpperCase() : null; + var rowQCState = row.QCStateLabel ? row.QCStateLabel.toUpperCase() : null; + + // deathIdMap is a snapshot taken before any row was processed, so it cannot see earlier rows of this same + // save. Track them separately: study.deaths is demographic, so a second row for one animal cannot be saved. + var deathsInTransaction = helper.getProperty('deathsInTransaction') || {}; + + var errorMsg = null; + // check if a death record already exists for this animal - if (idMap[row.Id].calculated_status.toUpperCase() === 'DEAD' && deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'COMPLETED') { - EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death record already exists for this animal.', 'ERROR'); + if (status === 'DEAD' && priorDeathQCState === 'COMPLETED') { + errorMsg = 'Death record already exists for this animal.'; } // check if the animal is at the center - else if (idMap[row.Id].calculated_status.toUpperCase() === 'SHIPPED') { - EHR.Server.Utils.addError(scriptErrors, 'Id', 'Animal is not at the center.', 'ERROR'); + else if (status === 'SHIPPED') { + errorMsg = 'Animal is not at the center.'; + } + else if (deathsInTransaction[row.Id]) { + errorMsg = 'This animal is entered more than once. Only one death record per animal can be saved.'; } // Check if an animal that's being entered is pending any request/review. // Note 1: When trying to enter a new record for an animal, the QCState = 'IN PROGRESS'. @@ -98,23 +109,28 @@ function onUpsert(helper, scriptErrors, row, oldRow) { // the QCState will get set to 'Review Required' - this way we can distinguish between the two states in the Death/Necropsy workflow. // If a user tries to submit a new Death record (identified by QCState = 'IN PROGRESS') for an animal that // already has a pending request/review status in study.deaths, then below error message will be displayed. - else if (row.QCStateLabel.toUpperCase() === 'IN PROGRESS' && - deathIdMap[row.Id] && deathIdMap[row.Id].QCStateLabel && - (deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'REQUEST: PENDING' || - deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'REVIEW REQUIRED')) { - EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death record is pending review for this animal', 'ERROR'); + else if (rowQCState === 'IN PROGRESS' && + (priorDeathQCState === 'REQUEST: PENDING' || priorDeathQCState === 'REVIEW REQUIRED')) { + errorMsg = 'Death record is pending review for this animal'; } // if 'Save Draft' record already exists, it doesn't allow to 'Save Draft' or 'Submit Death' // on the same animal again - throws an error "duplicate key value violates unique constraint" // So, added this check to allow 'Save Draft' record to be saved only once. - else if (oldRow === undefined && row.QCStateLabel.toUpperCase() === 'IN PROGRESS' && - deathIdMap[row.Id] && deathIdMap[row.Id].QCStateLabel && - deathIdMap[row.Id].QCStateLabel.toUpperCase() === 'IN PROGRESS') { - EHR.Server.Utils.addError(scriptErrors, 'Id', 'Death/Necropsy data entry is in progress for this animal', 'ERROR'); + else if (oldRow === undefined && rowQCState === 'IN PROGRESS' && priorDeathQCState === 'IN PROGRESS') { + errorMsg = 'Death/Necropsy data entry is in progress for this animal'; + } + // study.deaths is demographic (one row per animal), so any other new row for an animal with an existing + // record would fail on the unique constraint; report it as a validation error instead. Test record + // existence, not QC state: ETL/import-sourced rows can carry a null QCState. + else if (oldRow === undefined && deathIdMap[row.Id]) { + errorMsg = 'A death record already exists for this animal (' + (deathIdMap[row.Id].QCStateLabel || 'unknown state') + ').'; } - else if (!helper.isValidateOnly() && row.Id && row.date && row.QCStateLabel.toUpperCase() === 'COMPLETED') { - if (validIds.indexOf(row.id) !== -1) { + if (errorMsg) { + EHR.Server.Utils.addError(scriptErrors, 'Id', errorMsg, 'ERROR'); + } + else { + if (!helper.isValidateOnly() && row.Id && row.date && rowQCState === 'COMPLETED') { // update demographics demographicsUpdates.push({ @@ -128,26 +144,33 @@ function onUpsert(helper, scriptErrors, row, oldRow) { helper.getJavaHelper().updateDemographicsRecord(demographicsUpdates); console.log('updated demographics death date for animal: ' + row.Id); } - else { - console.log(row.id + " is not a valid animal id"); + + if (!helper.isValidateOnly() && row.date && row.QCStateLabel && EHR.Server.Security.getQCStateByLabel(row.QCStateLabel).PublicData) { + var qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId(); + + //add/update weight record + var weightRecord = { + Id: row.Id, + date: row.date, + weight: row.deathWeight, + taskid: row.taskid, + qcstate: qcstate, + performedby: row.performedby + }; + if (triggerHelper.upsertWeightRecord(weightRecord, false)) { + helper.addTableModified('study', 'weight'); + } } - } - if(row.QCStateLabel && EHR.Server.Security.getQCStateByLabel(row.QCStateLabel).PublicData) { - var qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId(); - - //add/update weight record - var weightRecord = { - Id: row.Id, - date: row.date, - weight: row.deathWeight, - taskid: row.taskid, - qcstate: qcstate, - performedby: row.performedby - }; - triggerHelper.upsertWeightRecord(weightRecord); + // mark only rows that passed, so a duplicate of a failed row reports that row's underlying error + deathsInTransaction[row.Id] = true; + helper.setProperty('deathsInTransaction', deathsInTransaction); } } + // insert-only: updates of existing death records keep their prior behavior + else if (oldRow === undefined) { + EHR.Server.Utils.addError(scriptErrors, 'Id', 'Id not found in the demographics table.', 'ERROR'); + } } } diff --git a/nbri_ehr/resources/queries/study/deaths.query.xml b/nbri_ehr/resources/queries/study/deaths.query.xml index 35c2e6a..e28e2f0 100644 --- a/nbri_ehr/resources/queries/study/deaths.query.xml +++ b/nbri_ehr/resources/queries/study/deaths.query.xml @@ -7,6 +7,15 @@ Death Date + + Death Type + + ehr_lookups + death_type + value + title + + Disposition diff --git a/nbri_ehr/resources/queries/study/deaths/.qview.xml b/nbri_ehr/resources/queries/study/deaths/.qview.xml index fdc063c..56eecec 100644 --- a/nbri_ehr/resources/queries/study/deaths/.qview.xml +++ b/nbri_ehr/resources/queries/study/deaths/.qview.xml @@ -6,6 +6,7 @@ + diff --git a/nbri_ehr/resources/queries/study/demographics.query.xml b/nbri_ehr/resources/queries/study/demographics.query.xml index bff3d17..f6fafa6 100644 --- a/nbri_ehr/resources/queries/study/demographics.query.xml +++ b/nbri_ehr/resources/queries/study/demographics.query.xml @@ -99,9 +99,6 @@ meaning - - Source - CITES diff --git a/nbri_ehr/resources/queries/study/demographics/.qview.xml b/nbri_ehr/resources/queries/study/demographics/.qview.xml index 56f1f23..d232062 100644 --- a/nbri_ehr/resources/queries/study/demographics/.qview.xml +++ b/nbri_ehr/resources/queries/study/demographics/.qview.xml @@ -4,7 +4,7 @@ - + @@ -12,7 +12,6 @@ - diff --git a/nbri_ehr/resources/queries/study/demographicsCagemates.sql b/nbri_ehr/resources/queries/study/demographicsCagemates.sql index b3464b7..4219925 100644 --- a/nbri_ehr/resources/queries/study/demographicsCagemates.sql +++ b/nbri_ehr/resources/queries/study/demographicsCagemates.sql @@ -22,10 +22,15 @@ SELECT FROM study.housing h JOIN study.housing h2 -ON (h2.Id.demographics.calculated_status = 'Alive' - AND (h.cage = h2.cage)) +-- cage holds a location key that already encodes the room, so caged animals match on cage alone. Group/pen rooms have +-- no cage, so those fall back to the room, which is only consulted when neither side has a cage. +ON ((h.cage = h2.cage OR (h.cage IS NULL AND h2.cage IS NULL AND h.room = h2.room)) + AND h2.Id.demographics.calculated_status = 'Alive' + AND h2.enddateTimeCoalesced >= now() + AND h2.qcstate.publicdata = true) WHERE h.enddateTimeCoalesced >= now() +AND h.qcstate.publicdata = true GROUP BY h.id, h.room, h.cage ) t ON (t.id = d.id) diff --git a/nbri_ehr/resources/queries/study/demographicsSource.sql b/nbri_ehr/resources/queries/study/demographicsSource.sql index 9de9dfd..6ee341e 100644 --- a/nbri_ehr/resources/queries/study/demographicsSource.sql +++ b/nbri_ehr/resources/queries/study/demographicsSource.sql @@ -17,7 +17,7 @@ SELECT WHEN T1.EarliestArrival IS NULL AND d.birth IS NOT NULL THEN true ELSE false END as fromCenter, - d.source as source, + T2.sourceFacility as source, CASE WHEN T1.EarliestArrival IS NULL AND d.birth IS NOT NULL THEN 'Born at NBRI' diff --git a/nbri_ehr/resources/queries/study/departure.query.xml b/nbri_ehr/resources/queries/study/departure.query.xml index 19845d1..5955592 100644 --- a/nbri_ehr/resources/queries/study/departure.query.xml +++ b/nbri_ehr/resources/queries/study/departure.query.xml @@ -13,7 +13,8 @@ ehr_lookups source - meaning + code + meaning diff --git a/nbri_ehr/resources/queries/study/drug/.qview.xml b/nbri_ehr/resources/queries/study/drug/.qview.xml index 3b72268..8489e4d 100644 --- a/nbri_ehr/resources/queries/study/drug/.qview.xml +++ b/nbri_ehr/resources/queries/study/drug/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/drug/Behavior.qview.xml b/nbri_ehr/resources/queries/study/drug/Behavior.qview.xml index a9769cb..1688ad3 100644 --- a/nbri_ehr/resources/queries/study/drug/Behavior.qview.xml +++ b/nbri_ehr/resources/queries/study/drug/Behavior.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/exemptions/.qview.xml b/nbri_ehr/resources/queries/study/exemptions/.qview.xml index 7e4278b..cdd0721 100644 --- a/nbri_ehr/resources/queries/study/exemptions/.qview.xml +++ b/nbri_ehr/resources/queries/study/exemptions/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/flags.query.xml b/nbri_ehr/resources/queries/study/flags.query.xml index 570a645..00a8119 100644 --- a/nbri_ehr/resources/queries/study/flags.query.xml +++ b/nbri_ehr/resources/queries/study/flags.query.xml @@ -23,8 +23,8 @@ ehr_lookups flag_values - objectid - value + value + Description diff --git a/nbri_ehr/resources/queries/study/flags/.qview.xml b/nbri_ehr/resources/queries/study/flags/.qview.xml index bf12e57..3f15f5b 100644 --- a/nbri_ehr/resources/queries/study/flags/.qview.xml +++ b/nbri_ehr/resources/queries/study/flags/.qview.xml @@ -2,12 +2,12 @@ - + - + diff --git a/nbri_ehr/resources/queries/study/housing.query.xml b/nbri_ehr/resources/queries/study/housing.query.xml index eeb5e0d..e1a21a3 100644 --- a/nbri_ehr/resources/queries/study/housing.query.xml +++ b/nbri_ehr/resources/queries/study/housing.query.xml @@ -21,7 +21,6 @@ ehr_lookups cage location - cage @@ -31,7 +30,7 @@ ehr_lookups rooms room - name + room @@ -45,6 +44,19 @@ + + + Total Cagemates + false + true + + study + housingTotalRoommates + lsid + + diff --git a/nbri_ehr/resources/queries/study/housing/.qview.xml b/nbri_ehr/resources/queries/study/housing/.qview.xml index bc780a5..5f01093 100644 --- a/nbri_ehr/resources/queries/study/housing/.qview.xml +++ b/nbri_ehr/resources/queries/study/housing/.qview.xml @@ -7,7 +7,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/housing/Active Housing.qview.xml b/nbri_ehr/resources/queries/study/housing/Active Housing.qview.xml index 4fc3eeb..54e07b9 100644 --- a/nbri_ehr/resources/queries/study/housing/Active Housing.qview.xml +++ b/nbri_ehr/resources/queries/study/housing/Active Housing.qview.xml @@ -4,6 +4,7 @@ + diff --git a/nbri_ehr/resources/queries/study/notes/.qview.xml b/nbri_ehr/resources/queries/study/notes/.qview.xml index 6cd9bab..0f3a556 100644 --- a/nbri_ehr/resources/queries/study/notes/.qview.xml +++ b/nbri_ehr/resources/queries/study/notes/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/observationSchedule/.qview.xml b/nbri_ehr/resources/queries/study/observationSchedule/.qview.xml index b91d5d4..628bcd9 100644 --- a/nbri_ehr/resources/queries/study/observationSchedule/.qview.xml +++ b/nbri_ehr/resources/queries/study/observationSchedule/.qview.xml @@ -3,7 +3,7 @@ - + @@ -11,7 +11,7 @@ - + \ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/observation_order/.qview.xml b/nbri_ehr/resources/queries/study/observation_order/.qview.xml index c02d2fb..40bb429 100644 --- a/nbri_ehr/resources/queries/study/observation_order/.qview.xml +++ b/nbri_ehr/resources/queries/study/observation_order/.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/observation_order/Active Behavior Orders.qview.xml b/nbri_ehr/resources/queries/study/observation_order/Active Behavior Orders.qview.xml index d4c5c11..c2c3d7e 100644 --- a/nbri_ehr/resources/queries/study/observation_order/Active Behavior Orders.qview.xml +++ b/nbri_ehr/resources/queries/study/observation_order/Active Behavior Orders.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/observation_order/Active Clinical Orders.qview.xml b/nbri_ehr/resources/queries/study/observation_order/Active Clinical Orders.qview.xml index 1638c87..f8bc797 100644 --- a/nbri_ehr/resources/queries/study/observation_order/Active Clinical Orders.qview.xml +++ b/nbri_ehr/resources/queries/study/observation_order/Active Clinical Orders.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/observation_order/Behavior Orders.qview.xml b/nbri_ehr/resources/queries/study/observation_order/Behavior Orders.qview.xml index 343d9b8..ad56df0 100644 --- a/nbri_ehr/resources/queries/study/observation_order/Behavior Orders.qview.xml +++ b/nbri_ehr/resources/queries/study/observation_order/Behavior Orders.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/observation_order/Clinical Orders.qview.xml b/nbri_ehr/resources/queries/study/observation_order/Clinical Orders.qview.xml index 5e1e749..24a0828 100644 --- a/nbri_ehr/resources/queries/study/observation_order/Clinical Orders.qview.xml +++ b/nbri_ehr/resources/queries/study/observation_order/Clinical Orders.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/pairingSummary/.qview.xml b/nbri_ehr/resources/queries/study/pairingSummary/.qview.xml index f0fdcf4..48d82d6 100644 --- a/nbri_ehr/resources/queries/study/pairingSummary/.qview.xml +++ b/nbri_ehr/resources/queries/study/pairingSummary/.qview.xml @@ -6,7 +6,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/pairingSummary/Active Pairing.qview.xml b/nbri_ehr/resources/queries/study/pairingSummary/Active Pairing.qview.xml index 14e0dfe..689f99f 100644 --- a/nbri_ehr/resources/queries/study/pairingSummary/Active Pairing.qview.xml +++ b/nbri_ehr/resources/queries/study/pairingSummary/Active Pairing.qview.xml @@ -9,7 +9,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/pairingSummary/Pairing History.qview.xml b/nbri_ehr/resources/queries/study/pairingSummary/Pairing History.qview.xml index 1a11617..81ee864 100644 --- a/nbri_ehr/resources/queries/study/pairingSummary/Pairing History.qview.xml +++ b/nbri_ehr/resources/queries/study/pairingSummary/Pairing History.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/pairings/.qview.xml b/nbri_ehr/resources/queries/study/pairings/.qview.xml index 26a8731..1a55dfa 100644 --- a/nbri_ehr/resources/queries/study/pairings/.qview.xml +++ b/nbri_ehr/resources/queries/study/pairings/.qview.xml @@ -6,7 +6,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/physicalExam/.qview.xml b/nbri_ehr/resources/queries/study/physicalExam/.qview.xml index caf3fb0..3fb7d6a 100644 --- a/nbri_ehr/resources/queries/study/physicalExam/.qview.xml +++ b/nbri_ehr/resources/queries/study/physicalExam/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/prc/.qview.xml b/nbri_ehr/resources/queries/study/prc/.qview.xml index f5ed643..089bd36 100644 --- a/nbri_ehr/resources/queries/study/prc/.qview.xml +++ b/nbri_ehr/resources/queries/study/prc/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/prcOverdue/.qview.xml b/nbri_ehr/resources/queries/study/prcOverdue/.qview.xml index b8bf120..7957d9f 100644 --- a/nbri_ehr/resources/queries/study/prcOverdue/.qview.xml +++ b/nbri_ehr/resources/queries/study/prcOverdue/.qview.xml @@ -3,7 +3,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/prcSchedule/.qview.xml b/nbri_ehr/resources/queries/study/prcSchedule/.qview.xml index 4604b77..227ae69 100644 --- a/nbri_ehr/resources/queries/study/prcSchedule/.qview.xml +++ b/nbri_ehr/resources/queries/study/prcSchedule/.qview.xml @@ -3,7 +3,7 @@ - + @@ -17,7 +17,7 @@ - + \ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/prc_order/.qview.xml b/nbri_ehr/resources/queries/study/prc_order/.qview.xml index b9de6c1..8da38d4 100644 --- a/nbri_ehr/resources/queries/study/prc_order/.qview.xml +++ b/nbri_ehr/resources/queries/study/prc_order/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/prc_order_report/.qview.xml b/nbri_ehr/resources/queries/study/prc_order_report/.qview.xml index 62d20f0..3402400 100644 --- a/nbri_ehr/resources/queries/study/prc_order_report/.qview.xml +++ b/nbri_ehr/resources/queries/study/prc_order_report/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/protocolAssignment.query.xml b/nbri_ehr/resources/queries/study/protocolAssignment.query.xml index ac61d1c..d95eb07 100644 --- a/nbri_ehr/resources/queries/study/protocolAssignment.query.xml +++ b/nbri_ehr/resources/queries/study/protocolAssignment.query.xml @@ -15,7 +15,6 @@ ehr protocol protocol - title diff --git a/nbri_ehr/resources/queries/study/treatmentSchedule.query.xml b/nbri_ehr/resources/queries/study/treatmentSchedule.query.xml index 59d1f2e..e6f2432 100644 --- a/nbri_ehr/resources/queries/study/treatmentSchedule.query.xml +++ b/nbri_ehr/resources/queries/study/treatmentSchedule.query.xml @@ -2,6 +2,7 @@ + Treatment Schedule /EHR/treatmentDetails.view?key=${lsid} primaryKey diff --git a/nbri_ehr/resources/queries/study/treatmentSchedule.sql b/nbri_ehr/resources/queries/study/treatmentSchedule.sql index 068cb4e..557e183 100644 --- a/nbri_ehr/resources/queries/study/treatmentSchedule.sql +++ b/nbri_ehr/resources/queries/study/treatmentSchedule.sql @@ -38,7 +38,6 @@ JOIN( timestampdiff('SQL_TSI_DAY', cast(t1.dateOnly AS timestamp), dr.dateOnly) + 1 AS daysElapsed, t1.enddate, t1.code, - t1.treatmentRecord, t1.volume, t1.vol_units, t1.concentration, diff --git a/nbri_ehr/resources/queries/study/treatmentSchedule/.qview.xml b/nbri_ehr/resources/queries/study/treatmentSchedule/.qview.xml index 4c57f0b..e773c14 100644 --- a/nbri_ehr/resources/queries/study/treatmentSchedule/.qview.xml +++ b/nbri_ehr/resources/queries/study/treatmentSchedule/.qview.xml @@ -3,7 +3,7 @@ - + @@ -22,7 +22,7 @@ - + \ No newline at end of file diff --git a/nbri_ehr/resources/queries/study/treatment_order/.qview.xml b/nbri_ehr/resources/queries/study/treatment_order/.qview.xml index 27608af..2b14b7b 100644 --- a/nbri_ehr/resources/queries/study/treatment_order/.qview.xml +++ b/nbri_ehr/resources/queries/study/treatment_order/.qview.xml @@ -2,7 +2,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/vitals/.qview.xml b/nbri_ehr/resources/queries/study/vitals/.qview.xml index f3b831c..b6bc349 100644 --- a/nbri_ehr/resources/queries/study/vitals/.qview.xml +++ b/nbri_ehr/resources/queries/study/vitals/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/queries/study/weight.query.xml b/nbri_ehr/resources/queries/study/weight.query.xml index d9ee743..ca0dfdc 100644 --- a/nbri_ehr/resources/queries/study/weight.query.xml +++ b/nbri_ehr/resources/queries/study/weight.query.xml @@ -29,6 +29,28 @@ + + + Percent Change + false + true + + study + weightPctChange + lsid + + + + Relative Change + false + true + + study + weightRelChange + lsid + +
diff --git a/nbri_ehr/resources/queries/study/weight/.qview.xml b/nbri_ehr/resources/queries/study/weight/.qview.xml index b27ffe4..b6d1a95 100644 --- a/nbri_ehr/resources/queries/study/weight/.qview.xml +++ b/nbri_ehr/resources/queries/study/weight/.qview.xml @@ -5,7 +5,7 @@ - + diff --git a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_manifest.xml b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_manifest.xml index 1d03f01..a4cd2a4 100644 --- a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_manifest.xml +++ b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_manifest.xml @@ -20,7 +20,6 @@ - diff --git a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml index 55e48ea..2ae212d 100644 --- a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml +++ b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml @@ -224,6 +224,15 @@ varchar + + varchar + + + varchar + + + varchar + @@ -469,9 +478,6 @@ varchar - - varchar - varchar @@ -530,6 +536,9 @@ double + + varchar +
@@ -617,30 +626,6 @@
- - - - varchar - http://cpas.labkey.com/Study#ParticipantId - - ptid - - - - timestamp - http://cpas.labkey.com/Study#VisitDate - http://cpas.labkey.com/Study#VisitDate - - - varchar - - - varchar - - - Historical Other - Captures miscellaneous historical health and research records for primates that do not fit other specific categories. -
HousingTracks housing assignments and location transfers for primates within the research facility. diff --git a/nbri_ehr/resources/reports/additionalReports.tsv b/nbri_ehr/resources/reports/additionalReports.tsv index 8eddd91..efaa2ad 100644 --- a/nbri_ehr/resources/reports/additionalReports.tsv +++ b/nbri_ehr/resources/reports/additionalReports.tsv @@ -49,5 +49,4 @@ behaviorRemarks Behavior query Behavior Remarks true study BehaviorClinRemarks clinObsBehavior Behavior query Observations true study behaviorObservations date false false qcstate/publicdata This report contains one record for each encounter with each animal, including surergies, exams, procedures, etc. clinremarks Clinical query Clinical Remarks true study ClinicalClinRemarks date false false qcstate/publicdata This report contains the clinical remarks entered about each animal physicalExam Clinical query Exam History True study physicalExam date false false qcstate/publicdata This report displays physical exam data for the selected animal -historicalOther General query Historical True study historicalOther date false false qcstate/publicdata This report displays historical events from legacy systems conceptionsByDam Reproductive Management query Conceptions by Dam true nbri_ehr ConceptionsByDam ConceptDate false false qcstate/publicdata This report displays conception records where the selected animal is the dam \ No newline at end of file diff --git a/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.000-26.001.sql b/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.000-26.001.sql index 964b91c..866fefc 100644 --- a/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.000-26.001.sql +++ b/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.000-26.001.sql @@ -9,6 +9,7 @@ CREATE TABLE nbri_ehr.Conception ConceptId VARCHAR(100), ConceptDate TIMESTAMP, ConceptTermDate TIMESTAMP, + Estimated BOOLEAN DEFAULT FALSE, Remark TEXT, Dam VARCHAR(100), Sire VARCHAR(100), diff --git a/nbri_ehr/resources/schemas/nbri_ehr.xml b/nbri_ehr/resources/schemas/nbri_ehr.xml index 6d076c8..2bc125d 100644 --- a/nbri_ehr/resources/schemas/nbri_ehr.xml +++ b/nbri_ehr/resources/schemas/nbri_ehr.xml @@ -581,6 +581,7 @@ Date + diff --git a/nbri_ehr/resources/scripts/nbri_triggers.js b/nbri_ehr/resources/scripts/nbri_triggers.js index e4ad95b..16b2ddc 100644 --- a/nbri_ehr/resources/scripts/nbri_triggers.js +++ b/nbri_ehr/resources/scripts/nbri_triggers.js @@ -40,9 +40,15 @@ exports.init = function (EHR) { }); }); + // the arrival and birth forms assign animals that do not have a demographics record yet, so those forms ask for Id validation to be relaxed + function isAllowAnyIdRequested(helper) { + helper.decodeExtraContextProperty('allowAnyId', false); + return helper.getProperty('allowAnyId') === true; // this can be true or an empty object + } + EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'protocolAssignment', function(event, helper) { helper.setScriptOptions({ - allowAnyId: false, + allowAnyId: isAllowAnyIdRequested(helper), requiresStatusRecalc: false, allowDatesInDistantPast: true }); @@ -50,7 +56,7 @@ exports.init = function (EHR) { EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'assignment', function(event, helper) { helper.setScriptOptions({ - allowAnyId: false, + allowAnyId: isAllowAnyIdRequested(helper), requiresStatusRecalc: false, allowDatesInDistantPast: true, skipAssignmentCheck: true, @@ -109,6 +115,12 @@ exports.init = function (EHR) { }); }); + EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'flags', function(event, helper) { + helper.setScriptOptions({ + allowFutureDates: true, + }); + }); + EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'treatment_order', function(event, helper) { helper.setScriptOptions({ allowFutureDates: true, diff --git a/nbri_ehr/resources/views/cageDetails.html b/nbri_ehr/resources/views/cageDetails.html index a0c5da2..fde98de 100644 --- a/nbri_ehr/resources/views/cageDetails.html +++ b/nbri_ehr/resources/views/cageDetails.html @@ -42,7 +42,7 @@ schemaName: 'ehr_lookups', queryName: 'cage', filterArray: detailFilterArray, - columns: 'cage,room,room/floor,room/floor/building,room/floor/building/area,outdoor', + columns: 'cage,room,room/building,room/building/area,outdoor', }, title: 'Cage Details', renderTo: 'cageDetails_' + webpart.wrapperDivId, @@ -56,7 +56,7 @@ queryName: 'cage', filterArray: detailFilterArray, sort: 'cage', - columns: 'cage,room,room/floor,room/floor/building,room/floor/building/area,outdoor', + columns: 'cage,room,room/building,room/building/area,outdoor', }).render('cageDetails_' + webpart.wrapperDivId); } 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 3e2850b..a5e983f 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js @@ -25,7 +25,7 @@ EHR.model.DataModelManager.registerMetadata('Arrival', { byQuery: { 'study.arrival': { 'cage': { - allowBlank: false, + // allowBlank: false, columnConfig: { fixed: true, width: 200 @@ -39,45 +39,28 @@ EHR.model.DataModelManager.registerMetadata('Arrival', { allowBlank: false }, 'Id/demographics/birth': { - allowBlank: false + // allowBlank: false }, 'Id/demographics/gender': { allowBlank: false }, 'Id/demographics/geographic_origin': { - allowBlank: false, + // allowBlank: false, columnConfig: { fixed: true, width: 200 } }, + // project and protocol are entered through the Project Assignment and Protocol Assignment sections project: { - xtype: 'combo', - columnConfig: { - width: 150 - }, - lookup: { - schemaName: 'ehr', - queryName: 'project', - keyColumn: 'project', - columns: 'project,name', - filterArray: [ - LABKEY.Filter.create('isActive', true, LABKEY.Filter.Types.EQUAL), - ] - }, - allowBlank: false + allowBlank: true, + hidden: true, + showInGrid: false }, arrivalProtocol: { - allowBlank: false, - columnConfig: { - width: 200 - }, - lookup: { - schemaName: 'ehr', - queryName: 'activeProtocols', - keyColumn: 'protocol', - columns: 'protocol,title' - }, + allowBlank: true, + hidden: true, + showInGrid: false }, performedby: { hidden: true, @@ -91,14 +74,14 @@ EHR.model.DataModelManager.registerMetadata('Arrival', { }, }, acquisitionType: { - allowBlank: false, + // allowBlank: false, columnConfig: { fixed: true, width: 150 }, }, arrivalType: { - allowBlank: false, + // allowBlank: false, columnConfig: { width: 200 } diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js index 4db08ad..4d38d3c 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Assignment.js @@ -37,12 +37,14 @@ EHR.model.DataModelManager.registerMetadata('Assignment', { fixed: true, width: 150 }, + // set displayColumn: ehr.protocol's title column (displayName) is not returned by this query lookup: { schemaName: 'ehr', queryName: 'activeProtocols', keyColumn: 'protocol', + displayColumn: 'protocol', columns: 'protocol,title' - }, + } } } } 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 7202b23..f266f1e 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js @@ -24,58 +24,68 @@ EHR.model.DataModelManager.registerMetadata('Birth', { }, byQuery: { 'study.birth': { + Id: { + allowBlank: false, + nullable: false + }, + date: { + allowBlank: false, + nullable: false + }, 'Id/demographics/species': { allowBlank: false, + nullable: false, columnConfig: { fixed: true, width: 250 } }, 'cage': { - allowBlank: false, + // allowBlank: false, columnConfig: { fixed: true, width: 200 }, }, - project: { - xtype: 'combo', - allowBlank: false, + type: { columnConfig: { - width: 150 + width: 200 }, - lookup: { - schemaName: 'ehr', - queryName: 'project', - keyColumn: 'project', - columns: 'project,name', - filterArray: [ - LABKEY.Filter.create('isActive', true, LABKEY.Filter.Types.EQUAL), - ] - } }, - birthProtocol: { + cond: { columnConfig: { width: 200 }, - allowBlank: false, - lookup: { - schemaName: 'ehr', - queryName: 'activeProtocols', - keyColumn: 'protocol', - columns: 'protocol,title' - }, + }, + // project and protocol are entered through the Project Assignment and Protocol Assignment sections + project: { + allowBlank: true, + hidden: true, + showInGrid: false + }, + birthProtocol: { + allowBlank: true, + hidden: true, + showInGrid: false }, 'Id/demographics/birth': { allowBlank: false }, 'Id/demographics/gender': { - allowBlank: false + allowBlank: false, + nullable: false }, conceptId: { + allowBlank: false, + nullable: false, columnConfig: { width: 150 } + }, + breedingType: { + columnConfig: { + width: 200 + } } } } diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Conception.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Conception.js index d862181..949d8cb 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Conception.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Conception.js @@ -35,6 +35,13 @@ EHR.model.DataModelManager.registerMetadata('Conception', { width: 200 }, }, + Estimated: { + xtype: 'checkbox', + defaultValue: false, + columnConfig: { + width: 100 + }, + }, Dam: { xtype: 'ehr-animalfield', lookups: false, diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js new file mode 100644 index 0000000..568a410 --- /dev/null +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Death.js @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * Metadata for the grid-based Bulk Deaths form. The columnConfig widths only take effect in the grid; they are ignored + * when the same fields render in a form panel. + */ +EHR.model.DataModelManager.registerMetadata('Death', { + allQueries: { + }, + byQuery: { + 'study.deaths': { + qcstate: { + hidden: true + }, + date: { + xtype: 'xdatetime', + editorConfig: { + dateFormat: 'Y-m-d', + timeFormat: 'H:i' + }, + columnConfig: { + width: 160 + } + }, + deathWeight: { + label: 'Weight (kg)', + columnConfig: { + width: 150 + } + }, + type: { + allowBlank: false, + nullable: false, + columnConfig: { + width: 160 + } + }, + reason: { + columnConfig: { + width: 160 + } + }, + remark: { + xtype: 'ehr-remarkfield', + columnConfig: { + width: 200 + } + } + } + } +}); diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/DeathNecropsy.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/DeathNecropsy.js index cb988e9..2f7b703 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/DeathNecropsy.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/DeathNecropsy.js @@ -19,13 +19,11 @@ EHR.model.DataModelManager.registerMetadata('DeathNecropsy', { }, }, deathWeight: { - label: 'Weight (kg)', - allowBlank: false, - nullable: false, + label: 'Weight (kg)' }, - reason: { + type: { allowBlank: false, - nullable: false, + nullable: false } }, 'study.necropsy': { diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/NBRIDefault.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/NBRIDefault.js index c5971be..ea19076 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/NBRIDefault.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/NBRIDefault.js @@ -327,7 +327,7 @@ EHR.model.DataModelManager.registerMetadata('Default', { flag: { allowBlank: false, lookup: { - columns: 'objectid,value,category,code', + columns: 'objectid,value,description,category,code', sort: 'category,code,value', filterArray: [LABKEY.Filter.create('datedisabled', null, LABKEY.Filter.Types.ISBLANK)] }, @@ -341,7 +341,7 @@ EHR.model.DataModelManager.registerMetadata('Default', { allowChooseOther: false })], listConfig: { - innerTpl: '{[(values.category ? ("" + LABKEY.Utils.encodeHtml(values.category) + ": ") : "") + LABKEY.Utils.encodeHtml(values.value)]}', + innerTpl: '{[(values.category ? ("" + LABKEY.Utils.encodeHtml(values.category) + ": ") : "") + LABKEY.Utils.encodeHtml(values.description || values.value)]}', getInnerTpl: function () { return this.innerTpl; } diff --git a/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js b/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js index dd59726..bfdb0fd 100644 --- a/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js +++ b/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js @@ -228,6 +228,10 @@ Ext4.define('NBRI_EHR.panel.SnapshotPanel', { toSet['flags'] = values.length ? '' + values.join('
') + '' : null; }, + getFlagDisplayValue: function(row) { + return row['flag/description'] || row['flag/value']; + }, + appendAssignments: function(toSet, results){ toSet['projectAssignment'] = null; diff --git a/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js b/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js new file mode 100644 index 0000000..ebdf711 --- /dev/null +++ b/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +/** + * Adds a birth record pre-populated from an existing conception record. + * + * @cfg {Object} targetStore + * @cfg {Object} formConfig + */ +Ext4.define('NBRI_EHR.window.StartWithConceptionWindow', { + extend: 'Ext.window.Window', + + initComponent: function(){ + Ext4.apply(this, { + title: 'Start with Conception', + modal: true, + closeAction: 'destroy', + border: true, + bodyStyle: 'padding: 5px', + width: 400, + defaults: { + border: false, + width: 370 + }, + items: [{ + html: 'Select a conception record. A new birth record will be added using the conception Id, along with the dam, sire and species from that conception.', + style: 'padding-bottom: 10px;' + },{ + xtype: 'labkey-combo', + itemId: 'conceptionField', + fieldLabel: 'Conception Id', + displayField: 'ConceptId', + valueField: 'ConceptId', + forceSelection: true, + queryMode: 'local', + anyMatch: true, + caseSensitive: false, + store: { + type: 'labkey-store', + schemaName: 'nbri_ehr', + queryName: 'Conception', + columns: 'ConceptId,ConceptDate,Dam,Sire', + sort: '-ConceptDate', + autoLoad: true + } + }], + buttons: [{ + text: 'Submit', + scope: this, + handler: this.onSubmit + },{ + text: 'Close', + handler: function(btn){ + btn.up('window').close(); + } + }] + }); + + this.callParent(arguments); + }, + + onSubmit: function(btn){ + var field = this.down('#conceptionField'); + var conceptId = field.getValue(); + if (!conceptId){ + Ext4.Msg.alert('Error', 'Must select a conception Id'); + return; + } + + var record = field.findRecordByValue(conceptId); + if (!record){ + Ext4.Msg.alert('Error', 'Unable to find the conception record for: ' + conceptId); + return; + } + + var dam = record.get('Dam'); + var sire = record.get('Sire'); + + btn.disable(); + this.getSpecies(dam, function(species, speciesError){ + this.addRow(conceptId, dam, sire, species); + btn.enable(); + this.close(); + + // the row is still added so the conception values are not lost, but a blank species would otherwise + // surface only as a bare "Species is required" error with no hint that the copy from the dam failed + if (speciesError){ + Ext4.Msg.alert('Species Not Copied', speciesError + ' Enter the species on the new birth record manually.'); + } + }, 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){ + if (!dam){ + callback.call(scope, 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', + 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 + '.'); + return; + } + + if (!rows[0].species){ + callback.call(scope, null, 'No species is recorded on the demographics record for dam ' + dam + '.'); + return; + } + + callback.call(scope, rows[0].species); + }, + 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') + '.'); + } + }); + }, + + addRow: function(conceptId, dam, sire, species){ + this.targetStore.add(this.targetStore.createModel({ + conceptId: conceptId, + 'Id/demographics/dam': dam, + 'Id/demographics/sire': sire, + 'Id/demographics/species': species + })); + } +}); + +EHR.DataEntryUtils.registerGridButton('NBRI_START_WITH_CONCEPTION', function(config){ + return Ext4.Object.merge({ + text: 'Start with Conception', + tooltip: EHR.DataEntryUtils.shouldShowTooltips() ? 'Click to add a birth record populated from an existing conception record' : undefined, + handler: function(btn){ + var grid = btn.up('gridpanel'); + if (!grid.store || !grid.store.hasLoaded()){ + console.log('no store or store hasnt loaded'); + return; + } + + // commit any in-progress cell edit first; the modal window blocks the grid, so an open editor would + // otherwise be abandoned and its pending value lost + var cellEditing = grid.getPlugin(grid.editingPluginId); + if (cellEditing){ + cellEditing.completeEdit(); + } + + Ext4.create('NBRI_EHR.window.StartWithConceptionWindow', { + targetStore: grid.store, + formConfig: grid.formConfig + }).show(); + } + }, config); +}); 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 5339e54..aa76225 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java @@ -212,6 +212,7 @@ private void registerDataEntry() EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIBulkClinicalFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIDepartureFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIDeathNecropsyFormType.class, this)); + EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIBulkDeathFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIHousingFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIMedicationTreatmentFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIProjectFormType.class, this)); diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIArrivalFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIArrivalFormType.java index b79b90b..61fe238 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIArrivalFormType.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIArrivalFormType.java @@ -23,6 +23,8 @@ import org.labkey.nbri_ehr.dataentry.section.NBRIAnimalDetailsFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIArrivalFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIArrivalInstructionsFormSection; +import org.labkey.nbri_ehr.dataentry.section.NBRIProjectAssignmentFormSection; +import org.labkey.nbri_ehr.dataentry.section.NBRIProtocolAssignmentFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRITaskFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIWeightFormSection; @@ -40,13 +42,17 @@ public NBRIArrivalFormType(DataEntryFormContext ctx, Module owner) new NBRITaskFormSection(), new NBRIAnimalDetailsFormSection(), new NBRIArrivalFormSection(), + new NBRIProtocolAssignmentFormSection(true, true, true), + new NBRIProjectAssignmentFormSection(true, true, true), new NBRIWeightFormSection(true, true) )); + addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Assignment.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Arrival.js")); for (FormSection s : getFormSections()) { + s.addConfigSource("Assignment"); s.addConfigSource("Arrival"); } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBirthFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBirthFormType.java index ebe9c2b..cbd66ea 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBirthFormType.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBirthFormType.java @@ -24,6 +24,8 @@ import org.labkey.nbri_ehr.dataentry.section.NBRIAnimalDetailsFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIBirthFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRIBirthInstructionsFormSection; +import org.labkey.nbri_ehr.dataentry.section.NBRIProjectAssignmentFormSection; +import org.labkey.nbri_ehr.dataentry.section.NBRIProtocolAssignmentFormSection; import org.labkey.nbri_ehr.dataentry.section.NBRITaskFormSection; import java.util.ArrayList; @@ -39,16 +41,20 @@ public NBRIBirthFormType (DataEntryFormContext ctx, Module owner) new NBRIBirthInstructionsFormSection(), new NBRITaskFormSection(), new NBRIAnimalDetailsFormSection(), - new NBRIBirthFormSection() + new NBRIBirthFormSection(), + new NBRIProtocolAssignmentFormSection(true, true, true), + new NBRIProjectAssignmentFormSection(true, true, true) )); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/plugin/RowEditor.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/NBRIDefault.js")); + addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Assignment.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Birth.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/window/AddAnimalsWindow.js")); for (FormSection s : getFormSections()) { + s.addConfigSource("Assignment"); s.addConfigSource("Birth"); } } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkDeathFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkDeathFormType.java new file mode 100644 index 0000000..5d95343 --- /dev/null +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBulkDeathFormType.java @@ -0,0 +1,66 @@ +/* + * 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.dataentry.form; + +import org.labkey.api.ehr.EHRService; +import org.labkey.api.ehr.dataentry.DataEntryFormContext; +import org.labkey.api.ehr.dataentry.FormSection; +import org.labkey.api.ehr.security.EHRCompletedInsertPermission; +import org.labkey.api.module.Module; +import org.labkey.api.security.permissions.AdminPermission; +import org.labkey.api.view.template.ClientDependency; +import org.labkey.nbri_ehr.dataentry.section.BaseFormSection; +import org.labkey.nbri_ehr.dataentry.section.NBRIAnimalDetailsFormSection; +import org.labkey.nbri_ehr.dataentry.section.NBRITaskFormSection; + +import java.util.Arrays; + +/** + * Admin-only form that records deaths only, as a grid so several animals can be entered at once. + */ +public class NBRIBulkDeathFormType extends NBRIBaseTaskFormType +{ + public static final String NAME = "BulkDeaths"; + public static final String LABEL = "Bulk Deaths"; + + public NBRIBulkDeathFormType(DataEntryFormContext ctx, Module owner) + { + super(ctx, owner, NAME, LABEL, "Colony Management", Arrays.asList( + new NBRITaskFormSection(), + new NBRIAnimalDetailsFormSection(), + new BaseFormSection("study", "deaths", "Deaths", EHRService.FORM_SECTION_LOCATION.Body, true, false) + )); + + addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Death.js")); + + for (FormSection s : getFormSections()) + { + s.addConfigSource("Death"); + } + } + + @Override + public boolean isAvailable() + { + return super.isAvailable() && getCtx().getContainer().hasPermission(getCtx().getUser(), AdminPermission.class); + } + + @Override + protected boolean canInsert() + { + return EHRService.get().hasPermission("study", "deaths", getCtx().getContainer(), getCtx().getUser(), EHRCompletedInsertPermission.class); + } +} 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 604cbb9..ddecd53 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 @@ -22,15 +22,34 @@ import org.labkey.api.query.FieldKey; import org.labkey.api.view.template.ClientDependency; +import java.util.ArrayList; import java.util.List; public class NBRIBirthFormSection extends NewAnimalFormSection { + // left to right column order of the Births grid; the demographics fields are not on study.birth, so they are added here + private static final List COLUMN_ORDER = List.of( + FieldKey.fromString("Id"), + FieldKey.fromString("date"), + FieldKey.fromString("conceptId"), + FieldKey.fromString("Id/demographics/species"), + FieldKey.fromString("Id/demographics/gender"), + FieldKey.fromString("Id/demographics/dam"), + FieldKey.fromString("Id/demographics/sire"), + FieldKey.fromString("cage"), + FieldKey.fromString("type"), + FieldKey.fromString("cond"), + FieldKey.fromString("breedingType"), + FieldKey.fromString("remark"), + FieldKey.fromString("performedby") + ); + public NBRIBirthFormSection() { super("study", "birth", "Births", false); addClientDependency(ClientDependency.supplierFromPath("ehr/window/FormBulkAddWindow.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/window/FormBulkAddWindow.js")); + addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/window/StartWithConceptionWindow.js")); } @Override @@ -46,14 +65,12 @@ public JSONObject toJSON(DataEntryFormContext ctx, boolean includeFormElements) @Override protected List getFieldKeys(TableInfo ti) { - List keys = super.getFieldKeys(ti); + List ordered = new ArrayList<>(COLUMN_ORDER); - keys.add(2, FieldKey.fromString("Id/demographics/species")); - keys.add(3, FieldKey.fromString("Id/demographics/gender")); - keys.add(4, FieldKey.fromString("Id/demographics/dam")); - keys.add(5, FieldKey.fromString("Id/demographics/sire")); + // anything not explicitly ordered above (hidden and system fields) keeps its default position at the end + super.getFieldKeys(ti).stream().filter(key -> !COLUMN_ORDER.contains(key)).forEach(ordered::add); - return keys; + return ordered; } @Override @@ -68,6 +85,7 @@ public List getTbarButtons() defaultButtons.add(idx, "NBRI_ADDANIMALS"); } defaultButtons.remove("COPYFROMSECTION"); + defaultButtons.addFirst("NBRI_START_WITH_CONCEPTION"); return defaultButtons; } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/demographics/ActiveFlagsDemographicsProvider.java b/nbri_ehr/src/org/labkey/nbri_ehr/demographics/ActiveFlagsDemographicsProvider.java index d327a44..f012b8a 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/demographics/ActiveFlagsDemographicsProvider.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/demographics/ActiveFlagsDemographicsProvider.java @@ -47,6 +47,7 @@ protected Set getFieldKeys() keys.add(FieldKey.fromString("flag")); keys.add(FieldKey.fromString("flag/category")); keys.add(FieldKey.fromString("flag/value")); + keys.add(FieldKey.fromString("flag/description")); keys.add(FieldKey.fromString("performedby")); keys.add(FieldKey.fromString("remark")); diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/history/NBRIHousingDataSource.java b/nbri_ehr/src/org/labkey/nbri_ehr/history/NBRIHousingDataSource.java index 91fdf41..0749f01 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/history/NBRIHousingDataSource.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/history/NBRIHousingDataSource.java @@ -36,7 +36,7 @@ public NBRIHousingDataSource(Module module) @Override protected Set getColumnNames() { - return PageFlowUtil.set("Id", "date", "cage/cage", "room/fullRoom", "reason", "remark"); + return PageFlowUtil.set("Id", "date", "cage/cage", "room/room", "reason", "remark"); } @Override @@ -44,7 +44,7 @@ protected String getHtml(Container c, Results rs, boolean redacted) throws SQLEx { StringBuilder sb = new StringBuilder(); - FieldKey room = FieldKey.fromString("room/fullRoom"); + FieldKey room = FieldKey.fromString("room/room"); FieldKey cage = FieldKey.fromString("cage/cage"); String value = "Unknown"; if (rs.hasColumn(cage) && rs.getObject(cage) != null) 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 a61a5fc..662ab54 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 @@ -323,7 +323,19 @@ public boolean deathExists(String id) return false; } - public void upsertWeightRecord(Map row) throws QueryUpdateServiceException, DuplicateKeyException, SQLException, BatchValidationException, InvalidKeyException + public boolean upsertWeightRecord(Map row) throws QueryUpdateServiceException, DuplicateKeyException, SQLException, BatchValidationException, InvalidKeyException + { + return upsertWeightRecord(row, true); + } + + /** + * When announceChanges is false, the nested weight trigger will not announce the modified id + * (skipAnnounceChangedParticipants). Callers must mark study.weight as modified on the outer helper + * (addTableModified) so the single announcement at trigger completion covers it. + * + * @return whether a weight record was written; false when there was nothing to record + */ + public boolean upsertWeightRecord(Map row, boolean announceChanges) throws QueryUpdateServiceException, DuplicateKeyException, SQLException, BatchValidationException, InvalidKeyException { BatchValidationException errors = new BatchValidationException(); Date date = ConvertHelper.convert(row.get("date"), Date.class); @@ -331,25 +343,16 @@ public void upsertWeightRecord(Map row) throws QueryUpdateServic TableInfo ti = getTableInfo("study", "weight"); - // If there is already a weight record for this task, update that record - SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id"), row.get("Id")); - filter.addCondition(FieldKey.fromString("taskid"), taskId); - TableSelector ts = new TableSelector(ti, PageFlowUtil.set("lsid", "objectid"), filter, null); - boolean updateRecord = ts.exists(); - - Map saveRow = new CaseInsensitiveHashMap<>(); - saveRow.put("Id", row.get("Id")); - saveRow.put("date", date); - saveRow.put("taskid", taskId); - saveRow.put("qcstate", row.get("qcstate")); - saveRow.put("performedby", row.get("performedby")); - if (updateRecord) - { - saveRow.put("objectid", ts.getMap().get("objectid")); - } - else + // If there is already a weight record for this task, update that record. A null taskid filter flips to + // "taskid IS NULL" and would match unrelated historical weights, so task-less entry (e.g. a non-EHR bulk + // import form) is insert-only. + Map existingRecord = null; + if (taskId != null) { - saveRow.put("objectid", new GUID().toString()); + SimpleFilter filter = new SimpleFilter(FieldKey.fromString("Id"), row.get("Id")); + filter.addCondition(FieldKey.fromString("taskid"), taskId); + TableSelector ts = new TableSelector(ti, PageFlowUtil.set("lsid", "objectid"), filter, null); + existingRecord = ts.getMap(); } Double weight = null; @@ -357,22 +360,51 @@ public void upsertWeightRecord(Map row) throws QueryUpdateServic { weight = ConvertHelper.convert(row.get("weight"), Double.class); } + + Map context = getExtraContext(); + if (!announceChanges) + context.put("skipAnnounceChangedParticipants", true); + + // Weight is optional, so with none entered there is nothing to record. Delete any record left by an earlier + // save rather than blanking it: the weight trigger only WARNs on a null weight and the default threshold + // filters that out, so the emptied record would survive the save. + if (weight == null) + { + if (existingRecord == null) + return false; + + Map keyRow = new CaseInsensitiveHashMap<>(); + keyRow.put("lsid", existingRecord.get("lsid")); + ti.getUpdateService().deleteRows(_user, _container, List.of(keyRow), null, context); + + return true; + } + + Map saveRow = new CaseInsensitiveHashMap<>(); + saveRow.put("Id", row.get("Id")); + saveRow.put("date", date); + saveRow.put("taskid", taskId); + saveRow.put("qcstate", row.get("qcstate")); + saveRow.put("performedby", row.get("performedby")); + saveRow.put("objectid", existingRecord != null ? existingRecord.get("objectid") : new GUID().toString()); saveRow.put("weight", weight); List> rows = new ArrayList<>(); rows.add(saveRow); - if (updateRecord) + if (existingRecord != null) { - ti.getUpdateService().updateRows(_user, _container, rows, null, null, getExtraContext()); + ti.getUpdateService().updateRows(_user, _container, rows, null, null, context); } else { - ti.getUpdateService().insertRows(_user, _container, rows, errors, null, getExtraContext()); + ti.getUpdateService().insertRows(_user, _container, rows, errors, null, context); } if (errors.hasErrors()) throw errors; + + return true; } public void clinicalMoveNotification(final String animalId, final String date) 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 19fe42d..b365680 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 @@ -93,16 +93,6 @@ public void customize(TableInfo table) customizeTasks(ti); } - if (matches(ti, "ehr_lookups", "rooms")) - { - customizeRooms(ti); - } - - if (matches(ti, "ehr_lookups", "floors")) - { - customizeFloors(ti); - } - if (matches(ti, "nbri_ehr", "necropsyTasks")) { addNecropsyReportLink(ti); @@ -131,6 +121,11 @@ public void customize(TableInfo table) customizeTreatmentOrder(ti); } + if (matches(ti, "study", "treatmentSchedule")) + { + customizeTreatmentSchedule(ti); + } + if (matches(ti, "study", "prc_order")) { customizeProcedureOrder(ti); @@ -576,60 +571,6 @@ public boolean isEditable() } } - private void customizeRooms(AbstractTableInfo ti) - { - ColumnInfo roomCol = ti.getColumn("name"); - ColumnInfo floorCol = ti.getColumn("floor"); - if (roomCol != null && floorCol != null && ti.getColumn("fullRoom") == null) - { - ExprColumn col = new ExprColumn(ti, new FieldKey(null, "fullRoom"), new SQLFragment("##ERROR"), JdbcType.VARCHAR, roomCol, floorCol) { - @Override - public SQLFragment getValueSql(String tableAlias) - { - // Need to subclass and override this function due to issue using ExprColumn.STR_TABLE_ALIAS with extensible columns - SQLFragment sql = new SQLFragment("(SELECT COALESCE(r.room, 'Room N/A') || ', ' || COALESCE(r.floor, 'Floor N/A') || ', ' || COALESCE(r.building, 'Building N/A') \n" + - " FROM (\n" + - " SELECT \n").append(roomCol.getValueSql(tableAlias)); - sql.append(" AS room,\n" + - " ff.name as floor,\n" + - " bb.description as building\n" + - " FROM ehr_lookups.floors ff \n" + - " JOIN ehr_lookups.buildings bb ON ff.building = bb.name\n" + - " WHERE ff.floor =\n").append(floorCol.getValueSql(tableAlias)); - sql.append(" ) r\n" + - " )"); - - return sql; - } - }; - col.setName("fullRoom"); - col.setLabel("Full Room"); - ti.addColumn(col); - } - } - - private void customizeFloors(AbstractTableInfo ti) - { - ColumnInfo floorCol = ti.getColumn("name"); - ColumnInfo bldgCol = ti.getColumn("building"); - if (floorCol != null && bldgCol != null && ti.getColumn("fullFloor") == null) - { - SQLFragment sql = new SQLFragment("(SELECT COALESCE(r.floor, 'Floor N/A') || ', ' || COALESCE(r.building, 'Building N/A') \n" + - " FROM (\n" + - " SELECT " + ExprColumn.STR_TABLE_ALIAS + ".name\n"); - sql.append(" AS floor,\n" + - " bb.description as building\n" + - " FROM ehr_lookups.buildings bb \n" + - " WHERE bb.name = " + ExprColumn.STR_TABLE_ALIAS + ".building\n"); - sql.append(" ) r\n" + - " )"); - - ExprColumn col = new ExprColumn(ti, "fullFloor", sql, JdbcType.VARCHAR, floorCol, bldgCol); - col.setLabel("Full Floor"); - ti.addColumn(col); - } - } - private void customizeTasks(AbstractTableInfo ti) { DetailsURL detailsURL = DetailsURL.fromString("/ehr/dataEntryFormDetails.view?formType=${formtype}&taskid=${taskid}"); @@ -746,7 +687,7 @@ public void doSharedCustomization(AbstractTableInfo ti) { UserSchema us = getEHRUserSchema(ti, "ehr_lookups"); col.setLabel("Species"); - col.setFk(new QueryForeignKey(ti.getUserSchema(), ti.getContainerFilter(), us, null, "species_codes", "code", "scientific_name")); + col.setFk(new QueryForeignKey(ti.getUserSchema(), ti.getContainerFilter(), us, null, "species_codes", "code", "common_name")); } if ("protocol".equalsIgnoreCase(col.getName()) && null == col.getFk() && !"protocol".equalsIgnoreCase(ti.getName())) { @@ -772,7 +713,7 @@ public void doSharedCustomization(AbstractTableInfo ti) { UserSchema us = getEHRUserSchema(ti, "ehr_lookups"); col.setLabel("Room"); - col.setFk(new QueryForeignKey(ti.getUserSchema(), ti.getContainerFilter(), us, null, "rooms", "room", "fullRoom")); + col.setFk(new QueryForeignKey(ti.getUserSchema(), ti.getContainerFilter(), us, null, "rooms", "room", "room")); col.setURL(StringExpressionFactory.createURL("/nbri_ehr/cageDetails.view?room=${room}")); } if ("building".equalsIgnoreCase(col.getName()) && !ti.getName().equalsIgnoreCase("buildings")) @@ -785,7 +726,7 @@ public void doSharedCustomization(AbstractTableInfo ti) { UserSchema us = getEHRUserSchema(ti, "ehr_lookups"); col.setLabel("Floor"); - col.setFk(new QueryForeignKey(ti.getUserSchema(), ti.getContainerFilter(), us, null, "floors", "floor", "fullFloor")); + col.setFk(new QueryForeignKey(ti.getUserSchema(), ti.getContainerFilter(), us, null, "floors", "floor", "name")); } if ("area".equalsIgnoreCase(col.getName()) && !ti.getName().equalsIgnoreCase("areas")) { @@ -896,15 +837,17 @@ private void ensureSortColumn(AbstractTableInfo ti, ColumnInfo baseColumn) private void customizeHousingTable(AbstractTableInfo ti) { + // ehr_lookups.cage is unique on (Container, Location), so the cage subqueries below must be container-scoped; + // a second EHR folder defining the same location would otherwise make them return multiple rows. + Container lookupContainer = EHRService.get().getEHRStudyContainer(ti.getUserSchema().getContainer()); + if (lookupContainer == null) + lookupContainer = ti.getUserSchema().getContainer(); // as DefaultEHRCustomizer does + if (ti.getColumn("room") == null && ti.getColumn("cage") != null) { - UserSchema us = getUserSchema(ti, "ehr_lookups"); - if (us != null) - { - SQLFragment roomSql = new SQLFragment("(SELECT room FROM ehr_lookups.cage WHERE location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)"); - ExprColumn roomCol = new ExprColumn(ti, "room", roomSql, JdbcType.VARCHAR, ti.getColumn("cage")); - ti.addColumn(roomCol); - } + SQLFragment roomSql = new SQLFragment("(SELECT room FROM ehr_lookups.cage WHERE Container = ? AND location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)", lookupContainer); + ExprColumn roomCol = new ExprColumn(ti, "room", roomSql, JdbcType.VARCHAR, ti.getColumn("cage")); + ti.addColumn(roomCol); ensureSortColumn(ti, ti.getColumn("room")); } @@ -913,8 +856,8 @@ private void customizeHousingTable(AbstractTableInfo ti) TableInfo realTable = getRealTable(ti); if (realTable != null && realTable.getColumn("participantid") != null && realTable.getColumn("date") != null && realTable.getColumn("enddate") != null) { - SQLFragment roomSql = new SQLFragment(realTable.getSqlDialect().getDateDiff(Calendar.DATE, "{fn curdate()}", "COALESCE((SELECT max(h2.enddate) as d FROM " + realTable.getSelectName() + " h2 LEFT JOIN ehr_lookups.cage cg ON h2.cage = cg.location " + - "WHERE h2.enddate IS NOT NULL AND h2.enddate <= " + ExprColumn.STR_TABLE_ALIAS + ".date AND h2.participantid = " + ExprColumn.STR_TABLE_ALIAS + ".participantid AND cg.room != (SELECT room FROM ehr_lookups.cage WHERE location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)), " + ExprColumn.STR_TABLE_ALIAS + ".date)")); + SQLFragment roomSql = new SQLFragment(realTable.getSqlDialect().getDateDiff(Calendar.DATE, "{fn curdate()}", "COALESCE((SELECT max(h2.enddate) as d FROM " + realTable.getSelectName() + " h2 LEFT JOIN ehr_lookups.cage cg ON h2.cage = cg.location AND cg.Container = ? " + + "WHERE h2.enddate IS NOT NULL AND h2.enddate <= " + ExprColumn.STR_TABLE_ALIAS + ".date AND h2.participantid = " + ExprColumn.STR_TABLE_ALIAS + ".participantid AND cg.room != (SELECT room FROM ehr_lookups.cage WHERE Container = ? AND location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)), " + ExprColumn.STR_TABLE_ALIAS + ".date)"), lookupContainer, lookupContainer); ExprColumn roomCol = new ExprColumn(ti, "daysInRoom", roomSql, JdbcType.INTEGER, realTable.getColumn("participantid"), realTable.getColumn("date"), realTable.getColumn("enddate")); roomCol.setLabel("Days In Room"); ti.addColumn(roomCol); @@ -1065,88 +1008,18 @@ private void customizeTreatmentOrder(AbstractTableInfo ti) { WrappedColumn col = new WrappedColumn(ti.getColumn("objectid"), "treatmentRecord"); col.setLabel("Record Treatment"); - col.setDisplayColumnFactory(new DisplayColumnFactory() { - - @Override - public DisplayColumn createRenderer(final ColumnInfo colInfo) - { - return new DataColumn(colInfo){ - - @Override - public void renderGridCellContents(RenderContext ctx, HtmlWriter out) - { - String objectid = (String)getBoundColumn().getValue(ctx); - Date date = (Date)ctx.get("date"); - String caseid = (String)ctx.get("caseid"); - String category = (String)ctx.get("category"); - ActionURL url = new ActionURL("ehr", "dataEntryForm", ti.getUserSchema().getContainer()); - if (!ti.getUserSchema().getContainer().hasPermission(ti.getUserSchema().getUser(), EHRClinicalEntryPermission.class)) - return; - - if (category.equals("Behavior")) - { - if (caseid != null) - { - url.addParameter("formType", "Behavioral Rounds"); - url.addParameter("caseid", caseid); - } - else - { - url.addParameter("formType", "Bulk Behavior Entry"); - } - } - else - { - if (caseid != null) - { - url.addParameter("formType", "Clinical Rounds"); - url.addParameter("caseid", caseid); - } - else - { - url.addParameter("formType", "medicationTreatment"); - } - } - - url.addParameter("treatmentid", objectid); - url.addParameter("scheduledDate", date.toString()); - - String returnUrl = new ActionURL("ehr", "animalHistory", ti.getUserSchema().getContainer()) + "#inputType:none&showReport:0&activeReport:clinMedicationSchedule"; - url.addParameter("returnUrl", returnUrl); - - out.write(LinkBuilder.labkeyLink("Record Treatment", url).target("_blank")); - } - - @Override - public void addQueryFieldKeys(Set keys) - { - super.addQueryFieldKeys(keys); - keys.add(getBoundColumn().getFieldKey()); - keys.add(FieldKey.fromString("date")); - keys.add(FieldKey.fromString("caseid")); - keys.add(FieldKey.fromString("category")); - } - - @Override - public boolean isSortable() - { - return false; - } - - @Override - public boolean isFilterable() - { - return false; - } + col.setDisplayColumnFactory(new TreatmentDisplayColumnFactory(false)); + ti.addColumn(col); + } + } - @Override - public boolean isEditable() - { - return false; - } - }; - } - }); + private void customizeTreatmentSchedule(AbstractTableInfo ti) + { + if (ti.getColumn("treatmentRecord") == null && ti.getColumn("objectid") != null) + { + WrappedColumn col = new WrappedColumn(ti.getColumn("objectid"), "treatmentRecord"); + col.setLabel("Record Treatment"); + col.setDisplayColumnFactory(new TreatmentDisplayColumnFactory(true)); ti.addColumn(col); } } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/table/TreatmentDisplayColumnFactory.java b/nbri_ehr/src/org/labkey/nbri_ehr/table/TreatmentDisplayColumnFactory.java new file mode 100644 index 0000000..83db906 --- /dev/null +++ b/nbri_ehr/src/org/labkey/nbri_ehr/table/TreatmentDisplayColumnFactory.java @@ -0,0 +1,130 @@ +/* + * 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.table; + +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.DataColumn; +import org.labkey.api.data.DisplayColumn; +import org.labkey.api.data.DisplayColumnFactory; +import org.labkey.api.data.RenderContext; +import org.labkey.api.ehr.security.EHRClinicalEntryPermission; +import org.labkey.api.query.FieldKey; +import org.labkey.api.util.DateUtil; +import org.labkey.api.util.LinkBuilder; +import org.labkey.api.view.ActionURL; +import org.labkey.api.writer.HtmlWriter; + +import java.util.Date; +import java.util.Set; + +/** + * Display column factory for creating Record Treatment links. When includeScheduledDate is set, the row's date is + * passed as the scheduledDate URL parameter, so it should only be set on tables whose date column is the scheduled + * slot being recorded (e.g. treatmentSchedule), not the treatment order's start date. + */ +public class TreatmentDisplayColumnFactory implements DisplayColumnFactory +{ + private final boolean _includeScheduledDate; + + public TreatmentDisplayColumnFactory(boolean includeScheduledDate) + { + _includeScheduledDate = includeScheduledDate; + } + + @Override + public DisplayColumn createRenderer(final ColumnInfo colInfo) + { + return new DataColumn(colInfo){ + + @Override + public void renderGridCellContents(RenderContext ctx, HtmlWriter out) + { + String objectid = (String)getBoundColumn().getValue(ctx); + Date date = (Date)ctx.get("date"); + String caseid = (String)ctx.get("caseid"); + String category = (String)ctx.get("category"); + ActionURL url = new ActionURL("ehr", "dataEntryForm", colInfo.getParentTable().getUserSchema().getContainer()); + if (!colInfo.getParentTable().getUserSchema().getContainer().hasPermission(colInfo.getParentTable().getUserSchema().getUser(), EHRClinicalEntryPermission.class)) + return; + + if (category == null) + return; + + if (category.equals("Behavior")) + { + if (caseid != null) + { + url.addParameter("formType", "Behavioral Rounds"); + url.addParameter("caseid", caseid); + } + else + { + url.addParameter("formType", "Bulk Behavior Entry"); + } + } + else + { + if (caseid != null) + { + url.addParameter("formType", "Clinical Rounds"); + url.addParameter("caseid", caseid); + } + else + { + url.addParameter("formType", "medicationTreatment"); + } + } + + url.addParameter("treatmentid", objectid); + if (_includeScheduledDate && date != null) + url.addParameter("scheduledDate", DateUtil.formatIsoDateShortTime(date)); + + String returnUrl = new ActionURL("ehr", "animalHistory", colInfo.getParentTable().getUserSchema().getContainer()) + "#inputType:none&showReport:0&activeReport:clinMedicationSchedule"; + url.addParameter("returnUrl", returnUrl); + + out.write(LinkBuilder.labkeyLink("Record Treatment", url).target("_blank")); + } + + @Override + public void addQueryFieldKeys(Set keys) + { + super.addQueryFieldKeys(keys); + keys.add(getBoundColumn().getFieldKey()); + keys.add(FieldKey.fromString("date")); + keys.add(FieldKey.fromString("caseid")); + keys.add(FieldKey.fromString("category")); + } + + @Override + public boolean isSortable() + { + return false; + } + + @Override + public boolean isFilterable() + { + return false; + } + + @Override + public boolean isEditable() + { + return false; + } + }; + } +} diff --git a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetDemographics.tsv b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetDemographics.tsv index 8dc76ff..93872c6 100644 --- a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetDemographics.tsv +++ b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetDemographics.tsv @@ -1,14 +1,14 @@ objectid Id QCStateLabel date birth death calculated_status gender sire dam species origin performedby -1 44444 Completed -1381d -1381d Alive 1 44442 44443 10 00001 1004 -2 44446 Completed -1406d -1406d Alive 1 44442 44443 10 00003 1004 -3 44445 Completed -1414d -1414d -726d Dead 2 44442 44443 10 00004 1004 -4 TEST6390238 Completed -3923d -3923d Shipped 2 3565069 5250080 9 00002 1004 -5 TEST5904521 Completed -5431d -5431d Shipped 1 8377984 9 00004 1004 -6 TEST3804589 Completed -5806d -5806d Shipped 1 493957 9749422 9 00005 1004 -7 TEST2312318 Completed -8069d -8069d Shipped 1 5748235 8739374 9 00002 1004 -8 TEST1993532 Completed -11808d -11808d -2259d Dead 2 5409336 3784452 9 00003 1004 -9 TEST4551032 Completed -6362d -6362d Alive 1 5030167 8416939 9 00001 1004 -11 44442 Completed -6100d -6100d Alive 1 8377984 10 00004 1004 -12 44443 Completed -6100d -6100d Alive 1 8377984 10 00004 1004 -13 44447 Completed -2600d -2600d Alive 1 8377984 10 00004 1004 -14 8377984 Completed -2600d -2600d Alive 1 8377984 10 00004 1004 +1 44444 Completed -1381d -1381d Alive M 44442 44443 MNE 00001 1004 +2 44446 Completed -1406d -1406d Alive M 44442 44443 MNE 00003 1004 +3 44445 Completed -1414d -1414d -726d Dead F 44442 44443 MNE 00004 1004 +4 TEST6390238 Completed -3923d -3923d Shipped F 3565069 5250080 MMU 00002 1004 +5 TEST5904521 Completed -5431d -5431d Shipped M 8377984 MMU 00004 1004 +6 TEST3804589 Completed -5806d -5806d Shipped M 493957 9749422 CMO 00005 1004 +7 TEST2312318 Completed -8069d -8069d Shipped M 5748235 8739374 MMU 00002 1004 +8 TEST1993532 Completed -11808d -11808d -2259d Dead F 5409336 3784452 MMU 00003 1004 +9 TEST4551032 Completed -6362d -6362d Alive M 5030167 8416939 MMU 00001 1004 +11 44442 Completed -6100d -6100d Alive M 8377984 MNE 00004 1004 +12 44443 Completed -6100d -6100d Alive F 8377984 MNE 00004 1004 +13 44447 Completed -2600d -2600d Alive M 8377984 MNE 00004 1004 +14 8377984 Completed -2600d -2600d Alive F 8377984 MNE 00004 1004 diff --git a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetFlags.tsv b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetFlags.tsv index 45004d8..ce33d90 100644 --- a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetFlags.tsv +++ b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetFlags.tsv @@ -1,11 +1,11 @@ objectId Id date enddate flag QCStateLabel performedby -1 TEST3804589 -2187d -2178d 1 Completed 1004 -2 TEST4551032 -2243d -2213d 2 Completed 1004 -3 TEST5904521 -2215d -2186d 3 Completed 1004 -4 TEST1112911 -2215d 4 Completed 1004 -5 TEST1112911 -2215d 5 Completed 1004 -6 44444 -1423d -560d 3 Completed 1004 -7 44444 -1423d -560d 1 Completed 1004 -10 44444 -1208d 3 Completed 1004 -8 44446 -1208d 3 Completed 1004 -9 TSTCP -1423d -560d 3 Completed 1004 +1 TEST3804589 -2187d -2178d Behavioral FLAG 1 Completed 1004 +2 TEST4551032 -2243d -2213d Capture FLAG 1 Completed 1004 +3 TEST5904521 -2215d -2186d Clinical FLAG 1 Completed 1004 +4 TEST1112911 -2215d Experimental FLAG 1 Completed 1004 +5 TEST1112911 -2215d Genetics FLAG 1 Completed 1004 +6 44444 -1423d -560d Clinical FLAG 1 Completed 1004 +7 44444 -1423d -560d Behavioral FLAG 1 Completed 1004 +10 44444 -1208d Clinical FLAG 1 Completed 1004 +8 44446 -1208d Clinical FLAG 1 Completed 1004 +9 TSTCP -1423d -560d Clinical FLAG 1 Completed 1004 diff --git a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetHousing.tsv b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetHousing.tsv index 23969c0..487f797 100644 --- a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetHousing.tsv +++ b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetHousing.tsv @@ -1,9 +1,9 @@ -objectid Id QCStateLabel date enddate cage performedby -1 44444 Completed -1381d -1321d 11 - Rm 202B - ZZA 1004 -2 44444 Completed -1321d 11 - Rm 202B - ZZB 1004 -3 44446 Completed -1406d -1316d 11 - Rm 202B - ZZA 1004 -4 44446 Completed -1316d 11 - Rm 202B - ZZC 1004 -5 TEST4551032 Completed -6362d 11 - Rm 202B - ZZD 1004 -6 44442 Completed -6100d -5920d 11 - Rm 202B - ZZA 1004 -7 44442 Completed -5920d 11 - Rm 202B - ZZC 1004 -8 44443 Completed -6100d 11 - Rm 202B - ZZF 1004 +objectid Id QCStateLabel date enddate room cage performedby +1 44444 Completed -1381d -1321d TestBuilding-R1 TestBuilding-R1-C1 1004 +2 44444 Completed -1321d TestBuilding-R1 TestBuilding-R1-C2 1004 +3 44446 Completed -1406d -1316d TestBuilding-R1 TestBuilding-R1-C1 1004 +4 44446 Completed -1316d TestBuilding-R2 TestBuilding-R2-C3 1004 +5 TEST4551032 Completed -6362d TestBuilding-R3 TestBuilding-R3-C4 1004 +6 44442 Completed -6100d -5920d TestBuilding-R1 TestBuilding-R1-C1 1004 +7 44442 Completed -5920d TestBuilding-R2 TestBuilding-R2-C3 1004 +8 44443 Completed -6100d TestBuilding-R1 TestBuilding-R1-C2 1004 diff --git a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetWeight.tsv b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetWeight.tsv index 75fd789..c47b7f7 100644 --- a/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetWeight.tsv +++ b/nbri_ehr/test/sampledata/nbri_ehr/study/study/datasets/datasetWeight.tsv @@ -1,6 +1,6 @@ objectid Id date weight remark QCStateLabel units performedby -1 TEST3804589 -5735d 0.037 vel praesent tincidunt Completed 1004 -2 TEST3804589 -5730d 0.035 erat et convallis Completed 1004 +1 TEST3804589 -5735d 0.040 vel praesent tincidunt Completed 1004 +2 TEST3804589 -5730d 0.040 erat et convallis Completed 1004 3 TEST3804589 -5727d 0.041 egestas pharetra Completed 1004 4 TEST3804589 -5722d 0.045 sed dui suscipit Completed 1004 5 TEST3804589 -5714d 0.058 a aliquet et tempus Completed 1004 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 c7587ae..afaab10 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 @@ -34,6 +34,8 @@ import org.labkey.remoteapi.query.ImportDataCommand; import org.labkey.remoteapi.query.InsertRowsCommand; import org.labkey.remoteapi.query.RowsResponse; +import org.labkey.remoteapi.query.SelectRowsCommand; +import org.labkey.remoteapi.query.SelectRowsResponse; import org.labkey.remoteapi.security.CreateUserResponse; import org.labkey.test.Locator; import org.labkey.test.TestFileUtils; @@ -103,10 +105,34 @@ public class NBRI_EHRTest extends AbstractGenericEHRTest implements PostgresOnly private static final String deadAnimalId = "D5454"; private static final String departedAnimalId = "H6767"; private static final String aliveAnimalId = "A4545"; + // never inserted into demographics; exercises the deaths trigger's unknown-Id rejection + private static final String unknownAnimalId = "X9999"; // Dedicated animal for testScheduledObservationTaskGrouping; provisioned (alive, housed, assigned) in // createTestSubjects so the clinical case form raises no warnings that would keep the validation banner up. private static final String taskGroupAnimalId = "TESTGRP9090"; + // Rooms are keyed by building and name, so every room fixture needs a building to hang off of. + // 'buildings' derives its key from the description, and 'SPF' is one of the areas seeded with the ehr_lookups schema. + private static final String BUILDING_ID = "TestBuilding"; + private static final String BUILDING_AREA = "SPF"; + + // Cage locations seeded by populateLocations, named for the room each one sits in. The cage trigger derives these + // from the room and cage, and housing records key off the location, so these are what belongs in a housing row's + // 'cage' field. datasetHousing.tsv spells the same values out, since a TSV cannot call cageLocation. + private static final String CAGE_IN_R1 = cageLocation("R1", "C1"); + private static final String CAGE_IN_R3 = cageLocation("R3", "C4"); + + // A group pen has no cage, so its location is the room key alone. Created by testGroupPenCagemates. + private static final String PEN_ROOM_NAME = "PEN1"; + private static final String[] PEN_ANIMALS = {"PEN0001", "PEN0002"}; + + // Housed with a cage but no room, which is how a record entered against a cage alone lands. Every cage seeded by + // populateLocations already has occupants from datasetHousing.tsv, so testCagematesWithoutRoom creates its own to + // keep the expected cagemate count exact. + private static final String ROOMLESS_CAGE_NAME = "C9"; + private static final String ROOMLESS_CAGE = cageLocation("R1", ROOMLESS_CAGE_NAME); + private static final String[] ROOMLESS_ANIMALS = {"CAGE0001", "CAGE0002"}; + private final String[] weightFields = {"Id", "date", "enddate", "project", "weight", FIELD_QCSTATELABEL, FIELD_OBJECTID, FIELD_LSID, "_recordid", "performedby"}; private final Object[] weightData1 = {getExpectedAnimalIDCasing("TESTSUBJECT1"), EHRClientAPIHelper.DATE_SUBSTITUTION, null, null, "12", EHRQCState.IN_PROGRESS.label, null, null, "_recordID", 1004}; @@ -244,20 +270,62 @@ private void populateFormulary() throws IOException, CommandException RowsResponse saveRowsResponse = insertRowsCommand.execute(getApiHelper().getConnection(), getContainerPath()); } + /** + * The key a room trigger derives for the given room name. Mirrors ehr_lookups/rooms.js. + */ + private static String roomKey(String roomName) + { + return BUILDING_ID + "-" + roomName; + } + + /** + * The location a cage trigger derives for the given room and cage. Mirrors ehr_lookups/cage.js. + */ + private static String cageLocation(String roomName, String cageName) + { + return roomKey(roomName) + "-" + cageName; + } + + /** + * Housing records store the derived room key, so the fixture rooms have to be named by key for the room + * lookups to resolve. The base implementation returns names that match no room in this study. + */ + @Override + protected String[] getRooms() + { + return new String[]{roomKey("R1"), roomKey("R2"), roomKey("R3")}; + } + + @LogMethod + private void populateBuildingRecords() throws Exception + { + InsertRowsCommand insertCmd = new InsertRowsCommand("ehr_lookups", "buildings"); + Map rowMap = new HashMap<>(); + // Supply the derived key rather than relying on the trigger to fill it, matching how the base class seeds rooms. + rowMap.put("name", BUILDING_ID); + rowMap.put("description", BUILDING_ID); + rowMap.put("area", BUILDING_AREA); + insertCmd.addRow(rowMap); + + insertCmd.execute(createDefaultConnection(), getContainerPath()); + } + @Override protected void populateRoomRecords() throws Exception { + populateBuildingRecords(); + InsertRowsCommand insertCmd = new InsertRowsCommand("ehr_lookups", "rooms"); Map rowMap = new HashMap<>(); rowMap.put("name", ROOM_ID); - rowMap.put("floor", "floor1"); + rowMap.put("building", BUILDING_ID); rowMap.put("housingType", 1); rowMap.put("housingCondition", 1); insertCmd.addRow(rowMap); rowMap = new HashMap<>(); rowMap.put("name", ROOM_ID2); - rowMap.put("floor", "floor2"); + rowMap.put("building", BUILDING_ID); rowMap.put("housingType", 1); rowMap.put("housingCondition", 1); insertCmd.addRow(rowMap); @@ -321,19 +389,21 @@ private void enableNotification(String notification) private void populateLocations() throws IOException, CommandException { goToEHRFolder(); + // BUILDING_ID is created by populateRoomRecords, which runs earlier as part of initProject. log("Inserting values in rooms"); InsertRowsCommand roomCmd = new InsertRowsCommand("ehr_lookups", "rooms"); - roomCmd.addRow(Map.of("name", "R1", "floor", "F1")); - roomCmd.addRow(Map.of("name", "R2", "floor", "F2")); - roomCmd.addRow(Map.of("name", "R3", "floor", "F3")); + roomCmd.addRow(Map.of("name", "R1", "building", BUILDING_ID)); + roomCmd.addRow(Map.of("name", "R2", "building", BUILDING_ID)); + roomCmd.addRow(Map.of("name", "R3", "building", BUILDING_ID)); roomCmd.execute(getApiHelper().getConnection(), getContainerPath()); + // 'location' is left out so the cage trigger derives it, exercising the same path production entry takes. log("Inserting values in cage"); InsertRowsCommand cageCmd = new InsertRowsCommand("ehr_lookups", "cage"); - cageCmd.addRow(Map.of("location", "L1", "cage", "C1", "room", "R1")); - cageCmd.addRow(Map.of("location", "L2", "cage", "C2", "room", "R1")); - cageCmd.addRow(Map.of("location", "L3", "cage", "C3", "room", "R2")); - cageCmd.addRow(Map.of("location", "L4", "cage", "C4", "room", "R3")); + cageCmd.addRow(Map.of("cage", "C1", "room", roomKey("R1"))); + cageCmd.addRow(Map.of("cage", "C2", "room", roomKey("R1"))); + cageCmd.addRow(Map.of("cage", "C3", "room", roomKey("R2"))); + cageCmd.addRow(Map.of("cage", "C4", "room", roomKey("R3"))); cageCmd.execute(getApiHelper().getConnection(), getContainerPath()); } @@ -363,13 +433,13 @@ private void addNBRIEhrLinks() @Override protected String getMale() { - return "3"; + return "M"; } @Override protected String getFemale() { - return "2"; + return "F"; } @Test @@ -407,13 +477,15 @@ protected void createTestSubjects() throws Exception //insert into demographics log("Creating test subjects"); + // demographics.species holds an ehr_lookups.species_codes code, and ehr_lookups.weight_ranges is keyed + // on that same code, so weight validation only fires for animals given a real code here. fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; data = new Object[][]{ - {SUBJECTS[0], "Rhesus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {SUBJECTS[1], "Cynomolgus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {SUBJECTS[2], "Marmoset", (new Date()).toString(), getFemale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {SUBJECTS[3], "Cynomolgus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {SUBJECTS[4], "Cynomolgus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} + {SUBJECTS[0], "MMU", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {SUBJECTS[1], "MNE", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {SUBJECTS[2], "CAE", (new Date()).toString(), getFemale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {SUBJECTS[3], "MNE", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {SUBJECTS[4], "MNE", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} }; insertCommand = getApiHelper().prepareInsertCommand("study", "demographics", "lsid", fields, data); getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", StringUtils.join(SUBJECTS, ";"), Filter.Operator.IN)); @@ -421,11 +493,11 @@ protected void createTestSubjects() throws Exception //for simplicity, also create the animals from MORE_ANIMAL_IDS right now data = new Object[][]{ - {MORE_ANIMAL_IDS[0], "Rhesus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {MORE_ANIMAL_IDS[1], "Cynomolgus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {MORE_ANIMAL_IDS[2], "Marmoset", (new Date()).toString(), getFemale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {MORE_ANIMAL_IDS[3], "Cynomolgus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, - {MORE_ANIMAL_IDS[4], "Cynomolgus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} + {MORE_ANIMAL_IDS[0], "MMU", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {MORE_ANIMAL_IDS[1], "MNE", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {MORE_ANIMAL_IDS[2], "CAE", (new Date()).toString(), getFemale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {MORE_ANIMAL_IDS[3], "MNE", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}, + {MORE_ANIMAL_IDS[4], "MNE", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} }; insertCommand = getApiHelper().prepareInsertCommand("study", "demographics", "lsid", fields, data); getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", StringUtils.join(MORE_ANIMAL_IDS, ";"), Filter.Operator.IN)); @@ -439,10 +511,10 @@ protected void createTestSubjects() throws Exception log("Creating initial housing records"); fields = new String[]{"Id", "date", "enddate", "room", "cage", "performedby"}; data = new Object[][]{ - {SUBJECTS[0], pastDate1, pastDate2, getRooms()[0], CAGES[0], 1004}, - {SUBJECTS[0], pastDate2, null, getRooms()[0], CAGES[0], 1004}, - {SUBJECTS[1], pastDate1, pastDate2, getRooms()[0], CAGES[0], 1004}, - {SUBJECTS[1], pastDate2, null, getRooms()[2], CAGES[2], 1004} + {SUBJECTS[0], pastDate1, pastDate2, getRooms()[0], CAGE_IN_R1, 1004}, + {SUBJECTS[0], pastDate2, null, getRooms()[0], CAGE_IN_R1, 1004}, + {SUBJECTS[1], pastDate1, pastDate2, getRooms()[0], CAGE_IN_R1, 1004}, + {SUBJECTS[1], pastDate2, null, getRooms()[2], CAGE_IN_R3, 1004} }; insertCommand = getApiHelper().prepareInsertCommand("study", "Housing", "lsid", fields, data); getApiHelper().deleteAllRecords("study", "Housing", new Filter("Id", StringUtils.join(SUBJECTS, ";"), Filter.Operator.IN)); @@ -478,7 +550,7 @@ protected void createTestSubjects() throws Exception log("Creating task grouping test subject"); fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; data = new Object[][]{ - {taskGroupAnimalId, "Rhesus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} + {taskGroupAnimalId, "MMU", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004} }; insertCommand = getApiHelper().prepareInsertCommand("study", "demographics", "lsid", fields, data); getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", taskGroupAnimalId)); @@ -486,7 +558,7 @@ protected void createTestSubjects() throws Exception fields = new String[]{"Id", "date", "enddate", "room", "cage", "performedby"}; data = new Object[][]{ - {taskGroupAnimalId, pastDate1, null, getRooms()[0], CAGES[0], 1004} + {taskGroupAnimalId, pastDate1, null, getRooms()[0], CAGE_IN_R1, 1004} }; insertCommand = getApiHelper().prepareInsertCommand("study", "Housing", "lsid", fields, data); getApiHelper().deleteAllRecords("study", "Housing", new Filter("Id", taskGroupAnimalId)); @@ -535,7 +607,7 @@ public void testWeightValidation() }; Map> expected = new HashMap<>(); expected.put("weight", Arrays.asList( - "WARN: Weight above the allowable value of 20.0 kg for Cynomolgus", + "WARN: Weight above the allowable value of 30.0 kg for MNE", "INFO: Weight gain of >10%. Last weight 12 kg") ); getApiHelper().testValidationMessage(DATA_ADMIN.getEmail(), "study", "weight", getWeightFields(), data, expected); @@ -576,13 +648,24 @@ public void testArrivalForm() arrivals.setGridCell(1, "acquisitionType", "Lab Transfer (Wild Born)"); arrivals.setGridCell(1, "Id", arrivedAnimal); arrivals.setGridCell(1, "cage", "C1"); - arrivals.setGridCell(1, "project", "640991"); - arrivals.setGridCell(1, "arrivalProtocol", "dummyprotocol"); - arrivals.setGridCell(1, "Id/demographics/gender", "female"); + arrivals.setGridCell(1, "Id/demographics/gender", "Female"); arrivals.setGridCell(1, "Id/demographics/geographic_origin", "BRAZIL"); - arrivals.setGridCell(1, "Id/demographics/species", "Macaca nemestrina PIG"); + arrivals.setGridCell(1, "Id/demographics/species", "Pig-Tailed Macaque"); arrivals.setGridCellJS(1, "Id/demographics/birth", now.minusDays(7).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); - arrivals.setGridCell(1, "sourceFacility", "BIOQUAL, Inc."); + arrivals.setGridCell(1, "sourceFacility", "Bioqual, Incorporated"); + + Ext4GridRef protocolAssignments = _helper.getExt4GridForFormSection("Protocol Assignment"); + _helper.addRecordToGrid(protocolAssignments); + protocolAssignments.setGridCell(1, "Id", arrivedAnimal); + protocolAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); + protocolAssignments.setGridCell(1, "protocol", "dummyprotocol"); + + Ext4GridRef projectAssignments = _helper.getExt4GridForFormSection("Project Assignment"); + _helper.addRecordToGrid(projectAssignments); + projectAssignments.setGridCell(1, "Id", arrivedAnimal); + projectAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); + projectAssignments.setGridCell(1, "project", "640991"); + submitForm("Submit Final", "Finalize"); goToSchemaBrowser(); @@ -590,13 +673,19 @@ public void testArrivalForm() table.setFilter("Id", "Equals", arrivedAnimal); CustomizeView view = table.openCustomizeGrid(); view.addColumn("cage"); - view.addColumn("project"); - view.addColumn("arrivalProtocol"); view.applyCustomView(); Assert.assertEquals("Invalid Arrival record", Arrays.asList(arrivedAnimal), table.getRowDataAsText(0, "Id")); Assert.assertEquals("Invalid Arrival record", Arrays.asList("C1"), table.getRowDataAsText(0, "cage")); - Assert.assertEquals("Invalid Arrival record", Arrays.asList("640991"), table.getRowDataAsText(0, "project")); - Assert.assertEquals("Invalid Arrival record", Arrays.asList("dummyprotocol"), table.getRowDataAsText(0, "arrivalProtocol")); + + goToSchemaBrowser(); + table = viewQueryData("study", "assignment"); + table.setFilter("Id", "Equals", arrivedAnimal); + Assert.assertEquals("Invalid project assignment", Arrays.asList("640991"), table.getRowDataAsText(0, "project")); + + goToSchemaBrowser(); + table = viewQueryData("study", "protocolAssignment"); + table.setFilter("Id", "Equals", arrivedAnimal); + Assert.assertEquals("Invalid protocol assignment", Arrays.asList("dummyprotocol"), table.getRowDataAsText(0, "protocol")); verifyRowCreated("study", "birth", arrivedAnimal, 1); verifyRowCreated("study", "assignment", arrivedAnimal, 1); @@ -606,15 +695,24 @@ public void testArrivalForm() } @Test - public void testBirthForm() throws IOException, CommandException + public void testBirthForm() throws Exception { String bornAnimal = "80801"; + String damId = "TESTDAM01"; + String sireId = "TESTSIRE01"; + // demographics.species holds an ehr_lookups.species_codes code; the grids display its common name + String damSpeciesCode = "CAP"; + String damSpecies = "Brown-Tufted Capuchin"; String conceptId = "TESTCONCEPT1"; + String breedingType = "Time-Mated"; LocalDateTime now = LocalDateTime.now(); + log("Creating the dam and sire of the conception"); + createBreedingPair(damId, sireId, damSpeciesCode); + log("Creating conception record"); InsertRowsCommand conception = new InsertRowsCommand("nbri_ehr", "Conception"); - conception.addRow(Map.of("ConceptId", conceptId, "ConceptDate", now.minusDays(160), "Dam", "TEST4551032")); + conception.addRow(Map.of("ConceptId", conceptId, "ConceptDate", now.minusDays(160), "Dam", damId, "Sire", sireId)); conception.execute(getApiHelper().getConnection(), getContainerPath()); gotoEnterData(); @@ -622,15 +720,47 @@ public void testBirthForm() throws IOException, CommandException lockForm(); Ext4GridRef births = _helper.getExt4GridForFormSection("Births"); - _helper.addRecordToGrid(births); + verifyBirthColumnOrder(births); + + log("Starting a birth record from the conception"); + births.clickTbarButton("Start with Conception"); + Window conceptionWindow = new Window.WindowFinder(getDriver()).withTitle("Start with Conception").waitFor(); + Ext4ComboRef conceptionCombo = _ext4Helper.queryOne("window #conceptionField", Ext4ComboRef.class); + Assert.assertNotNull("Conception Id field not found in the Start with Conception window", conceptionCombo); + conceptionCombo.waitForStoreLoad(); + conceptionCombo.setComboByDisplayValue(conceptId); + conceptionWindow.clickButton("Submit", 0); + births.waitForRowCount(1); + + log("Verifying the conception populated the new birth record"); + assertEquals("Conception Id was not copied from the conception", conceptId, births.getFieldValue(1, "conceptId")); + 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")); + + log("Verifying Conception Id is required"); + births.setGridCellJS(1, "conceptId", null); + waitForFormError("The field: Conception Id is required"); + births.setGridCellJS(1, "conceptId", conceptId); + births.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); births.setGridCell(1, "Id", bornAnimal); births.setGridCell(1, "cage", "C3"); - births.setGridCell(1, "Id/demographics/species", "Cebus apella CAP"); - births.setGridCell(1, "Id/demographics/gender", "female"); - births.setGridCell(1, "project", "795644"); - births.setGridCell(1, "birthProtocol", "protocol101"); - births.setGridCell(1, "conceptId", conceptId); + births.setGridCell(1, "Id/demographics/gender", "Female"); + births.setGridCell(1, "breedingType", breedingType); + + Ext4GridRef protocolAssignments = _helper.getExt4GridForFormSection("Protocol Assignment"); + _helper.addRecordToGrid(protocolAssignments); + protocolAssignments.setGridCell(1, "Id", bornAnimal); + protocolAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); + protocolAssignments.setGridCell(1, "protocol", "protocol101"); + + Ext4GridRef projectAssignments = _helper.getExt4GridForFormSection("Project Assignment"); + _helper.addRecordToGrid(projectAssignments); + projectAssignments.setGridCell(1, "Id", bornAnimal); + projectAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); + projectAssignments.setGridCell(1, "project", "795644"); + submitForm("Submit Final", "Finalize"); goToSchemaBrowser(); @@ -638,20 +768,39 @@ public void testBirthForm() throws IOException, CommandException table.setFilter("Id", "Equals", bornAnimal); Assert.assertEquals("Invalid Birth record", Arrays.asList(bornAnimal), table.getRowDataAsText(0, "Id")); Assert.assertEquals("Invalid Birth record", Arrays.asList("C3"), table.getRowDataAsText(0, "cage")); - Assert.assertEquals("Invalid Birth record", Arrays.asList("795644"), table.getRowDataAsText(0, "project")); - Assert.assertEquals("Invalid Birth record", Arrays.asList("protocol101"), table.getRowDataAsText(0, "birthProtocol")); Assert.assertEquals("Invalid Birth record", Arrays.asList(conceptId), table.getRowDataAsText(0, "conceptId")); + Assert.assertEquals("Invalid Birth record", Arrays.asList(breedingType), table.getRowDataAsText(0, "breedingType")); + + log("Verifying the dam and sire of the conception reached demographics"); + goToSchemaBrowser(); + table = viewQueryData("study", "demographics"); + table.setFilter("Id", "Equals", bornAnimal); + Assert.assertEquals("Invalid demographics record", Arrays.asList(damId), table.getRowDataAsText(0, "dam")); + Assert.assertEquals("Invalid demographics record", Arrays.asList(sireId), table.getRowDataAsText(0, "sire")); + Assert.assertEquals("Invalid demographics record", Arrays.asList(damSpecies), table.getRowDataAsText(0, "species")); + + goToSchemaBrowser(); + table = viewQueryData("study", "assignment"); + table.setFilter("Id", "Equals", bornAnimal); + Assert.assertEquals("Invalid project assignment", Arrays.asList("795644"), table.getRowDataAsText(0, "project")); + + goToSchemaBrowser(); + table = viewQueryData("study", "protocolAssignment"); + table.setFilter("Id", "Equals", bornAnimal); + Assert.assertEquals("Invalid protocol assignment", Arrays.asList("protocol101"), table.getRowDataAsText(0, "protocol")); verifyRowCreated("study", "assignment", bornAnimal, 1); verifyRowCreated("study", "protocolAssignment", bornAnimal, 1); verifyRowCreated("study", "housing", bornAnimal, 1); verifyRowCreated("study", "demographics", bornAnimal, 1); - log("Verifying conception outcome in ConceptionsByDam"); + log("Verifying conception outcome and offspring in ConceptionsByDam"); goToSchemaBrowser(); DataRegionTable report = viewQueryData("nbri_ehr", "ConceptionsByDam"); report.setFilter("ConceptId", "Equals", conceptId); + 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")); } @Test @@ -659,6 +808,8 @@ public void testPregnancyForm() throws IOException, CommandException { String animalId = "TEST4551032"; String conceptId = "TESTCONCEPT2"; + // a non-live outcome, so ConceptionsByDam reports it rather than falling through to 'Live Birth' + String result = "Fetal Death"; LocalDateTime now = LocalDateTime.now(); log("Creating conception record"); @@ -674,7 +825,7 @@ public void testPregnancyForm() throws IOException, CommandException _helper.addRecordToGrid(outcomes); outcomes.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); outcomes.setGridCell(1, "Id", animalId); - outcomes.setGridCell(1, "result", "Stillborn"); + outcomes.setGridCell(1, "result", result); outcomes.setGridCell(1, "conceptId", conceptId); submitForm("Submit Final", "Finalize"); @@ -682,7 +833,7 @@ public void testPregnancyForm() throws IOException, CommandException DataRegionTable table = viewQueryData("study", "pregnancy"); table.setFilter("Id", "Equals", animalId); Assert.assertEquals("Invalid Pregnancy Outcome record", Arrays.asList(animalId), table.getRowDataAsText(0, "Id")); - Assert.assertEquals("Invalid Pregnancy Outcome record", Arrays.asList("Stillborn"), table.getRowDataAsText(0, "result")); + 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")); log("Verifying conception outcome in ConceptionsByDam"); @@ -690,7 +841,7 @@ public void testPregnancyForm() throws IOException, CommandException DataRegionTable report = viewQueryData("nbri_ehr", "ConceptionsByDam"); report.setFilter("ConceptId", "Equals", conceptId); Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList(animalId), report.getRowDataAsText(0, "Id")); - Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList("Stillborn"), report.getRowDataAsText(0, "conceptionOutcome")); + Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList(result), report.getRowDataAsText(0, "conceptionOutcome")); } @Test @@ -706,13 +857,20 @@ public void testConceptionForm() lockForm(); Ext4GridRef conceptions = _helper.getExt4GridForFormSection("Conception"); + Assert.assertFalse("Breeding Type describes the birth and should no longer appear on the Conception form", + conceptions.isColumnPresent("breedingType", false)); + _helper.addRecordToGrid(conceptions); conceptions.setGridCell(1, "ConceptId", conceptId); conceptions.setGridCellJS(1, "ConceptDate", now.minusDays(30).format(_dateFormat)); conceptions.setGridCellJS(1, "ConceptTermDate", now.plusDays(135).format(_dateFormat)); + conceptions.setGridCellJS(1, "Estimated", true); conceptions.setGridCell(1, "Dam", damId); conceptions.setGridCell(1, "Sire", sireId); - conceptions.setGridCell(1, "Remark", "Conception entry test"); + // Remark renders as a textarea, which Ext4GridRef's cell editor helpers cannot drive: they only recognize + // an as the active editor, so the click that opens the textarea is followed by a retry click that + // the open textarea intercepts. Set it through the store instead. + conceptions.setGridCellJS(1, "Remark", "Conception entry test"); submitForm("Submit Final", "Finalize"); goToSchemaBrowser(); @@ -720,6 +878,7 @@ public void testConceptionForm() table.setFilter("ConceptId", "Equals", conceptId); Assert.assertEquals("Invalid Conception record", Arrays.asList(damId), table.getRowDataAsText(0, "Dam")); Assert.assertEquals("Invalid Conception record", Arrays.asList(sireId), table.getRowDataAsText(0, "Sire")); + Assert.assertEquals("Invalid Conception record", Arrays.asList("true"), table.getRowDataAsText(0, "Estimated")); Assert.assertEquals("Invalid Conception record", Arrays.asList("Conception entry test"), table.getRowDataAsText(0, "Remark")); log("Verifying unmatched conception appears as Unknown in ConceptionsByDam"); @@ -1130,9 +1289,9 @@ public void createSubjectsForDeathForm() throws IOException, CommandException getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), getApiHelper().prepareInsertCommand("study", "birth", "lsid", new String[]{"Id", "Date", "gender", "QCStateLabel", "performedby"}, new Object[][]{ - {aliveAnimalId, LocalDateTime.now().minusDays(30), "f", "Completed", 1004}, - {deadAnimalId, LocalDateTime.now().minusDays(30), "m", "Completed", 1004}, - {departedAnimalId, LocalDateTime.now().minusDays(30), "m", "Completed", 1004}, + {aliveAnimalId, LocalDateTime.now().minusDays(30), getFemale(), "Completed", 1004}, + {deadAnimalId, LocalDateTime.now().minusDays(30), getMale(), "Completed", 1004}, + {departedAnimalId, LocalDateTime.now().minusDays(30), getMale(), "Completed", 1004}, } ), getExtraContext()); @@ -1146,7 +1305,7 @@ public void createSubjectsForDeathForm() throws IOException, CommandException project.execute(getApiHelper().getConnection(), getContainerPath()); InsertRowsCommand housing = new InsertRowsCommand("study", "housing"); - housing.addRow(Map.of("Id", aliveAnimalId, "date", LocalDateTime.now().minusDays(10), "cage", "C4", "QCStateLabel", "Completed", "performedby", 1004)); + housing.addRow(Map.of("Id", aliveAnimalId, "date", LocalDateTime.now().minusDays(10), "room", getRooms()[2], "cage", CAGE_IN_R3, "QCStateLabel", "Completed", "performedby", 1004)); housing.execute(getApiHelper().getConnection(), getContainerPath()); log("Marking an animal dead"); @@ -1156,7 +1315,8 @@ public void createSubjectsForDeathForm() throws IOException, CommandException log("Marking an animal departed"); InsertRowsCommand departure = new InsertRowsCommand("study", "departure"); - departure.addRow(Map.of("Id", departedAnimalId, "date", LocalDateTime.now().minusDays(1), "destination", "Oregon NPRC", "performedby", 1004)); + // destination stores an ehr_lookups.source code; the facility name is only the display value + departure.addRow(Map.of("Id", departedAnimalId, "date", LocalDateTime.now().minusDays(1), "destination", "ORPRC", "performedby", 1004)); departure.execute(getApiHelper().getConnection(), getContainerPath()); } @@ -1184,7 +1344,14 @@ public void testDeathNecropsyForm() throws IOException, CommandException setFormElement(Locator.name("Id"), departedAnimalId); waitForText("Id: ERROR: Animal is not at the center."); + setFormElement(Locator.name("Id"), unknownAnimalId); + waitForText("Id: ERROR: Id not found in the demographics table."); + + setFormElement(Locator.name("Id"), deadAnimalId); + waitForText("Id: ERROR: Death record already exists for this animal."); + 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"); @@ -1193,6 +1360,15 @@ public void testDeathNecropsyForm() throws IOException, CommandException submitForm("Submit Death", "Confirm"); stopImpersonating(); + 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}}); + CommandException duplicateError = getApiHelper().doSaveRowsExpectingError(DATA_ADMIN.getEmail(), duplicateDeath, getExtraContext()); + Map> duplicateErrors = getApiHelper().extractErrors(duplicateError.getProperties()); + Assert.assertTrue("Expected duplicate death validation error, got: " + duplicateErrors, + duplicateErrors.getOrDefault("Id", List.of()).contains("ERROR: A death record already exists for this animal (Request: Pending).")); + log("Trigger notifications"); goToEHRFolder(); NotificationAdminPage adminPage = NotificationAdminPage.beginAt(this); @@ -1448,6 +1624,159 @@ public void testCalculatedAgeColumns() assertEquals("Calculated ages are incorrect", Arrays.asList("4.8", "4.0", "58.0"), row.subList(columnCount - 3, columnCount)); } + @Test + public void testRoomKeyDerivation() throws Exception + { + log("Verifying a room derives its key from its building and name"); + SelectRowsCommand selectCmd = new SelectRowsCommand("ehr_lookups", "rooms"); + selectCmd.setColumns(List.of("room", "name", "building")); + selectCmd.addFilter(new Filter("name", "R1")); + SelectRowsResponse response = selectCmd.execute(getApiHelper().getConnection(), getContainerPath()); + + Assert.assertEquals("Expected exactly one room named R1", 1, response.getRows().size()); + Map room = response.getRows().get(0); + Assert.assertEquals("Room key should combine the building and the name", roomKey("R1"), room.get("room")); + Assert.assertEquals("Room should belong to the seeded building", BUILDING_ID, room.get("building")); + } + + @Test + public void testRoomRequiresBuilding() + { + log("Verifying a room cannot be created without a building"); + InsertRowsCommand insertCmd = new InsertRowsCommand("ehr_lookups", "rooms"); + insertCmd.addRow(Map.of("name", "NOBUILDING")); + + try + { + insertCmd.execute(getApiHelper().getConnection(), getContainerPath()); + Assert.fail("Room insert should have been rejected when no building was supplied"); + } + catch (IOException | CommandException e) + { + Assert.assertTrue("Unexpected failure inserting a room without a building: " + e.getMessage(), + e.getMessage() != null && e.getMessage().contains("Building is required")); + } + } + + @Test + public void testDuplicateBuildingRejected() + { + log("Verifying a second building cannot reuse an existing description"); + // The description alone is the building key now, so reusing the seeded building's description would collide. + InsertRowsCommand insertCmd = new InsertRowsCommand("ehr_lookups", "buildings"); + insertCmd.addRow(Map.of("description", BUILDING_ID, "area", BUILDING_AREA)); + + try + { + insertCmd.execute(getApiHelper().getConnection(), getContainerPath()); + Assert.fail("Building insert should have been rejected when the description was already in use"); + } + catch (IOException | CommandException e) + { + Assert.assertTrue("Unexpected failure inserting a duplicate building: " + e.getMessage(), + e.getMessage() != null && e.getMessage().contains("already exists")); + } + } + + @Test + public void testGroupPenCagemates() throws Exception + { + String penRoom = roomKey(PEN_ROOM_NAME); + + log("Creating a group pen, whose location is the room with no cage"); + InsertRowsCommand roomCmd = new InsertRowsCommand("ehr_lookups", "rooms"); + roomCmd.addRow(Map.of("name", PEN_ROOM_NAME, "building", BUILDING_ID)); + roomCmd.execute(getApiHelper().getConnection(), getContainerPath()); + + // With no cage supplied the trigger derives the location as the room key alone. + InsertRowsCommand penCmd = new InsertRowsCommand("ehr_lookups", "cage"); + penCmd.addRow(Map.of("room", penRoom)); + penCmd.execute(getApiHelper().getConnection(), getContainerPath()); + + createAliveAnimals(PEN_ANIMALS); + + // The cage is deliberately left null: a penned animal is housed against the room, which is the case the + // cagemates query has to bound by room rather than by cage. + log("Housing two animals in the pen, with no cage"); + houseAnimals(PEN_ANIMALS, penRoom, null); + + log("Verifying penned animals resolve as each other's cagemates"); + assertCagemates(PEN_ANIMALS[0], 2, PEN_ANIMALS[1]); + } + + @Test + public void testCagematesWithoutRoom() throws Exception + { + log("Creating an unoccupied cage"); + InsertRowsCommand cageCmd = new InsertRowsCommand("ehr_lookups", "cage"); + cageCmd.addRow(Map.of("cage", ROOMLESS_CAGE_NAME, "room", roomKey("R1"))); + cageCmd.execute(getApiHelper().getConnection(), getContainerPath()); + + createAliveAnimals(ROOMLESS_ANIMALS); + + // The cage is a location key that already names its room, so the room is redundant here and nothing requires + // it. Cagemates must still resolve when it is absent. + log("Housing two animals in the same cage, with no room"); + houseAnimals(ROOMLESS_ANIMALS, null, ROOMLESS_CAGE); + + log("Verifying caged animals resolve as each other's cagemates without a room"); + assertCagemates(ROOMLESS_ANIMALS[0], 2, ROOMLESS_ANIMALS[1]); + } + + /** + * Creates living demographics records for the given animals, replacing any left behind by an earlier run. + */ + private void createAliveAnimals(String[] animalIds) throws Exception + { + log("Creating animals " + StringUtils.join(animalIds, ", ")); + String[] fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; + Object[][] data = new Object[animalIds.length][]; + for (int i = 0; i < animalIds.length; i++) + { + data[i] = new Object[]{animalIds[i], "Rhesus", (new Date()).toString(), getMale(), new Date(), "Alive", UUID.randomUUID().toString(), 1004}; + } + + SimplePostCommand insertCommand = getApiHelper().prepareInsertCommand("study", "demographics", "lsid", fields, data); + getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", StringUtils.join(animalIds, ";"), Filter.Operator.IN)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + } + + /** + * Opens a completed housing record for each animal at the given location, replacing any left behind by an earlier + * run. Either the room or the cage may be null, which is how records entered against one alone land. + */ + private void houseAnimals(String[] animalIds, String room, String cage) throws Exception + { + String[] fields = new String[]{"Id", "date", "enddate", "room", "cage", "QCStateLabel", "performedby"}; + Object[][] data = new Object[animalIds.length][]; + for (int i = 0; i < animalIds.length; i++) + { + data[i] = new Object[]{animalIds[i], new Date(), null, room, cage, EHRQCState.COMPLETED.label, 1004}; + } + + SimplePostCommand insertCommand = getApiHelper().prepareInsertCommand("study", "Housing", "lsid", fields, data); + getApiHelper().deleteAllRecords("study", "Housing", new Filter("Id", StringUtils.join(animalIds, ";"), Filter.Operator.IN)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + } + + /** + * Asserts the cagemates report resolves the expected companions for an animal. A null total means the join matched + * nothing at all, which is reported as its own failure rather than as an unexpected count. + */ + private void assertCagemates(String animalId, int expectedTotal, String expectedCompanion) throws Exception + { + SelectRowsCommand selectCmd = new SelectRowsCommand("study", "demographicsCagemates"); + selectCmd.addFilter(new Filter("Id", animalId)); + SelectRowsResponse response = selectCmd.execute(getApiHelper().getConnection(), getContainerPath()); + + Assert.assertEquals("Expected one cagemates row for " + animalId, 1, response.getRows().size()); + Map cagemates = response.getRows().get(0); + Assert.assertNotNull("Cagemates resolved no location for " + animalId, cagemates.get("total")); + Assert.assertEquals("Unexpected cagemate count for " + animalId, expectedTotal, ((Number) cagemates.get("total")).intValue()); + Assert.assertTrue("Cagemate list should name " + expectedCompanion + ", was: " + cagemates.get("animals"), + String.valueOf(cagemates.get("animals")).contains(expectedCompanion)); + } + @Test public void testLookupPage() throws Exception { @@ -1615,6 +1944,43 @@ private int countLines(File file) throws Exception } } + // Creates the parents of a conception. They need an ehr_lookups.species_codes code because the Start with + // Conception window copies the dam's species onto the newborn, and the test asserts the resulting record. + private void createBreedingPair(String damId, String sireId, String species) throws Exception + { + String[] fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; + 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} + }; + SimplePostCommand insertCommand = getApiHelper().prepareInsertCommand("study", "demographics", "lsid", fields, data); + getApiHelper().deleteAllRecords("study", "demographics", new Filter("Id", damId + ";" + sireId, Filter.Operator.IN)); + getApiHelper().doSaveRows(DATA_ADMIN.getEmail(), insertCommand, getExtraContext()); + } + + // Asserts the Births columns appear in the expected left to right order. Relative position is checked rather + // than absolute index so that hidden and system columns can come and go without breaking the test. + 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", "type", "cond", "breedingType", "remark", "performedby"); + + int previousIdx = 0; + String previousCol = null; + for (String col : expectedOrder) + { + int idx = births.getIndexOfColumn(col, true); + Assert.assertTrue("Births column '" + col + "' should appear to the right of '" + previousCol + "'", idx > previousIdx); + previousIdx = idx; + previousCol = col; + } + } + + private void waitForFormError(String message) + { + waitFor(() -> isTextPresent(message), "Form did not report: " + message, WAIT_FOR_JAVASCRIPT); + } + private void verifyRowCreated(String schema, String query, String animalId, int rowCount) { goToSchemaBrowser();