diff --git a/.gitignore b/.gitignore index 9d44d4a..b3db51d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ supervisord* .phpunit.result.cache +.DS_Store diff --git a/trpcultivate_germplasm/config/install/trpcultivate_germplasm.settings.yml b/trpcultivate_germplasm/config/install/trpcultivate_germplasm.settings.yml new file mode 100644 index 0000000..b97f76c --- /dev/null +++ b/trpcultivate_germplasm/config/install/trpcultivate_germplasm.settings.yml @@ -0,0 +1,14 @@ +terms: + accession: 0 + genus: 0 + species: 0 + name: 0 + institute_code: 0 + institute_name: 0 + pedigree: 0 + country_of_origin_code: 0 + synonym: 0 + biological_status_of_accession_code: 0 + breeding_method_DbId: 0 + subtaxa: 0 + stock_relationship_type_synonym: 0 \ No newline at end of file diff --git a/trpcultivate_germplasm/config/schema/trpcultivate_germplasm.schema.yml b/trpcultivate_germplasm/config/schema/trpcultivate_germplasm.schema.yml new file mode 100644 index 0000000..77701f4 --- /dev/null +++ b/trpcultivate_germplasm/config/schema/trpcultivate_germplasm.schema.yml @@ -0,0 +1,47 @@ +trpcultivate_germplasm.settings: + type: config_object + label: 'Germplasm Package Configuration' + mapping: + terms: + type: mapping + label: 'Package default terms' + mapping: + accession: + type: integer + label: 'The cvterm ID of the accession for a germplasm' + genus: + type: integer + label: 'The cvterm ID of the genus of a germplasm' + species: + type: integer + label: 'The cvterm ID of the species of a germplasm' + name: + type: integer + label: 'The cvterm ID of the name of a germplasm' + institute_code: + type: integer + label: 'The cvterm ID of the institute code where the germplasm was bred' + institute_name: + type: integer + label: 'The cvterm ID of the institute name where the germplasm was bred' + pedigree: + type: integer + label: 'The cvterm ID of the pedigree information for a germplasm' + country_of_origin_code: + type: integer + label: 'The cvterm ID of the country of origin of a germplasm' + synonym: + type: integer + label: 'The cvterm ID of a synonym of a germplasm name' + biological_status_of_accession_code: + type: integer + label: 'The cvterm ID of the biological status of a germplasm accession' + breeding_method_DbId: + type: integer + label: 'The cvterm ID of the germplasm breeding method' + subtaxa: + type: integer + label: 'The cvterm ID of the subtaxa of a germplasm' + stock_relationship_type_synonym: + type: integer + label: 'The cvterm ID of the stock_relationship synonym' diff --git a/trpcultivate_germplasm/src/Plugin/TripalImporter/GermplasmAccessionImporter.php b/trpcultivate_germplasm/src/Plugin/TripalImporter/GermplasmAccessionImporter.php new file mode 100644 index 0000000..5692a1e --- /dev/null +++ b/trpcultivate_germplasm/src/Plugin/TripalImporter/GermplasmAccessionImporter.php @@ -0,0 +1,911 @@ +create(). + * + * OVERRIDES create() from the parent, ChadoImporterBase.php, in order to introduce the + * config factory + * + * Since we have implemented the ContainerFactoryPluginInterface this static function + * will be called behind the scenes when a Plugin Manager uses createInstance(). Specifically + * this method is used to determine the parameters to pass to the contructor. + * + * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * @param array $configuration + * @param string $plugin_id + * @param mixed $plugin_definition + * + * @return static + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('tripal_chado.database'), + $container->get('config.factory') + ); + } + + /** + * Implements __contruct(). + * + * OVERRIDES __construct() from the parent, ChadoImporterBase.php, in order to introduce + * the config factory + * + * Since we have implemented the ContainerFactoryPluginInterface, the constructor + * will be passed additional parameters added by the create() function. This allows + * our plugin to use dependency injection without our plugin manager service needing + * to worry about it. + * + * @param array $configuration + * @param string $plugin_id + * @param mixed $plugin_definition + * @param Drupal\tripal_chado\Database\ChadoConnection $connection + * @param + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, ChadoConnection $connection, ConfigFactoryInterface $config_factory) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $connection); + + $this->config_factory = $config_factory; + } + + /** + * @{inheritdoc} + */ + public function describeUploadFileFormat() { + + $file_types = $this->plugin_definition['file_types']; + + $output = "The input file should be a tab separated file (" . implode(', ', $file_types) . ") with the following columns. "; + $output .= "For more detailed information on this format including links to lookup various codes, please see "; + $output .= 'the official documentation.'; + + $columns = [ + 'Germplasm Name' => 'Name of this germplasm accession (e.g. CDC Redberry)', + 'External Database' => 'The institution who assigned the accession. (e.g. KnowPulse Germplasm)', + 'Accession Number' => 'A unique identifier for the accession (e.g. KP:GERM58)', + 'Germplasm Species' => 'The species of the accession (e.g. culinaris)', + 'Germplasm Subtaxa' => 'The rank below species is specified first, followed by the name (e.g. var. medullare)', + 'Institute Code' => 'The code for the Institute that bred the material (e.g. CUAC)', + 'Institute Name' => 'The name of the Institute that bred the material (e.g. "Crop Development Center, University of Saskatchewan")', + 'Country of Origin Code' => '3-letter ISO 3166-1 code of the country in which the sample was originally sourced (e.g. 124)', + 'Biological Status of Accession' => 'The 3 digit code representing the biological status of the accession (e.g. 410)', + 'Breeding Method' => 'The unique identifier for the breeding method used to create this germplasm (e.g. "Recurrent selection")', + 'Pedigree' => 'The cross name and optional selection history (e.g. 1049F^3/819-5R)', + 'Synonyms' => 'Any synonyms of the accession, separated by a comma. (e.g. Redberry)', + ]; + + $required_col = ['Germplasm Name', 'External Database', 'Accession Number', 'Germplasm Species']; + // @TODO: Make this red, but Drupal makes it difficult :) + $required_markup = '*'; + + $output .= '
    '; + foreach ($columns as $title => $definition) { + if (in_array($title, $required_col)) { + $output .= '
  1. ' . $title . $required_markup . ': ' . $definition . '
  2. '; + } + else { + $output .= '
  3. ' . $title . ': ' . $definition . '
  4. '; + } + } + $output .= '
'; + + return $output; + } + + /** + * Set a cvterm with its cvterm_id + * + * @param string $key + * A key used in the config settings.yml + * @param int $cvterm_id + * @return TRUE + */ + public function setCVterm($key, $cvterm_id) { + $this->cvterms[$key] = $cvterm_id; + return TRUE; + } + + /** + * Get a cvterm ID, given a key that maps to the config settings.yml + * + * @param string $key + * The cvterm name + * @return int + * The cvterm ID + */ + public function getCVterm($key) { + return $this->cvterms[$key]; + } + + /** + * {@inheritDoc} + */ + public function form($form, &$form_state) { + $form = parent::form($form, $form_state); + + // Select the entire genus field and make sure it is sorted and distinct + $genus_query = $this->connection->select('1:organism', 'o') + ->fields('o',['genus']) + ->orderBy('genus') + ->distinct(); + $genus = $genus_query->execute()->fetchAllKeyed(0,0); + + $form['instructions'] = [ + '#weight' => -99, + '#markup' => ' +

Import Germplasm Accessions

+

Use this form to import germplasm accessions into Chado with metadata that meet the BrAPI standards. Please confirm the file format and column order before upload as this will insert records into your Chado database.

+ ', + ]; + + $form['genus_name'] = [ + '#weight' => -80, + '#type' => 'select', + '#title' => t('Genus'), + '#options' => $genus, + '#required' => TRUE, + '#description' => t('Select the genus of the germplasm accessions in your file. If your file consists of multiple genus, it is best practice to separate it into one file per genus and upload each one individually.'), + ]; + + return $form; + } + + /** + * @see TripalImporter::formValidate() + */ + public function formValidate($form, &$form_state){ + // Nothing to validate since the genus field is set to "required". + } + + /** + * Checks if our terms have been set already from the config file. + * This is helpful for automated test functionality where terms are + * set there using our setCVterm() function. + * If not already set, then the value of the term is set using setCVterm() + * here. + */ + public function setUpCVterms(){ + + $germplasm_config = $this->config_factory->get('trpcultivate_germplasm.settings'); + // Iterate through our cvterms + // If it hasn't been set before, set it now + foreach($this->cvterms as $term){ + if (!isset($this->cvterms[$term])){ + $terms_string = 'terms.' . $term; + $this->setCVterm($term, $germplasm_config->get($terms_string)); + } + } + } + + /** + * @see TripalImporter::run() + */ + public function run(){ + + // Grabbing our arguments from the form + $arguments = $this->getArguments(); + + // The path to the uploaded file is always made available using the + // 'files' argument. The importer can support multiple files, therefore + // this is an array of files, where each has a 'file_path' key specifying + // where the file is located on the server. + $file_path = $arguments['files'][0]['file_path']; + if (!file_exists($file_path)) { + throw new \Exception( + t("File does not exist: @file", ['@file' => $file_path]) + ); + } + + // Grab the genus name + $genus_name = $arguments['run_args']['genus_name']; + + // Make sure our CVterms are all set + $this->setUpCVterms(); + + // Check if the stock_synonym table exists before moving forward + if (!$this->connection->schema()->tableExists('stock_synonym')) { + throw new \Exception( + t("Could not find stock_synonym table in the current database schema.") + ); + } + + // Set up the ability to track progress so we can report it to the user + $filesize = filesize($file_path); + $this->setTotalItems($filesize); + $this->setItemsHandled(0); + $bytes_read = 0; + $line_count = 0; + + // Open the file and start iterating through each line + $GERMPLASM_FILE = fopen($file_path, 'r'); + if(!$GERMPLASM_FILE) { + throw new \Exception( + t("Could not open file: @file", ['@file' => $file_path]) + ); + } + + while (!feof($GERMPLASM_FILE)){ + $current_line = fgets($GERMPLASM_FILE); + $line_count++; + + // Calculate how many bytes we have read from the file and let the + // importer know how many have been processed so it can provide a + // progress indicator. + $bytes_read += mb_strlen($current_line); + $this->setItemsHandled($bytes_read); + + // Check for empty lines, comment lines and a header line + $current_line = trim($current_line); + if ($current_line == '') continue; + if (preg_match('/^#/', $current_line)) continue; + if (preg_match('/^Germplasm/i', $current_line)) continue; + + // Split our columns into an array for easier processing + $germplasm_columns = explode("\t", $current_line); + $num_columns = count($germplasm_columns); + if (count($germplasm_columns) < 4) { + $this->logger->error("Insufficient number of columns detected (<4) for line # @line", ['@line' => $line_count]); + $this->error_tracker = TRUE; + // Continue to next line since we already know this will cascade into + // further errors + continue; + } + + // Collect our values from our current line into variables + // Since the 1st 4 columns are required, make sure there are values there + for($i=0; $i<4; $i++) { + $column = $i+1; + if ($germplasm_columns[$i] == '') { + $this->logger->error("Column @column is required and cannot be empty for line # @line", ['@column' => $column, '@line' => $line_count]); + $this->error_tracker = TRUE; + // Continue to next line since we already know this will cascade into + // further errors + continue 2; + } + } + $germplasm_name = $germplasm_columns[0]; + $external_database = $germplasm_columns[1]; + $accession_number = $germplasm_columns[2]; + $germplasm_species = $germplasm_columns[3]; + $germplasm_subtaxa = $germplasm_columns[4] ?? ''; + $stock_properties = [ + 'institute_code' => $germplasm_columns[5] ?? '', + 'institute_name' => $germplasm_columns[6] ?? '', + 'country_of_origin_code' => $germplasm_columns[7] ?? '', + 'biological_status_of_accession_code' => $germplasm_columns[8] ?? '', + 'breeding_method_DbId' => $germplasm_columns[9] ?? '', + 'pedigree' => $germplasm_columns[10] ?? '' + ]; + $synonyms = $germplasm_columns[11] ?? ''; + + // Here we are calling 5 separate functions to check for and insert various + // parts of the input file. Everything is wrapped in a try-catch to ensure + // a useful error message can be passed onto the user and that all errors + // that the file encounters can be reported at one time and not committed + // to the database. + try { + // STEP 1: Pull out the organism ID for the current germplasm + $organism_id = $this->getOrganismID($genus_name, $germplasm_species, $germplasm_subtaxa); + + // STEP 2: Check/Insert this germplasm into the Chado stock table + if ($organism_id) { + $stock_id = $this->getStockID($germplasm_name, $accession_number, $organism_id); + } + + if (isset($stock_id) && ($stock_id != null)) { + // STEP 3: Load the external database info into Chado dbxref table + $dbxref_id = $this->getDbxrefID($external_database, $stock_id, $accession_number); + + // STEP 4: Load stock properties + $load_props = $this->loadStockProperties($stock_id, $stock_properties); + + // STEP 5: Load synonyms + $load_synonyms = $this->loadSynonyms($stock_id, $synonyms, $organism_id); + } + } catch ( \Exception $e ) { + $this->logger->error("An unusual error occurred when processing germplasm \"@germplasm\". Here is the stack trace: \n" . $e->getMessage() . "\n", ['@germplasm' => $germplasm_name] ); + $this->error_tracker = TRUE; + } + } + // Check the error flag + // If true, throw an exception explaining that nothing will be added to the database + // unless errors are resolved + if ($this->error_tracker) { + throw new \Exception( + t("The database transaction was not commited due to the presence of one or more errors. Please fix all errors and try the import again.") + ); + } + else { + $this->logger->notice("Reached end of file without encountering any errors. Transaction will be committed to the database."); + } + } + + /** + * Checks if an organism exists in Chado and returns the primary key, + * otherwise throws an error if the organism does not exist or there + * are multiple matches + * + * @param string $genus_name + * The genus of the organism. + * @param string $germplasm_species + * The species of the organism. + * @param string $germplasm_subtaxa + * Optional. Must consist of two strings, one of the subtaxon type + * followed by the name. For example: "subspecies chadoii". + * @return int|false + * The value of the primary key for the organism record in Chado. + * If no single primary key can be retrieved, then FALSE is returned. + */ + public function getOrganismID($genus_name, $germplasm_species, $germplasm_subtaxa) { + + $organism_name = $genus_name . ' ' . $germplasm_species; + if ($germplasm_subtaxa) { + $organism_name = $organism_name . ' ' . $germplasm_subtaxa; + } + $organism_array = chado_get_organism_id_from_scientific_name($organism_name); + + if (!$organism_array) { + $this->logger->error("Could not find an organism \"@organism_name\" in the database.", ['@organism_name' => $organism_name]); + $this->error_tracker = TRUE; + return false; + } + // We also want to check if we were given only one value back, as there is + // potential to retrieve multiple organism IDs + if (is_array($organism_array) && (count($organism_array) > 1)) { + $this->logger->error("Found more than one organism ID for \"@organism_name\" when only 1 was expected.", ['@organism_name' => $organism_name]); + $this->error_tracker = TRUE; + return false; + } + + return $organism_array[0]; + } + + /** + * Checks if a stock exists in Chado and if not, inserts it and returns the primary + * key in the stock table. If the stock already exists, logs an error + * + * @param string $germplasm_name + * The name of the germplasm. + * @param string $accession_number + * A unique identifier for the germplasm accession. + * @param int $organism_id + * The primary key of the stock's organism in the organism table + * @return int|false + * The value of the primary key for the stock record in Chado. If the stock already + * exists and does not match the accession number or type, or it cannot be inserted, + * then FALSE is returned. + */ + public function getStockID($germplasm_name, $accession_number, $organism_id) { + + $accession_type_id = $this->getCVterm('accession'); + + // First query the stock table: + // 1. Using a regular condition to ensure the organism_id is a match + // 2. Create an OR condition group to look for records that match germplasm name OR + // the uniquename. Since the unique constraint is organism_id/uniquename/type_id, + // we have to make sure this combo doesn't already exist with a different germplasm + // name. + $query = $this->connection->select('1:stock', 's') + ->fields('s', ['stock_id', 'name', 'uniquename', 'type_id']) + ->condition('s.organism_id', $organism_id, '='); + + $orGroup = $query->orConditionGroup() + ->condition('s.name', $germplasm_name, '=') + ->condition('s.uniquename', $accession_number, '='); + + // Now add the OR condition group to the query + $query->condition($orGroup); + $record = $query->execute()->fetchAll(); + + // We may have retrieved 1+ records that share the germplasm name and/or 1+ records that + // share the accession_number. In this case, throw an error since there's no way to + // enter a new record with a unique organism_id/uniquename/type_id combo in this scenario + if (sizeof($record) >= 2) { + $stock_string_array = []; + foreach ($record as $stock_hit) { + $stock_string = $stock_hit->name . " (uniquename=" . $stock_hit->uniquename . "; stock_id=" . $stock_hit->stock_id . ")"; + array_push($stock_string_array, $stock_string); + } + + $this->logger->error("Found more than one stock ID for \"@germplasm_name\" and/or \"@accession\". The existing stocks are: @stock_list", ['@germplasm_name' => $germplasm_name, '@accession' => $accession_number, '@stock_list' => implode(", ", $stock_string_array)]); + $this->error_tracker = TRUE; + return false; + } + + elseif (sizeof($record) == 1) { + // Handle the situation where a stock record exists + // Here we are individually checking that our uniquename, name and type_id all match + // what is in the input file. This is to provide an informative error message if one + // of these don't match. In the future, we may want to handle each case differently. + // For example, some groups may want to allow the same germplasm name but a different + // type_id to be allowed. + // 1. Check the uniquename matches the accession_number column in the file + if ($accession_number != $record[0]->uniquename) { + $this->logger->error("A stock already exists for \"@germplasm_name\" but with an accession of \"@accession\" which does not match the input file.", ['@germplasm_name' => $germplasm_name, '@accession' => $record[0]->uniquename]); + $this->error_tracker = TRUE; + return false; + } + // 2. Check that our germplasm name matches + if ($germplasm_name != $record[0]->name) { + $this->logger->error("A stock already exists for accession \"@accession\" but with a germplasm name of \"@germplasm_name\" which does not match the input file.", ['@germplasm_name' => $record[0]->name, '@accession' => $accession_number]); + $this->error_tracker = TRUE; + return false; + } + // 3. Check the type_id is of type accession + if ($accession_type_id != $record[0]->type_id) { + $this->logger->error("A stock already exists for \"@germplasm_name\" but with a type ID of \"@type\" which is not of type \"accession\".", ['@germplasm_name' => $germplasm_name, '@type' => $accession_type_id]); + $this->error_tracker = TRUE; + return false; + } + // Confirmed that the selected record matches what's in the upload file, so return the stock_id + return $record[0]->stock_id; + } + // Confirmed that a stock record doesn't yet exist, so now we create one + else { + $values = [ + 'organism_id' => $organism_id, + 'name' => $germplasm_name, + 'uniquename' => $accession_number, + 'type_id' => $accession_type_id + ]; + + $this->logger->notice("Inserting \"@germplasm_name\".", ['@germplasm_name' => $germplasm_name]); + + $result = $this->connection->insert('1:stock') + ->fields($values) + ->execute(); + + // If the primary key is available, then the insert worked and we can return it + if ($result) { + return $result; + } + else { + $this->logger->error("Insertion of \"@germplasm_name\" failed.", ['@germplasm_name' => $germplasm_name]); + $this->error_tracker = TRUE; + return false; + } + } + } + + /** + * Checks if a dbxref exists in Chado and if not, inserts it. Then, updates + * the stock table to include the dbxref_id. Returns the primary + * key in the dbxref table. + * + * @param string $external_database + * The name of the institution who assigned the accession. + * @param int $stock_id + * The value of the primary key for the stock record in Chado. + * @param string $accession_number + * A unique identifier for the germplasm accession. + * @return int|false + * The value of the primary key for the dbxref record in Chado. + */ + public function getDbxrefID($external_database, $stock_id, $accession_number) { + // ------------------------------------------------- + // Check if the external database exists in chado.db + // If not, report an error + // ------------------------------------------------- + $db_query = $this->connection->select('1:db', 'db') + ->fields('db', ['db_id']); + $db_query->condition('db.name', $external_database, '='); + $db_record = $db_query->execute()->fetchAll(); + + if (sizeof($db_record) >= 2) { + $this->logger->error("Found more than one db ID for \"@external_db\".", ['@external_db' => $external_database]); + $this->error_tracker = TRUE; + return false; + } + + elseif (sizeof($db_record) == 0) { + $this->logger->error("Unable to find \"@external_db\" in chado.db.", ['@external_db' => $external_database]); + $this->error_tracker = TRUE; + return false; + } + + // Confirmed that a single record of this external database exists + $db_id = $db_record[0]->db_id; + + // ------------------------------------------------- + // Check if the dbxref for this stock already exists + // If not, insert it + // ------------------------------------------------- + $dbx_query = $this->connection->select('1:dbxref', 'dbx') + ->fields('dbx', ['dbxref_id']); + $dbx_query->condition('dbx.accession', $accession_number, '=') + ->condition('dbx.db_id', $db_id, '='); + $dbx_record = $dbx_query->execute()->fetchAll(); + + if (sizeof($dbx_record) >= 2) { + $this->logger->error("Found more than one dbxref ID for \"@accession\".", ['@accession' => $accession_number]); + $this->error_tracker = TRUE; + return false; + } + elseif (sizeof($dbx_record) == 1) { + $dbxref_id = $dbx_record[0]->dbxref_id; + } + // Couldn't find the dbxref_id for this accession, so insert it + else { + $values = [ + 'db_id' => $db_id, + 'accession' => $accession_number + ]; + $result = $this->connection->insert('1:dbxref') + ->fields($values) + ->execute(); + + // If the primary key is not available, then the insert failed + if (!$result) { + $this->logger->error("Insertion of \"@accession\" into chado.dbxref failed.", ['@accession' => $accession_number]); + $this->error_tracker = TRUE; + return false; + } + else { + $dbxref_id = $result; + } + } + + // ------------------------------------------------------------------------ + // Update the stock table to include the dbxref_id + // After making sure a different one doesn't already exist (throw an error) + // ------------------------------------------------------------------------ + $stock_query = $this->connection->select('1:stock', 's') + ->fields('s', ['dbxref_id']); + $stock_query->condition('s.stock_id', $stock_id, '='); + $stock_record = $stock_query->execute()->fetchAll(); + + if ($stock_record[0]->dbxref_id == "") { + $update_stock = $this->connection->update('1:stock') + ->fields(['dbxref_id' => $dbxref_id]) + ->condition('stock_id', $stock_id, '=') + ->execute(); + + // Since update queries return the number of rows affected, check that only one row was changed + if ($update_stock != 1) { + $this->logger->error("An attempt to update the dbxref_id of \"@stock\" reported that \"@number\" rows were affected.", ['@stock' => $accession_number, '@number' => $update_stock]); + $this->error_tracker = TRUE; + return false; + } + else { + return $dbxref_id; + } + } + // Otherwise, the correct dbxref_id might already be set so we're good to go + elseif ($stock_record[0]->dbxref_id == $dbxref_id) { + return $dbxref_id; + } + // OR, it is something entirely different - so report an error + else { + $this->logger->error("There is already a primary dbxref_id for stock ID \"@stock\" that does not match the external database and accession provided in the file (@external_db:@accession).", ['@stock' => $stock_id, '@external_db' => $external_database, '@accession' => $accession_number]); + $this->error_tracker = TRUE; + return false; + } + + } + + /** + * Checks each property within an array and inserts them into chado.stockprop. + * Returns true if the insert was successful. + * + * @param int $stock_id + * The value of the primary key for the stock record in Chado. + * @param array $stock_properties + * An array of optional properties to be attached to a stock + * @return boolean + * Returns true if inserting all the properties was successful, + * including if there are no properties + */ + public function loadStockProperties($stock_id, $stock_properties) { + + foreach ($stock_properties as $property => $prop_value) { + + // Skip if the value of this property is empty + if (($prop_value !== '0') && empty($prop_value)) { continue; } + // Lookup the CV term + $cvterm_id = $this->getCVterm($property); + if ($cvterm_id) { + // Try to lookup the stockprop_id in Chado + $stockprop_query = $this->connection->select('1:stockprop', 'sp') + ->fields('sp', ['stockprop_id', 'value', 'rank']) + ->condition('sp.stock_id', $stock_id, '=') + ->condition('sp.type_id', $cvterm_id, '='); + $stockprop_record = $stockprop_query->execute()->fetchAll(); + // If one or more record(s) exists for this stock, check if one is the same as in + // the file. If not, then add it but increase the rank by 1 + if (sizeof($stockprop_record) >= 1) { + $maxrank = 0; + foreach ($stockprop_record as $record) { + $found = false; + if ($record->value == $prop_value) { + $found = true; + break; + } + else { + $rank = $record->rank; + if ($rank > $maxrank) { $maxrank = $rank; } + } + } + if ($found == false) { + // Insert this property into the stockprop table and increment the max rank by one + $values = [ + 'stock_id' => $stock_id, + 'type_id' => $cvterm_id, + 'value' => $prop_value, + 'rank' => ++$maxrank + ]; + $result = $this->connection->insert('1:stockprop') + ->fields($values) + ->execute(); + + // If the primary key is not available, then the insert failed + if (!$result) { + $this->logger->error("Insertion of stock property \"@property\" into chado.stockprop failed.", ['@property' => $property]); + $this->error_tracker = TRUE; + return false; + } + } + } + + // If no records exist, then insert the property as normal + else { + $values = [ + 'stock_id' => $stock_id, + 'type_id' => $cvterm_id, + 'value' => $prop_value + ]; + $result = $this->connection->insert('1:stockprop') + ->fields($values) + ->execute(); + + // If the primary key is not available, then the insert failed + if (!$result) { + $this->logger->error("Insertion of stock property \"@property\" into chado.stockprop failed.", ['@property' => $property]); + $this->error_tracker = TRUE; + return false; + } + } + } + else { + $this->logger->error("Unable to retrieve the cvterm_id of property \"@property\"", ['@property' => $property]); + $this->error_tracker = TRUE; + return false; + } + } + } + + /** + * Loads each synonym into the chado.synonym and chado.stock_synonym + * tables. Returns true if the insert was successful. + * + * @param int $stock_id + * The value of the primary key for the stock record in Chado. + * @param string $stock_properties + * The name that is a synonym of the current germplasm. Multiple + * synonyms may be specified, in which case they are expected to + * be separated using a comma or semicolon. + * @return boolean + * Returns true if inserting all the properties was successful, + * including if there are no properties + */ + public function loadSynonyms($stock_id, $synonyms, $organism_id) { + + if ($synonyms) { + // Separate out multiple synonyms if we have them by either + // semicolons or commas. Whitespace is optional + $all_synonyms = preg_split("/[;,]\s*/", $synonyms); + + foreach ($all_synonyms as $synonym) { + $synonym = trim($synonym); + $synonym_type_id = $this->getCVterm('synonym'); + + // Check for and load any synonyms to chado.synonym + $synonym_query = $this->connection->select('1:synonym', 's') + ->fields('s', ['synonym_id']) + ->condition('s.name', $synonym, '=') + ->condition('s.type_id', $synonym_type_id, '='); + $synonym_ids = $synonym_query->execute()->fetchCol(); + + // Make sure there aren't 2 or more records for this synonym + if (sizeof($synonym_ids) >= 2) { + $this->logger->error("Found more than one synonym for \"@synonym\" in chado.synonym (synonym_ids @ids).", ['@synonym' => $synonym, '@ids' => implode(', ', $synonym_ids)]); + $this->error_tracker = TRUE; + return false; + } + elseif (sizeof($synonym_ids) == 1) { + $synonym_id = $synonym_ids[0]; + } + // Can't find a synonym in the chado.synonym table, so insert it + else { + $values = [ + 'name' => $synonym, + 'type_id' => $synonym_type_id, + 'synonym_sgml' => '' + ]; + $result = $this->connection->insert('1:synonym') + ->fields($values) + ->execute(); + + // If the primary key is not available, then the insert failed + if (!$result) { + $this->logger->error("Insertion of \"@synonym\" into chado.synonym failed.", ['@synonym' => $synonym]); + $this->error_tracker = TRUE; + return false; + } + else { + $synonym_id = $result; + } + } + // ------------------------------------------------------------------------ + // Create a synonym-stock connection via chado.stock_synonym + // ------------------------------------------------------------------------ + // First check if this stock-synonym connection already exists + $synonym_stock_query = $this->connection->select('1:stock_synonym', 'ss') + ->fields('ss', ['stock_synonym_id']) + ->condition('ss.synonym_id', $synonym_id, '=') + ->condition('ss.stock_id', $stock_id, '='); + $synonym_stock_record = $synonym_stock_query->execute()->fetchAll(); + + // Make sure there aren't 2 or more records + // Should not be possible due to a unique constraint + if (sizeof($synonym_stock_record) >= 2) { + $this->logger->error("Found more than one stock-synonym connection for stock ID \"@stock\" and synonym \"@synonym\" in chado.stock_synonym.", ['@stock' => $stock_id, '@synonym' => $synonym]); + $this->error_tracker = TRUE; + return false; + } + // If 1 result was returned, just ignore it and move on + + // Otherwise, create it + elseif (sizeof($synonym_stock_record) == 0) { + $values = [ + 'synonym_id' => $synonym_id, + 'stock_id'=> $stock_id, + 'pub_id'=> '1', // Set to the NULL publication and hopefully someone will update it later :) + ]; + $result = $this->connection->insert('1:stock_synonym') + ->fields($values) + ->execute(); + + // If the primary key is not available, then the insert failed + if (!$result) { + $this->logger->error("Insertion of stock ID \"@stock\" and synonym \"@synonym\" into chado.stock_synonym failed.", ['@stock' => $stock_id, '@synonym' => $synonym]); + $this->error_tracker = TRUE; + return false; + } + } + + // ------------------------------------------------------------------------ + // Lastly, check if our synonym name is in the stock table. If yes, THEN create + // a stock_relationship to connect these 2 stocks (ie: the current stock and the + // stock matching the name of the synonym). + // ------------------------------------------------------------------------ + $stock_relationship_type_id = $this->getCVterm('stock_relationship_type_synonym'); + + $stock_query = $this->connection->select('1:stock', 'st') + ->fields('st', ['stock_id']) + ->condition('st.name', $synonym, '=') + ->condition('st.organism_id', $organism_id, '='); + $stock_record = $stock_query->execute()->fetchAll(); + + // Make sure there aren't 2 or more records + if (sizeof($stock_record) >= 2) { + $this->logger->notice("Found more than one match for synonym name \"@synonym\" in chado.stock.", ['@synonym' => $synonym]); + } + elseif (sizeof($stock_record) == 1) { + // Query the stock_relationship table to see if this relationship already exists + $stock_id_of_synonym = $stock_record[0]->stock_id; + $stock_relationship_query = $this->connection->select('1:stock_relationship', 'str') + ->fields('str', ['stock_relationship_id']) + ->condition('str.subject_id', $stock_id_of_synonym, '=') + ->condition('str.object_id', $stock_id, '=') + ->condition('type_id', $stock_relationship_type_id, '='); + $stock_relationship_record = $stock_relationship_query->execute()->fetchAll(); + // If 2+ relationships exist, then report an error + if (sizeof($stock_relationship_record) >= 2) { + $this->logger->error("Found more than one stock relationship for synonym name \"@synonym\" and stock ID \"@stock\" in chado.stock_relationship.", ['@synonym' => $synonym, '@stock' => $stock_id]); + $this->error_tracker = TRUE; + return false; + } + // If 1 result, carry on + + // If no results, create the stock relationship + if (sizeof($stock_relationship_record) == 0) { + $values = [ + 'subject_id' => $stock_id_of_synonym, + 'type_id' => $stock_relationship_type_id, + 'object_id' => $stock_id + ]; + $result = $this->connection->insert('1:stock_relationship') + ->fields($values) + ->execute(); + + // If the primary key is not available, then the insert failed + if (!$result) { + $this->logger->error("Insertion of stock ID \"@stock\" and stock ID of its synonym \"@sid_synonym\" into chado.stock_relationship failed.", ['@stock' => $stock_id, '@sid_synonym' => $stock_id_of_synonym]); + $this->error_tracker = TRUE; + return false; + } + } + } + else { + $this->logger->notice("Synonym \"@synonym\" was not found in the stock table, so no stock_relationship was made with stock ID \"@stock\".", ['@synonym' => $synonym, '@stock' => $stock_id]); + } + } + // Cycled through all the synonyms by this point, and if false hasn't been + // returned, then return true + return true; + } + } + /* + * {@inheritdoc} + */ + public function postRun() { + // Nothing to clean up. + } + + /** + * @see TripalImporter::formSubmit() + */ + public function formSubmit($form, &$form_state) { + + } +} diff --git a/trpcultivate_germplasm/tests/src/Fixtures/incomplete_example.txt b/trpcultivate_germplasm/tests/src/Fixtures/incomplete_example.txt new file mode 100644 index 0000000..7e05a4f --- /dev/null +++ b/trpcultivate_germplasm/tests/src/Fixtures/incomplete_example.txt @@ -0,0 +1,3 @@ +Germplasm Name External Database Accession Number Germplasm Species Germplasm Subtaxa Institute Code Institute Name Country of Origin Code Biological Status of Accession Breeding Method Pedigree Synonyms +Test1 TestDB T1 databasica subspecies chadoii +Test2 TestDB T1 databasica subspecies chadoii \ No newline at end of file diff --git a/trpcultivate_germplasm/tests/src/Fixtures/missing_required_example.txt b/trpcultivate_germplasm/tests/src/Fixtures/missing_required_example.txt new file mode 100644 index 0000000..0b297ba --- /dev/null +++ b/trpcultivate_germplasm/tests/src/Fixtures/missing_required_example.txt @@ -0,0 +1,8 @@ +# These +# are +# header +# lines + +Germplasm Name External Database Accession Number Germplasm Species Germplasm Subtaxa Institute Code Institute Name Country of Origin Code Biological Status of Accession Breeding Method Pedigree Synonyms +Test4 T4 databasica subspecies chadoii +Test3 TestDB diff --git a/trpcultivate_germplasm/tests/src/Fixtures/props_syns_example.txt b/trpcultivate_germplasm/tests/src/Fixtures/props_syns_example.txt new file mode 100644 index 0000000..8952927 --- /dev/null +++ b/trpcultivate_germplasm/tests/src/Fixtures/props_syns_example.txt @@ -0,0 +1,7 @@ +# These +# are +# header +# lines + +Germplasm Name External Database Accession Number Germplasm Species Germplasm Subtaxa Institute Code Institute Name Country of Origin Code Biological Status of Accession Breeding Method Pedigree Synonyms +Test5 TestDB T5 databasica subspecies chadoii 500 Breeder line synonym1;synonym2;synonym3 diff --git a/trpcultivate_germplasm/tests/src/Fixtures/simple_example.txt b/trpcultivate_germplasm/tests/src/Fixtures/simple_example.txt new file mode 100644 index 0000000..10547ac --- /dev/null +++ b/trpcultivate_germplasm/tests/src/Fixtures/simple_example.txt @@ -0,0 +1,3 @@ +Germplasm Name External Database Accession Number Germplasm Species Germplasm Subtaxa Institute Code Institute Name Country of Origin Code Biological Status of Accession Breeding Method Pedigree Synonyms +Test1 TestDB T1 databasica subspecies chadoii +Test2 TestDB T2 databasica subspecies chadoii \ No newline at end of file diff --git a/trpcultivate_germplasm/tests/src/Functional/InstallTest.php b/trpcultivate_germplasm/tests/src/Functional/InstallTest.php index c0a47e2..a445bc0 100644 --- a/trpcultivate_germplasm/tests/src/Functional/InstallTest.php +++ b/trpcultivate_germplasm/tests/src/Functional/InstallTest.php @@ -20,7 +20,7 @@ class InstallTest extends ChadoTestBrowserBase { * * @var array */ - protected static $modules = ['help', 'trpcultivate_germplasm']; + protected static $modules = ['help', 'tripal_chado']; /** * The name of your module in the .info.yml @@ -38,6 +38,25 @@ class InstallTest extends ChadoTestBrowserBase { */ protected static $help_text_excerpt = 'specialized Tripal fields and importers for germplasm'; + /** + * {@inheritdoc} + */ + protected function setUp() :void { + + parent::setUp(); + + // Ensure we see all logging in tests. + \Drupal::state()->set('is_a_test_environment', TRUE); + + // Open connection to Chado + $this->connection = $this->getTestSchema(ChadoTestBrowserBase::PREPARE_TEST_CHADO); + + $moduleHandler = $this->container->get('module_handler'); + $moduleInstaller = $this->container->get('module_installer'); + $this->assertFalse($moduleHandler->moduleExists('trpcultivate_germplasm')); + $this->assertTrue($moduleInstaller->install(['trpcultivate_germplasm'])); + } + /** * Tests that a specific set of pages load with a 200 response. */ diff --git a/trpcultivate_germplasm/tests/src/Kernel/TripalImporter/GermplasmAccessionImporter/GermplasmAccessionImporterRunTest.php b/trpcultivate_germplasm/tests/src/Kernel/TripalImporter/GermplasmAccessionImporter/GermplasmAccessionImporterRunTest.php new file mode 100644 index 0000000..2fb690a --- /dev/null +++ b/trpcultivate_germplasm/tests/src/Kernel/TripalImporter/GermplasmAccessionImporter/GermplasmAccessionImporterRunTest.php @@ -0,0 +1,385 @@ + [ + 'id' => 'trpcultivate-germplasm-accession', + 'label' => 'Tripal Cultivate: Germplasm Accessions', + 'description' => 'Imports germplasm accessions into Chado with metadata meeting BrAPI standards.', + 'file_types' => ["tsv", "txt"], + 'use_analysis' => FALSE, + 'require_analysis' => FALSE, + 'upload_title' => 'Germplasm Accession Import', + 'upload_description' => 'This should not be visible!', + 'button_text' => 'Import Germplasm Accessions', + 'file_upload' => True, + 'file_load' => True, + 'file_remote' => True, + 'file_required' => True, + 'cardinality' => 1, + ], + ]; + + // Make the organism ID accessible by all the functions + public $organism_id; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Ensure we see all logging in tests. + \Drupal::state()->set('is_a_test_environment', TRUE); + + // Open connection to Chado + $this->connection = $this->getTestSchema(ChadoTestKernelBase::PREPARE_TEST_CHADO); + + // Ensure we can access file_managed related functionality from Drupal. + // ... users need access to system.action config? + $this->installConfig(['system', 'trpcultivate_germplasm']); + // ... managed files are associated with a user. + $this->installEntitySchema('user'); + // ... Finally the file module + tables itself. + $this->installEntitySchema('file'); + $this->installSchema('file', ['file_usage']); + $this->installSchema('tripal_chado', ['tripal_custom_tables']); + // Ensure we have our tripal import tables. + $this->installSchema('tripal', ['tripal_import', 'tripal_jobs']); + // Create and log-in a user. + $this->setUpCurrentUser(); + + // We need to mock the logger to test the progress reporting. + $container = \Drupal::getContainer(); + $mock_logger = $this->getMockBuilder(\Drupal\tripal\Services\TripalLogger::class) + ->onlyMethods(['notice','error']) + ->getMock(); + $mock_logger->method('notice') + ->willReturnCallback(function($message, $context, $options) { + print str_replace(array_keys($context), $context, $message); + return NULL; + }); + $mock_logger->method('error') + ->willReturnCallback(function($message, $context, $options) { + print str_replace(array_keys($context), $context, $message); + return NULL; + }); + $container->set('tripal.logger', $mock_logger); + $this->logger = $mock_logger; + + $this->config_factory = \Drupal::configFactory(); + $this->importer = new \Drupal\trpcultivate_germplasm\Plugin\TripalImporter\GermplasmAccessionImporter( + [], + 'trpcultivate-germplasm-accession', + $this->definitions, + $this->connection, + $this->config_factory + ); + + $subtaxa_cvterm_id = $this->getCVtermID('TAXRANK', '0000023'); + $this->importer->setCVterm('accession', 9); + $this->importer->setCVterm('subtaxa', $subtaxa_cvterm_id); + $this->importer->setCVterm('institute_code', 10); + $this->importer->setCVterm('institute_name', 11); + $this->importer->setCVterm('country_of_origin_code',12); + $this->importer->setCVterm('biological_status_of_accession_code', 13); + $this->importer->setCVterm('breeding_method_DbId', 14); + $this->importer->setCVterm('pedigree', 15); + $this->importer->setCVterm('synonym', 16); + $this->importer->setCVterm('stock_relationship_type_synonym', 17); + + // Create the stock_synonym table + $this->createStockSynonymTable(); + + // Insert our organism + $subtaxa_cvterm_id = $this->importer->getCVterm('subtaxa'); + $this->organism_id = $this->connection->insert('1:organism') + ->fields([ + 'genus' => 'Tripalus', + 'species' => 'databasica', + 'infraspecific_name' => 'chadoii', + 'type_id' => $subtaxa_cvterm_id, + ]) + ->execute(); + + // Insert an external db + $db_id = $this->connection->insert('1:db') + ->fields([ + 'name' => 'TestDB', + ]) + ->execute(); + } + + /** + * Tests focusing on the Germplasm Accession Importer run() function + * using a simple example file that only populates required columns + * + * @group germ_accession_importer + */ + public function testGermplasmAccessionImporterRunSimple() { + + $simple_example_file = __DIR__ . '/../../../Fixtures/simple_example.txt'; + + $genus = 'Tripalus'; + $run_args = ['genus_name' => $genus, 'schema_name' => $this->testSchemaName]; + $file_details = ['file_local' => $simple_example_file]; + + $this->importer->createImportJob($run_args, $file_details); + $this->importer->prepareFiles(); + ob_start(); + tripal_run_importer_run($this->importer, $this->logger); + $printed_output = ob_get_clean(); + $this->assertStringContainsString('Inserting "Test2".', $printed_output, "Did not get the expected output when running the run() method on simple_example.txt."); + + // Now check the db for our 2 new stocks + $stock_query = $this->connection->select('1:stock', 's') + ->fields('s', ['organism_id', 'name', 'uniquename', 'type_id']); + $stock_record = $stock_query->execute()->fetchAll(); + + // Stock: Test1 + $this->assertEquals($stock_record[0]->organism_id, $this->organism_id, "The inserted organism ID and the selected organism ID for stock Test1 don't match."); + $this->assertEquals($stock_record[0]->name, 'Test1', "The inserted stock.name and the selected name for stock Test1 don't match."); + $this->assertEquals($stock_record[0]->uniquename, 'T1', "The inserted stock.uniquename and the selected uniquename for stock Test1 don't match."); + $this->assertEquals($stock_record[0]->type_id, 9, "The inserted type_id and the selected type_id for stock Test1 don't match."); + // Stock: Test2 + $this->assertEquals($stock_record[1]->organism_id, $this->organism_id, "The inserted organism ID and the selected organism ID for stock Test2 don't match."); + $this->assertEquals($stock_record[1]->name, 'Test2', "The inserted stock.name and the selected name for stock Test2 don't match."); + $this->assertEquals($stock_record[1]->uniquename, 'T2', "The inserted stock.uniquename and the selected uniquename for stock Test2 don't match."); + $this->assertEquals($stock_record[1]->type_id, 9, "The inserted type_id and the selected type_id for stock Test2 don't match."); + + // Make sure that the stockprop and synonyms table are empty + $stockprop_count_query = $this->connection->select('1:stockprop', 'sp') + ->countQuery()->execute()->fetchField(); + $this->assertEquals(0, $stockprop_count_query, "The row count of the stockprop table is not empty, despite there be no stock properties to insert from simple_example.txt."); + + $synonym_count_query = $this->connection->select('1:synonym', 'syn') + ->countQuery()->execute()->fetchField(); + $this->assertEquals(0, $synonym_count_query, "The row count of the synonym table is not empty, despite there be no syonyms to insert from simple_example.txt."); + } + + /** + * Tests focusing on the Germplasm Accession Importer run() function + * using a file where some required columns are missing + * + * @group germ_accession_importer + */ + public function testGermplasmAccessionImporterRunMissing() { + + $problem_example_file = __DIR__ . '/../../../Fixtures/missing_required_example.txt'; + + $genus = 'Tripalus'; + $run_args = ['genus_name' => $genus, 'schema_name' => $this->testSchemaName]; + $file_details = ['file_local' => $problem_example_file]; + + $this->importer->createImportJob($run_args, $file_details); + $this->importer->prepareFiles(); + + // Need a try-catch since errors in this file will trigger the error flag exception + $exception_caught = FALSE; + try { + ob_start(); + tripal_run_importer_run($this->importer, $this->logger); + } + catch ( \Exception $e ) { + $exception_caught = TRUE; + } + $printed_output = ob_get_clean(); + $this->assertTrue($exception_caught, 'Did not catch exception that should have occurred due to missing required columns.'); + $this->assertStringContainsString('Column 2 is required and cannot be empty for line # 7', $printed_output, "Did not get the expected output regarding line #7 when running the run() method on missing_required_example.txt."); + $this->assertStringContainsString('Insufficient number of columns detected (<4) for line # 8', $printed_output, "Did not get the expected output regarding line #8 when running the run() method on missing_required_example.txt."); + + // Double check that neither germplasm made it to the database + $stock_count_query = $this->connection->select('1:stock', 's') + ->countQuery()->execute()->fetchField(); + $this->assertEquals(0, $stock_count_query, "The row count of the stock table is not empty, despite expecting to skip stocks in missing_required_example.txt."); + } + + /** + * Tests focusing on the Germplasm Accession Importer run() function + * using a more complicated file where some optional columns are specified + * + * @group germ_accession_importer + */ + public function testGermplasmAccessionImporterRunComplex() { + + $complex_example_file = __DIR__ . '/../../../Fixtures/props_syns_example.txt'; + + $genus = 'Tripalus'; + $run_args = ['genus_name' => $genus, 'schema_name' => $this->testSchemaName]; + $file_details = ['file_local' => $complex_example_file]; + + $stock_type_id = $this->importer->getCVterm('accession'); + $stockprop_bsoac_type_id = $this->importer->getCVterm('biological_status_of_accession_code'); + $stockprop_bmDbId_type_id = $this->importer->getCVterm('breeding_method_DbId'); + $stock_relationship_type_id = $this->importer->getCVterm('stock_relationship_type_synonym'); + + // -------------------------------------------------------------------- + // 1. Insert one of the synonyms in our test file into the database as + // a stock. This way we can ensure a stock_relationship record is created + + $stock_id_of_synonym = $this->connection->insert('1:stock') + ->fields([ + 'organism_id' => $this->organism_id, + 'name' => 'synonym2', + 'uniquename' => 'synonym2', + 'type_id' => $stock_type_id, + ]) + ->execute(); + + $this->importer->createImportJob($run_args, $file_details); + $this->importer->prepareFiles(); + ob_start(); + tripal_run_importer_run($this->importer, $this->logger); + $printed_output = ob_get_clean(); + $this->assertStringContainsString('Inserting "Test5".Synonym "synonym1" was not found in the stock table, so no stock_relationship was made with stock ID "2".Synonym "synonym3" was not found in the stock table, so no stock_relationship was made with stock ID "2".', $printed_output, "Did not get the expected output regarding synonyms when running the run() method on props_syns_example.txt."); + + // -------------------------------------------------------------------- + // 2. Check that the stock properties inserted correctly + + // Count the number of stock properties in the database + $stockprop_count_query = $this->connection->select('1:stockprop', 'sp') + ->countQuery()->execute()->fetchField(); + $this->assertEquals(2, $stockprop_count_query, "The row count of the stockprop table after inserting 2 stock properties values is not correct."); + + // Grab the stock ID + $stock_query = $this->connection->select('1:stock', 's') + ->fields('s', ['stock_id']) + ->condition('name', 'Test5'); + $stock_record = $stock_query->execute()->fetchAll(); + $stock_id = $stock_record[0]->stock_id; + + $stockprop_query = $this->connection->select('1:stockprop', 'sp') + ->fields('sp', ['stock_id', 'type_id', 'value']); + $stockprop_records = $stockprop_query->execute()->fetchAll(); + $this->assertEquals($stockprop_records[0]->stock_id, $stock_id, 'The inserted stock_id and the existing stock_id for the first stockprop does not match for stock Test5.'); + $this->assertEquals($stockprop_records[0]->type_id, $stockprop_bsoac_type_id, 'The inserted type_id and the existing type_id for the first stockprop does not match for stock Test5.'); + $this->assertEquals($stockprop_records[0]->value, 500, 'The value of the inserted stock property "Biological Status of Accession" does not match what was in the file for stock Test5.'); + $this->assertEquals($stockprop_records[1]->stock_id, $stock_id, 'The inserted stock_id and the existing stock_id for the second stockprop does not match for stock Test5.'); + $this->assertEquals($stockprop_records[1]->type_id, $stockprop_bmDbId_type_id, 'The inserted type_id and the existing type_id for the second stockprop does not match for stock Test5.'); + $this->assertEquals($stockprop_records[1]->value, 'Breeder line', 'The value of the inserted stock property "Breeding Method" does not match what was in the file for stock Test5.'); + + // -------------------------------------------------------------------- + // 3. Check on our synonyms + + // Count number of synonyms in the synonym table + $synonym_count_query = $this->connection->select('1:synonym', 'sy') + ->countQuery()->execute()->fetchField(); + $this->assertEquals(3, $synonym_count_query, "Expected there to be 3 synonyms in the synonym table after inserting stock Test5."); + + // Count the number of records in stock_synonym + $stock_synonym_count_query = $this->connection->select('1:stock_synonym', 'ssy') + ->countQuery()->execute()->fetchField(); + $this->assertEquals(3, $stock_synonym_count_query, "Expected there to be 3 records in the stock_synonym table after inserting stock Test5."); + + // Count the number of records in stock_relationship + $stock_relationship_count_query = $this->connection->select('1:stock_relationship', 'sr') + ->countQuery()->execute()->fetchField(); + $this->assertEquals(1, $stock_relationship_count_query, "Expected there to be 1 record in the stock_relationship table after inserting stock Test5."); + + $stock_relationship_query = $this->connection->select('1:stock_relationship', 'sr') + ->fields('sr', ['subject_id', 'object_id', 'type_id', 'value']); + $stock_relationship_record = $stock_relationship_query->execute()->fetchAll(); + $this->assertEquals($stock_relationship_record[0]->subject_id, $stock_id_of_synonym, 'The subject ID of the stock_relationship that was inserted is not the expected stock ID of synonym2'); + $this->assertEquals($stock_relationship_record[0]->object_id, $stock_id, 'The object ID of the stock_relationship that was inserted is not the expected stock ID of Test5'); + $this->assertEquals($stock_relationship_record[0]->type_id, $stock_relationship_type_id, 'The type ID of the stock_relationship that was inserted is not of type synonym'); + } + + /** + * Tests focusing on the Germplasm Accession Importer run() function + * using an example file that should specifically cause an exception + * to occur due to the following cases: + * 1. A non-existant organism in the database + * 2. Attempt to insert a duplicate stock accession + * + * @group germ_accession_importer + */ + public function testGermplasmAccessionImporterRunIncomplete() { + + // Test for a non-existant file + $non_existant_file = __DIR__ . '/does_not_exist.txt'; + + $genus = 'Sally'; + $run_args = ['genus_name' => $genus, 'schema_name' => $this->testSchemaName]; + $file_details = ['file_local' => $non_existant_file]; + + $this->importer->createImportJob($run_args, $file_details); + $this->importer->prepareFiles(); + + $exception_caught = FALSE; + try { + tripal_run_importer_run($this->importer, $this->logger); + } catch ( \Exception $e ) { + $exception_caught = TRUE; + } + $this->assertStringContainsString("File does not exist:", $e->getMessage(), "Expected an exception message that file \"does_not_exist.txt\", does not, in fact, exist."); + $this->assertTrue($exception_caught, "Did not catch exception for a non-existant file as input."); + + $incomplete_example_file = __DIR__ . '/../../../Fixtures/incomplete_example.txt'; + + // Test for a non-existant organism + $genus = 'Sally'; + $run_args = ['genus_name' => $genus, 'schema_name' => $this->testSchemaName]; + $file_details = ['file_local' => $incomplete_example_file]; + + $this->importer->createImportJob($run_args, $file_details); + $this->importer->prepareFiles(); + + $exception_caught = FALSE; + ob_start(); + try { + tripal_run_importer_run($this->importer, $this->logger); + } catch ( \Exception $e ) { + $exception_caught = TRUE; + } + $printed_output = ob_get_clean(); + $this->assertStringContainsString("Could not find an organism", $printed_output, "Expected an error that an organism in the file 'incomplete_example.txt' could not be found in the database."); + $this->assertTrue($exception_caught, "Did not catch exception for trying to insert germplasm with a non-existant organism."); + + // Test for existing organism, but try to enter 2 germplasm with the same name but separate accession numbers + // Note: File used is still incomplete_example.txt + $genus = 'Tripalus'; + $run_args = ['genus_name' => $genus, 'schema_name' => $this->testSchemaName]; + + $this->importer->createImportJob($run_args, $file_details); + $this->importer->prepareFiles(); + + $exception_caught = FALSE; + ob_start(); + try { + tripal_run_importer_run($this->importer, $this->logger); + } catch ( \Exception $e ) { + $exception_caught = TRUE; + } + $printed_output = ob_get_clean(); + $this->assertStringContainsString('A stock already exists for accession "T1" but with a germplasm name of "Test1" which does not match the input file.', $printed_output, "Expected an error that a stock accession 'T1' already exists in the file 'incomplete_example.txt'"); + $this->assertTrue($exception_caught, "Did not catch exception for trying to insert duplicate stock accession numbers."); + + // Now query stock table to ensure the database transaction was + // successfully rolled back + $stock_count_query = $this->connection->select('1:stock', 's') + ->countQuery()->execute()->fetchField(); + $this->assertEquals(0, $stock_count_query, 'The chado.stock table is not empty despite a database rollback being triggered by an error.'); + } +} diff --git a/trpcultivate_germplasm/tests/src/Kernel/TripalImporter/GermplasmAccessionImporter/GermplasmAccessionImporterTest.php b/trpcultivate_germplasm/tests/src/Kernel/TripalImporter/GermplasmAccessionImporter/GermplasmAccessionImporterTest.php new file mode 100644 index 0000000..dac947f --- /dev/null +++ b/trpcultivate_germplasm/tests/src/Kernel/TripalImporter/GermplasmAccessionImporter/GermplasmAccessionImporterTest.php @@ -0,0 +1,685 @@ + [ + 'id' => 'trpcultivate-germplasm-accession', + 'label' => 'Tripal Cultivate: Germplasm Accessions', + 'description' => 'Imports germplasm accessions into Chado with metadata meeting BrAPI standards.', + 'file_types' => ["tsv", "txt"], + 'use_analysis' => FALSE, + 'require_analysis' => FALSE, + 'upload_title' => 'Germplasm Accession Import', + 'upload_description' => 'This should not be visible!', + 'button_text' => 'Import Germplasm Accessions', + 'file_upload' => True, + 'file_load' => True, + 'file_remote' => True, + 'file_required' => True, + 'cardinality' => 1, + ], + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Ensure we see all logging in tests. + \Drupal::state()->set('is_a_test_environment', TRUE); + + // Open connection to Chado + $this->connection = $this->getTestSchema(ChadoTestKernelBase::PREPARE_TEST_CHADO); + + // Ensure we can access file_managed related functionality from Drupal. + // ... users need access to system.action config? + $this->installConfig(['system', 'trpcultivate_germplasm']); + // ... managed files are associated with a user. + $this->installEntitySchema('user'); + // ... Finally the file module + tables itself. + $this->installEntitySchema('file'); + $this->installSchema('file', ['file_usage']); + $this->installSchema('tripal_chado', ['tripal_custom_tables']); + + // We need to mock the logger to test the progress reporting. + $container = \Drupal::getContainer(); + $mock_logger = $this->getMockBuilder(\Drupal\tripal\Services\TripalLogger::class) + ->onlyMethods(['notice','error']) + ->getMock(); + $mock_logger->method('notice') + ->willReturnCallback(function($message, $context, $options) { + print str_replace(array_keys($context), $context, $message); + return NULL; + }); + $mock_logger->method('error') + ->willReturnCallback(function($message, $context, $options) { + print str_replace(array_keys($context), $context, $message); + return NULL; + }); + $container->set('tripal.logger', $mock_logger); + + $this->config_factory = \Drupal::configFactory(); + $this->importer = new \Drupal\trpcultivate_germplasm\Plugin\TripalImporter\GermplasmAccessionImporter( + [], + 'trpcultivate-germplasm-accession', + $this->definitions, + $this->connection, + $this->config_factory + ); + + $subtaxa_cvterm_id = $this->getCVtermID('TAXRANK', '0000023'); + $this->importer->setCVterm('accession', 9); + $this->importer->setCVterm('subtaxa', $subtaxa_cvterm_id); + $this->importer->setCVterm('institute_code', 10); + $this->importer->setCVterm('institute_name', 11); + $this->importer->setCVterm('country_of_origin_code',12); + $this->importer->setCVterm('biological_status_of_accession_code', 13); + $this->importer->setCVterm('breeding_method_DbId', 14); + $this->importer->setCVterm('pedigree', 15); + $this->importer->setCVterm('synonym', 16); + $this->importer->setCVterm('stock_relationship_type_synonym', 17); + + $this->createStockSynonymTable(); + } + + /** + * Tests focusing on the Germplasm Accession Importer form. + * + * @group germ_accession_importer + */ + public function testGermplasmAccessionImporterForm() { + + $plugin_id = 'trpcultivate-germplasm-accession'; + + // Build the form using the Drupal form builder. + $form = \Drupal::formBuilder()->getForm( + 'Drupal\tripal\Form\TripalImporterForm', + $plugin_id + ); + // Ensure we are able to build the form. + $this->assertIsArray($form, + 'We expect the form builder to return a form but it did not.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // Now that we have provided a plugin_id, we expect it to have... + // title matching our importer label. + $this->assertArrayHasKey('#title', $form, + "The form should have a title set."); + $this->assertEquals('Tripal Cultivate: Germplasm Accessions', $form['#title'], + "The title should match the label annotated for our plugin."); + // the plugin_id stored in a value form element. + $this->assertArrayHasKey('importer_plugin_id', $form, + "The form should have an element to save the plugin_id."); + $this->assertEquals($plugin_id, $form['importer_plugin_id']['#value'], + "The importer_plugin_id[#value] should be set to our plugin_id."); + // a submit button. + $this->assertArrayHasKey('button', $form, + "The form should not have a submit button since we indicated a specific importer."); + + // We should also have our importer-specific form elements added to the form! + $this->assertArrayHasKey('instructions', $form, + "The form should include an instructions form element."); + $this->assertArrayHasKey('genus_name', $form, + "The form should include a genus_name form element."); + $this->assertArrayHasKey('file', $form, + "The form should include a file form element."); + + // Our default annotation indicates there should be no analysis element. + $this->assertArrayNotHasKey('analysis_id', $form, + "The from should not include analysis element, yet one exists."); + } + + /** + * Tests focusing on the Germplasm Accession Importer getOrganismID() function + * + * @group germ_accession_importer + */ + public function testGermplasmAccessionImporterGetOrganismID() { + + // Insert an organism + $subtaxa_cvterm_id = $this->getCVtermID('TAXRANK', '0000023'); + + $organism_id = $this->connection->insert('1:organism') + ->fields([ + 'genus' => 'Tripalus', + 'species' => 'databasica', + 'infraspecific_name' => 'chadoii', + 'type_id' => $subtaxa_cvterm_id, + ]) + ->execute(); + + $grabbed_organism_id = $this->importer->getOrganismID('Tripalus', 'databasica', 'subspecies chadoii'); + $this->assertEquals($grabbed_organism_id, $organism_id, "The organism ID grabbed by the importer does not match the one that was inserted into the database."); + + // Try an organism that does not currently exist + ob_start(); + $non_existent_organism_id = $this->importer->getOrganismID('Nullus', 'organismus', ''); + $printed_output = ob_get_clean(); + $this->assertEquals('Could not find an organism "Nullus organismus" in the database.', $printed_output, + "Did not get the expected error message when testing for a non-existant organism."); + + // Not testing if multiple organisms are retrieved, since Chado should be preventing such a situation + } + + /** + * Tests focusing on the Germplasm Accession Importer getStockID() function + * + * @group germ_accession_importer + */ + public function testGermplasmAccessionImporterGetStockID() { + + // Insert an organism + $subtaxa_cvterm_id = $this->getCVtermID('TAXRANK', '0000023'); + + $organism_id = $this->connection->insert('1:organism') + ->fields([ + 'genus' => 'Tripalus', + 'species' => 'databasica', + 'infraspecific_name' => 'chadoii', + 'type_id' => $subtaxa_cvterm_id, + ]) + ->execute(); + + // Insert a stock + $stock_id = $this->connection->insert('1:stock') + ->fields([ + 'organism_id' => $organism_id, + 'name' => 'stock1', + 'uniquename' => 'TEST:1', + 'type_id' => 9, + ]) + ->execute(); + + // Test that the stock just inserted gets selected + $grabbed_stock_id = $this->importer->getStockID('stock1', 'TEST:1', $organism_id); + $this->assertEquals($grabbed_stock_id, $stock_id, "The stock ID grabbed by the importer does not match the one that was inserted into the database."); + + // Test that a stock not in the database successfully gets inserted + ob_start(); + $created_stock_id = $this->importer->getStockID('stock2', 'TEST:2', $organism_id); + $printed_output = ob_get_clean(); + $this->assertEquals('Inserting "stock2".', $printed_output, + "Did not get the expected notice message when inserting a new stock."); + + $stock2_query = $this->connection->select('1:stock', 's') + ->fields('s', ['stock_id']) + ->condition('organism_id', $organism_id, '=') + ->condition('name', 'stock2', '=') + ->condition('uniquename', 'TEST:2', '=') + ->condition('type_id', 9, '='); + $stock2_record = $stock2_query->execute()->fetchAll(); + $this->assertEquals($created_stock_id, $stock2_record[0]->stock_id, "The stock ID inserted for \"stock2\" does not match the stock ID returned by getStockID()."); + + // No test for if the insert fails, since most likely will get a complaint from Chado + + // Test for a stock name + organism that already exists but has a different accession + ob_start(); + $grabbed_dup_stock_name = $this->importer->getStockID('stock1', 'TEST:1000', $organism_id); + $printed_output = ob_get_clean(); + $this->assertEquals('A stock already exists for "stock1" but with an accession of "TEST:1" which does not match the input file.', $printed_output, + "Did not get the expected error message when testing for duplicate stock names."); + + // Now test for multiple stocks with the same name and accession, but different type_id + $stock_id = $this->connection->insert('1:stock') + ->fields([ + 'organism_id' => $organism_id, + 'name' => 'stock1', + 'uniquename' => 'TEST:1', + 'type_id' => 10, + ]) + ->execute(); + + ob_start(); + $grabbed_dup_stock_id = $this->importer->getStockID('stock1', 'TEST:1', $organism_id); + $printed_output = ob_get_clean(); + $this->assertStringContainsString('Found more than one stock ID for "stock1"', $printed_output, "Did not get the expected error message when testing for duplicate stock IDs."); + } + + /** + * Tests focusing on the Germplasm Accession Importer getDbxrefID() function + * + * @group germ_accession_importer + */ + public function testGermplasmAccessionImporterGetDbxrefID() { + + // Insert an organism + $subtaxa_cvterm_id = $this->getCVtermID('TAXRANK', '0000023'); + + $organism_id = $this->connection->insert('1:organism') + ->fields([ + 'genus' => 'Tripalus', + 'species' => 'databasica', + 'infraspecific_name' => 'chadoii', + 'type_id' => $subtaxa_cvterm_id, + ]) + ->execute(); + + // Insert a stock + $accession = 'TEST:1'; + + $stock_id = $this->connection->insert('1:stock') + ->fields([ + 'organism_id' => $organism_id, + 'name' => 'stock1', + 'uniquename' => $accession, + 'type_id' => 9, + ]) + ->execute(); + + // Attempt to call the function before inserting an external database + ob_start(); + $non_existing_external_db = $this->importer->getDbxrefID('PRETEND', $stock_id, $accession); + $printed_output = ob_get_clean(); + $this->assertEquals('Unable to find "PRETEND" in chado.db.', $printed_output, + "Did not get the expected error message when looking up an external database that does not yet exist."); + + // Verify that the stock has an empty dbxref_id + $empty_stock_query = $this->connection->select('1:stock', 's') + ->fields('s', ['dbxref_id']); + $empty_stock_query->condition('s.stock_id', $stock_id, '='); + $empty_stock_record = $empty_stock_query->execute()->fetchAll(); + + $this->assertEmpty($empty_stock_record[0]->dbxref_id, "The stock just inserted (stock1) already has a dbxref_id."); + + // Now add an external db + $db_id = $this->connection->insert('1:db') + ->fields([ + 'name' => 'Test DB', + ]) + ->execute(); + + // ----------------------------- ROUND 1 ------------------------------- + // Call the function and check that dbxref is inserted and stock updated + $round_one_dbxref = $this->importer->getDbxrefID('Test DB', $stock_id, $accession); + + // Check that the dbxref was inserted successfully + $r1_dbx_query = $this->connection->select('1:dbxref', 'dbx') + ->fields('dbx', ['dbxref_id']); + $r1_dbx_query->condition('dbx.accession', $accession, '=') + ->condition('dbx.db_id', $db_id, '='); + $r1_dbx_record = $r1_dbx_query->execute()->fetchAll(); + + $this->assertEquals($round_one_dbxref, $r1_dbx_record[0]->dbxref_id, "The dbxref_id that was inserted does not match what was queried."); + + // Check that the stock was updated successfully + $updated_stock_query = $this->connection->select('1:stock', 's') + ->fields('s', ['dbxref_id']); + $updated_stock_query->condition('s.stock_id', $stock_id, '='); + $updated_stock_record = $updated_stock_query->execute()->fetchAll(); + + $this->assertEquals($round_one_dbxref, $updated_stock_record[0]->dbxref_id, "The stock just inserted (stock1) was not successfully updated with a dbxref_id."); + + // ----------------------------- ROUND 2 ------------------------------- + // Call the function again and check that the results are still the same + // The purpose of this test is to trigger the elseif statements for both + // the dbxref check and stock.dbxref_id + $round_two_dbxref = $this->importer->getDbxrefID('Test DB', $stock_id, $accession); + + // Check that the dbxref was selected successfully + $r2_dbx_query = $this->connection->select('1:dbxref', 'dbx') + ->fields('dbx', ['dbxref_id']); + $r2_dbx_query->condition('dbx.accession', $accession, '=') + ->condition('dbx.db_id', $db_id, '='); + $r2_dbx_record = $r2_dbx_query->execute()->fetchAll(); + + $this->assertEquals($round_two_dbxref, $r2_dbx_record[0]->dbxref_id, "The dbxref_id has changed unexpectedly in round 2."); + + // Check that the stock was updated successfully + $r2_updated_stock_query = $this->connection->select('1:stock', 's') + ->fields('s', ['dbxref_id']); + $r2_updated_stock_query->condition('s.stock_id', $stock_id, '='); + $r2_updated_stock_record = $r2_updated_stock_query->execute()->fetchAll(); + + $this->assertEquals($round_two_dbxref, $r2_updated_stock_record[0]->dbxref_id, "The dbxref_id of stock1 was unexpectedly changed in round 2 from round 1."); + // --------------------------------------------------------------------- + // Manually insert a dbxref with the same accession but a different db_id + $second_db_name = 'Second Test DB'; + $second_db_id = $this->connection->insert('1:db') + ->fields([ + 'name' => $second_db_name, + ]) + ->execute(); + + $second_dbxref_id = $this->connection->insert('1:dbxref') + ->fields([ + 'db_id' => $second_db_id, + 'accession' => $accession + ]) + ->execute(); + + ob_start(); + $multiple_dbxref_accessions = $this->importer->getDbxrefID($second_db_name, $stock_id, $accession); + $printed_output = ob_get_clean(); + $this->assertEquals('There is already a primary dbxref_id for stock ID "1" that does not match the external database and accession provided in the file (Second Test DB:TEST:1).', $printed_output, + "Did not get the expected error message when inserting a dbxref with an existing accession with a different db."); + } + + /** + * Tests focusing on the Germplasm Accession Importer loadStockProperties() function + * + * @group germ_accession_importer + */ + public function testGermplasmAccessionImporterLoadStockProperties() { + // Insert an organism + $subtaxa_cvterm_id = $this->getCVtermID('TAXRANK', '0000023'); + + $organism_id = $this->connection->insert('1:organism') + ->fields([ + 'genus' => 'Tripalus', + 'species' => 'databasica', + 'infraspecific_name' => 'chadoii', + 'type_id' => $subtaxa_cvterm_id, + ]) + ->execute(); + + // Insert a stock + $stock_id = $this->connection->insert('1:stock') + ->fields([ + 'organism_id' => $organism_id, + 'name' => 'stock1', + 'uniquename' => 'TEST:1', + 'type_id' => 9, + ]) + ->execute(); + + // Declare our stock property variables with empty values + $empty_string = ''; + $stock_empty_props = [ + 'institute_code' => $empty_string, + 'institute_name' => $empty_string, + 'country_of_origin_code' => $empty_string, + 'biological_status_of_accession_code' => $empty_string, + 'breeding_method_DbId' => $empty_string, + 'pedigree' => $empty_string + ]; + + // "Load" our empty values and check that the stockprop table is empty + // before and after + $sp_initial_count = $this->connection->select('1:stockprop', 'sp') + ->condition('sp.stock_id', $stock_id, '=') + ->countQuery()->execute()->fetchField(); + $this->importer->loadStockProperties($stock_id, $stock_empty_props); + $sp_empty_count = $this->connection->select('1:stockprop', 'sp') + ->condition('sp.stock_id', $stock_id, '=') + ->countQuery()->execute()->fetchField(); + $this->assertEquals($sp_initial_count, $sp_empty_count, "The row count of the stockprop table before and after inserting empty values is not the same."); + + // Now load in all 6 stock properties for this stock_id + $stock_props = [ + 'institute_code' => 'CUAC', + 'institute_name' => 'Crop Development Center, University of Saskatchewan', + 'country_of_origin_code' => 124, + 'biological_status_of_accession_code' => 410, + 'breeding_method_DbId' => 'Recurrent selection', + 'pedigree' => '1049F^3/819-5R' + ]; + $stock_prop_count = count($stock_props); + + $this->importer->loadStockProperties($stock_id, $stock_props); + $sp_six_count = $this->connection->select('1:stockprop', 'sp') + ->condition('sp.stock_id', $stock_id, '=') + ->countQuery()->execute()->fetchField(); + $this->assertEquals($sp_six_count, $stock_prop_count, "The row count of the stockprop table after inserting 6 values is not correct."); + + // Select a random property to see if it inserted correctly + $sp_six_institute_name = $this->connection->select('1:stockprop', 'sp') + ->fields('sp', ['value']) + ->condition('sp.stock_id', $stock_id, '=') + ->condition('sp.type_id', 11, '='); + $sp_six_institute_name_record = $sp_six_institute_name->execute()->fetchAll(); + $this->assertEquals($sp_six_institute_name_record[0]->value,'Crop Development Center, University of Saskatchewan', "The selected stockprop value for institute name does not match what was inserted."); + + // Now add some new properties for the same stock_id + $new_stock_props = [ + 'biological_status_of_accession_code' => 500, // New value + 'breeding_method_DbId' => 'Breeder line', // New value + 'pedigree' => '1049F^3/819-5R' // Old value + ]; + $new_sp_count = count($new_stock_props); + // 2 should be added to the stockprop table, 1 should not + $total_expected_sp_count = $sp_six_count + $new_sp_count - 1; + + $this->importer->loadStockProperties($stock_id, $new_stock_props); + $sp_eight_count = $this->connection->select('1:stockprop', 'sp') + ->condition('sp.stock_id', $stock_id, '=') + ->countQuery()->execute()->fetchField(); + $this->assertEquals($sp_eight_count, $total_expected_sp_count, "The row count of the stockprop table after inserting 2 additional values is not correct."); + + // Select one of our new properties and compare the ranks + $sp_eight_breeding_method_DbId = $this->connection->select('1:stockprop', 'sp') + ->fields('sp', ['value', 'rank']) + ->condition('sp.stock_id', $stock_id, '=') + ->condition('sp.type_id', 14, '='); + $sp_eight_breeding_method_DbId_record = $sp_eight_breeding_method_DbId->execute()->fetchAll(); + $first_value = $sp_eight_breeding_method_DbId_record[0]->value; + $this->assertEquals($first_value,'Recurrent selection', "The selected stockprop value for the first breeding_method_db_id does not match what was inserted."); + $second_value = $sp_eight_breeding_method_DbId_record[1]->value; + $this->assertEquals($second_value,'Breeder line', "The selected stockprop value for the second breeding_method_db_id does not match what was inserted."); + + $first_rank = $sp_eight_breeding_method_DbId_record[0]->rank; + $second_rank = $sp_eight_breeding_method_DbId_record[1]->rank; + $this->assertGreaterThan($first_rank, $second_rank, "The rank of the second inserted stockprop for breeding_method_db_id is not greater than the first one."); + + // Ensure only one record is retrieved for pedigree since the second array + // contained an identical value for it + $sp_eight_pedigree_count = $this->connection->select('1:stockprop', 'sp') + ->condition('sp.stock_id', $stock_id, '=') + ->condition('sp.type_id', 15, '=') + ->countQuery()->execute()->fetchField(); + $this->assertEquals($sp_eight_pedigree_count, 1, "The number of records for stockprop pedigree is not 1."); + } + + /** + * Tests focusing on the Germplasm Accession Importer loadSynonyms() function + * + * @group germ_accession_importer + */ + public function testGermplasmAccessionImporterLoadSynonyms() { + + // Insert an organism + $subtaxa_cvterm_id = $this->getCVtermID('TAXRANK', '0000023'); + + $organism_id = $this->connection->insert('1:organism') + ->fields([ + 'genus' => 'Tripalus', + 'species' => 'databasica', + 'infraspecific_name' => 'chadoii', + 'type_id' => $subtaxa_cvterm_id, + ]) + ->execute(); + + // Insert a stock + $stock_id = $this->connection->insert('1:stock') + ->fields([ + 'organism_id' => $organism_id, + 'name' => 'stock1', + 'uniquename' => 'TEST:1', + 'type_id' => 9, + ]) + ->execute(); + + // Attempt to load an empty string (ie. an empty column in the file) + $stock1_synonym_empty = ''; + $this->importer->loadSynonyms($stock_id, $stock1_synonym_empty, $organism_id); + + // Make sure no synonyms were entered + $synonym_empty_count = $this->connection->select('1:synonym', 's') + ->fields('s', ['name']) + ->condition('s.name', $stock1_synonym_empty, '=') + ->countQuery()->execute()->fetchField(); + + $this->assertEquals($synonym_empty_count, 0, "The number of record in the synonym table is not zero despite trying to add an empty string."); + + // ------------------------------------------------------------------------ + // Load a single synonym + $stock1_synonym_1 = 's1'; + ob_start(); + $this->importer->loadSynonyms($stock_id, $stock1_synonym_1, $organism_id); + $printed_output = ob_get_clean(); + + // STEP 1: Check the synonym table + $stock1_synonym_query = $this->connection->select('1:synonym', 's') + ->fields('s', ['synonym_id', 'name']) + ->condition('s.name', $stock1_synonym_1, '='); + $stock1_synonym_record = $stock1_synonym_query->execute()->fetchAll(); + + $this->assertEquals($stock1_synonym_record[0]->name, $stock1_synonym_1, "The selected synonym in the synonym table does not match what was just inserted."); + + // STEP 2: Check the stock_synonym table + // Grab the synonym_id to pull it out of the stock_synonym table + $s1_synonym_id = $stock1_synonym_record[0]->synonym_id; + + $stock1_stock_synonym_query = $this->connection->select('1:stock_synonym', 'ss') + ->fields('ss', ['stock_id', 'synonym_id']) + ->condition('ss.synonym_id', $s1_synonym_id, '='); + $stock1_stock_synonym_record = $stock1_stock_synonym_query->execute()->fetchAll(); + + $this->assertEquals($stock1_stock_synonym_record[0]->stock_id, $stock_id, "The synonym s1 in the stock_synonym table does not contain the correct stock_id."); + + // STEP 3: Check the stock_relationship table. We expect to have 0 records + // since the stock_relationship only gets created if the synonym itself is + // in the stock table + $stock1_stock_relationship_count = $this->connection->select('1:stock_relationship', 'sr') + ->fields('sr', ['subject_id', 'object_id']) + ->condition('sr.subject_id', $s1_synonym_id, '=') + ->condition('sr.object_id', $stock_id, '=') + ->countQuery()->execute()->fetchField(); + + $this->assertEquals($stock1_stock_relationship_count, 0, "Did not expect for one or more stock_relationships to be present in the stock_relationship table."); + + // We can also check the output of our command as we expect a notice to be given + $this->assertEquals('Synonym "s1" was not found in the stock table, so no stock_relationship was made with stock ID "1".', $printed_output, + "Did not get the expected notice message when adding a synonym that does not exist in the stock table."); + + // ------------------------------------------------------------------------ + + // Attempt to insert another synonym for stock1. This time, also add the + // synonym to the stock table and confirm that we have a stock_relationship + $stock1_synonym_2 = 's1_2'; + $stock_id_of_synonym = $this->connection->insert('1:stock') + ->fields([ + 'organism_id' => $organism_id, + 'name' => $stock1_synonym_2, + 'uniquename' => 'TEST:2', + 'type_id' => 9, + ]) + ->execute(); + + $this->importer->loadSynonyms($stock_id, $stock1_synonym_2, $organism_id); + + // Make sure this synonym was added to chado.synonym + $stock1_synonym_2_query = $this->connection->select('1:synonym', 's') + ->fields('s', ['name']) + ->condition('s.name', $stock1_synonym_2, '='); + $stock1_synonym_2_record = $stock1_synonym_2_query->execute()->fetchAll(); + + $this->assertEquals($stock1_synonym_2_record[0]->name, $stock1_synonym_2, "The second synonym added to the synonym table does not match what was just inserted."); + + // Check that we now have 2 records in chado.stock_synonym + $stock_synonym_count = $this->connection->select('1:stock_synonym', 'ss') + ->fields('ss', ['stock_synonym_id']) + ->countQuery()->execute()->fetchField(); + + $this->assertEquals($stock_synonym_count, 2, "The stock_synonym table does not contain the expected 2 records."); + + // Check that we have exactly 1 record in the stock_relationship table now + $stock1_stock_relationship_synonym2_count = $this->connection->select('1:stock_relationship', 'sr') + ->fields('sr', ['subject_id', 'object_id']) + ->condition('sr.subject_id', $stock_id_of_synonym, '=') + ->condition('sr.object_id', $stock_id, '=') + ->condition('sr.type_id', $this->importer->getCVterm('stock_relationship_type_synonym', '=')) + ->countQuery()->execute()->fetchField(); + + $this->assertEquals($stock1_stock_relationship_synonym2_count, 1, "Did not count the expected single stock_relationship between stock1 and its 2nd synonym, s1_2."); + + // Add a comma separated list of synonyms + $stock_comma = 'stock-comma'; + $stock_comma_id = $this->connection->insert('1:stock') + ->fields([ + 'organism_id' => $organism_id, + 'name' => $stock_comma, + 'uniquename' => 'TEST:3', + 'type_id' => 9, + ]) + ->execute(); + + $synonyms_comma = 'syn1, syn2'; + $syn1 = 'syn1'; + $syn2 = 'syn2'; + ob_start(); + $this->importer->loadSynonyms($stock_comma_id, $synonyms_comma, $organism_id); + ob_end_clean(); + + // Check both synonyms are in chado.synonym + $synonyms_comma_query = $this->connection->select('1:synonym', 's') + ->fields('s', ['name']); + $group = $synonyms_comma_query->orConditionGroup() + ->condition('s.name', $syn1, '=') + ->condition('s.name', $syn2, '='); + $synonyms_comma_query->condition($group); + $synonyms_comma_record = $synonyms_comma_query->execute()->fetchAll(); + + $this->assertEquals($synonyms_comma_record[0]->name, $syn1, "The synonym table does not contain the expected synonym syn1"); + $this->assertEquals($synonyms_comma_record[1]->name, $syn2, "The synonym table does not contain the expected synonym syn2"); + + // Add a semicolon separated list of synonyms + $stock_semicolon = 'stock-semicolon'; + $stock_semicolon_id = $this->connection->insert('1:stock') + ->fields([ + 'organism_id' => $organism_id, + 'name' => $stock_semicolon, + 'uniquename' => 'TEST:4', + 'type_id' => 9, + ]) + ->execute(); + + $synonyms_semicolon = 'syn3;syn4'; + $syn3 = 'syn3'; + $syn4 = 'syn4'; + ob_start(); + $this->importer->loadSynonyms($stock_semicolon_id, $synonyms_semicolon, $organism_id); + ob_end_clean(); + + // Check both synonyms are in chado.synonym + $synonyms_semicolon_query = $this->connection->select('1:synonym', 's') + ->fields('s', ['name']); + $group = $synonyms_semicolon_query->orConditionGroup() + ->condition('s.name', $syn3, '=') + ->condition('s.name', $syn4, '='); + $synonyms_semicolon_query->condition($group); + $synonyms_semicolon_record = $synonyms_semicolon_query->execute()->fetchAll(); + + $this->assertEquals($synonyms_semicolon_record[0]->name, $syn3, "The synonym table does not contain the expected synonym syn3"); + $this->assertEquals($synonyms_semicolon_record[1]->name, $syn4, "The synonym table does not contain the expected synonym syn4"); + + // @todo: Test the case when a stock_relationship previously exists + + // Do my final queries to ensure everything we expect to be in the tables is there + // stock table + // $stock_table_query = $this->connection->select('1:stock', 'st') + // ->fields('st', ['stock_id', 'name', 'uniquename', 'organism_id']); + // $stock_table_records = $stock_table_query->execute(); + // $stock_table_array_actual = $stock_table_records->fetchAllAssoc('stock_id'); + // print_r($stock_table_records->fetchAllAssoc('stock_id')); + } +} diff --git a/trpcultivate_germplasm/tests/src/Traits/GermplasmAccessionImporterTestTrait.php b/trpcultivate_germplasm/tests/src/Traits/GermplasmAccessionImporterTestTrait.php new file mode 100644 index 0000000..b2811b5 --- /dev/null +++ b/trpcultivate_germplasm/tests/src/Traits/GermplasmAccessionImporterTestTrait.php @@ -0,0 +1,89 @@ + 'stock_synonym', + 'description' => 'Linking table between stock and synonym.', + 'fields' => [ + 'stock_synonym_id' => [ + 'type' => 'serial', + 'not null' => TRUE, + ], + 'synonym_id' => [ + 'size' => 'big', + 'type' => 'int', + 'not null' => TRUE, + ], + 'stock_id' => [ + 'size' => 'big', + 'type' => 'int', + 'not null' => TRUE, + ], + 'pub_id' => [ + 'size' => 'big', + 'type' => 'int', + 'not null' => TRUE, + ], + 'is_current' => [ + 'type' => 'int', + 'default' => 0, + ], + 'is_internal' => [ + 'type' => 'int', + 'default' => 0, + ], + ], + 'primary key' => [ + 'stock_synonym_id', + ], + 'unique_keys' => [ + 'stock_synonym_c1' => ['synonym_id', 'stock_id', 'pub_id'], + ], + 'indexes' => [ + 'stock_synonym_idx1' => [ + 0 => 'synonym_id', + ], + 'stock_synonym_idx2' => [ + 0 => 'stock_id', + ], + 'stock_synonym_idx3' => [ + 0 => 'pub_id', + ], + ], + 'foreign keys' => [ + 'synonym' => [ + 'table' => 'synonym', + 'columns' => [ + 'synonym_id' => 'synonym_id', + ], + ], + 'stock' => [ + 'table' => 'stock', + 'columns' => [ + 'stock_id' => 'stock_id', + ], + ], + 'pub' => [ + 'table' => 'pub', + 'columns' => [ + 'pub_id' => 'pub_id', + ], + ], + ], + ]; + + $custom_tables = \Drupal::service('tripal_chado.custom_tables'); + $custom_table = $custom_tables->create($table, $this->connection->getSchemaName()); + $custom_table->setTableSchema($schema); + } +} diff --git a/trpcultivate_germplasm/trpcultivate_germplasm.install b/trpcultivate_germplasm/trpcultivate_germplasm.install new file mode 100644 index 0000000..1fd7fab --- /dev/null +++ b/trpcultivate_germplasm/trpcultivate_germplasm.install @@ -0,0 +1,90 @@ + 'stock_synonym', + 'description' => 'Linking table between stock and synonym.', + 'fields' => [ + 'stock_synonym_id' => [ + 'type' => 'serial', + 'not null' => TRUE, + ], + 'synonym_id' => [ + 'size' => 'big', + 'type' => 'int', + 'not null' => TRUE, + ], + 'stock_id' => [ + 'size' => 'big', + 'type' => 'int', + 'not null' => TRUE, + ], + 'pub_id' => [ + 'size' => 'big', + 'type' => 'int', + 'not null' => TRUE, + ], + 'is_current' => [ + 'type' => 'int', + 'default' => 0, + ], + 'is_internal' => [ + 'type' => 'int', + 'default' => 0, + ], + ], + 'primary key' => [ + 'stock_synonym_id', + ], + 'unique_keys' => [ + 'stock_synonym_c1' => ['synonym_id', 'stock_id', 'pub_id'], + ], + 'indexes' => [ + 'stock_synonym_idx1' => [ + 0 => 'synonym_id', + ], + 'stock_synonym_idx2' => [ + 0 => 'stock_id', + ], + 'stock_synonym_idx3' => [ + 0 => 'pub_id', + ], + ], + 'foreign keys' => [ + 'synonym' => [ + 'table' => 'synonym', + 'columns' => [ + 'synonym_id' => 'synonym_id', + ], + ], + 'stock' => [ + 'table' => 'stock', + 'columns' => [ + 'stock_id' => 'stock_id', + ], + ], + 'pub' => [ + 'table' => 'pub', + 'columns' => [ + 'pub_id' => 'pub_id', + ], + ], + ], + ]; + + $connection = \Drupal::service('tripal_chado.database'); + $schema_name = $connection->getSchemaName(); + $custom_tables = \Drupal::service('tripal_chado.custom_tables'); + $custom_table = $custom_tables->create($table, $schema_name); + $custom_table->setTableSchema($schema); +}