diff --git a/tripal/src/Services/TripalPublish.php b/tripal/src/Services/TripalPublish.php index c596721855..980fc91690 100644 --- a/tripal/src/Services/TripalPublish.php +++ b/tripal/src/Services/TripalPublish.php @@ -476,12 +476,20 @@ protected function getEntityTitles($matches) { $delta = 0; $field = $this->field_info[$field_name]['instance']; $main_prop = $field->mainPropertyName(); - $value = $match[$field_name][$delta][$main_prop]['value']->getValue(); - if ($value !== NULL) { - $entity_title = trim(preg_replace("/\[$field_name\]/", $value, $entity_title)); + $value = ''; + if (array_key_exists($delta, $match[$field_name])) { + $value = $match[$field_name][$delta][$main_prop]['value']->getValue(); } + if ($value === NULL) { + $value = ''; + } + $entity_title = trim(preg_replace("/\[$field_name\]/", $value, $entity_title)); } } + // Trim any trailing spaces and remove double spaces. Double spaces + // can occur if a token replacement has no value but there are spaces + // around it. + $entity_title = trim(preg_replace('/\s\s+/', ' ', $entity_title)); $titles[] = $entity_title; } return $titles; @@ -638,7 +646,7 @@ protected function findFieldItems($field_name, $entities) { $items = []; $sql = " - SELECT entity_id FROM {" . $field_table . "}\n + SELECT entity_id, delta FROM {" . $field_table . "}\n WHERE bundle = :bundle\n AND entity_id IN (:entity_ids[])\n"; @@ -660,7 +668,11 @@ protected function findFieldItems($field_name, $entities) { ]; $results = $database->query($sql, $args); while ($result = $results->fetchAssoc()) { - $items[$result['entity_id']] = $result['entity_id']; + $entity_id = $result['entity_id']; + if (!array_key_exists($entity_id, $items)) { + $items[$entity_id] = []; + } + $items[$entity_id][$result['delta']] = TRUE; } $this->setItemsHandled($batch_num); $batch_num++; @@ -710,8 +722,11 @@ protected function countFieldMatches(string $field_name, array $matches) : int { * An associative array that maps entity titles to their keys. * @param array $existing * An associative array of entities that already have an existing item for this field. + * + * @return int + * The number of items inserted for the field. */ - protected function insertFieldItems($field_name, $matches, $titles, $entities, $existing) { + protected function insertFieldItems($field_name, $matches, $titles, $entities, $existing, &$published) { $database = \Drupal::database(); $field_table = 'tripal_entity__' . $field_name; @@ -733,49 +748,60 @@ protected function insertFieldItems($field_name, $matches, $titles, $entities, $ $init_sql = rtrim($init_sql, ", "); $init_sql .= ") VALUES\n"; + $i = 0; $j = 0; $total = 0; $batch_num = 1; $sql = ''; $args = []; + $num_inserted = 0; + - // Iterate through the matches. + // Iterate through the matches. Each match corresponds to a single + // entity. The titles provided should be in order of the entities + // in the matches array. foreach ($matches as $match) { - $title = $titles[$total]; + + $title = $titles[$i]; $entity_id = $entities[$title]; + $i++; - $num_delta = count(array_keys($match[$field_name])); - for ($delta = 0; $delta < $num_delta; $delta++) { + // Iterate through the "items" of each feild and insert a record value + // for each item. + $num_items = count(array_keys($match[$field_name])); + for ($delta = 0; $delta < $num_items; $delta++) { $j++; $total++; // No need to add items to those that are already published. - if (array_key_exists($entity_id, $existing)) { - continue; - } - - // Add items to those that are not already published. - $sql .= "(:bundle_$j, :deleted_$j, :entity_id_$j, :revision_id_$j, :langcode_$j, :delta_$j, "; - $args[":bundle_$j"] = $this->bundle; - $args[":deleted_$j"] = 0; - $args[":entity_id_$j"] = $entity_id; - $args[":revision_id_$j"] = 1; - $args[":langcode_$j"] = 'und'; - $args[":delta_$j"] = $delta; - foreach (array_keys($this->required_types[$field_name]) as $key) { - $placeholder = ':' . $field_name . '_'. $key . '_' . $j; - $sql .= $placeholder . ', '; - $args[$placeholder] = $match[$field_name][$delta][$key]['value']->getValue(); + if (!array_key_exists($entity_id, $existing) or + !array_key_exists($delta, $existing[$entity_id])) { + + $published[$entity_id] = $title; + + // Add items to those that are not already published. + $sql .= "(:bundle_$j, :deleted_$j, :entity_id_$j, :revision_id_$j, :langcode_$j, :delta_$j, "; + $args[":bundle_$j"] = $this->bundle; + $args[":deleted_$j"] = 0; + $args[":entity_id_$j"] = $entity_id; + $args[":revision_id_$j"] = 1; + $args[":langcode_$j"] = 'und'; + $args[":delta_$j"] = $delta; + foreach (array_keys($this->required_types[$field_name]) as $key) { + $placeholder = ':' . $field_name . '_'. $key . '_' . $j; + $sql .= $placeholder . ', '; + $args[$placeholder] = $match[$field_name][$delta][$key]['value']->getValue(); + } + $sql = rtrim($sql, ", "); + $sql .= "),\n"; + $num_inserted++; } - $sql = rtrim($sql, ", "); - $sql .= "),\n"; // If we've reached the size of the batch then let's do the insert. if ($j == $batch_size or $total == $num_matches) { if (count($args) > 0) { $sql = rtrim($sql, ",\n"); $sql = $init_sql . $sql; - $database->query($sql, $args); } $this->setItemsHandled($batch_num); @@ -788,6 +814,7 @@ protected function insertFieldItems($field_name, $matches, $titles, $entities, $ } } } + return $num_inserted; } /** @@ -879,18 +906,21 @@ public function publish($filters = []) { } $total_items = 0; + $published_entities = []; foreach ($this->field_info as $field_name => $field_info) { $this->logger->notice(" Checking for published items for the field: $field_name..."); $existing_field_items = $this->findFieldItems($field_name, $entities); - $num_field_items = $this->countFieldMatches($field_name, $matches); - $this->logger->notice(" Publishing " . number_format($num_field_items) . " items for field: $field_name..."); - $this->insertFieldItems($field_name, $matches, $titles, $entities, $existing_field_items); - $total_items += $num_field_items; + $num_inserted = $this->insertFieldItems($field_name, $matches, $titles, + $entities, $existing_field_items, $published_entities); + + $this->logger->notice(" Published " . number_format($num_inserted) . " items for field: $field_name..."); + $total_items += $num_inserted; } - $this->logger->notice("Published " . number_format(count($new_matches)) . " new entities, and " . number_format($total_items) . " field values."); + $this->logger->notice("Published " . number_format(count(array_keys($published_entities))) + . " new entities, and " . number_format($total_items) . " field values."); $this->logger->notice('Done'); - return $entities; + return $published_entities; } } diff --git a/tripal/src/TripalStorage/TripalStorageBase.php b/tripal/src/TripalStorage/TripalStorageBase.php index f5117c8cbb..b53af3e8b4 100644 --- a/tripal/src/TripalStorage/TripalStorageBase.php +++ b/tripal/src/TripalStorage/TripalStorageBase.php @@ -228,20 +228,34 @@ protected function cloneValues($values) { } /** - * A helper function to add a new item for a field by cloning delta 0. + * Sets the values for a field to be empty. + * + * If the delta value doesn't exist in the values array then a new values + * array is added. * * @param array $values * An array of property values. * @param string $field_name * The name of the field to addd an item to. */ - protected function addEmptyValuesItem(&$values, $field_name) { + protected function resetValuesItem(&$values, $field_name, $delta) { + $is_new = FALSE; + + // Is the caller wanting to add a new element? If so, add one. $num_items = count($values[$field_name]); - $values[$field_name][$num_items] = []; - foreach ($values[$field_name][0] as $key => $value) { - $values[$field_name][$num_items][$key] = []; - $values[$field_name][$num_items][$key]['value'] = clone $value['value']; - $values[$field_name][$num_items][$key]['value']->setValue(NULL); + if ($delta > $num_items - 1) { + $values[$field_name][$delta] = []; + $is_new = TRUE; + } + + // Reset the values to NULL. Use the first values element + // to get the keys. + foreach ($values[$field_name][0] as $key => $details) { + if ($is_new) { + $values[$field_name][$delta][$key] = []; + $values[$field_name][$delta][$key]['value'] = clone $details['value']; + } + $values[$field_name][$delta][$key]['value']->setValue(NULL); } } diff --git a/tripal/src/TripalVocabTerms/PluginManagers/TripalCollectionPluginManager.php b/tripal/src/TripalVocabTerms/PluginManagers/TripalCollectionPluginManager.php index 30c2603876..2533d09d53 100644 --- a/tripal/src/TripalVocabTerms/PluginManagers/TripalCollectionPluginManager.php +++ b/tripal/src/TripalVocabTerms/PluginManagers/TripalCollectionPluginManager.php @@ -148,7 +148,7 @@ public function getCollectionList() { * @param string $name * The name. * - * @return Drupal\tripal\TripalVocabTerms\TripalCollectionPluginBase|NULL + * @return \Drupal\tripal\TripalVocabTerms\TripalCollectionPluginBase|NULL * The loaded collection plugin or NULL. */ public function loadCollection($name) { diff --git a/tripal_chado/src/Plugin/Field/FieldType/ChadoContactTypeDefault.php b/tripal_chado/src/Plugin/Field/FieldType/ChadoContactTypeDefault.php index 4f9954965d..1d7fdc4137 100644 --- a/tripal_chado/src/Plugin/Field/FieldType/ChadoContactTypeDefault.php +++ b/tripal_chado/src/Plugin/Field/FieldType/ChadoContactTypeDefault.php @@ -161,6 +161,7 @@ public static function tripalTypes($field_definition) { // An intermediate linker table is used else { // Define the linker table that links the base table to the object table. + // E.g.: project_contact.project_contact_id $properties[] = new ChadoIntStoragePropertyType($entity_type_id, self::$id, 'linker_id', self::$record_id_term, [ 'action' => 'store_pkey', 'drupal_store' => TRUE, @@ -168,6 +169,7 @@ public static function tripalTypes($field_definition) { ]); // Define the link between the base table and the linker table. + // E.g.: project.project_id>project_contact.project_id $properties[] = new ChadoIntStoragePropertyType($entity_type_id, self::$id, 'link', $linker_left_term, [ 'action' => 'store_link', 'drupal_store' => TRUE, @@ -175,6 +177,7 @@ public static function tripalTypes($field_definition) { ]); // Define the link between the linker table and the object table. + // E.g.: project_contact.contact_id $properties[] = new ChadoIntStoragePropertyType($entity_type_id, self::$id, $linker_fkey_column, $linker_fkey_term, [ 'action' => 'store', 'drupal_store' => TRUE, diff --git a/tripal_chado/src/Plugin/Field/FieldType/ChadoOrganismTypeDefault.php b/tripal_chado/src/Plugin/Field/FieldType/ChadoOrganismTypeDefault.php index 70a174c780..c8008c9356 100644 --- a/tripal_chado/src/Plugin/Field/FieldType/ChadoOrganismTypeDefault.php +++ b/tripal_chado/src/Plugin/Field/FieldType/ChadoOrganismTypeDefault.php @@ -105,7 +105,7 @@ public static function tripalTypes($field_definition) { // Cvterm table, to retrieve the name for the organism type $cvterm_schema_def = $chado->schema()->getTableDef('cvterm', ['format' => 'Drupal']); - $infraspecific_type_term = $mapping->getColumnTermId('cvterm', 'name'); + $infraspecific_type_term = $mapping->getColumnTermId('organism', 'type_id'); $infraspecific_type_len = $cvterm_schema_def['fields']['name']['size']; // Scientific name is built from several fields combined with space characters diff --git a/tripal_chado/src/Plugin/Field/FieldWidget/ChadoAdditionalTypeWidgetDefault.php b/tripal_chado/src/Plugin/Field/FieldWidget/ChadoAdditionalTypeWidgetDefault.php index 373b19d1a0..95a8b8a7fc 100644 --- a/tripal_chado/src/Plugin/Field/FieldWidget/ChadoAdditionalTypeWidgetDefault.php +++ b/tripal_chado/src/Plugin/Field/FieldWidget/ChadoAdditionalTypeWidgetDefault.php @@ -139,6 +139,7 @@ public function massageFormValues(array $values, array $form, FormStateInterface $idSpace_manager = \Drupal::service('tripal.collection_plugin_manager.idspace'); foreach ($values as $delta => $item) { $matches = []; + $values[$delta]['type_id'] = NULL; if (preg_match('/(.+?)\(([^\(]+?):(.+?)\)/', $item['term_autoc'], $matches)) { $termIdSpace = $matches[2]; $termAccession = $matches[3]; diff --git a/tripal_chado/src/Plugin/TripalStorage/ChadoStorage.php b/tripal_chado/src/Plugin/TripalStorage/ChadoStorage.php index 90484cc9d0..ba20592e1e 100644 --- a/tripal_chado/src/Plugin/TripalStorage/ChadoStorage.php +++ b/tripal_chado/src/Plugin/TripalStorage/ChadoStorage.php @@ -255,12 +255,12 @@ public function loadValues(&$values) : bool { foreach ($base_tables as $base_table) { // Do the select for the base tables - $this->records->selectRecords($base_table, $base_table); + $this->records->selectItems($base_table, $base_table); // Then do the selects for the ancillary tables. $tables = $this->records->getAncillaryTables($base_table); foreach ($tables as $table_alias) { - $this->records->selectRecords($base_table, $table_alias); + $this->records->selectItems($base_table, $table_alias); } } $this->setPropValues($values, $this->records); @@ -314,52 +314,51 @@ public function findValues($values) { foreach ($base_tables as $base_table) { // First we find all matching base records. - $matches = $this->records->findRecords($base_table, $base_table); + $entity_matches = $this->records->findRecords($base_table, $base_table); // Now for each matching base record we need to select // the ancillary tables. - foreach ($matches as $match) { + foreach ($entity_matches as $match) { // Clone the value array for this match. $new_values = $this->cloneValues($values); - // Iterate through tables that have conditions. We don't want to - // query tables that only have a condition with a link to the base - // table because these records aren't providing any filters to limit - // the base records. + // Limit base records by iterating through tables with conditions. $tables = $this->records->getAncillaryTablesWithCond($base_table); - $found_match = TRUE; foreach ($tables as $table_alias) { - $num_found = $match->selectRecords($base_table, $table_alias); - - // In order for a set of records to be considered found it must - // match all criteria, which means all ancillary tables must - // return results. - // @todo: when we need more fancy querying where we can set - // "or" clauses then this will need to be adjust. For now, we - // only use findValues() for publishing and in this case all - // criteria must be met. - if ($num_found == 0) { - $found_match = FALSE; + + // Now find any items for this linked table. + $num_items_found = $match->selectItems($base_table, $table_alias); + if ($num_items_found == 0) { continue; } - // Add any additional items to the values array that are needed. - $num_items = $match->getNumTableItems($base_table, $table_alias); - for ($i = 0; $i < $num_items - 1; $i++) { - $table_fields = $match->getTableFields($base_table, $table_alias); - foreach ($table_fields as $field_name) { - $this->addEmptyValuesItem($new_values, $field_name); + // Prepare the values array to receive all the new values. We'll + // get all the fields for this ancillary table and then + // reset the values in the new cloned values array for all of + // those fields. + $table_fields = $match->getTableFields($base_table, $table_alias); + foreach ($table_fields as $field_name) { + for ($i = 0; $i < $num_items_found; $i++) { + $this->resetValuesItem($new_values, $field_name, $i); } } } // Now set the values. - if ($found_match) { - $this->setPropValues($new_values, $match); - $found_list[] = $new_values; + $this->setPropValues($new_values, $match); + + // Remove any values that are not valid. + foreach ($new_values as $field_name => $deltas) { + foreach ($deltas as $delta => $properties) { + $is_valid = $this->isFieldValid($field_name, $delta, $new_values); + if (!$is_valid) { + unset($new_values[$field_name][$delta]); + } + } } + $found_list[] = $new_values; } } } @@ -440,22 +439,25 @@ protected function setPropValues(&$values, ChadoRecords $records) { $column_alias = $value_col_info['column_alias']; // For values that come from joins, we need to use the root table - // becuase this is the table that will have the value. - $my_delta = $delta; - if($action == 'read_value' and array_key_exists('join', $path_array)) { + // because this is the table that will have the value. + if ($action == 'read_value' and array_key_exists('join', $path_array)) { $root_alias = $value_col_info['root_alias']; $table_alias = $root_alias; } - // Anytime we need to pull data from the base table, the delta + // Anytime we need to pull a value from the base table, the delta // should always be zero. There will only ever be one base record. + // This is needed because all fields use a `record_id` which has + // a path that is set for the base table. + $value_delta = $delta; if ($table_alias == $base_table) { - $my_delta = 0; + $value_delta = 0; } - // Set the value. - $value = $records->getColumnValue($base_table, $table_alias, $my_delta, $column_alias); - $values[$field_name][$delta][$key]['value']->setValue($value); + $value = $records->getColumnValue($base_table, $table_alias, $value_delta, $column_alias); + if ($value !== NULL) { + $values[$field_name][$delta][$key]['value']->setValue($value); + } } } } @@ -526,6 +528,55 @@ protected function setPropValues(&$values, ChadoRecords $records) { } } + + /** + * Checks if a field has all necessary elements to be considered 'found'. + * + * The ChadoRecords class will search for all records necessary to + * populate the values of the fields for a content type. This works well + * when all conditions are set for the insertValues() and loadValues(). + * However, for the findValues() function there are often no criteria set + * and we want to find all linked records associated with a base record. + * All Chado fields will have a `record_id` property and the value of that + * comes from the base table. This means that all fields will have at + * least one property set even if nothing was found. So we need to know + * if the field has a valid set of property values. If so, we can + * proceed as if the field was "found" otherwise, we should remove the + * field values as nothing was found. + * + * A field is valid if all of the properties that have an action of 'store' + * have a non NULL value and if all required properties have a non NULL value. + * + * @param string $field_name + * The name of the field. + * @param integer $delta + * The field item's delta value. + * @param array $values + * An array of field values. + * @return boolean + * returns TRUE if the field has all necessary elements for inserting + * into the Drupal tables for publishing. FALSE otherwise. + */ + protected function isFieldValid($field_name, $delta, $values) { + + foreach ($values[$field_name][$delta] as $key => $prop_value) { + /** @var \Drupal\tripal\TripalStorage\StoragePropertyTypeBase $prop_type **/ + $prop_type = $this->getPropertyType($field_name, $key); + $prop_settings = $prop_type->getStorageSettings(); + $action = $prop_settings['action']; + $is_store = preg_match('/^store/', $action); + $value = $prop_value['value']->getValue(); + $is_required = $prop_type->getRequired(); + if ($is_store and $value === NULL) { + return FALSE; + } + if ($is_required and $value === NULL) { + return FALSE; + } + } + return TRUE; + } + /** * Indexes a values array for easy lookup. * @@ -930,6 +981,10 @@ protected function handleReadValue(array $context, StoragePropertyValue $prop_va /** * Takes a path string for a field property and converts it to an array structure. * + * @param string $field_name + * The name of the field. + * @param string $base_table + * The name of the base table for thie field. * @param mixed $path * A string continaining the path. Note: this is a recursive function and on * recursive calls this variable will be n array. Hence, the type is "mixed".* @@ -946,7 +1001,8 @@ protected function handleReadValue(array $context, StoragePropertyValue $prop_va * @return array * */ - protected function parsePath(string $field_name, string $base_table, mixed $path, array $aliases = [], string $as = '', string $full_path = '') { + protected function parsePath(string $field_name, string $base_table, mixed $path, + array $aliases = [], string $as = '', string $full_path = '') { // If the path is a string then split it. $path_arr = []; @@ -1020,7 +1076,7 @@ protected function parsePath(string $field_name, string $base_table, mixed $path $sub_path_arr = $this->parsePath($field_name, $base_table, $path_arr, $aliases, $as, $full_path); } // If there are no more joins, then we need to set the value column to be - // the same as the last column in tge join. + // the same as the last column in the join. else { $ret_array['join']['value_column'] = $right_column; $ret_array['join']['value_alias'] = $as ? $as : $right_column; @@ -1051,7 +1107,6 @@ protected function parsePath(string $field_name, string $base_table, mixed $path if (array_key_exists($table_alias, $aliases)) { $chado_table = $aliases[$table_alias]; } - // If the base table is not the same as the root table then // we should add the field name to the colun alias. Otherwise // we may have conflicts if mutiple fields use the same alias. @@ -1072,7 +1127,8 @@ protected function parsePath(string $field_name, string $base_table, mixed $path } // There is no period in the path so there is no Chado table. We are at the - // end of the path with joins and we can just return the value column. + // end of the path with joins and the value column is not the same as the + // right join column. We can just return the value column. else { // If the base table is not the same as the root table then // we should add the field name to the colun alias. Otherwise @@ -1255,9 +1311,8 @@ static public function drupalEntityIdLookupCallback($context) { } $record_id = $record_id->getValue('value'); - // During publish, record_id may be null. In this particular case, return null. if (!$record_id) { - return NULL; + return -1; } // Given the Chado record ID and bundle term, we can lookup the Drupal entity ID. diff --git a/tripal_chado/src/TripalStorage/ChadoRecords.php b/tripal_chado/src/TripalStorage/ChadoRecords.php index ec3a0ab852..89782ef73c 100644 --- a/tripal_chado/src/TripalStorage/ChadoRecords.php +++ b/tripal_chado/src/TripalStorage/ChadoRecords.php @@ -187,7 +187,9 @@ protected function initTable($elements) : bool { // for the base table that should be included in the record. 'columns' => [], - // An array the indicates which fields want column values. + // An array mapping which Tripal fields want which Chado column values. + // The key is the column alias and the value is an array, one entry + // for each field/property that uses the value. 'field_columns' => [], // Conditinos for this table when performing a query. @@ -216,7 +218,14 @@ protected function initTable($elements) : bool { // any columns from joined tables. There is no guarnatee that fields // won't give the same name to the same fields in the same tables so // these values will be indexed by the field and key they belong to. - 'values' => [] + 'values' => [], + + // A boolean to indicate if any values have been set. We can't + // rely on checking if all values are empty because it could be + // possible that all values are meant to be empty. This value will + // get set when a query is successful for the table and values have + // been set. + 'has_values' => FALSE, ]; } return TRUE; @@ -687,9 +696,9 @@ public function setLinks(string $base_table) { // if this column is an ID field and links to this base table then update the value. if (array_key_exists($column_alias, $record['link_columns'])) { - $base_table = $record['link_columns'][$column_alias]; - $record_id = $record_id; - $this->setColumnValue($base_table, $table_alias, $delta, $column_alias, $record_id); + $base_table = $record['link_columns'][$column_alias]; + $this->records[$base_table]['tables'][$table_alias]['items'][$delta]['values'][$column_alias] = $record_id; + // If a condition exists for this id set it as well. if (array_key_exists($column_alias, $record['conditions'])) { @@ -824,11 +833,6 @@ public function getAncillaryTables(string $base_table) { /** * For the given base table, returns non base tables that have conditions set. * - * Excludes tables whose only condition is the linker column to the base - * table. This function is useful when finding values. We don't want - * to iterate through tables that won't have any records to filter so - * we can use this function results to exclude those tables. - * * @param string $base_table * The name of the Chado table used as a base table. * @@ -841,17 +845,11 @@ public function getAncillaryTablesWithCond(string $base_table) : array { $tables = $this->getAncillaryTables($base_table); foreach ($tables as $table_alias) { $items = $this->getTableItems($base_table, $table_alias); - $linker_cols = array_keys($items[0]['link_columns']); foreach (array_keys($items[0]['conditions']) as $column_alias) { - if (in_array($column_alias, $linker_cols) and - $items[0]['link_columns'][$column_alias] == $base_table) { - continue; - } - $ret_val[] = $table_alias; - + $ret_val[$table_alias] = 1; } } - return $ret_val; + return array_keys($ret_val); } /** * Returns the list of tables currently handled by this object. @@ -1064,7 +1062,8 @@ public function getColumnFieldAliases(string $base_table, string $table_alias, i * @return bool * TRUE if the value was set, FALSE otherwise */ - protected function setColumnValue(string $base_table, string $table_alias, int $delta, string $column_alias, $value) : bool { + protected function setColumnValue(string $base_table, string $table_alias, + int $delta, string $column_alias, $value) : bool { if (!array_key_exists($base_table, $this->records)) { throw new \Exception(t('ChadoRecords::setColumnValue(): The base table has not been added to the ChadoRecords object: @base_table.', @@ -1086,6 +1085,7 @@ protected function setColumnValue(string $base_table, string $table_alias, int $ // Set the value. $this->records[$base_table]['tables'][$table_alias]['items'][$delta]['values'][$column_alias] = $value; + $this->records[$base_table]['tables'][$table_alias]['items'][$delta]['has_values'] = TRUE; return TRUE; } @@ -1105,7 +1105,7 @@ protected function setColumnValue(string $base_table, string $table_alias, int $ * @return mixed * The value of the field. */ - public function getColumnValue(string $base_table, string $table_alias, int $delta, $column_alias) { + public function getColumnValue(string $base_table, string $table_alias, int $delta, string $column_alias) { if (!array_key_exists($base_table, $this->records)) { throw new \Exception(t('ChadoRecords::getFieldValue(): The base table has not been added to the ChadoRecords object: @base_table.', @@ -1123,10 +1123,16 @@ public function getColumnValue(string $base_table, string $table_alias, int $del return NULL; } - return $items[$delta]['values'][$column_alias]; + // If the values were set then return it, otherwise return NULL; + if ($items[$delta]['has_values'] === TRUE) { + return $items[$delta]['values'][$column_alias]; + } + + return NULL; } + /** * Returns the records object as an array. * @@ -1523,10 +1529,11 @@ protected function validateRequired($base_table, $delta, $record_id, $record) { if (count($missing) > 0) { // Documentation for how to create a violation is here // https://github.com/symfony/validator/blob/6.1/ConstraintViolation.php - $message = 'The item cannot be saved because the following values are missing. '; + $message = 'The item cannot be saved because the following fields for the Chado ' + . '"' . $base_table . '" table are missing. '; $params = []; foreach ($missing as $col) { - $message .= ucfirst($col) . ", "; + $message .= $col . ", "; } $message = substr($message, 0, -2) . '.'; $this->violations[] = new ConstraintViolation(t($message, $params)->render(), @@ -1635,37 +1642,36 @@ public function insertRecords(string $base_table, string $table_alias) { } /** - * Queries for multiple records in Chado for a given table.. + * Queries for multiple records in Chado for a given table. * * @param string $base_table * The name of the Chado table used as a base table. - * @param string $table_alias - * The alias of the table. For the base table, use the same table name as - * base tables don't have aliases. + * @param string $base_table_alias + * The alias of the base table. + * + * @return array + * An array of \Drupal\tripal_chado\TripalStorage\ChadoRecords objects. * * @throws \Exception */ - public function findRecords(string $base_table, string $table_alias) { + public function findRecords(string $base_table, string $base_table_alias) { $found_records = []; - // Make sure all IDs are up to date. - $this->setLinks($base_table); - // Get information about this Chado table. - $chado_table = $this->getTableFromAlias($base_table, $table_alias); + $chado_table = $this->getTableFromAlias($base_table, $base_table_alias); - // Iterate through each item of the table and perform an insert. - $items = $this->getTableItems($base_table, $table_alias); + // Iterate through each and perform a select. + $items = $this->getTableItems($base_table, $base_table_alias); foreach ($items as $delta => $record) { // Start the select - $select = $this->connection->select('1:' . $chado_table, $table_alias); + $select = $this->connection->select('1:' . $chado_table, $base_table_alias); // Add the fields in the chado table. foreach ($record['columns'] as $column_alias) { $chado_column = $record['column_aliases'][$column_alias]['chado_column']; - $select->addField($table_alias, $chado_column, $column_alias); + $select->addField($base_table_alias, $chado_column, $column_alias); } // Add in any joins. @@ -1696,9 +1702,8 @@ public function findRecords(string $base_table, string $table_alias) { if (array_key_exists($column_alias, $record['link_columns']) and !$this->getRecordID($base_table)) { continue; } - $select->condition($table_alias . '.' . $column_alias, $value['value'], $value['operation']); + $select->condition($base_table_alias . '.' . $column_alias, $value['value'], $value['operation']); } - $this->field_debugger->reportQuery($select, "Select Query for $chado_table ($delta)"); // Execute the query. @@ -1708,24 +1713,25 @@ public function findRecords(string $base_table, string $table_alias) { ['@table' => $chado_table, '@record' => print_r($record, TRUE)])); } + // Iterate through the results and create a new record for each one. while ($values = $results->fetchAssoc()) { + // We start by cloning the records array that was used to query. + $new_record = new ChadoRecords($this->field_debugger, $this->logger, $this->connection); + $new_record->copyRecords($this); + // Update the values in the new record. foreach ($values as $column_alias => $value) { - $this->setColumnValue($base_table, $table_alias, $delta, $column_alias, $value); + if ($value !== NULL) { + $new_record->setColumnValue($base_table, $base_table_alias, $delta, $column_alias, $value); - // If this is the base table be sure to set the record ID. - if ($base_table === $table_alias and array_key_exists($column_alias, $record['link_columns'])) { - $this->setRecordID($base_table, $value); - $this->setLinks($base_table); + // If this is the base table be sure to set the record ID. + if ($base_table === $base_table_alias and array_key_exists($column_alias, $record['link_columns'])) { + $new_record->setRecordID($base_table, $value); + } } } - // We start by cloning the records array - // (includes all tables, not just the current $base_table) - $new_record = new ChadoRecords($this->field_debugger, $this->logger, $this->connection); - $new_record->copyRecords($this); - // Save the new record object. to be returned later. $found_records[] = $new_record; } @@ -1752,11 +1758,6 @@ public function updateRecords($base_table, $table_alias) { // Get the Chado table for this given table alias. $chado_table = $this->getTableFromAlias($base_table, $table_alias); - // Get information about this Chado table. - $schema = $this->connection->schema(); - $table_def = $schema->getTableDef($chado_table, ['format' => 'drupal']); - $pkey = $table_def['primary key']; - // Iterate through each item of the table and perform an insert. $items = $this->getTableItems($base_table, $table_alias); foreach ($items as $delta => $record) { @@ -1876,7 +1877,11 @@ public function deleteRecords(string $base_table, string $table_alias, bool $gra } /** - * Selects a single record from Chado. + * Selects the items for a given table in a record object. + * + * This function is used for the findValues() and loadValues() functions so + * it needs to be able to find multiple records from the base table and + * multiple items from an ancillary table. * * @param string $base_table * The name of the Chado table used as a base table. @@ -1887,18 +1892,18 @@ public function deleteRecords(string $base_table, string $table_alias, bool $gra * @throws \Exception * * @return int - * Returns the number of records for this table that were found. + * Returns the number of items for this table that were found. */ - public function selectRecords(string $base_table, string $table_alias) : int { + public function selectItems(string $base_table, string $table_alias) : int { - // Indicates the number of records that were found for this table. + // Indicates the number of items that were found for this table. // We need to return the number found because even if no records are found // the `values` array of $this->records will still have the values that were // provided to it. Since we use that same array for updates/inserts it // makes sense for those values to be there. So, we need something to // indicate if we actually did find values on a `loadValues()` or // `findValues()` call. - $num_found = 0; + $items_found = 0; // Make sure all IDs are up to date. $this->setLinks($base_table); @@ -1906,7 +1911,7 @@ public function selectRecords(string $base_table, string $table_alias) : int { // Get the Chado table for this given table alias. $chado_table = $this->getTableFromAlias($base_table, $table_alias); - // Iterate through each item of the table and perform an insert. + // Iterate through each item of the table and perform a select. $items = $this->getTableItems($base_table, $table_alias); foreach ($items as $delta => $record) { @@ -1942,7 +1947,8 @@ public function selectRecords(string $base_table, string $table_alias) : int { $left_alias = $join_info['on']['left_alias']; $left_column = $join_info['on']['left_column']; - $select->leftJoin('1:' . $right_table, $right_alias, $left_alias . '.' . $left_column . '=' . $right_alias . '.' . $right_column); + $select->leftJoin('1:' . $right_table, $right_alias, + $left_alias . '.' . $left_column . '=' . $right_alias . '.' . $right_column); foreach ($join_info['columns'] as $column) { $join_column = $column['chado_column']; @@ -1966,32 +1972,36 @@ public function selectRecords(string $base_table, string $table_alias) : int { $results = $select->execute(); if (!$results) { throw new \Exception(t('Failed to select record in the Chado "@table" table. Record: @record', - ['@table' => $chado_table, '@record' => print_r($record, TRUE)])); + ['@table' => $chado_table, '@record' => print_r($record, TRUE)])); } // Update the values in the record. - $num_records = $delta; + $current_items = count($this->getTableItems($base_table, $table_alias)); + $i = 0; while ($values = $results->fetchAssoc()) { - // If we have more records than we have items then we have a field - // with cardinality > 1 and this select is part of a findValues() - // call. For a loadValues() then the delta should already be set for - // all the property items. We need to create a copy of the previous - // item so we can add the new values. - if ($num_records + 1 > count($this->getTableItems($base_table, $table_alias))) { + + // On a loadValues() then the record array will have all of the items + // available. On a findValues() the records array is empty and we need + // to expand it. The following will allow us to expand the + // items array if we don't have enough elements. + if ($delta + $i > $current_items - 1) { $this->addEmptyTableItem($base_table, $table_alias); } + foreach ($values as $column_alias => $value) { - $this->setColumnValue($base_table, $table_alias, $num_records, $column_alias, $value); - // If this is the base table be sure to set the record ID. - if ($base_table === $table_alias and array_key_exists($column_alias, $record['link_columns'])) { - $this->setRecordID($base_table, $value); + if ($value !== NULL) { + $this->setColumnValue($base_table, $table_alias, $delta + $i, $column_alias, $value); + // If this is the base table be sure to set the record ID. + if ($base_table === $table_alias and array_key_exists($column_alias, $record['link_columns'])) { + $this->setRecordID($base_table, $value); + } } } - $num_records++; - $num_found++; + $i++; + $items_found++; } } - return $num_found; + return $items_found; } /** diff --git a/tripal_chado/tests/src/Functional/Services/ChadoTripalPublishTest.php b/tripal_chado/tests/src/Functional/Services/ChadoTripalPublishTest.php new file mode 100644 index 0000000000..9abfe47967 --- /dev/null +++ b/tripal_chado/tests/src/Functional/Services/ChadoTripalPublishTest.php @@ -0,0 +1,589 @@ +insert('1:organism'); + $insert->fields([ + 'genus' => $details['genus'], + 'species' => $details['species'], + 'type_id' => array_key_exists('type_id', $details) ? $details['type_id'] : NULL, + 'infraspecific_name' => array_key_exists('infraspecific_name', $details) ? $details['infraspecific_name'] : NULL, + 'abbreviation' => array_key_exists('abbreviation', $details) ? $details['abbreviation'] : NULL, + 'common_name' => array_key_exists('common_name', $details) ? $details['common_name'] : NULL, + 'comment' => array_key_exists('comment', $details) ? $details['comment'] : NULL, + ]); + return $insert->execute(); + } + + /** + * A helper function for adding a project record to Chado. + * + * @param \Drupal\tripal\TripalDBX\TripalDbxConnection $chado + * A chado database object. + * @param array $details + * The key/value pairs of entries for the project. The keys correspond + * to the columns of the project table. + * @return int + * The project_id + */ + public function addChadoProject($chado, $details) { + $insert = $chado->insert('1:project'); + $insert->fields([ + 'name' => $details['name'], + 'description' => array_key_exists('description', $details) ? $details['description'] : NULL, + ]); + return $insert->execute(); + } + + /** + * A helper function for adding a contact record to Chado. + * + * @param \Drupal\tripal\TripalDBX\TripalDbxConnection $chado + * A chado database object. + * @param array $details + * The key/value pairs of entries for the contact. The keys correspond + * to the columns of the contact table. + * @return int + * The contact_id + */ + public function addChadoContact($chado, $details) { + $insert = $chado->insert('1:contact'); + $insert->fields([ + 'name' => $details['name'], + 'type_id' => array_key_exists('type_id', $details) ? $details['type_id'] : NULL, + 'description' => array_key_exists('description', $details) ? $details['description'] : NULL, + ]); + return $insert->execute(); + } + + /** + * A helper function for adding a project_contact record to Chado. + * + * @param \Drupal\tripal\TripalDBX\TripalDbxConnection $chado + * A chado database object. + * @param array $details + * The key/value pairs of entries for the project_contact. The keys correspond + * to the columns of the project_contact table. + * @return int + * The project_contact_id + */ + public function addChadoProjectContact($chado, $details) { + $insert = $chado->insert('1:project_contact'); + $insert->fields([ + 'project_id' => $details['project_id'], + 'contact_id' => $details['contact_id'], + ]); + return $insert->execute(); + } + + /** + * A helper function for adding a property to a record in Chado. + * + * @param \Drupal\tripal\TripalDBX\TripalDbxConnection $chado + * A chado database object. + * @param string $base_table + * The base table to which the property should be added. + * @param array $details + * The key/value pairs of entries for the property. The keys correspond + * to the columns of the property table. + * + * @return int + * The property primary key. + */ + public function addProperty($chado, $base_table, $details) { + + $insert = $chado->insert('1:' . $base_table . 'prop'); + $insert->fields([ + $base_table . '_id' => $details[$base_table . '_id'], + 'value' => $details['value'], + 'type_id' => $details['type_id'], + 'rank' => $details['rank'], + ]); + return $insert->execute(); + } + + + /** + * A helper function to test if the elements of a field item are present. + * + * @param string $bundle + * The content type bundle name (e.g. 'organism'). + * @param string $field_name + * The name of the field that should be queried. + * @param int $num_expected + * The number of items that are expected to be found when applying the + * conditions specified in the $match argument. + * @param array $match + * An array of key/value pairs where the keys are the column names of + * field table in Drupal and the values are those to match in a select + * condition. All fields other than the 'entity_id', 'bundle', 'delta' + * 'deleted', 'langcode', and 'revision' have the field name as a prefix. + * But the keys need not include the prefix, just the field property key. + * The field name prefix will be added automatically. + * @param array $check + * An array of key/value pairs where the keys are the column names of the + * field table in Drupal and the values are checked that they match + * what is in the table. The same rules apply for the key naming as in + * the $match argument. + */ + public function checkFieldItem($bundle, $field_name, $num_expected, $match, $check) { + + $drupal_columns = ['bundle', 'entity_id', 'revision' ,'delta', 'deleted', 'langcode']; + + $public = \Drupal::service('database'); + $select = $public->select('tripal_entity__' . $field_name, 'f'); + $select->fields('f'); + $select->condition('bundle', $bundle); + foreach ($match as $key => $val) { + $column_name = $key; + if (!in_array($key, $drupal_columns)) { + $column_name = $field_name . '_' . $key; + } + $select->condition($column_name, $val); + } + $select->orderBy('delta'); + $result = $select->execute(); + $records = $result->fetchAll(); + + $this->assertCount($num_expected, $records, + 'The number of items expected for field "' . $field_name .'" with bundle "' + . $bundle . '" is not correct.'); + + foreach ($records as $delta => $record) { + + // Make sure we have an entity ID for the specified record. + $this->assertNotNull($record->entity_id, + 'The entity_id for a published item is missing for the field "' + . $field_name . '" at delta ' . $delta); + + // Make sure the expected values are present. + foreach ($check as $key => $val) { + $column_name = $key; + if (!in_array($key, $drupal_columns)) { + $column_name = $field_name . '_' . $key; + } + $this->assertEquals($val, $record->$column_name, + 'The value for, "' . $column_name . '", is not correct what we expected.'); + } + } + } + + /** + * A helper function to add fields to the organism content types used in the tests. + */ + public function attachOrganismPropertyFields() { + + /** @var \Drupal\tripal\Services\TripalFieldCollection $fields_service **/ + // Now add a ChadoProperty field for the two types of properties. + $fields_service = \Drupal::service('tripal.tripalfield_collection'); + $prop_field1 = [ + 'name' => 'field_note', + 'content_type' => 'organism', + 'label' => 'Note', + 'type' => 'chado_property_type_default', + 'description' => "A note about this organism.", + 'cardinality' => -1, + 'required' => FALSE, + 'storage_settings' => [ + 'storage_plugin_id' => 'chado_storage', + 'storage_plugin_settings'=> [ + 'base_table' => 'organism', + 'prop_table' => 'organismprop' + ], + ], + 'settings' => [ + 'termIdSpace' => 'local', + 'termAccession' => "Note", + ], + 'display' => [ + 'view' => [ + 'default' => [ + 'region' => 'content', + 'label' => 'above', + 'weight' => 15 + ], + ], + 'form' => [ + 'default'=> [ + 'region'=> 'content', + 'weight' => 15 + ], + ], + ], + ]; + $reason = ''; + $is_valid = $fields_service->validate($prop_field1, $reason); + $this->assertTrue($is_valid, $reason); + $is_added = $fields_service->addBundleField($prop_field1); + $this->assertTrue($is_added, 'The organism property field "local:Note" could not be added.'); + + // Now add a ChadoProperty field for the two types of properties. + $prop_field2 = [ + 'name' => 'field_comment', + 'content_type' => 'organism', + 'label' => 'Comment', + 'type' => 'chado_property_type_default', + 'description' => "A comment about this organism.", + 'cardinality' => -1, + 'required' => FALSE, + 'storage_settings' => [ + 'storage_plugin_id' => 'chado_storage', + 'storage_plugin_settings'=> [ + 'base_table' => 'organism', + 'prop_table' => 'organismprop' + ], + ], + 'settings' => [ + 'termIdSpace' => 'schema', + 'termAccession' => "comment", + ], + 'display' => [ + 'view' => [ + 'default' => [ + 'region' => 'content', + 'label' => 'above', + 'weight' => 15 + ], + ], + 'form' => [ + 'default'=> [ + 'region'=> 'content', + 'weight' => 15 + ], + ], + ], + ]; + $reason = ''; + $is_valid = $fields_service->validate($prop_field2, $reason); + $this->assertTrue($is_valid, $reason); + $is_added = $fields_service->addBundleField($prop_field2); + $this->assertTrue($is_added, + 'The Organism property field "schema:comment" could not be added.'); + } + + /** + * Tests the TripalContentTypes class public functions. + */ + public function testChadoTripalPublishService() { + + // Prepare Chado + $chado = $this->createTestSchema(ChadoTestBrowserBase::PREPARE_TEST_CHADO); + + // Add the CV terms. These normally get added during a prepare and + // the Chado schema is prepared but not Drupal schema and it needs to + // know about the terms used for content types and fields. + $terms_setup = \Drupal::service('tripal_chado.terms_init'); + $terms_setup->installTerms(); + + // Create the terms for the field property storage types. + /** @var \Drupal\tripal\TripalVocabTerms\PluginManagers\TripalIdSpaceManager $idsmanager */ + $idsmanager = \Drupal::service('tripal.collection_plugin_manager.idspace'); + + $local_db = $idsmanager->loadCollection('local', "chado_id_space"); + $note_term = new TripalTerm(); + $note_term->setName('Note'); + $note_term->setIdSpace('local'); + $note_term->setVocabulary('local'); + $note_term->setAccession('Note'); + $local_db->saveTerm($note_term); + + $schema_db = $idsmanager->loadCollection('schema', "chado_id_space"); + $comment_term = new TripalTerm(); + $comment_term->setName('comment'); + $comment_term->setIdSpace('schema'); + $comment_term->setVocabulary('schema'); + $comment_term->setAccession('comment'); + $schema_db->saveTerm($comment_term); + + // Make sure we have the content types and fields that we want to test. + $collection_ids = ['general_chado']; + $content_type_setup = \Drupal::service('tripal.tripalentitytype_collection'); + $fields_setup = \Drupal::service('tripal.tripalfield_collection'); + $content_type_setup->install($collection_ids); + $fields_setup->install($collection_ids); + + /** @var \Drupal\tripal\Services\TripalPublish $publish */ + $publish = \Drupal::service('tripal.publish'); + + // + // Test publishing when no records are available. + // + $publish->init('organism', 'chado_storage'); + $entities = $publish->publish(); + $this->assertTrue(count($entities) == 0, + 'The TripalPublish service should return 0 entities when no records are available.'); + + // + // Test publishing a single record. + // + $taxrank_db = $idsmanager->loadCollection('TAXRANK', "chado_id_space"); + $subspecies_term_id = $taxrank_db->getTerm('0000023')->getInternalId(); + + $organism_id = $this->addChadoOrganism($chado, [ + 'genus' => 'Oryza', + 'species' => 'species', + 'abbreviation' => 'O. sativa', + 'type_id' => $subspecies_term_id, + 'infraspecific_name' => 'Japonica', + 'comment' => 'rice is nice' + ]); + $entities = $publish->publish(); + $this->assertTrue(count(array_keys($entities)) == 1, + 'The TripalPublish service should have published 1 organism.'); + + // Test that entries were added for all field items and that fields that + // shouldn't be saved in Drupal are NULL. + $this->checkFieldItem('organism', 'organism_genus', 1, + ['record_id' => $organism_id], + ['bundle' => 'organism', 'entity_id' => 1, 'value' => NULL]); + + $this->checkFieldItem('organism', 'organism_species', 1, + ['record_id' => $organism_id], + ['bundle' => 'organism', 'entity_id' => 1, 'value' => NULL]); + + $this->checkFieldItem('organism', 'organism_abbreviation', 1, + ['record_id' => $organism_id], + ['bundle' => 'organism', 'entity_id' => 1, 'value' => NULL]); + + $this->checkFieldItem('organism', 'organism_infraspecific_name', 1, + ['record_id' => $organism_id], + ['bundle' => 'organism', 'entity_id' => 1, 'value' => NULL]); + + $this->checkFieldItem('organism', 'organism_infraspecific_type', 1, + ['record_id' => $organism_id], + ['bundle' => 'organism', 'entity_id' => 1, 'type_id' => NULL]); + + $this->checkFieldItem('organism', 'organism_comment', 1, + ['record_id' => $organism_id], + ['bundle' => 'organism', 'entity_id' => 1, 'value' => NULL]); + + // Test that the title via token replacement is working. + $this->assertTrue(array_values($entities)[0] == 'Oryza species subspecies Japonica', + 'The title of a Chado organism is incorrect after publishing: ' . array_values($entities)[0] . '!=' . 'Oryza species subspecies Japonica'); + + // + // Test a second entity. Also use a title without all tokens + // + $organism_id2 = $this->addChadoOrganism($chado, [ + 'genus' => 'Gorilla', + 'species' => 'gorilla', + 'abbreviation' => 'G. gorilla', + 'comment' => 'Gorilla' + ]); + $entities = $publish->publish(); + $this->assertEquals('Gorilla gorilla ', array_values($entities)[0], + 'The title of Chado organism with missing tokens is incorrect after publishing'); + + + $this->checkFieldItem('organism', 'organism_genus', 1, + ['record_id' => $organism_id2], + ['bundle' => 'organism', 'entity_id' => 2, 'value' => NULL]); + + $this->checkFieldItem('organism', 'organism_species', 1, + ['record_id' => $organism_id2], + ['bundle' => 'organism', 'entity_id' => 2, 'value' => NULL]); + + $this->checkFieldItem('organism', 'organism_abbreviation', 1, + ['record_id' => $organism_id2], + ['bundle' => 'organism', 'entity_id' => 2, 'value' => NULL]); + + $this->checkFieldItem('organism', 'organism_infraspecific_name', 0, + ['record_id' => $organism_id2], + ['bundle' => 'organism', 'entity_id' => 2, 'value' => NULL]); + + $this->checkFieldItem('organism', 'organism_infraspecific_type', 1, + ['record_id' => $organism_id2], + ['bundle' => 'organism', 'entity_id' => 2, 'type_id' => NULL]); + + $this->checkFieldItem('organism', 'organism_comment', 1, + ['record_id' => $organism_id2], + ['bundle' => 'organism', 'entity_id' => 2, 'value' => NULL]); + + // + // Test publishing properties. + // + $comment_type_id = $schema_db->getTerm('comment')->getInternalId(); + $note_type_id = $local_db->getTerm('Note')->getInternalId(); + $this->attachOrganismPropertyFields(); + $this->addProperty($chado, 'organism', [ + 'organism_id' => $organism_id, + 'type_id' => $note_type_id, + 'value' => 'Note 1', + 'rank' => 1, + ]); + $this->addProperty($chado, 'organism', [ + 'organism_id' => $organism_id, + 'type_id' => $note_type_id, + 'value' => 'Note 0', + 'rank' => 0, + ]); + $this->addProperty($chado, 'organism', [ + 'organism_id' => $organism_id, + 'type_id' => $note_type_id, + 'value' => 'Note 2', + 'rank' => 2, + ]); + + $this->addProperty($chado, 'organism', [ + 'organism_id' => $organism_id, + 'type_id' => $comment_type_id, + 'value' => 'Comment 0', + 'rank' => 0, + ]); + $this->addProperty($chado, 'organism', [ + 'organism_id' => $organism_id, + 'type_id' => $comment_type_id, + 'value' => 'Comment 1', + 'rank' => 1, + ]); + + + // Now publish the organism content type again. + $publish->init('organism', 'chado_storage'); + $entities = $publish->publish(); + + // Because we added properties for the first organism we should set it's + // entity in those returned, but not the gorilla organism. + $this->assertEquals('Oryza species subspecies Japonica', array_values($entities)[0], + 'The Oryza species subspecies Japonica organism should appear in the published list because it has new properties.'); + $this->assertCount(1, array_values($entities), + 'There should only be one published entity for a single organism with new properties.'); + + // Check that the property values got published. The type_id should be + // NULL because that's not stored in Drupal. + $this->checkFieldItem('organism', 'field_note', 1, + ['record_id' => $organism_id, 'prop_id' => 1], + ['type_id' => NULL, 'linker_id' => $organism_id, + 'bundle' => 'organism', 'entity_id' => 1]); + + $this->checkFieldItem('organism', 'field_note', 1, + ['record_id' => $organism_id, 'prop_id' => 2], + ['type_id' => NULL, 'linker_id' => $organism_id, + 'bundle' => 'organism', 'entity_id' => 1]); + + $this->checkFieldItem('organism', 'field_note', 1, + ['record_id' => $organism_id, 'prop_id' => 3], + ['type_id' => NULL, 'linker_id' => $organism_id, + 'bundle' => 'organism', 'entity_id' => 1]); + + $this->checkFieldItem('organism', 'field_comment', 1, + ['record_id' => $organism_id, 'prop_id' => 4], + ['type_id' => NULL, 'linker_id' => $organism_id, + 'bundle' => 'organism', 'entity_id' => 1]); + + $this->checkFieldItem('organism', 'field_comment', 1, + ['record_id' => $organism_id, 'prop_id' => 5], + ['type_id' => NULL, 'linker_id' => $organism_id, + 'bundle' => 'organism', 'entity_id' => 1]); + + // Check that only the exact number of properties were published. + $this->checkFieldItem('organism', 'field_note', 3, ['entity_id' => 1], []); + $this->checkFieldItem('organism', 'field_comment', 2, ['entity_id' => 1], []); + + // + // Test publishing a field that uses a linker table. + // + + // Create and publish the contacts and the project. + $contact_db = $idsmanager->loadCollection('TCONTACT', "chado_id_space"); + $person_term_id = $contact_db->getTerm('0000003')->getInternalId(); + $contact_id1 = $this->addChadoContact($chado, [ + 'name' => 'John Doe', + 'type_id' => $person_term_id, + 'description' => 'Bioinformaticist extrodinaire' + ]); + $contact_id2 = $this->addChadoContact($chado, [ + 'name' => 'Lady Gaga', + 'type_id' => $person_term_id, + 'description' => 'Pop star' + ]); + $project_id1 = $this->addChadoProject($chado, [ + 'name' => 'Bad Project', + 'description' => 'Want your bad project' + ]); + $project_id2 = $this->addChadoProject($chado, [ + 'name' => 'Project Face', + 'description' => 'I wanna project like they do in Texas, please' + ]); + $project_contact_id1 = $this->addChadoProjectContact($chado, [ + 'project_id' => $project_id1, + 'contact_id' => $contact_id1, + ]); + $project_contact_id2 = $this->addChadoProjectContact($chado, [ + 'project_id' => $project_id1, + 'contact_id' => $contact_id2, + ]); + $project_contact_id3 = $this->addChadoProjectContact($chado, [ + 'project_id' => $project_id2, + 'contact_id' => $contact_id2, + ]); + + // Now publish the projects and contacts. We check that 3 items are + // published because there is a null contact and currently there is + // nothing to prevent that contact from being published. (Issue #1809) + $publish->init('contact', 'chado_storage'); + $entities = $publish->publish(); + $this->assertCount(3, $entities, + 'Failed to publish 3 contact entities.'); + + $publish->init('project', 'chado_storage'); + $entities = $publish->publish(); + $this->assertCount(2, $entities, + 'Failed to publish 2 project entities.'); + + // Make sure that the linked records are also published for each project. + // The chado_contact_type_default is the field we're testing got published. + $this->checkFieldItem('project', 'project_contact', 1, + ['record_id' => $project_id1, 'linker_id' => $project_contact_id1], + ['link' => $project_id1, 'bundle' => 'project', 'entity_id' => 6, 'contact_id' => $contact_id1]); + + $this->checkFieldItem('project', 'project_contact', 1, + ['record_id' => $project_id1, 'linker_id' => $project_contact_id2], + ['link' => $project_id1, 'bundle' => 'project', 'entity_id' => 6, 'contact_id' => $contact_id2]); + + $this->checkFieldItem('project', 'project_contact', 1, + ['record_id' => $project_id2, 'linker_id' => $project_contact_id3], + ['link' => $project_id2, 'bundle' => 'project', 'entity_id' => 7, 'contact_id' => $contact_id2]); + + // Check that only the exact number of linked items were published. + $this->checkFieldItem('project', 'project_contact', 2, ['entity_id' => 6], []); + $this->checkFieldItem('project', 'project_contact', 1, ['entity_id' => 7], []); + + } +}