Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
1b45178
[autocommit] bumping build number
rob-ouser-bi May 19, 2025
bcd7f16
Update version number
nickpalladino May 19, 2025
0a9f8ba
[autocommit] bumping build number
rob-ouser-bi May 19, 2025
da98919
[BI-2654] - replaced reserved keyword with allowed value
mlm483 May 29, 2025
fc36129
Increased signup token expiration from 1hr to 24hr
nickpalladino May 29, 2025
a290899
Merge pull request #465 from Breeding-Insight/bug/BI-2654
mlm483 May 30, 2025
a3b80c7
[autocommit] bumping build number
rob-ouser-bi May 30, 2025
1842f02
Update species migration to work with new uuids
nickpalladino Jun 3, 2025
f97f6d4
[autocommit] bumping build number
rob-ouser-bi Jun 3, 2025
7a6be40
Update species sql for tests
nickpalladino Jun 4, 2025
e916fc3
Merge branch 'release/1.1.1' of https://github.com/Breeding-Insight/b…
nickpalladino Jun 4, 2025
dbec72a
[autocommit] bumping build number
rob-ouser-bi Jun 4, 2025
7129705
Add security issuer env for tests
nickpalladino Jun 4, 2025
fab1f52
Merge branch 'release/1.1.1' of https://github.com/Breeding-Insight/b…
nickpalladino Jun 4, 2025
e05bad5
[autocommit] bumping build number
rob-ouser-bi Jun 4, 2025
c0e1e99
Change dbid to uuid
nickpalladino Jun 4, 2025
4dd0964
[autocommit] bumping build number
rob-ouser-bi Jun 4, 2025
1492b12
Update postgres to 17.5
nickpalladino Jun 4, 2025
d6242a6
[autocommit] bumping build number
rob-ouser-bi Jun 4, 2025
cd4148b
Merge branch 'release/1.1.1' into feature/BI-2616
nickpalladino Jun 5, 2025
942ebf1
Merge pull request #467 from Breeding-Insight/feature/BI-2616
nickpalladino Jun 5, 2025
aac64d8
[autocommit] bumping build number
rob-ouser-bi Jun 5, 2025
a9fd111
[BI-2632] added back germplasmListExport()
davedrp Jun 11, 2025
b424fbf
[BI-2632] added test for germplasmListExport()
davedrp Jun 11, 2025
3a374ed
Merge pull request #471 from Breeding-Insight/bug/BI-2632
davedrp Jun 11, 2025
75dca96
[autocommit] bumping build number
rob-ouser-bi Jun 11, 2025
522a191
Merge pull request #470 from Breeding-Insight/bug/BI-2664
nickpalladino Jun 12, 2025
9ed32eb
[autocommit] bumping build number
rob-ouser-bi Jun 12, 2025
44114ed
First pass fix, new list only case looks good
nickpalladino Jun 12, 2025
eb98f66
Update pedigree count to handle update case
nickpalladino Jun 13, 2025
2b10e02
Only update pedigree if no existing and file has pedigree
nickpalladino Jun 13, 2025
1ad3176
Merge pull request #472 from Breeding-Insight/bug/BI-2639
nickpalladino Jun 17, 2025
4ff31d3
[autocommit] bumping build number
rob-ouser-bi Jun 17, 2025
f48c2a0
Update version
nickpalladino Jun 18, 2025
ba5f963
[autocommit] bumping build number
rob-ouser-bi Jun 18, 2025
d9e1ca0
Fix version number
nickpalladino Jun 19, 2025
3c86f26
[autocommit] bumping build number
rob-ouser-bi Jun 19, 2025
62f8b18
[BI-2656] - added null check
mlm483 Jul 9, 2025
5d25aab
[BI-2656] - prevent setting timestamp to null
mlm483 Jul 10, 2025
14eeca3
[BI-2656] - added integration test
mlm483 Jul 16, 2025
35f6303
[BI-2656] - cleaned up test
mlm483 Jul 16, 2025
42c9c2f
[BI-2656] - handled timestamp edge case
mlm483 Jul 29, 2025
35238b1
Merge pull request #475 from Breeding-Insight/bug/BI-2656-release
mlm483 Jul 30, 2025
b8ba6f9
[autocommit] bumping build number
rob-ouser-bi Jul 30, 2025
365c3b2
[BI-2656] - added error logging
mlm483 Aug 8, 2025
2ddc8c9
Merge pull request #484 from Breeding-Insight/bug/BI-2656-logging
mlm483 Aug 8, 2025
77f5a1f
[autocommit] bumping build number
rob-ouser-bi Aug 8, 2025
d4d3873
[BI-2656] - updated isChanged logic
mlm483 Aug 12, 2025
64c0cf8
Merge pull request #485 from Breeding-Insight/bug/BI-2656-qa
mlm483 Aug 13, 2025
49d7ef6
[autocommit] bumping build number
rob-ouser-bi Aug 13, 2025
57aebce
[autocommit] bumping build number
rob-ouser-bi Aug 28, 2025
a137a7c
Merge branch 'develop' into release/1.1.1
mlm483 Aug 28, 2025
c1ea5ac
[autocommit] bumping build number
rob-ouser-bi Aug 28, 2025
91d0848
fix merge issue
mlm483 Aug 28, 2025
b769afc
[autocommit] bumping build number
rob-ouser-bi Aug 28, 2025
0a03071
fix test
mlm483 Aug 28, 2025
0c250e5
[autocommit] bumping build number
rob-ouser-bi Aug 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ public HttpResponse<Response<DataResponse<List<BrAPIGermplasm>>>> getGermplasm(
return HttpResponse.status(HttpStatus.UNPROCESSABLE_ENTITY, "Error parsing requested date format");
}
}

