diff --git a/nbri_ehr/resources/data/editable_lookups.tsv b/nbri_ehr/resources/data/editable_lookups.tsv index edde79b..c12dbd0 100644 --- a/nbri_ehr/resources/data/editable_lookups.tsv +++ b/nbri_ehr/resources/data/editable_lookups.tsv @@ -10,6 +10,7 @@ ehr_lookups alopecia_type Behavior Alopecia Type Behavior observation fixed valu ehr_lookups alopecia_regrowth Behavior Alopecia Regrowth Behavior observation fixed values. ehr_lookups amount_units Clinical Amount Units "Used in drugs, treatments given and treatments ordered datasets." ehr_lookups app_score Behavior Appetite Score Behavior observation fixed values. +ehr_lookups areas Housing Areas Top level of the housing location hierarchy. ehr_lookups arrival_type Colony Management Arrival Type Types of arrivals. ehr_lookups arthritis_types Clinical Arthritis Types Clinical observation fixed values. ehr_lookups att_score Behavior Attitude Score Behavior observation fixed values. @@ -23,6 +24,8 @@ ehr_lookups blood_draw_tube_type Clinical Blood Draw Tube Type Used in blood dra 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 buildings Housing Buildings Buildings within an area. +ehr_lookups cage Housing Cages Cage locations within a room. 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. @@ -58,6 +61,7 @@ ehr_lookups fecal_smear_score Clinical Fecal Smear Score Clinical observation fi ehr_lookups feed_assess_types Behavior Fecal Assessment Types Behavior observation fixed values. ehr_lookups flag_categories Colony Management Flag Categories Used to specify flag behavior. ehr_lookups flag_values Colony Management Flag Values Animal flag values. +ehr_lookups floors Housing Floors Floors within a building. ehr_lookups gastro_types Clinical Gastrointestinal Types Clinical observation fixed values. ehr_lookups gender_codes Colony Management Sex Codes Used in demographics dataset. ehr_lookups general_obs Clinical General Observation Types Clinical observation types. @@ -69,6 +73,7 @@ ehr_lookups housing_reason Housing Housing Move Reason Used in housing dataset f ehr_lookups hyd_score Clinical Hydration Score Clinical observation fixed values. ehr_lookups incision_score Clinical Incision Status Clinical observation fixed values. ehr_lookups indoor_outdoor Colony Management Indoor/Outdoor Cage details +ehr investigators Colony Management Investigators Investigators available for protocol and project assignment. ehr_lookups keyword Clinical Keyword Clinical observation fixed values. ehr_lookups lameness Clinical Lameness Clinical observation fixed values. ehr_lookups mass_type Clinical Mass Type Clinical observation fixed values. @@ -102,6 +107,8 @@ ehr_lookups procedure_category Clinical Procedure Category Used in procedures to ehr_lookups procedures Clinical Procedures List of animal procedures.F ehr_lookups problem_list_category Clinical Problem List Category Used in problem list dataset. Set when opening a case. ehr_lookups problem_list_subcategory Clinical Problem List Subcategory Used in problem list to categorize problems. +ehr project Colony Management Projects Projects animals can be assigned to. +ehr protocol Colony Management Protocols IACUC protocols animals can be assigned to. ehr_lookups protocol_category Colony Management Protocol Category ehr_lookups protocol_state Colony Management Protocol State ehr_lookups protocol_type Colony Management Protocol Type @@ -112,6 +119,7 @@ ehr_lookups regulatory_stress_levels Colony Management Regulatory Stress Levels ehr_lookups req_order_state Colony Management Request Order State ehr_lookups req_order_type Colony Management Request Order Type ehr_lookups respiratory_observations Clinical Respiratory Observations Used in clinical observations. +ehr_lookups rooms Housing Rooms Rooms within a building and floor. ehr_lookups routes Clinical Treatment Routes Used in drug and treatment datasets. ehr_lookups sib_score Behavior SIB Score Behavior observation fixed values. ehr_lookups skin_problem Clinical Skin Problems Clinical observation fixed values. diff --git a/nbri_ehr/resources/data/source.tsv b/nbri_ehr/resources/data/source.tsv index 5c3f18a..a62d9eb 100644 --- a/nbri_ehr/resources/data/source.tsv +++ b/nbri_ehr/resources/data/source.tsv @@ -1,72 +1,155 @@ code meaning +ACCULAB Acculab Life Sciences +ABL Advanced Bioscience Laboratories +AECOM Albert Einstein College of Medicine +ALLERGAN Allergan, Inc +ALPHAGEN Alpha Genesis +ASU Arizona State University +AFRRI Armed Forces Radiobiological Res. AAI Asiatic Animal Imports +BATTELLE Battelle Research +BCM Baylor College of Medicine +BEDFORD-VA Bedford VA Hospital BIOQUAL Bioqual, Incorporated +BIOTOX Biotox Sciences +BLACKPOOL Blackpool Zoological Gardens +BOBCROW Bob Crow (USDA 58B142) +BOSTONU Boston University BRANDEIS Brandeis University +BWH Brigham and Women's Hospital +BRISTOL Bristol-Myers Company +BRONX-VA Bronx VA Hospital +BWEF BWEF-Primegen CA-DPH Calif Dept Public Health Facilities +CIMR Calif. Institute for Medical Research +CALTECH California Institute of Technology CA-DOH California State Department of Health CPRC Carribean Primate Center CWRU Case Western Reserve University +CDC-GL Center for Disease Control - Garcia.Lema 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 +CHOP Childrens Hospital of Philadelphia CHIMR Christ Hospital Inst. for Medical Res. +CMC Colorado Mountain College CSU Colorado State University +COLUMBIA Columbia University +CONNAUGHT Connaught Laboratories CORNELL Cornell University COULSTON Coulston Foundation COVANCE Covance Research Products, Inc. PRIMGEN CSP-Primgen CUTTER Cutter Lab +DHMRI David H. Murdock Res Institute DMT Del Mundo Trading +DENVER-ZOO Denver Zoological Gardens +DODGE-ZOO Dodge City Zoo +DOHENY Doheny Eye Foundation +DOWNSTATE Downstate Medical Center +DUKE Duke University Medical Center EPZ El Paso Zoological Gardens ENVIGO Envigo +FOLSOM-ZOO Folsom Zoo +GENENTECH Genentech, Inc. HAHNEMANN Hahnemann University HL Hazelton Laboratories HLA Hazelton Laboratories America Inc. +HEMKER-ZOO Hemker Park and Zoo +INDY-ZOO Indianapolis Zoo UNK Institution Unknown ISU-VDL Iowa State Univ, Vet Diagnostic Lab +ISIS Isis Services JVL Jan Vacek Limited JHMC Jewish Hospital & Medical Center +JHU Johns Hopkins University JRI Johnson Research Institute KNLPC Kunming National Laboratory Primate Ctr. LABS Laboratory Animal Breeders and Services LABSINDO Labsindo +LEGACY Legacy Research Institute LEMSIP LEMSIP, New York Medical Center, RDI LAIR Letterman Army Res Inst-Presidio LB Litton Bionetics LLU Loma Linda University +LSU-SOM Louisiana State Univ School of Medicine +MADIGAN Madigan Army Center +MWRI Magee Womens Research Institute +MIT Massachusetts Institute of Technology +MCW-ARC Med College of Wisconsin, Anim Res Ctr +MCG Medical College of Georgia +MEDRI Medical Research Institute +METRO-ZOO Metro Richmond Zoo MSU-MI Michigan State University +MICKE-ZOO Micke Grove Zoo +MPLS-VA Minneapolis Veterans Hospital +MONKEYJNGL Monkey Jungle MSU-MT Montana State University +MONTG-ZOO Montgomery Zoo +MOORPRK-ZO Moorpark Zoo +MSSM Mount Sinai School of Medicine MPI MPI Research NASA-ARC NASA-Ames Research Center +NIH National Institutes of Health NAMRL Naval Aerospace Medical Research Lab +NEURALINK Neuralink +NSI Neurosciences Institute NEPRC New England Primate Research Center +NMSU-PRI New Mex State Univ, Primate Research Inst +NICHOLS Nichols Exotics NIHAC NIH Animal Center +NEU Northeastern University +NWU Northwestern University +OAKLAND-ZO Oakland Zoo +OSU Ohio State University +OHSU Oregon Health Sciences University +OPR Oregon Primate Rescue ORPRC Oregon Regional Primate Research Center +OSAGE Osage Research Primates +PALOALTO-V Palo Alto Veteran's Administration PARC-SAF Parc Safari African +PENN-ZOO Pennington Zoo PPC Perrine Primate Center PPP Peruvian Primatological Project PET-FARM Pet Farm +PTDEF-ZOO Point Defiance Zoo PRIMLAB Primate Laboratory PPI Primate Products, Incorporated +PRINCETON Princeton University PRIVATE Private Party RIEPT Res. Inst. of Exp. Pathology & Therapy +SCRIPPS Research Institute of Scripps Clinic +RTI Research Triangle Institute +ROCKEFELLR Rockefeller University SALK Salk Institute SFGH San Francisco General Hospital +SFMC San Francisco Medical Center +SANTANA-ZO Santa Ana Zoo +SCHERING Schering, Inc. +SCIENCEXYZ Science XYZ +SHARP-CAB Sharp Cabrillo Hospital SNBL Shin Nippon Biomedical Laboratories SICONBREC Siconbrec SBM Sierra Biomedical +SINCLAIR Sinclair Research Center, LLC SKB Smith, Kline, Beecham +SKERI Smith-Kettlewell Eye Research Institute +SDPC Sonora Desert Primate Conservancy +STPO South Texas Primate Observatory SIU Southern Illinois University SIU-SOM Southern Illinois University Med School SORI Southern Research Institute SFRE Southwest Foundation for Res. & Educ. +SNPRC Southwest Nat. Primate Center SWRF Southwest Research Foundation SWRI Southwest Research Institute SOPF Stanford Outdoor Primate Facility SRI Stanford Research Institute +SU-ARF Stanford University Animal Res. Facility +SUNY-B State Univ of New York at Buffalo SUNY-SB State Univ of New York at Stony Brook SXZ Suzhou Xishan Zhongke Lab Animal Co. SYNTEX Syntex (USA) Incorporated @@ -74,6 +157,7 @@ TARPON Tarpon Zoo TTUHSC Texas Tech Health Science Center BROOKS-AFB Texas, Brooks Air Force Base TPI The Parkinson's Institute +TJU Thomas Jefferson University TNPRC Tulane (Delta) Regional Primate Res Ctr UCB UC Berkeley UCLA UC Los Angeles @@ -81,23 +165,61 @@ UCR UC Riverside UCSD UC San Diego UCSF UC San Francisco Vivarium UC-ARS Univ of Calif, Animal Resources Service +UC-DBS Univ of Calif, Dept Biological Sci. GPC Univ of Gottingen Primate Center UH-RAF Univ of Hawaii, Research Animal Facility +UMASS-MS Univ of Massachusetts, Med School OUHSC Univ of Oklahoma Health Sci. Center +UTMB-G Univ of Texas Medical Branch, Galveston UTHSC-H Univ of Texas, Health Sci Ctr, Houston +UTHSC Univ. Texas Health Science Center +UMDNJ University Med/Dental of New Jersey +UAB University of Alabama, Birmingham +UAZ University of Arizona +UBC University of British Columbia +UCI University of California, Irvine +UCHICAGO University of Chicago UCHSC University of Colorado Health Sciences UCMC University of Colorado Medical Center +UFL University of Florida +UGA University of Georgia UIC University of Illinois at Chicago +UIOWA-OAK University of Iowa, Oakdale Facility +KUMC University of Kansas Medical Center +ULOUISVL University of Louisville +UMD University of Maryland +UMIAMI University of Miami +UMICH University of Michigan +UMN University of Minnesota +UMMC University of Mississippi Medical Center +MIZZOU University of Missouri, Columbia +UNMC University of Nebraska Medical Center UNV University of Nevada UNM University of New Mexico +UOREGON University of Oregon +UPENN University of Pennsylvania +PITT-DLAR University of Pittsburgh Lab Animal Rscs +PITT-SOM University of Pittsburgh Medical School UPR University of Puerto Rico +USC University of Southern California +UTENN University of Tennessee UTSCC University of Texas System Cancer Center +UTEP University of Texas, El Paso +UUMC University of Utah Medical Center UNK-CN Unknown Institution, China +USC-LA USC Livingston Annex +VA V.A. Hospital VBS Valley Biosystems +VANDERBILT Vanderbilt University +VCU Virginia Commonwealth University WFSM Wake Forest School of Medicine WANPRC Washington Regional Primate Research Ctr +WUSTL Washington University School of Medicine +WHITTAKER Whittaker Bioproducts WHMC Wilford Hall Medical Center WRPRC Wisconsin Regional Primate Research Ctr WAI Woodward Asiatic Imports WWP World Wide Primates, Inc. +YALE Yale University YPRC Yemassee Primate Research Center +YRPRC Yerkes Regional Primate Research Ctr diff --git a/nbri_ehr/resources/data/species.tsv b/nbri_ehr/resources/data/species.tsv index 12d3118..754461b 100644 --- a/nbri_ehr/resources/data/species.tsv +++ b/nbri_ehr/resources/data/species.tsv @@ -2,12 +2,12 @@ Common Scientific Name Id Prefix Mhc Prefix Max Blood Draw (mL/kg) Max Percent B 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 +Olive Baboon Papio Cynocephalus 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 +Cynomolgus / Crab-Eating Macaque Macaca Fascicularis 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 diff --git a/nbri_ehr/resources/data/species_codes.tsv b/nbri_ehr/resources/data/species_codes.tsv index feb4100..a914993 100644 --- a/nbri_ehr/resources/data/species_codes.tsv +++ b/nbri_ehr/resources/data/species_codes.tsv @@ -2,12 +2,12 @@ Code Scientific Name Common Name Description Genus Species Date Disabled MMU Macaca Mulatta Rhesus Macaque MNE Macaca Nemestrina Pig-Tailed Macaque MRA Macaca Radiata Bonnet Macaque -PAN Papio Anubis Olive Baboon +PAN Papio Cynocephalus 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 +MCY Macaca Fascicularis Cynomolgus / Crab-Eating Macaque MFU Macaca Fuscata Japanese Macaque CTA Cercopithecus Talapoin Talapoin Monkey CMA Cercopithecus M Albogulari Sykes' Monkey diff --git a/nbri_ehr/resources/domain-templates/ehr_lookups.template.xml b/nbri_ehr/resources/domain-templates/ehr_lookups.template.xml index 36eb0f9..008fafb 100644 --- a/nbri_ehr/resources/domain-templates/ehr_lookups.template.xml +++ b/nbri_ehr/resources/domain-templates/ehr_lookups.template.xml @@ -3,6 +3,9 @@ xmlns:dat="http://labkey.org/data/xml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + + + @@ -35,6 +52,9 @@ string + + dateTime + diff --git a/nbri_ehr/resources/queries/ehr_lookups/buildings.query.xml b/nbri_ehr/resources/queries/ehr_lookups/buildings.query.xml index 90dd3ea..df82540 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/buildings.query.xml +++ b/nbri_ehr/resources/queries/ehr_lookups/buildings.query.xml @@ -16,6 +16,12 @@ areas area description + + + + + + diff --git a/nbri_ehr/resources/queries/ehr_lookups/cage.query.xml b/nbri_ehr/resources/queries/ehr_lookups/cage.query.xml index 3d15c46..c3b3060 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/cage.query.xml +++ b/nbri_ehr/resources/queries/ehr_lookups/cage.query.xml @@ -13,6 +13,12 @@ ehr_lookups rooms room + + + + + + diff --git a/nbri_ehr/resources/queries/ehr_lookups/floors.query.xml b/nbri_ehr/resources/queries/ehr_lookups/floors.query.xml index ba3ff03..cc667cc 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/floors.query.xml +++ b/nbri_ehr/resources/queries/ehr_lookups/floors.query.xml @@ -13,6 +13,12 @@ buildings name description + + + + + + diff --git a/nbri_ehr/resources/queries/ehr_lookups/rooms.query.xml b/nbri_ehr/resources/queries/ehr_lookups/rooms.query.xml index 0b522c5..a4030d9 100644 --- a/nbri_ehr/resources/queries/ehr_lookups/rooms.query.xml +++ b/nbri_ehr/resources/queries/ehr_lookups/rooms.query.xml @@ -6,7 +6,27 @@ Rooms - + + + false + true + true + true + true + + ehr_lookups + buildings + name + description + + + + + + + + /nbri_ehr/cageDetails.view?room=${room} diff --git a/nbri_ehr/resources/queries/study/arrival.js b/nbri_ehr/resources/queries/study/arrival.js index 8ee71bb..d374944 100644 --- a/nbri_ehr/resources/queries/study/arrival.js +++ b/nbri_ehr/resources/queries/study/arrival.js @@ -8,6 +8,27 @@ require("ehr/triggers").initScript(this); var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); var idsToSync = []; +// opens one assignment record against the animal being entered; each dataset carries the assignment under its own field +function createAssignment(scriptErrors, dataset, fieldName, value, row) { + if (!value) + return; + + var assignmentRec = { + Id: row.Id, + date: row.date, + taskid: row.taskid, + remark: row.remark, + qcstate: row.qcstate, + performedby: row.performedby + }; + assignmentRec[fieldName] = value; + + var error = triggerHelper.createAssignmentRecord(dataset, row.Id, assignmentRec); + if (error) { + EHR.Server.Utils.addError(scriptErrors, 'Id', error, 'ERROR'); + } +} + EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'Arrival', function(event, helper){ // the script scope can outlive a single save, so never inherit ids from a prior one @@ -68,26 +89,11 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even } } + // an animal arrives already assigned to a project, a protocol and a group, all entered on the arrival row if (row.Id && row.date) { - - let assignmentRec = { - Id: row.Id, - date: row.date, - taskid: row.taskid, - remark: row.remark, - qcstate: row.qcstate, - performedby: row.performedby - } - - if (row.project) { - assignmentRec['project'] = row.project; - triggerHelper.createAssignmentRecord("assignment", row.Id, assignmentRec); - } - - if (row.arrivalProtocol) { - assignmentRec['protocol'] = row.arrivalProtocol; - triggerHelper.createAssignmentRecord("protocolAssignment", row.Id, assignmentRec); - } + createAssignment(scriptErrors, 'assignment', 'project', row.project, row); + createAssignment(scriptErrors, 'protocolAssignment', 'protocol', row.arrivalProtocol, row); + createAssignment(scriptErrors, 'animal_group_members', 'groupId', row.groupId, row); } // if 'cage', labeled as "Initial Location" is provided, then insert into housing. diff --git a/nbri_ehr/resources/queries/study/arrival.query.xml b/nbri_ehr/resources/queries/study/arrival.query.xml index 0955496..84e30cc 100644 --- a/nbri_ehr/resources/queries/study/arrival.query.xml +++ b/nbri_ehr/resources/queries/study/arrival.query.xml @@ -32,7 +32,6 @@ true - 80 Initial Location true true @@ -40,7 +39,6 @@ ehr_lookups cage location - cage @@ -61,6 +59,15 @@ title + + Group + + ehr_lookups + breeding_type + value + title + + Source Facility diff --git a/nbri_ehr/resources/queries/study/birth.js b/nbri_ehr/resources/queries/study/birth.js index b5a34c6..6531bc9 100644 --- a/nbri_ehr/resources/queries/study/birth.js +++ b/nbri_ehr/resources/queries/study/birth.js @@ -9,6 +9,31 @@ EHR.Server.Utils = require("ehr/utils").EHR.Server.Utils; var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id); var idsToSync = []; +// conception ids claimed by the rows of this save that have already been validated. Rows entered together are not in +// study.birth yet when each one is checked, so this is the only way the one-birth-per-conception rule can see them. +var conceptIdsInSave = []; + +// opens one assignment record against the animal being entered; each dataset carries the assignment under its own field +function createAssignment(scriptErrors, dataset, fieldName, value, row) { + if (!value) + return; + + var assignmentRec = { + Id: row.Id, + date: row.date, + taskid: row.taskid, + remark: row.remark, + qcstate: row.qcstate, + performedby: row.performedby + }; + assignmentRec[fieldName] = value; + + var error = triggerHelper.createAssignmentRecord(dataset, row.Id, assignmentRec); + if (error) { + EHR.Server.Utils.addError(scriptErrors, 'Id', error, 'ERROR'); + } +} + function onInit(event, helper){ helper.setScriptOptions({ allowAnyId: true, @@ -24,6 +49,7 @@ function onInit(event, helper){ // the script scope can outlive a single save, so never inherit ids from a prior one idsToSync = []; + conceptIdsInSave = []; helper.decodeExtraContextProperty('birthsInTransaction'); } @@ -54,12 +80,19 @@ 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', 'WARN'); + var claimedBySavedRow = triggerHelper.totalRecords('study', 'birth', 'conceptId', row.conceptId) > conceptIdThreshold; + + // rows are validated one at a time and collected below, so this list holds the earlier rows of this save only + var claimedByEarlierRow = conceptIdsInSave.indexOf(row.conceptId) > -1; + + if (claimedBySavedRow || claimedByEarlierRow) { + EHR.Server.Utils.addError(scriptErrors, 'conceptId', 'This conception Id is already used by another birth record', 'ERROR'); } + conceptIdsInSave.push(row.conceptId); + if (triggerHelper.totalRecords('study', 'pregnancy', 'conceptId', row.conceptId) > 0) { - EHR.Server.Utils.addError(scriptErrors, 'conceptId', 'This conception Id is already used by a pregnancy outcome record', 'INFO'); + EHR.Server.Utils.addError(scriptErrors, 'conceptId', 'This conception Id is already used by a pregnancy outcome record', 'WARN'); } } @@ -69,29 +102,14 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even row.qcstate = helper.getJavaHelper().getQCStateForLabel(row.QCStateLabel).getRowId(); } - if (row.Id && row.date) { - - let assignmentRec = { - Id: row.Id, - date: row.date, - taskid: row.taskid, - remark: row.remark, - qcstate: row.qcstate, - performedby: row.performedby - } - - if (row.project) { - assignmentRec['project'] = row.project; - triggerHelper.createAssignmentRecord("assignment", row.Id, assignmentRec); - } + if (!helper.isGeneratedByServer() && !helper.isValidateOnly()) { - if (row.birthProtocol) { - assignmentRec['protocol'] = row.birthProtocol; - triggerHelper.createAssignmentRecord("protocolAssignment", row.Id, assignmentRec); + // an animal is born already assigned to a project, a protocol and a group, all entered on the birth row + if (row.Id && row.date) { + createAssignment(scriptErrors, 'assignment', 'project', row.project, row); + createAssignment(scriptErrors, 'protocolAssignment', 'protocol', row.birthProtocol, row); + createAssignment(scriptErrors, 'animal_group_members', 'groupId', row.groupId, row); } - } - - if (!helper.isGeneratedByServer() && !helper.isValidateOnly()) { // if 'cage', labeled as "Birth Location" is provided, then insert into housing. if (row.cage && row.Id && row.date) { diff --git a/nbri_ehr/resources/queries/study/birth.query.xml b/nbri_ehr/resources/queries/study/birth.query.xml index 034b7f0..f424994 100644 --- a/nbri_ehr/resources/queries/study/birth.query.xml +++ b/nbri_ehr/resources/queries/study/birth.query.xml @@ -22,14 +22,12 @@ Birth Location - 80 true true ehr_lookups cage location - cage @@ -60,6 +58,15 @@ title + + Group + + ehr_lookups + breeding_type + value + title + + Breeding Type diff --git a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml index 14fbe82..eed9f6a 100644 --- a/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml +++ b/nbri_ehr/resources/referenceStudy/study/datasets/datasets_metadata.xml @@ -128,6 +128,10 @@ varchar + + Group + varchar + varchar @@ -247,6 +251,10 @@ varchar + + Group + varchar + varchar diff --git a/nbri_ehr/resources/web/nbri_ehr/field/ConceptionField.js b/nbri_ehr/resources/web/nbri_ehr/field/ConceptionField.js new file mode 100644 index 0000000..050daa0 --- /dev/null +++ b/nbri_ehr/resources/web/nbri_ehr/field/ConceptionField.js @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +/** + * The conception Id field on the Births form. The value is never typed: clicking it opens the same window the Start + * with Conception button uses, and the conception picked there is copied onto the row that was clicked. It serves as + * both the grid's cell editor and the row editor's form field, so it looks for the row it is editing in either place. + */ +Ext4.define('NBRI_EHR.field.ConceptionField', { + extend: 'Ext.form.field.Trigger', + alias: 'widget.nbri_ehr-conceptionField', + + editable: false, + triggerCls: 'x4-form-search-trigger', + + initComponent: function(){ + this.callParent(arguments); + + if (!this.triggerToolTip){ + this.triggerToolTip = 'Click to pick the conception this birth came from'; + } + + this.on('render', function(){ + this.triggerEl.set({'data-qtip': Ext4.htmlEncode(this.triggerToolTip)}); + }, this); + }, + + initEvents: function(){ + this.callParent(arguments); + + if (this.readOnly){ + return; + } + + // the base class only watches the trigger itself, but the field holds nothing that can be typed, so a click + // anywhere in it should open the window + this.mon(this.inputEl, 'click', this.onTriggerClick, this); + + // as a cell editor the field is created and focused by the same single click that starts editing the cell, + // which the grid consumes. Opening on focus as well keeps that one click enough. In the row editor focus + // arrives by tabbing through the form, where a window must not open, so this is limited to the editor. + if (this.inEditor){ + this.on('focus', this.onTriggerClick, this); + } + }, + + onTriggerClick: function(){ + // both the click and the focus can report the same gesture, and the window is modal, so only ever open one + if (this.readOnly || this.disabled || this.pickerOpen){ + return; + } + + this.pickerOpen = true; + + var target = this.getTargetRow(); + if (!target){ + this.pickerOpen = false; + return; + } + + var picker = Ext4.create('NBRI_EHR.window.StartWithConceptionWindow', target); + picker.on('destroy', function(){ + this.pickerOpen = false; + }, this); + picker.show(); + }, + + // as a cell editor the row comes from the grid's editing plugin; in the row editor it is whatever record the form + // is bound to. Either way the window is handed the row so it updates it rather than adding a new one. + getTargetRow: function(){ + var grid = this.up('gridpanel'); + if (grid){ + var editingPlugin = grid.getPlugin(grid.editingPluginId); + var record = editingPlugin ? editingPlugin.getActiveRecord() : null; + + // close the cell editor before the modal window covers the grid; an editor left open would write its + // stale value back over the conception copied onto the row + if (editingPlugin){ + editingPlugin.completeEdit(); + } + + return { + targetStore: grid.store, + formConfig: grid.formConfig, + targetRecord: record + }; + } + + var form = this.up('form'); + var boundRecord = form ? form.getForm().getRecord() : null; + if (!boundRecord){ + return null; + } + + return { + targetStore: boundRecord.store, + targetRecord: boundRecord + }; + } +}); 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 af0c748..9883d37 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Arrival.js @@ -17,13 +17,6 @@ Ext4.onReady(function() { }); EHR.model.DataModelManager.registerMetadata('Arrival', { - allQueries: { - // lowercase to match the key Default.js and Assignment.js use; a differently-cased key shadows theirs entirely - // rather than merging with it - 'enddate': { - hidden: true - } - }, byQuery: { 'study.arrival': { 'cage': { @@ -62,16 +55,53 @@ EHR.model.DataModelManager.registerMetadata('Arrival', { width: 200 } }, - // project and protocol are entered through the Project Assignment and Protocol Assignment sections + // an animal joins the colony already assigned to a project, a protocol and a group; the trigger script + // opens the matching assignment record for each one project: { - allowBlank: true, - hidden: true, - showInGrid: false + xtype: 'combo', + allowBlank: false, + nullable: false, + columnConfig: { + fixed: true, + width: 150 + }, + lookup: { + schemaName: 'ehr', + queryName: 'project', + keyColumn: 'project', + columns: 'project,name', + filterArray: [ + LABKEY.Filter.create('isActive', true, LABKEY.Filter.Types.EQUAL), + ] + } }, arrivalProtocol: { - allowBlank: true, - hidden: true, - showInGrid: false + xtype: 'combo', + allowBlank: false, + nullable: false, + columnConfig: { + 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' + } + }, + groupId: { + allowBlank: false, + nullable: false, + columnConfig: { + width: 200 + }, + lookup: { + // the shared default filters on a date column that the breeding type lookup does not have + filterArray: [] + } }, performedby: { hidden: true, 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 de3c560..bed0ab7 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Birth.js @@ -17,13 +17,6 @@ Ext4.onReady(function() { }); EHR.model.DataModelManager.registerMetadata('Birth', { - allQueries: { - // lowercase to match the key Default.js and Assignment.js use; a differently-cased key shadows theirs entirely - // rather than merging with it - 'enddate': { - hidden: true - } - }, byQuery: { 'study.birth': { Id: { @@ -77,16 +70,51 @@ EHR.model.DataModelManager.registerMetadata('Birth', { width: 200 }, }, - // project and protocol are entered through the Project Assignment and Protocol Assignment sections + // an animal joins the colony already assigned to a project, a protocol and a group; the trigger script + // opens the matching assignment record for each one project: { - allowBlank: true, - hidden: true, - showInGrid: false + xtype: 'combo', + allowBlank: false, + nullable: false, + columnConfig: { + width: 150 + }, + lookup: { + schemaName: 'ehr', + queryName: 'project', + keyColumn: 'project', + columns: 'project,name', + filterArray: [ + LABKEY.Filter.create('isActive', true, LABKEY.Filter.Types.EQUAL), + ] + } }, birthProtocol: { - allowBlank: true, - hidden: true, - showInGrid: false + xtype: 'combo', + allowBlank: false, + nullable: false, + columnConfig: { + 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' + } + }, + groupId: { + allowBlank: false, + nullable: false, + columnConfig: { + width: 200 + }, + lookup: { + // the shared default filters on a date column that the breeding type lookup does not have + filterArray: [] + } }, 'Id/demographics/birth': { allowBlank: false @@ -95,16 +123,14 @@ EHR.model.DataModelManager.registerMetadata('Birth', { allowBlank: false, nullable: false }, - // see the note above on the fields the Start with Conception window populates + // the conception is picked rather than typed: clicking the field, in the grid or in the row editor, opens + // the same window the Start with Conception button uses and copies that conception onto the row conceptId: { + xtype: 'nbri_ehr-conceptionField', allowBlank: false, nullable: false, columnConfig: { - width: 150, - editable: false - }, - formEditorConfig: { - readOnly: true + width: 150 } }, breedingType: { 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 ea19076..1bca93c 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/NBRIDefault.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/NBRIDefault.js @@ -86,6 +86,9 @@ EHR.model.DataModelManager.registerMetadata('Default', { fixed: true, width: 150 }, + lookup: { + filterArray: [LABKEY.Filter.create('dateDisabled', null, LABKEY.Filter.Types.ISBLANK)] + } }, QCState: { hidden: true, diff --git a/nbri_ehr/resources/web/nbri_ehr/model/sources/Rearrival.js b/nbri_ehr/resources/web/nbri_ehr/model/sources/Rearrival.js index 059847e..c9b83ea 100644 --- a/nbri_ehr/resources/web/nbri_ehr/model/sources/Rearrival.js +++ b/nbri_ehr/resources/web/nbri_ehr/model/sources/Rearrival.js @@ -57,6 +57,12 @@ EHR.model.DataModelManager.registerMetadata('Rearrival', { hidden: true, showInGrid: false }, + // a rearriving animal keeps the assignments it already had; new ones are made on the Assignment forms + groupId: { + allowBlank: true, + hidden: true, + showInGrid: false + }, } } }); \ No newline at end of file diff --git a/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js b/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js index bfdb0fd..0ee4d92 100644 --- a/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js +++ b/nbri_ehr/resources/web/nbri_ehr/panel/SnapshotPanel.js @@ -255,17 +255,13 @@ Ext4.define('NBRI_EHR.panel.SnapshotPanel', { let paRecords = results.getData()['protocolAssignments']; let values = []; - let val; if (Ext4.isArray(paRecords) && paRecords.length > 0) { Ext4.each(paRecords, function(record) { - val = record['protocol/displayName']; + let val = LABKEY.Utils.encodeHtml(record['protocol']); if (record['protocol/InvestigatorId/lastName']) { - val += " - " + LABKEY.Utils.encodeHtml(record['protocol/InvestigatorId/lastName']); + val += ' - ' + LABKEY.Utils.encodeHtml(record['protocol/InvestigatorId/lastName']); } - if (record['protocol/InvestigatorId/firstName']) { - val += ", " + LABKEY.Utils.encodeHtml(record['protocol/InvestigatorId/firstName']); - } - values.push(LABKEY.Utils.encodeHtml(val)); + values.push(val); }); } diff --git a/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js b/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js index ebdf711..9c33be5 100644 --- a/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js +++ b/nbri_ehr/resources/web/nbri_ehr/window/StartWithConceptionWindow.js @@ -5,17 +5,23 @@ */ /** - * Adds a birth record pre-populated from an existing conception record. + * Populates a birth record from an existing conception record. + * + * With no targetRecord a new birth row is added; with one, only that row's conception fields are replaced and the + * rest of the row is left untouched. * * @cfg {Object} targetStore * @cfg {Object} formConfig + * @cfg {Object} [targetRecord] the birth row to populate, or null to add a new one */ Ext4.define('NBRI_EHR.window.StartWithConceptionWindow', { extend: 'Ext.window.Window', initComponent: function(){ + var isExistingRow = !!this.targetRecord; + Ext4.apply(this, { - title: 'Start with Conception', + title: isExistingRow ? 'Change Conception' : 'Start with Conception', modal: true, closeAction: 'destroy', border: true, @@ -26,12 +32,15 @@ Ext4.define('NBRI_EHR.window.StartWithConceptionWindow', { 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.', + html: isExistingRow + ? 'Select a conception record. The conception Id, dam, sire and species on this birth record will be replaced with the values from that conception. Everything else on the row is left as it is.' + : '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', + value: isExistingRow ? this.targetRecord.get('conceptId') : null, displayField: 'ConceptId', valueField: 'ConceptId', forceSelection: true, @@ -81,14 +90,14 @@ Ext4.define('NBRI_EHR.window.StartWithConceptionWindow', { btn.disable(); this.getSpecies(dam, function(species, speciesError){ - this.addRow(conceptId, dam, sire, species); + this.applyConception(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 + // the row is still populated 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.'); + Ext4.Msg.alert('Species Not Copied', speciesError + ' Enter the species on the birth record manually.'); } }, this); }, @@ -128,13 +137,21 @@ Ext4.define('NBRI_EHR.window.StartWithConceptionWindow', { }); }, - addRow: function(conceptId, dam, sire, species){ - this.targetStore.add(this.targetStore.createModel({ + applyConception: function(conceptId, dam, sire, species){ + var values = { conceptId: conceptId, 'Id/demographics/dam': dam, 'Id/demographics/sire': sire, 'Id/demographics/species': species - })); + }; + + // only the fields the conception owns are written, so anything already entered on the row survives + if (this.targetRecord){ + this.targetRecord.set(values); + return; + } + + this.targetStore.add(this.targetStore.createModel(values)); } }); 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 6e988db..0b87214 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/NBRI_EHRModule.java @@ -218,9 +218,6 @@ private void registerDataEntry() EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIDeathFormType.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)); - EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIProtocolFormType.class, this)); - EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIInvestigatorsFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIPregnancyFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIWeightFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIFlagsFormType.class, this)); @@ -228,10 +225,6 @@ private void registerDataEntry() EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRINotesFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRICasesFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIBehavioralCasesFormType.class, this)); - EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIBuildingFormType.class, this)); - EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIRoomFormType.class, this)); - EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIFloorFormType.class, this)); - EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRICageFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIClinicalObservationsFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIClinicalRoundsFormType.class, this)); EHRService.get().registerFormType(new DefaultDataEntryFormFactory(NBRIAnimalTrainingFormType.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 b8d2e6e..b79b90b 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,9 +23,6 @@ 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.NBRIGroupAssignmentFormSection; -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; @@ -43,20 +40,13 @@ public NBRIArrivalFormType(DataEntryFormContext ctx, Module owner) new NBRITaskFormSection(), new NBRIAnimalDetailsFormSection(), new NBRIArrivalFormSection(), - new NBRIProtocolAssignmentFormSection(true, true, true), - new NBRIProjectAssignmentFormSection(true, true, true), - new NBRIGroupAssignmentFormSection(true, true, true), new NBRIWeightFormSection(true, true) )); - addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Assignment.js")); - addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/AnimalGroupMembers.js")); addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/model/sources/Arrival.js")); for (FormSection s : getFormSections()) { - s.addConfigSource("Assignment"); - s.addConfigSource("AnimalGroupMembers"); 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 78be0d6..ebe9c2b 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,9 +24,6 @@ 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.NBRIGroupAssignmentFormSection; -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; @@ -42,23 +39,16 @@ public NBRIBirthFormType (DataEntryFormContext ctx, Module owner) new NBRIBirthInstructionsFormSection(), new NBRITaskFormSection(), new NBRIAnimalDetailsFormSection(), - new NBRIBirthFormSection(), - new NBRIProtocolAssignmentFormSection(true, true, true), - new NBRIProjectAssignmentFormSection(true, true, true), - new NBRIGroupAssignmentFormSection(true, true, true) + new NBRIBirthFormSection() )); 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/AnimalGroupMembers.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("AnimalGroupMembers"); s.addConfigSource("Birth"); } } diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBuildingFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBuildingFormType.java deleted file mode 100644 index 2628766..0000000 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIBuildingFormType.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.dataentry.DataEntryFormContext; -import org.labkey.api.ehr.dataentry.forms.AdminLinksFormType; -import org.labkey.api.module.Module; -import org.labkey.api.security.permissions.AdminPermission; -import org.labkey.api.view.ActionURL; - -import java.util.ArrayList; - -public class NBRIBuildingFormType extends AdminLinksFormType -{ - public NBRIBuildingFormType(DataEntryFormContext ctx, Module owner) - { - super(ctx, owner, "Building", "Building", "Locations", new ArrayList<>()); - } - - @Override - protected ActionURL dataEntryLink() - { - ActionURL url = new ActionURL("ldk", "updateQuery", getCtx().getContainer()); - url.addParameter("schemaName", "ehr_lookups"); - url.addParameter("query.queryName", "buildings"); - url.addParameter("showImport", "true"); - return url; - } - - @Override - public boolean isAvailable() - { - return super.isAvailable() && getCtx().getContainer().hasPermission(getCtx().getUser(), AdminPermission.class); - } -} diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRICageFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRICageFormType.java deleted file mode 100644 index eb7ed4e..0000000 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRICageFormType.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.dataentry.DataEntryFormContext; -import org.labkey.api.ehr.dataentry.forms.AdminLinksFormType; -import org.labkey.api.module.Module; -import org.labkey.api.security.permissions.AdminPermission; -import org.labkey.api.view.ActionURL; - -import java.util.ArrayList; - -public class NBRICageFormType extends AdminLinksFormType -{ - public NBRICageFormType(DataEntryFormContext ctx, Module owner) - { - super(ctx, owner, "Cage", "Cage", "Locations", new ArrayList<>()); - } - - @Override - protected ActionURL dataEntryLink() - { - ActionURL url = new ActionURL("ldk", "updateQuery", getCtx().getContainer()); - url.addParameter("schemaName", "ehr_lookups"); - url.addParameter("query.queryName", "cage"); - url.addParameter("showImport", "true"); - return url; - } - - @Override - public boolean isAvailable() - { - return super.isAvailable() && getCtx().getContainer().hasPermission(getCtx().getUser(), AdminPermission.class); - } -} diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIFloorFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIFloorFormType.java deleted file mode 100644 index d847464..0000000 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIFloorFormType.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.dataentry.DataEntryFormContext; -import org.labkey.api.ehr.dataentry.forms.AdminLinksFormType; -import org.labkey.api.module.Module; -import org.labkey.api.security.permissions.AdminPermission; -import org.labkey.api.view.ActionURL; - -import java.util.ArrayList; - -public class NBRIFloorFormType extends AdminLinksFormType -{ - public NBRIFloorFormType(DataEntryFormContext ctx, Module owner) - { - super(ctx, owner, "Floor", "Floor", "Locations", new ArrayList<>()); - } - - @Override - protected ActionURL dataEntryLink() - { - ActionURL url = new ActionURL("ldk", "updateQuery", getCtx().getContainer()); - url.addParameter("schemaName", "ehr_lookups"); - url.addParameter("query.queryName", "floors"); - url.addParameter("showImport", "true"); - return url; - } - - @Override - public boolean isAvailable() - { - return super.isAvailable() && getCtx().getContainer().hasPermission(getCtx().getUser(), AdminPermission.class); - } -} diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIInvestigatorsFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIInvestigatorsFormType.java deleted file mode 100644 index 14055b2..0000000 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIInvestigatorsFormType.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.dataentry.DataEntryFormContext; -import org.labkey.api.ehr.dataentry.forms.AdminLinksFormType; -import org.labkey.api.ehr.security.EHRDataAdminPermission; -import org.labkey.api.module.Module; -import org.labkey.api.view.ActionURL; - -import java.util.ArrayList; - -public class NBRIInvestigatorsFormType extends AdminLinksFormType -{ - public NBRIInvestigatorsFormType(DataEntryFormContext ctx, Module owner) - { - super(ctx, owner, "Investigators", "Investigators", "Admin", new ArrayList<>()); - } - - @Override - protected ActionURL dataEntryLink() - { - ActionURL url = new ActionURL("ldk", "updateQuery", getCtx().getContainer()); - url.addParameter("schemaName", "ehr"); - url.addParameter("query.queryName", "investigators"); - url.addParameter("showImport", "true"); - return url; - } - - @Override - public boolean isAvailable() - { - return (super.isAvailable() || getCtx().getContainer().hasPermission(getCtx().getUser(), EHRDataAdminPermission.class)); - } -} diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIProjectFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIProjectFormType.java deleted file mode 100644 index 2a0b6cb..0000000 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIProjectFormType.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.dataentry.DataEntryFormContext; -import org.labkey.api.ehr.dataentry.forms.AdminLinksFormType; -import org.labkey.api.ehr.security.EHRProtocolEditPermission; -import org.labkey.api.module.Module; -import org.labkey.api.view.ActionURL; - -import java.util.ArrayList; - -public class NBRIProjectFormType extends AdminLinksFormType -{ - public NBRIProjectFormType(DataEntryFormContext ctx, Module owner) - { - super(ctx, owner, "Projects", "Projects", "Admin", new ArrayList<>()); - } - - @Override - protected ActionURL dataEntryLink() - { - ActionURL url = new ActionURL("ldk", "updateQuery", getCtx().getContainer()); - url.addParameter("schemaName", "ehr"); - url.addParameter("query.queryName", "project"); - url.addParameter("showImport", "true"); - return url; - } - - @Override - public boolean isAvailable() - { - return (super.isAvailable() || getCtx().getContainer().hasPermission(getCtx().getUser(), EHRProtocolEditPermission.class)); - } -} diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIProtocolFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIProtocolFormType.java deleted file mode 100644 index 540602a..0000000 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIProtocolFormType.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.dataentry.DataEntryFormContext; -import org.labkey.api.ehr.dataentry.forms.AdminLinksFormType; -import org.labkey.api.ehr.security.EHRProtocolEditPermission; -import org.labkey.api.module.Module; -import org.labkey.api.view.ActionURL; - -import java.util.ArrayList; - -public class NBRIProtocolFormType extends AdminLinksFormType -{ - public NBRIProtocolFormType(DataEntryFormContext ctx, Module owner) - { - super(ctx, owner, "Protocols", "Protocols", "Admin", new ArrayList<>()); - } - - @Override - protected ActionURL dataEntryLink() - { - ActionURL url = new ActionURL("ldk", "updateQuery", getCtx().getContainer()); - url.addParameter("schemaName", "ehr"); - url.addParameter("query.queryName", "protocol"); - url.addParameter("showImport", "true"); - return url; - } - - @Override - public boolean isAvailable() - { - return (super.isAvailable() || getCtx().getContainer().hasPermission(getCtx().getUser(), EHRProtocolEditPermission.class)); - } -} diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIRoomFormType.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIRoomFormType.java deleted file mode 100644 index 4ce0aa4..0000000 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/form/NBRIRoomFormType.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.dataentry.DataEntryFormContext; -import org.labkey.api.ehr.dataentry.forms.AdminLinksFormType; -import org.labkey.api.module.Module; -import org.labkey.api.security.permissions.AdminPermission; -import org.labkey.api.view.ActionURL; - -import java.util.ArrayList; - -public class NBRIRoomFormType extends AdminLinksFormType -{ - public NBRIRoomFormType(DataEntryFormContext ctx, Module owner) - { - super(ctx, owner, "Room", "Room", "Locations", new ArrayList<>()); - } - - @Override - protected ActionURL dataEntryLink() - { - ActionURL url = new ActionURL("ldk", "updateQuery", getCtx().getContainer()); - url.addParameter("schemaName", "ehr_lookups"); - url.addParameter("query.queryName", "rooms"); - url.addParameter("showImport", "true"); - return url; - } - - @Override - public boolean isAvailable() - { - return super.isAvailable() && getCtx().getContainer().hasPermission(getCtx().getUser(), AdminPermission.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 326b540..8088c16 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIBirthFormSection.java @@ -38,6 +38,9 @@ public class NBRIBirthFormSection extends NewAnimalFormSection FieldKey.fromString("Id/demographics/sire"), FieldKey.fromString("cage"), FieldKey.fromString("Id/demographics/socialCode"), + FieldKey.fromString("project"), + FieldKey.fromString("birthProtocol"), + FieldKey.fromString("groupId"), FieldKey.fromString("type"), FieldKey.fromString("breedingType"), FieldKey.fromString("remark"), @@ -47,9 +50,8 @@ public class NBRIBirthFormSection extends NewAnimalFormSection 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")); + addClientDependency(ClientDependency.supplierFromPath("nbri_ehr/field/ConceptionField.js")); } @Override @@ -84,6 +86,7 @@ public List getTbarButtons() defaultButtons.remove(idx); defaultButtons.add(idx, "NBRI_ADDANIMALS"); } + defaultButtons.remove("ANIMAL_ID_SERIES"); defaultButtons.remove("COPYFROMSECTION"); defaultButtons.addFirst("NBRI_START_WITH_CONCEPTION"); return defaultButtons; @@ -94,7 +97,6 @@ public List getTbarMoreActionButtons() { List defaultButtons = super.getTbarMoreActionButtons(); defaultButtons.remove("GUESSPROJECT"); - defaultButtons.add("NBRI_FORM_BULK_ADD"); return defaultButtons; } } \ No newline at end of file diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIConceptionFormSection.java b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIConceptionFormSection.java index f2cac6a..e8ed5d8 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIConceptionFormSection.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/dataentry/section/NBRIConceptionFormSection.java @@ -20,5 +20,8 @@ public class NBRIConceptionFormSection extends BaseFormSection public NBRIConceptionFormSection(String label) { super("nbri_ehr", "conception", label, "ehr-gridpanel", true, false, false); + + // a conception is not recorded against a batch of animals, so drop the Add Batch button + setAllowBulkAdd(false); } } 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 ded4d22..d88b672 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 @@ -708,28 +708,28 @@ public String createAssignmentRecord(String dataset, String id, Map> rows = new ArrayList<>(); rows.add(saveRow); 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 dd7e2c5..9fbbae2 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 @@ -118,9 +118,11 @@ public class NBRI_EHRTest extends AbstractGenericEHRTest implements PostgresOnly 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. + // from the room and cage, and both housing records and the data entry cage pickers key off the location, so these + // are what belongs in a 'cage' field and what the pickers list. 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_R2 = cageLocation("R2", "C3"); 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. @@ -638,7 +640,7 @@ public void testArrivalForm() throws IOException, CommandException String arrivedAnimal = "30905"; // demographics.socialCode holds an ehr_lookups.social_code code; the grids display its title String socialCode = "Acquired"; - // animal_group_members.groupId holds an ehr_lookups.breeding_type code; the grids display its title + // the group is an ehr_lookups.breeding_type code, carried to animal_group_members; the grids display its title String animalGroup = "Assigned Breeding Protocol"; LocalDateTime now = LocalDateTime.now(); @@ -652,7 +654,7 @@ public void testArrivalForm() throws IOException, CommandException arrivals.setGridCell(1, "arrivalType", "Non-quarantine Arrival"); arrivals.setGridCell(1, "acquisitionType", "Lab Transfer (Wild Born)"); arrivals.setGridCell(1, "Id", arrivedAnimal); - arrivals.setGridCell(1, "cage", "C1"); + arrivals.setGridCell(1, "cage", CAGE_IN_R1); arrivals.setGridCell(1, "Id/demographics/gender", "Female"); arrivals.setGridCell(1, "Id/demographics/geographic_origin", "BRAZIL"); arrivals.setGridCell(1, "Id/demographics/species", "Pig-Tailed Macaque"); @@ -665,23 +667,24 @@ public void testArrivalForm() throws IOException, CommandException waitForFormError("The field: Social Code is required"); arrivals.setGridCell(1, "Id/demographics/socialCode", socialCode); - 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"); + // the animal's opening project, protocol and group are entered on the arrival row itself; the trigger script + // opens the matching assignment record for each one + arrivals.setGridCell(1, "project", "640991"); + arrivals.setGridCell(1, "arrivalProtocol", "dummyprotocol"); + arrivals.setGridCell(1, "groupId", animalGroup); - 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"); + log("Verifying the opening project, protocol and group are required"); + arrivals.setGridCellJS(1, "project", null); + waitForFormError("The field: Project is required"); + arrivals.setGridCell(1, "project", "640991"); - Ext4GridRef groupAssignments = _helper.getExt4GridForFormSection("Group Assignments"); - _helper.addRecordToGrid(groupAssignments); - groupAssignments.setGridCell(1, "Id", arrivedAnimal); - groupAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); - groupAssignments.setGridCell(1, "groupId", animalGroup); + arrivals.setGridCellJS(1, "arrivalProtocol", null); + waitForFormError("The field: Protocol is required"); + arrivals.setGridCell(1, "arrivalProtocol", "dummyprotocol"); + + arrivals.setGridCellJS(1, "groupId", null); + waitForFormError("The field: Group is required"); + arrivals.setGridCell(1, "groupId", animalGroup); submitForm("Submit Final", "Finalize"); @@ -692,7 +695,7 @@ public void testArrivalForm() throws IOException, CommandException view.addColumn("cage"); 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(CAGE_IN_R1), table.getRowDataAsText(0, "cage")); goToSchemaBrowser(); table = viewQueryData("study", "assignment"); @@ -744,7 +747,7 @@ public void testBirthForm() throws Exception String breedingType = "Time-Mated"; // demographics.socialCode holds an ehr_lookups.social_code code; the grids display its title String socialCode = "Mother-rearing (for indoors)"; - // animal_group_members.groupId holds an ehr_lookups.breeding_type code; the grids display its title + // the group is an ehr_lookups.breeding_type code, carried to animal_group_members; the grids display its title String animalGroup = "Project Breeding"; LocalDateTime now = LocalDateTime.now(); @@ -764,14 +767,7 @@ public void testBirthForm() throws Exception 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); + startWithConception(births, conceptId, 1); log("Verifying the conception populated the new birth record"); assertEquals("Conception Id was not copied from the conception", conceptId, births.getFieldValue(1, "conceptId")); @@ -786,7 +782,7 @@ public void testBirthForm() throws Exception 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, "cage", CAGE_IN_R2); births.setGridCell(1, "Id/demographics/gender", "Female"); births.setGridCell(1, "breedingType", breedingType); births.setGridCell(1, "Id/demographics/socialCode", socialCode); @@ -796,23 +792,24 @@ public void testBirthForm() throws Exception waitForFormError("The field: Social Code is required"); births.setGridCell(1, "Id/demographics/socialCode", socialCode); - 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"); + // the animal's opening project, protocol and group are entered on the birth row itself; the trigger script + // opens the matching assignment record for each one + births.setGridCell(1, "project", "795644"); + births.setGridCell(1, "birthProtocol", "protocol101"); + births.setGridCell(1, "groupId", animalGroup); + + log("Verifying the opening project, protocol and group are required"); + births.setGridCellJS(1, "project", null); + waitForFormError("The field: Project is required"); + births.setGridCell(1, "project", "795644"); - 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"); + births.setGridCellJS(1, "birthProtocol", null); + waitForFormError("The field: Protocol is required"); + births.setGridCell(1, "birthProtocol", "protocol101"); - Ext4GridRef groupAssignments = _helper.getExt4GridForFormSection("Group Assignments"); - _helper.addRecordToGrid(groupAssignments); - groupAssignments.setGridCell(1, "Id", bornAnimal); - groupAssignments.setGridCellJS(1, "date", now.minusDays(1).format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); - groupAssignments.setGridCell(1, "groupId", animalGroup); + births.setGridCellJS(1, "groupId", null); + waitForFormError("The field: Group is required"); + births.setGridCell(1, "groupId", animalGroup); submitForm("Submit Final", "Finalize"); @@ -820,7 +817,7 @@ public void testBirthForm() throws Exception DataRegionTable table = viewQueryData("study", "birth"); 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(CAGE_IN_R2), table.getRowDataAsText(0, "cage")); Assert.assertEquals("Invalid Birth record", Arrays.asList(conceptId), table.getRowDataAsText(0, "conceptId")); Assert.assertEquals("Invalid Birth record", Arrays.asList(breedingType), table.getRowDataAsText(0, "breedingType")); @@ -871,6 +868,166 @@ public void testBirthForm() throws Exception Assert.assertEquals("Invalid ConceptionsByDam row", Arrays.asList(bornAnimal), report.getRowDataAsText(0, "offspring")); } + @Test + public void testDuplicateConceptionRejected() throws Exception + { + String firstAnimal = "80811"; + String secondAnimal = "80812"; + String damId = "TESTDAM03"; + String sireId = "TESTSIRE03"; + String damSpeciesCode = "CAP"; + String firstConcept = "TESTCONCEPT4"; + String secondConcept = "TESTCONCEPT5"; + String socialCode = "Mother-rearing (for indoors)"; + String animalGroup = "Project Breeding"; + // the severity prefix is part of the message the server sends, so asserting on it also pins the rule at + // ERROR, which is what refuses the save + String duplicateError = "ERROR: This conception Id is already used by another birth record"; + LocalDateTime now = LocalDateTime.now(); + + log("Creating the dam and sire of the conceptions"); + createBreedingPair(damId, sireId, damSpeciesCode); + + log("Creating two conception records for that pair"); + InsertRowsCommand conceptions = new InsertRowsCommand("nbri_ehr", "Conception"); + conceptions.addRow(Map.of("ConceptId", firstConcept, "ConceptDate", now.minusDays(200), "Dam", damId, "Sire", sireId)); + conceptions.addRow(Map.of("ConceptId", secondConcept, "ConceptDate", now.minusDays(160), "Dam", damId, "Sire", sireId)); + conceptions.execute(getApiHelper().getConnection(), getContainerPath()); + + gotoEnterData(); + waitAndClickAndWait(Locator.linkWithText("Birth")); + lockForm(); + + Ext4GridRef births = _helper.getExt4GridForFormSection("Births"); + + log("Entering two births that both claim the first conception"); + startWithConception(births, firstConcept, 1); + fillBirthRow(births, 1, firstAnimal, now.minusDays(1), socialCode, animalGroup); + startWithConception(births, firstConcept, 2); + fillBirthRow(births, 2, secondAnimal, now.minusDays(1), socialCode, animalGroup); + + // Live validation only sends the row that just changed, so the rows of one form entry first reach the + // server together on submit. A rule that compares them therefore reports by refusing the save rather than + // while the form is being filled. + log("Verifying the save is refused while both births claim one conception"); + submitFormExpectingError(duplicateError); + + log("Verifying the save is accepted once the second birth points at its own conception"); + births.setGridCellJS(2, "conceptId", secondConcept); + waitForNoFormError(duplicateError); + + submitForm("Submit Final", "Finalize"); + + goToSchemaBrowser(); + DataRegionTable table = viewQueryData("study", "birth"); + table.setFilter("conceptId", "Equals", firstConcept); + Assert.assertEquals("The first conception should have exactly one birth", 1, table.getDataRowCount()); + Assert.assertEquals("Wrong birth recorded against the first conception", + Arrays.asList(firstAnimal), table.getRowDataAsText(0, "Id")); + + goToSchemaBrowser(); + table = viewQueryData("study", "birth"); + table.setFilter("conceptId", "Equals", secondConcept); + Assert.assertEquals("The second conception should have exactly one birth", 1, table.getDataRowCount()); + Assert.assertEquals("Wrong birth recorded against the second conception", + Arrays.asList(secondAnimal), table.getRowDataAsText(0, "Id")); + + // the refused save is logged server side, so account for it or the run's server error check fails the test + checkExpectedErrors(1); + } + + @Test + public void testConceptionPickedFromGridCell() throws Exception + { + String bornAnimal = "80813"; + String firstDam = "TESTDAM04"; + String firstSire = "TESTSIRE04"; + String secondDam = "TESTDAM05"; + String secondSire = "TESTSIRE05"; + // different species on each pair, so the copy from the newly picked dam is visible + String firstSpeciesCode = "CAP"; + String secondSpeciesCode = "MMU"; + String firstConcept = "TESTCONCEPT6"; + String secondConcept = "TESTCONCEPT7"; + String socialCode = "Mother-rearing (for indoors)"; + String animalGroup = "Project Breeding"; + LocalDateTime now = LocalDateTime.now(); + + log("Creating a breeding pair and a conception for each"); + createBreedingPair(firstDam, firstSire, firstSpeciesCode); + createBreedingPair(secondDam, secondSire, secondSpeciesCode); + + InsertRowsCommand conceptions = new InsertRowsCommand("nbri_ehr", "Conception"); + conceptions.addRow(Map.of("ConceptId", firstConcept, "ConceptDate", now.minusDays(200), "Dam", firstDam, "Sire", firstSire)); + conceptions.addRow(Map.of("ConceptId", secondConcept, "ConceptDate", now.minusDays(190), "Dam", secondDam, "Sire", secondSire)); + conceptions.execute(getApiHelper().getConnection(), getContainerPath()); + + gotoEnterData(); + waitAndClickAndWait(Locator.linkWithText("Birth")); + lockForm(); + + Ext4GridRef births = _helper.getExt4GridForFormSection("Births"); + startWithConception(births, firstConcept, 1); + fillBirthRow(births, 1, bornAnimal, now.minusDays(1), socialCode, animalGroup); + births.setGridCell(1, "breedingType", "Time-Mated"); + + // the codes behind these lookups are not spelled out in the test, so remember what the row carries and + // compare afterwards rather than asserting against a literal + Object dateBefore = births.getFieldValue(1, "date"); + Object genderBefore = births.getFieldValue(1, "Id/demographics/gender"); + Object socialCodeBefore = births.getFieldValue(1, "Id/demographics/socialCode"); + Object breedingTypeBefore = births.getFieldValue(1, "breedingType"); + Object groupBefore = births.getFieldValue(1, "groupId"); + + log("Opening the picker from the Conception Id cell of the existing row"); + // a single click starts the grid's editor, which focuses the field and opens the window; startEditing() is + // avoided here because it retries the click when it cannot find a focused editor behind the modal window + births.getCell(1, "conceptId").findElement(getDriver()).click(); + Window changeWindow = new Window.WindowFinder(getDriver()).withTitle("Change Conception").waitFor(); + Ext4ComboRef conceptionCombo = _ext4Helper.queryOne("window #conceptionField", Ext4ComboRef.class); + Assert.assertNotNull("Conception Id field not found in the Change Conception window", conceptionCombo); + conceptionCombo.waitForStoreLoad(); + Assert.assertEquals("The window should open on the conception the row already carries", + firstConcept, conceptionCombo.getValue()); + + conceptionCombo.setComboByDisplayValue(secondConcept); + changeWindow.clickButton("Submit", 0); + + log("Verifying the picked conception replaced the conception fields on that row"); + waitFor(() -> secondConcept.equals(births.getFieldValue(1, "conceptId")), + "Conception Id was not replaced by the picked conception", WAIT_FOR_JAVASCRIPT); + assertEquals("Dam was not replaced from the picked conception", secondDam, births.getFieldValue(1, "Id/demographics/dam")); + assertEquals("Sire was not replaced from the picked conception", secondSire, births.getFieldValue(1, "Id/demographics/sire")); + assertEquals("Species was not replaced from the dam of the picked conception", secondSpeciesCode, births.getFieldValue(1, "Id/demographics/species")); + + log("Verifying nothing else on the row was touched"); + assertEquals("Animal Id should have been left alone", bornAnimal, births.getFieldValue(1, "Id")); + assertEquals("Birth date should have been left alone", dateBefore, births.getFieldValue(1, "date")); + assertEquals("Gender should have been left alone", genderBefore, births.getFieldValue(1, "Id/demographics/gender")); + assertEquals("Social Code should have been left alone", socialCodeBefore, births.getFieldValue(1, "Id/demographics/socialCode")); + assertEquals("Breeding Type should have been left alone", breedingTypeBefore, births.getFieldValue(1, "breedingType")); + assertEquals("Group should have been left alone", groupBefore, births.getFieldValue(1, "groupId")); + assertEquals("Project should have been left alone", "795644", String.valueOf(births.getFieldValue(1, "project"))); + assertEquals("Protocol should have been left alone", "protocol101", births.getFieldValue(1, "birthProtocol")); + + submitForm("Submit Final", "Finalize"); + + log("Verifying the saved birth carries the picked conception"); + goToSchemaBrowser(); + DataRegionTable table = viewQueryData("study", "birth"); + table.setFilter("Id", "Equals", bornAnimal); + Assert.assertEquals("Saved birth does not carry the picked conception", + Arrays.asList(secondConcept), table.getRowDataAsText(0, "conceptId")); + + goToSchemaBrowser(); + table = viewQueryData("study", "demographics"); + table.setFilter("Id", "Equals", bornAnimal); + Assert.assertEquals("Demographics does not carry the dam of the picked conception", + Arrays.asList(secondDam), table.getRowDataAsText(0, "dam")); + Assert.assertEquals("Demographics does not carry the sire of the picked conception", + Arrays.asList(secondSire), table.getRowDataAsText(0, "sire")); + } + @Test public void testPregnancyForm() throws IOException, CommandException { @@ -2064,6 +2221,34 @@ 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. + // Adds a birth row through the Start with Conception window, the only way to fill the conception fields on a row + // that does not exist yet. + private void startWithConception(Ext4GridRef births, String conceptId, int expectedRowCount) + { + 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(expectedRowCount); + } + + // Fills in everything a birth row needs beyond what the conception supplies, so the form can be submitted. + // Birth Location is left blank on purpose: it is optional, and skipping it keeps housing out of these tests. + private void fillBirthRow(Ext4GridRef births, int rowIdx, String animalId, LocalDateTime birthDate, + String socialCode, String animalGroup) + { + births.setGridCellJS(rowIdx, "date", birthDate.format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING))); + births.setGridCell(rowIdx, "Id", animalId); + births.setGridCell(rowIdx, "Id/demographics/gender", "Female"); + births.setGridCell(rowIdx, "Id/demographics/socialCode", socialCode); + births.setGridCell(rowIdx, "project", "795644"); + births.setGridCell(rowIdx, "birthProtocol", "protocol101"); + births.setGridCell(rowIdx, "groupId", animalGroup); + } + private void createBreedingPair(String damId, String sireId, String species) throws Exception { String[] fields = new String[]{"Id", "Species", "Birth", "Gender", "date", "calculated_status", "objectid", "performedby"}; @@ -2081,8 +2266,8 @@ private void createBreedingPair(String damId, String sireId, String species) thr private void verifyBirthColumnOrder(Ext4GridRef births) { List expectedOrder = List.of("Id", "date", "conceptId", "Id/demographics/species", "Id/demographics/gender", - "Id/demographics/dam", "Id/demographics/sire", "cage", "Id/demographics/socialCode", "type", - "breedingType", "remark", "performedby"); + "Id/demographics/dam", "Id/demographics/sire", "cage", "Id/demographics/socialCode", "project", + "birthProtocol", "groupId", "type", "breedingType", "remark", "performedby"); int previousIdx = 0; String previousCol = null; @@ -2100,6 +2285,11 @@ private void waitForFormError(String message) waitFor(() -> isTextPresent(message), "Form did not report: " + message, WAIT_FOR_JAVASCRIPT); } + private void waitForNoFormError(String message) + { + waitFor(() -> !isTextPresent(message), "Form kept reporting: " + message, WAIT_FOR_JAVASCRIPT); + } + /** * Reads a date field for one animal through the API rather than off a grid, so assertions compare stored values * instead of formatted display text, and normalizes to the day: event dates are entered with the time stripped, @@ -2181,6 +2371,11 @@ private void setFormSectionFieldUntilWarningClears(String section, String label, } private void submitForm(String buttonText, String windowTitle) + { + submitForm(buttonText, windowTitle, true); + } + + private void submitForm(String buttonText, String windowTitle, boolean expectNavigation) { //Give time for errors to disappear after validation Locator.tagContainingText("div", "The form has the following errors and warnings:") @@ -2200,7 +2395,24 @@ private void submitForm(String buttonText, String windowTitle) submitFinalBtn.findElement(getDriver()).click(); msgWindow = new Window.WindowFinder(this.getDriver()).withTitleContaining(windowTitle).waitFor(); } - msgWindow.clickButton("Yes"); + // a successful save navigates to the form's success URL, but a refused one leaves the browser on the form, + // so only the former can wait for a page load + if (expectNavigation) + msgWindow.clickButton("Yes"); + else + msgWindow.clickButton("Yes", 0); + } + + /** + * Submits the form expecting the save to be refused, and asserts the given message is reported. Dismisses the + * alert the panel raises so the form can be corrected and submitted again. A refused save is logged as a server + * error, which the caller has to account for through checkExpectedErrors. + */ + private void submitFormExpectingError(String message) + { + submitForm("Submit Final", "Finalize", false); + waitForFormError(message); + new Window.WindowFinder(getDriver()).withTitle("Error").waitFor().clickButton("OK", 0); } private void gotoEnterData()