@Get("/programs/{programId}/germplasm/lists/{listDbId}/export{?fileExtension}")
@Produces(value = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,19 @@ public static String constructGermplasmListName(String listName, Program program
return String.format("%s [%s-germplasm]", listName, program.getKey());
}

public void updateBrAPIGermplasm(BrAPIGermplasm germplasm, Program program, UUID listId, boolean commit, boolean updatePedigree) {
/**
* Will mutate synonym and pedigree fields if changed and meet change criteria
*
* @param germplasm germplasm object
* @param program program
* @param listId list id
* @param commit flag indicating if commit changes should be made
* @param updatePedigree flag indicating if pedigree should be updated
* @return mutated indicator
*/
public boolean updateBrAPIGermplasm(BrAPIGermplasm germplasm, Program program, UUID listId, boolean commit, boolean updatePedigree) {

boolean mutated = false;

if (updatePedigree) {
if (!StringUtils.isBlank(getFemaleParentAccessionNumber())) {
Expand All @@ -170,6 +182,7 @@ public void updateBrAPIGermplasm(BrAPIGermplasm germplasm, Program program, UUID
if (!StringUtils.isBlank(getMaleParentEntryNo())) {
germplasm.putAdditionalInfoItem(BrAPIAdditionalInfoFields.GERMPLASM_MALE_PARENT_ENTRY_NO, getMaleParentEntryNo());
}
mutated = true;
}

// Append synonyms to germplasm that don't already exist
Expand All @@ -181,6 +194,7 @@ public void updateBrAPIGermplasm(BrAPIGermplasm germplasm, Program program, UUID
brapiSynonym.setSynonym(synonym);
if (!existingSynonyms.contains(brapiSynonym)) {
germplasm.addSynonymsItem(brapiSynonym);
mutated = true;
}
}
}
Expand All @@ -193,6 +207,8 @@ public void updateBrAPIGermplasm(BrAPIGermplasm germplasm, Program program, UUID
if (commit) {
setUpdateCommitFields(germplasm, program.getKey());
}

return mutated;
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public PendingImportObject<BrAPIObservation> constructPendingObservation() {
original = observation.getValue();
}

if (!isTimestampMatched()) {
if (!isTimestampMatched() && StringUtils.isNotBlank(timestamp)) {
// Update the timestamp
DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT;
String formattedTimeStampValue = formatter.format(observationService.parseDateTime(timestamp));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ public AppendOverwriteMiddlewareContext process(AppendOverwriteMiddlewareContext
BrAPIObservation observation = gson.fromJson(gson.toJson(observationByObsHash.get(observationHash)), BrAPIObservation.class);

// Is there a change to the prior data?
if (isChanged(cellData, observation, cell.timestamp)) {
if (isChanged(cellData, observation, cell.timestamp, tsColByPheno.containsKey(phenoColumnName))) {

// Is prior data protected?
/**
Expand Down Expand Up @@ -398,14 +398,15 @@ public AppendOverwriteMiddlewareContext process(AppendOverwriteMiddlewareContext
}
}

private boolean isChanged(String cellData, BrAPIObservation observation, String newTimestamp) {
private boolean isChanged(String cellData, BrAPIObservation observation, String newTimestamp, boolean timestampColumnPresent) {
if (!cellData.isBlank() && !cellData.equals(observation.getValue())){
return true;
}
if (StringUtils.isBlank(newTimestamp)) {
return (observation.getObservationTimeStamp()!=null);
// Only check timestamp if the TS:<trait> column was present in the uploaded file and there's a valid timestamp.
if (timestampColumnPresent && !StringUtils.isBlank(newTimestamp)) {
return !observationService.parseDateTime(newTimestamp).equals(observation.getObservationTimeStamp());
}
return !observationService.parseDateTime(newTimestamp).equals(observation.getObservationTimeStamp());
return false;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.breedinginsight.brapps.importer.services.processors.experiment.service;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.brapi.v2.model.core.BrAPISeason;
Expand All @@ -42,6 +43,7 @@
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Singleton
public class ObservationService {
private final ExperimentUtilities experimentUtilities;
Expand Down Expand Up @@ -115,6 +117,7 @@ public OffsetDateTime parseDateTime(String dateString) {
LocalDate localDate = LocalDate.parse(dateString, formatter);
return localDate.atStartOfDay().atOffset(ZoneOffset.UTC);
} catch (DateTimeParseException ex) {
log.error("Failed to parse timestamp: \"{}\".", dateString);
// If both parsing attempts fail, return null
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public class GermplasmProcessor implements Processor {
List<List<BrAPIGermplasm>> postOrder = new ArrayList<>();
BrAPIListNewRequest importList = new BrAPIListNewRequest();

private int numNewPedigreeConnections = 0;

public static String missingGIDsMsg = "The following GIDs were not found in the database: %s";
public static String missingParentalGIDsMsg = "The following parental GIDs were not found in the database: %s";
public static String missingParentalEntryNoMsg = "The following parental entry numbers were not found in the database: %s";
Expand Down Expand Up @@ -333,7 +335,7 @@ public Map<String, ImportPreviewStatistics> process(ImportUpload upload, List<Br
createPostOrder();

// Construct our response object
return getStatisticsMap(importRows);
return getStatisticsMap();
}

private void processNewGermplasm(Germplasm germplasm, ValidationErrors validationErrors, Map<String, ProgramBreedingMethodEntity> breedingMethods,
Expand Down Expand Up @@ -361,6 +363,10 @@ private void processNewGermplasm(Germplasm germplasm, ValidationErrors validatio
validateGermplasmName(germplasm, i+2, validationErrors);
validatePedigree(germplasm, i + 2, validationErrors);

if (germplasm.pedigreeExists()) {
numNewPedigreeConnections++;
}

BrAPIGermplasm newGermplasm = germplasm.constructBrAPIGermplasm(program, breedingMethod, user, commit, BRAPI_REFERENCE_SOURCE, nextVal, importListId);

newGermplasmList.add(newGermplasm);
Expand All @@ -385,6 +391,9 @@ private Germplasm removeBreedingMethodBlanks(Germplasm germplasm) {
private boolean processExistingGermplasm(Germplasm germplasm, ValidationErrors validationErrors, List<BrAPIImport> importRows, Program program, UUID importListId, boolean commit, PendingImport mappedImportRow, int rowIndex) {
BrAPIGermplasm existingGermplasm;
String gid = germplasm.getAccessionNumber();
boolean mutated = false;
boolean updatePedigree = false;

if (germplasmByAccessionNumber.containsKey(gid)) {
existingGermplasm = germplasmByAccessionNumber.get(gid).getBrAPIObject();
// Serialize and deserialize to deep copy
Expand All @@ -410,17 +419,26 @@ private boolean processExistingGermplasm(Germplasm germplasm, ValidationErrors v
}
}

if(germplasm.pedigreeExists()) {
// if no existing pedigree and file has pedigree then validate and update
if(germplasm.pedigreeExists() && !hasPedigree(existingGermplasm)) {
validatePedigree(germplasm, rowIndex + 2, validationErrors);
updatePedigree = true;
}

germplasm.updateBrAPIGermplasm(existingGermplasm, program, importListId, commit, true);

updatedGermplasmList.add(existingGermplasm);
mappedImportRow.setGermplasm(new PendingImportObject<>(ImportObjectState.MUTATED, existingGermplasm));
importList.addDataItem(existingGermplasm.getGermplasmName());
mutated = germplasm.updateBrAPIGermplasm(existingGermplasm, program, importListId, commit, updatePedigree);

if (mutated) {
updatedGermplasmList.add(existingGermplasm);
mappedImportRow.setGermplasm(new PendingImportObject<>(ImportObjectState.MUTATED, existingGermplasm));
if (updatePedigree) {
numNewPedigreeConnections++;
}
} else {
mappedImportRow.setGermplasm(new PendingImportObject<>(ImportObjectState.EXISTING, existingGermplasm));
}

// add to list regardless of mutated or not
importList.addDataItem(existingGermplasm.getGermplasmName());
return true;
}

Expand Down Expand Up @@ -523,20 +541,17 @@ private boolean canUpdatePedigreeNoEqualsCheck(BrAPIGermplasm existingGermplasm,
germplasm.pedigreeExists();
}

private Map<String, ImportPreviewStatistics> getStatisticsMap(List<BrAPIImport> importRows) {
private Map<String, ImportPreviewStatistics> getStatisticsMap() {

ImportPreviewStatistics germplasmStats = ImportPreviewStatistics.builder()
.newObjectCount(newGermplasmList.size())
.ignoredObjectCount(germplasmByAccessionNumber.size())
.build();

//Modified logic here to check for female parent accession number or entry no, removed check for male due to assumption that shouldn't have only male parent
int newObjectCount = newGermplasmList.stream().filter(newGermplasm -> newGermplasm != null).collect(Collectors.toList()).size();
// TODO: numNewPedigreeConnections is global modified in existing and new flows, refactor at some point
ImportPreviewStatistics pedigreeConnectStats = ImportPreviewStatistics.builder()
.newObjectCount(importRows.stream().filter(germplasmImport ->
germplasmImport.getGermplasm() != null &&
(germplasmImport.getGermplasm().getFemaleParentAccessionNumber() != null || germplasmImport.getGermplasm().getFemaleParentEntryNo() != null)
).collect(Collectors.toList()).size()).build();
.newObjectCount(numNewPedigreeConnections)
.build();

return Map.of(
"Germplasm", germplasmStats,
Expand Down Expand Up @@ -658,7 +673,8 @@ public void postBrapiData(Map<Integer, PendingImport> mappedBrAPIImport, Program
}

// Create list
if (!newGermplasmList.isEmpty() || !updatedGermplasmList.isEmpty()) {
// create & update flows both unconditionally add germplasm names to importList so use that for check
if (!importList.getData().isEmpty()) {
try {
// Create germplasm list
brAPIListDAO.createBrAPILists(List.of(importList), program.getId(), upload);
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ web:
logout:
url: ${web.base-url}
signup:
url-timeout: 60m
url-timeout: 24h
signup:
url: ${web.base-url}/signup
success:
Expand Down
18 changes: 2 additions & 16 deletions src/main/resources/brapi/sql/R__species.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,6 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.

-- See the NOTICE file distributed with this work for additional information
-- regarding copyright ownership.
--
-- 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.

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

DO $$
Expand All @@ -39,6 +24,7 @@ BEGIN
• Do it this way so no schema changes are required
• Removed the Honey Bee special case because all systems will be starting fresh
------------------------------------------------------------------------------------------ */

INSERT INTO crop (id, auth_user_id, crop_name)
SELECT
uuid_generate_v5('9a4deca9-4068-46a3-9efe-db0c181f491a'::uuid,
Expand All @@ -62,4 +48,4 @@ BEGIN
-- Only rewrite the row if name changed
UPDATE SET crop_name = EXCLUDED.crop_name
WHERE crop.crop_name IS DISTINCT FROM EXCLUDED.crop_name;
END $$;
END $$;
6 changes: 2 additions & 4 deletions src/main/resources/version.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,5 @@
# limitations under the License.
#


version=v1.2.0+1045
versionInfo=https://github.com/Breeding-Insight/bi-api/commit/85abdce79bee7d43816835a1750d4e6668f52d7b

version=v1.2.0+1055
versionInfo=https://github.com/Breeding-Insight/bi-api/commit/0a030718b0b626689e4e19a7acf17be481ac6add
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.util.Map;
import java.util.Objects;


import static io.micronaut.http.HttpRequest.GET;
import static io.micronaut.http.HttpRequest.POST;
import static org.junit.jupiter.api.Assertions.*;
Expand Down Expand Up @@ -243,6 +244,7 @@ public void getAllGermplasmListsSuccess() {
}
}
}

@ParameterizedTest
@CsvSource(value = {"CSV", "XLSX", "XLS"})
@SneakyThrows
Expand Down Expand Up @@ -275,6 +277,7 @@ public void germplasmListExport(String extension) {
int dataSize = download.rowCount();
assertEquals(3, dataSize, "Wrong number of germplasm were returned");
}

@Test
@SneakyThrows
public void getAllGermplasmByListSuccess() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,82 @@ public void importNewObsAfterFirstExpWithObs(boolean commit) {
}
}

/*
Scenario:
- an experiment was created with observations and timestamps
- do a second upload with additional observations for the experiment, but without the timestamp column
- verify the second set of observations get uploaded successfully
*/
@Test
@SneakyThrows
public void importNewObsAfterFirstExpWithObsAndTimestamps() {
log.debug("importNewObsAfterFirstExpWithObsAndTimestamps");
List<Trait> traits = importTestUtils.createTraits(2);
Program program = createProgram("Exp with TS and additional Uploads ", "EXTSAU", "EXTSAU", BRAPI_REFERENCE_SOURCE, createGermplasm(1), traits);
Map<String, Object> newExp = new HashMap<>();
newExp.put(Columns.GERMPLASM_GID, "1");
newExp.put(Columns.TEST_CHECK, "T");
newExp.put(Columns.EXP_TITLE, "Test Exp");
newExp.put(Columns.EXP_UNIT, "Plot");
newExp.put(Columns.EXP_TYPE, "Phenotyping");
newExp.put(Columns.ENV, "New Env");
newExp.put(Columns.ENV_LOCATION, "Location A");
newExp.put(Columns.ENV_YEAR, "2025");
newExp.put(Columns.EXP_UNIT_ID, "a-1");
newExp.put(Columns.REP_NUM, "1");
newExp.put(Columns.BLOCK_NUM, "1");
newExp.put(Columns.ROW, "1");
newExp.put(Columns.COLUMN, "1");
newExp.put(traits.get(0).getObservationVariableName(), "1");
newExp.put("TS:" + traits.get(0).getObservationVariableName(), "2019-12-19T12:14:50Z");

// In the first upload, only 1 trait should be present.
List<Trait> initialTraits = List.of(traits.get(0));
importTestUtils.uploadAndFetchWorkflow(importTestUtils.writeExperimentDataToFile(List.of(newExp), initialTraits, false), null, true, client, program, mappingId, newExperimentWorkflowId);

BrAPITrial brAPITrial = brAPITrialDAO.getTrialsByName(List.of((String)newExp.get(Columns.EXP_TITLE)), program).get(0);
Optional<BrAPIExternalReference> trialIdXref = Utilities.getExternalReference(brAPITrial.getExternalReferences(), String.format("%s/%s", BRAPI_REFERENCE_SOURCE, ExternalReferenceSource.TRIALS.getName()));
assertTrue(trialIdXref.isPresent());
BrAPIStudy brAPIStudy = brAPIStudyDAO.getStudiesByExperimentID(UUID.fromString(trialIdXref.get().getReferenceId()), program).get(0);

BrAPIObservationUnit ou = ouDAO.getObservationUnitsForStudyDbId(brAPIStudy.getStudyDbId(), program).get(0);
Optional<BrAPIExternalReference> ouIdXref = Utilities.getExternalReference(ou.getExternalReferences(), String.format("%s/%s", BRAPI_REFERENCE_SOURCE, ExternalReferenceSource.OBSERVATION_UNITS.getName()));
assertTrue(ouIdXref.isPresent());

Map<String, Object> newObservation = new HashMap<>();
newObservation.put(Columns.GERMPLASM_GID, "1");
newObservation.put(Columns.TEST_CHECK, "T");
newObservation.put(Columns.EXP_TITLE, "Test Exp");
newObservation.put(Columns.EXP_UNIT, "Plot");
newObservation.put(Columns.EXP_TYPE, "Phenotyping");
newObservation.put(Columns.ENV, "New Env");
newObservation.put(Columns.ENV_LOCATION, "Location A");
newObservation.put(Columns.ENV_YEAR, "2025");
newObservation.put(Columns.EXP_UNIT_ID, "a-1");
newObservation.put(Columns.REP_NUM, "1");
newObservation.put(Columns.BLOCK_NUM, "1");
newObservation.put(Columns.ROW, "1");
newObservation.put(Columns.COLUMN, "1");
newObservation.put("Plot "+OBSERVATION_UNIT_ID_SUFFIX, ouIdXref.get().getReferenceId());
newObservation.put(traits.get(0).getObservationVariableName(), "1");
newObservation.put(traits.get(1).getObservationVariableName(), "1");

// Send overwrite parameters in request body to allow the append workflow to work normally.
Map<String, String> userData = Map.of("overwrite", "true", "overwriteReason", "testing");
JsonObject result = importTestUtils.uploadAndFetchWorkflow(importTestUtils.writeExperimentDataToFile(List.of(newObservation), traits, true), userData, true, client, program, mappingId, appendOverwriteWorkflowId);

JsonArray previewRows = result.get("preview").getAsJsonObject().get("rows").getAsJsonArray();
assertEquals(1, previewRows.size());
JsonObject row = previewRows.get(0).getAsJsonObject();

assertEquals("EXISTING", row.getAsJsonObject("trial").get("state").getAsString());
assertEquals("EXISTING", row.getAsJsonObject("location").get("state").getAsString());
assertEquals("EXISTING", row.getAsJsonObject("study").get("state").getAsString());
assertEquals("EXISTING", row.getAsJsonObject("observationUnit").get("state").getAsString());
assertRowSaved(newObservation, program, traits);

}

/*
Scenario:
- Create an experiment with valid observations.
Expand Down