diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..7af37f2b5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,60 @@ +name: Bug Report +description: Create a report to help us improve Tripal +body: + - type: markdown + attributes: + value: | + # BUG/ERROR report + + ## System Information + All information is available in your site's administrator report area + (Administration Toolbar > Reports > Status Report) + - type: input + id: tripal_version + attributes: + label: Tripal Version + - type: input + id: drupal_version + attributes: + label: Drupal Version + - type: input + id: postgresql_version + attributes: + label: PostgreSQL Version + - type: input + id: php_version + attributes: + label: PHP Version + - type: markdown + attributes: + value: | + ## Issue Description + Please describe your issue here. Some information you might want to include: + - the page you're seeing the issue on + - what behavior you're experiencing versus what you expect + - really anything you think might best help us help you! + For required fields that do not make sense for this issue, please enter **n/a**. + - type: textarea + id: general_issue_description + attributes: + label: General Description + description: A general description of the issue + validations: + required: true + - type: textarea + id: steps_to_reproduce + attributes: + label: Steps to reproduce + description: What steps are necessary to recreate the bug/error? Clear instructions here will make it easier for us to fix the problem! + - type: textarea + id: error_messages + attributes: + label: Error messages + description: Please include any error messages you find. + placeholder: Pasted text is automatically formatted cleanly, no need for backticks. + render: shell + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Upload screenshots into this field, and optionally provide a description of each. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/core_development_task.yml b/.github/ISSUE_TEMPLATE/core_development_task.yml new file mode 100644 index 000000000..22ee20488 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/core_development_task.yml @@ -0,0 +1,24 @@ +name: Core Development Task +description: For keeping track of Tripal 4 core development. +body: + - type: markdown + attributes: + value: | + # Core Tripal 4 Development Task + Remember to tag this issue with 1 or more groups that it relates to. + + I suggest using the "Development Branch" section to the right once you create the issue. + This links the branch to the issue and will automatically close the issue when the PR is merged. + - type: textarea + id: task_description + attributes: + label: Task Description + description: Describe the core development task. + validations: + required: true + - type: input + id: branch_name + attributes: + label: Branch Name + description: Fill this in after submitting this issue and opening a new branch. + value: tv4g[0-9]-issue\d+-[optional short descriptor] diff --git a/.github/ISSUE_TEMPLATE/discussion_question.yml b/.github/ISSUE_TEMPLATE/discussion_question.yml new file mode 100644 index 000000000..5bbeb43ad --- /dev/null +++ b/.github/ISSUE_TEMPLATE/discussion_question.yml @@ -0,0 +1,20 @@ +name: Discussion / Question +description: Discuss a topic / Ask for help. +body: + - type: markdown + attributes: + value: | + # Instructions + Our intent is to use Github as an open forum to help community members connect. + If you are looking to do any of the following, you are in the right place! and Thank You! + - Ask for help/documentation/clarification of Tripal Functionality and Site Building/Management + - Discuss controlled vocabulary terms for your data + - Ask for input on site/module design questions + - State your intent to develop a specific extension module + - Really any type of discussion or question -we want to hear from you! + - type: textarea + id: discussion_question + attributes: + label: Discussion or Question + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/tripal_4_feature_request.yml b/.github/ISSUE_TEMPLATE/tripal_4_feature_request.yml new file mode 100644 index 000000000..59d552287 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/tripal_4_feature_request.yml @@ -0,0 +1,55 @@ +name: Tripal 4 Feature Request +description: Suggest an idea for this project +body: + - type: markdown + attributes: + value: | + # Tripal 4 Feature Request + ## Instructions + The following template is meant to structure your feature request. + Please keep in mind, this issue may evolve into discussion for an extension module if it's decided that the feature is not a good fit for Tripal Core. + + Go over all the following points, and select the options that best apply to you. + - type: dropdown + id: existing_problem + attributes: + label: This feature attempts to solve an existing problem + options: + - "Yes" + - "No" + - type: dropdown + id: open_to_develop_or_collab_extensino + attributes: + label: I am open to developing or collaborating on an extension module if this is not a good fit for Tripal Core + description: No pressure here, just good to know upfront :-) + options: + - "Yes" + - "No" + - type: dropdown + id: urgency + attributes: + label: Is this feature urgent? + options: + - "Yes" + - "No" + - type: textarea + id: description + attributes: + label: Description + description: A clear and conccise description of what you want to happen. + - type: textarea + id: use-case + attributes: + label: Your specific use case + description: Please describe how you would use this feature in your own Tripal site and why you need it. + - type: textarea + id: generally_applicable + attributes: + label: Generally Applicable + description: Why do you feel this is generally applicable? + placeholder: Suggest other use cases if possible. + - type: textarea + id: additional_information_screenshots + attributes: + label: Additional information/screenshots + description: Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..b2d010f9a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ + + + + + + + +# + +### Issue # + + +### Tripal Version: + +## Description + + + +## Testing? + + + diff --git a/tripal/src/Entity/TripalEntity.php b/tripal/src/Entity/TripalEntity.php index 8e0d42a6f..3bda5cac2 100755 --- a/tripal/src/Entity/TripalEntity.php +++ b/tripal/src/Entity/TripalEntity.php @@ -11,7 +11,7 @@ use Drupal\tripal\TripalField\Interfaces\TripalFieldItemInterface; use Drupal\field\Entity\FieldConfig; use Symfony\Component\Routing\Route; - +use Drupal\tripal\TripalField\TripalFieldItemBase; /** * Defines the Tripal Content entity. @@ -129,12 +129,8 @@ public function setTitle($title = NULL, $cache = []) { $bundle = \Drupal\tripal\Entity\TripalEntityType::load($this->getType()); } - // If no title was supplied then we should try to generate one using the - // default format set by admins. - // @todo figure out how to override the title while still allowing - // tokenized titles to be updated on edit. $title = $bundle->getTitleFormat(); - $title = $this->replaceTokens($title, ['tripal_entity_type' => $bundle]); + $title = $this->replaceTokens($title, $bundle); $this->title = $title; } @@ -165,8 +161,7 @@ public function setAlias($path_alias = NULL) { $bundle = \Drupal\tripal\Entity\TripalEntityType::load($this->getType()); $path_alias = $bundle->getURLFormat(); - $path_alias = $this->replaceTokens($path_alias, - ['tripal_entity_type' => $bundle]); + $path_alias = $this->replaceTokens($path_alias, $bundle); } // Ensure there is a leading slash. @@ -253,126 +248,37 @@ public function setPublished($published) { } /** - * {@inheritdoc} + * Replaces tokens in a given tokens in URL Aliases and Titles. + * + * @param string $string + * The string to replace. + * @param array $cache */ - public function replaceTokens($string, $cache = []) { - - // Pull any items out of the cache. - if (isset($cache['bundle'])) { - $bundle_entity = $cache['bundle']; - } - else { - $bundle_entity = \Drupal\tripal\Entity\TripalEntityType::load($this->getType()); - } - - // Determine which tokens were used in the format string - $used_tokens = []; - if (preg_match_all('/\[.*?\]/', $string, $matches)) { - $used_tokens = $matches[0]; - } - - // If there are no tokens then just return the string. - if (count($used_tokens) == 0) { - return $string; - } - - // @todo UPGRADE THIS CODE ONCE TRIPAL FIELDS HAVE BEEN ADDED - // - // If the fields are not loaded for the entity then we want to load them - // but we won't do a field_attach_load() as that will load all of the - // fields. For syncing (publishing) of content loading all fields for - // all synced entities causes extreme slowness, so we'll only attach - // the necessary fields for replacing tokens. - // $attach_fields = []; - // - // foreach ($used_tokens as $token) { - // $token = preg_replace('/[\[\]]/', '', $token); - // $elements = explode(',', $token); - // $field_name = array_shift($elements); - // - // if (!property_exists($entity, $field_name) or empty($entity->{$field_name})) { - // $field = field_info_field($field_name); - // $storage = $field['storage']; - // $attach_fields[$storage['type']]['storage'] = $storage; - // $attach_fields[$storage['type']]['fields'][] = $field; - // } - // } - // - // // If we have any fields that need attaching, then do so now. - // if (count(array_keys($attach_fields)) > 0) { - // foreach ($attach_fields as $storage_type => $details) { - // $field_ids = []; - // $storage = $details['storage']; - // $fields = $details['fields']; - // foreach ($fields as $field) { - // $field_ids[$field['id']] = [$entity->id]; - // } - // $entities = [$entity->id => $entity]; - // module_invoke($storage['module'], 'field_storage_load', 'TripalEntity', - // $entities, FIELD_LOAD_CURRENT, $field_ids, []); - // } - // } - - // Now that all necessary fields are attached process the tokens. - foreach ($used_tokens as $token) { - $token = preg_replace('/[\[\]]/', '', $token); - $elements = explode(',', $token); - $field_name = array_shift($elements); - $value = ''; - - // The TripalBundle__bundle_id is a special token for substituting the - // bundle id. - if ($token === 'TripalBundle__bundle_id') { - // This token should be the id of the TripalBundle. - $value = $bundle_entity->getID(); - } - // The TripalBundle__bundle_id is a special token for substituting the - // entity id. - elseif ($token === 'TripalEntity__entity_id') { - // This token should be the id of the TripalEntity. - $value = $this->getID(); - } - elseif ($token == 'TripalEntityType__label') { - $value = $bundle_entity->getLabel(); - } - else { - $value_obj = $this->get($field_name); - if ($value_obj) { - // A field may have multiple properties. We should use the - // main property key for the field when replacing the value. - $field_type = $this->getFieldDefinition($field_name)->getType(); - $field_types = \Drupal::service('plugin.manager.field.field_type'); - $field_type_def = $field_types->getDefinition($field_type); - $field_class = $field_type_def['class']; - $main_key = $field_class::mainPropertyName(); - - // Get the value if the property has one. - $value_raw = $value_obj->getValue(); - $value = ''; - if (array_key_exists($main_key, $value_raw[0])) { - $value = $value_raw[0][$main_key]; - } - - // If the value is an array it means we have sub elements and we can - // descend through the array to look for matching value. - if (is_array($value) and count($elements) > 0) { - // @todo we still need to handle this case. - $value = ''; - //$value = _tripal_replace_entity_tokens_for_elements($elements, $value); - } + protected function replaceTokens($string, $bundle) { + + // Intiailze the Tripal token parser service. + /** @var \Drupal\tripal\Services\TripalTokenParser $token_parser **/ + $token_parser = \Drupal::service('tripal.token_parser'); + $token_parser->initParser($bundle, $this); + $field_defs = $this->getFieldDefinitions(); + foreach ($field_defs as $field_name => $field_def) { + /** @var \Drupal\Core\Field\FieldItemList $items **/ + $items = $this->get($field_name); + if ($items->count() == 1) { + /** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item **/ + /** @var \Drupal\Core\TypedData\TypedDataInterface $prop **/ + $item = $items->get(0); + if (! $item instanceof TripalFieldItemBase) { + continue; + } + $props = $item->getProperties(); + foreach ($props as $prop) { + $token_parser->addFieldValue($field_name, $prop->getName(), $prop->getValue()); } - } - - // We can't support tokens that have multiple elements (i.e. in an array). - if (is_array($value)) { - $string = str_replace('[' . $token . ']', '', $string); - } - else { - $string = str_replace('[' . $token . ']', $value ?? '', $string); } } - - return $string; + $replaced = $token_parser->replaceTokens([$string]); + return $replaced[0]; } /** @@ -480,6 +386,19 @@ public static function getValuesArray($entity) { $tsid = $item->tripalStorageId(); + // If the Tripal Storage Backend is not set on a Tripal-based field, + // we will log an error and not support the field. If developers want + // to use Drupal storage for a Tripal-based field then they need to + // indicate that by using our Drupal SQL Storage option OR by not + // creating a Tripal-based field at all depending on their needs. + if (empty($tsid)) { + \Drupal::logger('tripal')->error('The Tripal-based field :field on + this content type must indicate a TripalStorage backend and currently does not.', + [':field' => $field_name] + ); + continue; + } + // Create instance of the storage plugin so we can add the properties // to it as we go. if (!array_key_exists($tsid, $tripal_storages)) { @@ -565,7 +484,7 @@ public function preSave(EntityStorageInterface $storage) { } // Set the property values that should be saved in Drupal, everything - // else will stay in the underlying data store (e.g. Chado).. + // else will stay in the underlying data store (e.g. Chado). $delta_remove = []; $fields = $this->getFields(); foreach ($fields as $field_name => $items) { @@ -579,6 +498,19 @@ public function preSave(EntityStorageInterface $storage) { $delta = $item->getName(); $tsid = $item->tripalStorageId(); + // If the Tripal Storage Backend is not set on a Tripal-based field, + // we will log an error and not support the field. If developers want + // to use Drupal storage for a Tripal-based field then they need to + // indicate that by using our Drupal SQL Storage option OR by not + // creating a Tripal-based field at all depending on their needs. + if (empty($tsid)) { + \Drupal::logger('tripal')->error('The Tripal-based field :field on + this content type must indicate a TripalStorage backend and currently does not.', + [':field' => $field_name] + ); + continue; + } + // Load into the entity the properties that are to be stored in Drupal. $prop_values = []; $prop_types = []; @@ -671,6 +603,19 @@ public static function postLoad(EntityStorageInterface $storage, array &$entitie $delta = $item->getName(); $tsid = $item->tripalStorageId(); + // If the Tripal Storage Backend is not set on a Tripal-based field, + // we will log an error and not support the field. If developers want + // to use Drupal storage for a Tripal-based field then they need to + // indicate that by using our Drupal SQL Storage option OR by not + // creating a Tripal-based field at all depending on their needs. + if (empty($tsid)) { + \Drupal::logger('tripal')->error('The Tripal-based field :field on + this content type must indicate a TripalStorage backend and currently does not.', + [':field' => $field_name] + ); + continue; + } + // Create a new properties array for this field item. $prop_values = []; $prop_types = []; diff --git a/tripal/src/Form/TripalImporterForm.php b/tripal/src/Form/TripalImporterForm.php index a0fc8af1d..1b8e29316 100644 --- a/tripal/src/Form/TripalImporterForm.php +++ b/tripal/src/Form/TripalImporterForm.php @@ -37,6 +37,11 @@ public function buildForm(array $form, FormStateInterface $form_state, $plugin_i $importer = $importer_manager->createInstance($plugin_id); $importer_def = $importer_manager->getDefinitions()[$plugin_id]; + if (array_key_exists('cardinality', $importer_def) and $importer_def['cardinality'] != 1) { + \Drupal::messenger()->addError('Error in the definition of this importer. Tripal Importers' + . ' currently only support cardinality of 1, see Tripal issue #1635'); + } + $form['#title'] = $importer_def['label']; $form['importer_plugin_id'] = [ @@ -50,9 +55,13 @@ public function buildForm(array $form, FormStateInterface $form_state, $plugin_i $form['file'] = [ '#type' => 'fieldset', '#title' => $importer_def['upload_title'], - '#description' => $importer_def['upload_description'], '#weight' => -15, ]; + + $form['file']['upload_description'] = [ + '#type' => 'markup', + '#markup' => $importer->describeUploadFileFormat(), + ]; } if (array_key_exists('file_upload', $importer_def) and $importer_def['file_upload'] == TRUE) { @@ -118,12 +127,30 @@ public function buildForm(array $form, FormStateInterface $form_state, $plugin_i $form = array_merge($form, $importer_form); } + // We should only add a submit button if this importer uses a button. + // Examples of importers who don't use this button are multi-page forms. + if (array_key_exists('use_button', $importer_def) AND $importer_def['use_button'] !== FALSE) { - $form['button'] = [ - '#type' => 'submit', - '#value' => $importer_def['button_text'], - '#weight' => 10, - ]; + // We will allow specific importers to disable this button based on the state of the form. + // By default it is enabled. + $disabled = FALSE; + // Unless the annotation says it should be disabled by default.. + if (array_key_exists('submit_disabled', $importer_def) AND $importer_def['submit_disabled'] === TRUE) { + $disabled = TRUE; + } + // But if they set the storage to indicate we should disable/enable it + // then we will do whatever they say ;-). + $storage = $form_state->getStorage(); + if (array_key_exists('disable_TripalImporter_submit', $storage)) { + $disabled = $storage['disable_TripalImporter_submit']; + } + $form['button'] = [ + '#type' => 'submit', + '#value' => $importer_def['button_text'], + '#weight' => 10, + '#disabled' => $disabled, + ]; + } return $form; } @@ -227,45 +254,72 @@ public function validateForm(array &$form, FormStateInterface $form_state) { $importer_def = $importer_manager->getDefinitions()[$plugin_id]; $file_local = NULL; - $file_upload = NULL; $file_remote = NULL; + $file_upload = NULL; $file_existing = NULL; - // Get the form values for the file. + // Determine which file source was specified. if (array_key_exists('file_local', $importer_def) and $importer_def['file_local'] == TRUE) { $file_local = trim($form_values['file_local']); + } + if (array_key_exists('file_upload', $importer_def) and $importer_def['file_upload'] == TRUE) { + $file_upload = trim($form_values['file_upload']); + if (array_key_exists('file_upload_existing', $form_values) and $form_values['file_upload_existing']) { + $file_existing = $form_values['file_upload_existing']; + } + } + if (array_key_exists('file_remote', $importer_def) and $importer_def['file_remote'] == TRUE) { + $file_remote = trim($form_values['file_remote']); + } + + // How many methods were specified for the source of the file? + $n_methods = ($file_local?1:0) + ($file_remote?1:0) + (($file_upload or $file_existing)?1:0); + // If a file is required, the user must provide at least one file source method. + if (array_key_exists('file_required', $importer_def) and ($importer_def['file_required'] == TRUE) and ($n_methods == 0)) { + $form_state->setErrorByName('file_local', t('You must provide a file location or upload a file.')); + } + // No more than one method can be specified. + elseif ($n_methods > 1) { + $field = $file_remote?'file_remote':'file_local'; + $form_state->setErrorByName($field, t('You have specified more than one source option for' + . ' the file, only one may be used at a time.')); + } + // A single file source has been provided. If local or remote, check that it is valid. + else { // If the file is local make sure it exists on the local filesystem. if ($file_local) { // check to see if the file is located local to Drupal $file_local = trim($file_local); - $dfile = $_SERVER['DOCUMENT_ROOT'] . base_path() . $file_local; + $dfile = \Drupal::root() . '/' . $file_local; if (!file_exists($dfile)) { // if not local to Drupal, the file must be someplace else, just use // the full path provided $dfile = $file_local; } if (!file_exists($dfile)) { - // form_set_error('file_local', t("Cannot find the file on the system. Check that the file exists or that the web server has permissions to read the file.")); - $form_state->setErrorByName('file_local', t("Cannot find the file on the system. Check that the file exists or that the web server has permissions to read the file.")); + $form_state->setErrorByName('file_local', t('Cannot find the file on the system.' + . ' Check that the file exists or that the web server has permission to read the file.')); } } - } - if (array_key_exists('file_upload', $importer_def) and $importer_def['file_upload'] == TRUE) { - $file_upload = trim($form_values['file_upload']); - if (array_key_exists('file_upload_existing', $form_values) and $form_values['file_upload_existing']) { - $file_existing = $form_values['file_upload_existing']; - } - } - if (array_key_exists('file_remote', $importer_def) and $importer_def['file_remote'] == TRUE) { - $file_remote = trim($form_values['file_remote']); - } + elseif ($file_remote) { + // Validate that the remote URI is of the correct format. + if (filter_var($file_remote, FILTER_VALIDATE_URL) === false) { + $form_state->setErrorByName('file_remote', t('The Remote Path provided is not a valid URI.')); + } - // The user must provide at least an uploaded file or a local file path. - if ($importer_def['file_required'] == TRUE and !$file_upload and !$file_local and !$file_remote and !$file_existing) { - $form_state->setErrorByName('file_local', t("You must provide a file.")); + // Validate a correct format remote URI to make sure it can be + // accessed, with successful HTTP response code 200. + else { + $headers = @get_headers($file_remote); + if (($headers === false) or (!is_array($headers)) or (!strpos($headers[0], '200'))) { + $form_state->setErrorByName('file_remote', t('The Remote Path provided cannot be accessed.' + . ' Check that it is correct.')); + } + } + } } // Now allow the loader to do validation of its form additions. $importer->formValidate($form, $form_state); } -} \ No newline at end of file +} diff --git a/tripal/src/Plugin/TripalStorage/DrupalSqlStorage.php b/tripal/src/Plugin/TripalStorage/DrupalSqlStorage.php new file mode 100644 index 000000000..b8b7da3cc --- /dev/null +++ b/tripal/src/Plugin/TripalStorage/DrupalSqlStorage.php @@ -0,0 +1,97 @@ +getStorageSettings(); + $storage_settings['drupal_store'] = TRUE; + $type->setStorageSettings($storage_settings); + } + + // Now let TripalStorageBase deal with these improved property types. + parent::addTypes($field_name, $types); + } + + /** + * {@inheritDoc} + */ + public function updateValues(&$values): bool { + // No need to do anything here. This is handled by the + // default SQL storage provided by Drupal + return TRUE; + } + + /** + * {@inheritDoc} + */ + public function deleteValues($values): bool { + // No need to do anything here. This is handled by the + // default SQL storage provided by Drupal + return TRUE; + } + + /** + * {@inheritDoc} + */ + public function findValues($values) { + /** @todo: impelment this function properly **/ + return $values; + } + + /** + * {@inheritDoc} + */ + public function insertValues(&$values): bool { + // No need to do anything here. This is handled by the + // default SQL storage provided by Drupal + return TRUE; + } + + + /** + * {@inheritDoc} + */ + public function loadValues(&$values): bool { + // No need to do anything here. This is handled by the + // default SQL storage provided by Drupal + return TRUE; + } + + /** + * {@inheritDoc} + */ + public function validateValues($values) { + // No need to do anything here. This is handled by the + // default SQL storage provided by Drupal + $violations = []; + return $violations; + } +} diff --git a/tripal/src/Services/TripalFieldCollection.php b/tripal/src/Services/TripalFieldCollection.php index 9e7626b5c..fd545c236 100644 --- a/tripal/src/Services/TripalFieldCollection.php +++ b/tripal/src/Services/TripalFieldCollection.php @@ -62,7 +62,7 @@ public static function create(ContainerInterface $container) { * * This function will only add defaults if the value is not already present * in the $field_def array. You can retrieve a fully populated definition - * array, with derfaults, by not passing an argument. This function will + * array, with defaults, by not passing an argument. This function will * remove any keys in the definition array that are not supported. * * @return array $field_def @@ -201,7 +201,7 @@ public function setFieldDefDefaults(array $field_def = []) : array { * Validates a field definition array. * * This function can be used to check a field definition prior to adding - * the field to a Tripal content type.. + * the field to a Tripal content type. * * @param array $field_def * A definition array for the field. @@ -321,7 +321,7 @@ public function install() { * - storage_plugin_id: the name of the storage plugin * (e.g. 'chado_storage'). * - storage_plugin_setings: an array of any settings that the storage - * plugin expects for the field.. + * plugin expects for the field. * - settings: (array) Any other settings needed for the field. Every * field can have different settings. * - display: Provides details for display of the field. By default it @@ -361,7 +361,7 @@ public function install() { * 'cardinality' => 1, * 'required' => TRUE, * 'storage_settings' => [ - * 'storage_plugin_id' => 'tripal_default_storage', + * 'storage_plugin_id' => 'drupal_sql_storage', * 'storage_plugin_settings'=> [ * ], * 'max_length' => 255, @@ -413,7 +413,7 @@ public function addBundleField($field_def) : bool { try { - // Check if field storage exists for this field. If not, add it.. + // Check if field storage exists for this field. If not, add it. $field_storage = FieldStorageConfig::loadByName('tripal_entity', $field_def['name']); if (!$field_storage) { $field_storage = FieldStorageConfig::create([ diff --git a/tripal/src/Services/TripalTokenParser.php b/tripal/src/Services/TripalTokenParser.php new file mode 100644 index 000000000..d8f3c0757 --- /dev/null +++ b/tripal/src/Services/TripalTokenParser.php @@ -0,0 +1,189 @@ +bundle; + } + + /** + * Returns the array of values given to the parser. + * @return array + */ + public function getValues() { + return $this->values; + } + + /** + * Returns the entity given to the parser. + * + * @return \Drupal\tripal\Entity\TripalEntity + */ + public function getEntity() { + return $this->entity; + } + + /** + * Returns the names of the fields that have been added. + * + * @return array + */ + public function getFieldNames() { + return array_keys($this->fields); + } + + /** + * + * @param TripalEntity $entity + */ + public function setEntity(TripalEntity $entity) { + if ($entity->getType() != $this->bundle->getId()) { + throw new \Exception(t('TripalTokenParser: The entity provide is not of the same time as the bundle')); + } + + $this->entity = $entity; + } + + + /** + * Initializes the token parser service + * + * The content type or bundle Id. + * @param string \Drupal\tripal\Entity\TripalEntityType $bundle + */ + public function initParser(TripalEntityType $bundle, TripalEntity $entity = NULL) { + $this->bundle = $bundle; + if ($entity) { + $this->setEntity($entity); + } + + // Get the field manager, field definitions for the bundle type, and + // the field type manager. + /** @var \Drupal\Core\Entity\EntityFieldManager $field_manager **/ + /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager **/ + $field_manager = \Drupal::service('entity_field.manager'); + $field_defs = $field_manager->getFieldDefinitions('tripal_entity', $bundle->getID()); + $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); + + // Iterate over the field definitions for the bundle and createa field instance. + /** @var \Drupal\Core\Field\BaseFieldDefinition $field_definition **/ + $field_definition = NULL; + foreach ($field_defs as $field_name => $field_definition) { + if (!empty($field_definition->getTargetBundle())) { + $configuration = [ + 'field_definition' => $field_definition, + 'name' => $field_name, + 'parent' => NULL, + ]; + $field = $field_type_manager->createInstance($field_definition->getType(), $configuration); + $this->fields[$field_name] = $field; + } + } + } + + /** + * Adds the field values that should be used for replacement. + * + * @param string $field_name + * The name of the field that the value belongs to + * @param StoragePropertyValue $value + * The property values + */ + public function addFieldValue($field_name, string $key, $value){ + $this->values[$field_name][$key] = $value; + } + + /** + * Replaces the tokens with field values within the provided strings. + * + * @param array $tokenized_strings + * Ann array of strings with field names as tokens. Field name should be + * surrounded by square brackets. + * + * @return array + * An array with all of the strings from the input $tokenized_strings array + * but with field tokens replaced with approprivate values. + */ + public function replaceTokens(array $tokenized_strings) { + + $replaced = $tokenized_strings; + foreach ($tokenized_strings as $index => $tokenized_string) { + $value = NULL; + + // Get the tokens in the string. + $tokens = []; + $matches = []; + if (preg_match_all('/\[.*?\]/', $tokenized_string, $matches)) { + $tokens = $matches[0]; + } + foreach ($tokens as $token) { + $token = preg_replace('/\[/', '', $token); + $token = preg_replace('/\]/', '', $token); + + // Look for values for bundle or entity related tokens. + if ($token === 'TripalBundle__bundle_id') { + $value = $this->bundle->getID(); + $replaced[$index] = trim(preg_replace("/\[$token\]/", $value, $replaced[$index])); + } + elseif ($token == 'TripalEntityType__label') { + $value = $this->bundle->getLabel(); + $replaced[$index] = trim(preg_replace("/\[$token\]/", $value, $replaced[$index])); + } + elseif ($token === 'TripalEntity__entity_id' and !is_null($this->entity)) { + $value = $this->entity->getID(); + $replaced[$index] = trim(preg_replace("/\[$token\]/", $value, $replaced[$index])); + } + // Look for values for field related tokens + elseif (in_array($token, array_keys($this->fields))) { + $field = $this->fields[$token]; + $key = $field->mainPropertyName(); + if (array_key_exists($token, $this->values)) { + $value = $this->values[$token][$key]; + if (!is_null($value)) { + $replaced[$index] = trim(preg_replace("/\[$token\]/", $value, $replaced[$index])); + } + } + } + } + } + + return $replaced; + } +} diff --git a/tripal/src/TripalDBX/TripalDbxConnection.php b/tripal/src/TripalDBX/TripalDbxConnection.php index 71ae1e207..959918f0d 100644 --- a/tripal/src/TripalDBX/TripalDbxConnection.php +++ b/tripal/src/TripalDBX/TripalDbxConnection.php @@ -62,8 +62,8 @@ * and offers, beside others, the follwing methods: addIndex(), * addPrimaryKey(), addUniqueKey(), createTable(), dropField(), dropIndex(), * dropPrimaryKey(), dropTable(), dropUniqueKey(), fieldExists(), - * findPrimaryKeyColumns(), findTables(), indexExists(), renameTable(), - * tableExists() and more from the documentation. + * findPrimaryKeyColumns(), findTables(), indexExists(), renameTable() + * and more from the documentation. * * A couple of methods have been added to this class to complete the above list. * @@ -697,9 +697,9 @@ public function setExtraSchema(string $schema_name, int $index = 2) :void { */ public function getVersion() :string { - if ((NULL === $this->version) && !empty($this->usedSchemas[1])) { + if (!is_numeric($this->version) && !empty($this->usedSchemas[1])) { // Get the version of the schema. - $this->version = $this->findVersion(); + $this->version = (string) $this->findVersion(); } return $this->version ?? ''; @@ -978,6 +978,8 @@ function ($matches) { * Find the prefix for a table. * * OVERRIDES \Drupal\Core\Database\Connection:tablePrefix(). + * REMOVED IN Drupal 10.1.x + * SEE https://www.drupal.org/node/3260849 * * This function is for when you want to know the prefix of a table. This * is not used in prefixTables due to performance reasons. @@ -1034,6 +1036,17 @@ public function tablePrefix( } } + /** + * Returns the prefix of the tables. + * + * OVERRIDES \Drupal\Core\Database\Connection:getPrefix(). + * + * @return string $prefix + */ + public function getPrefix(): string { + return $this->usedSchemas[1] . '.'; + } + /** * Executes all the given SQL statements into the current schema. * diff --git a/tripal/src/TripalDBX/TripalDbxSchema.php b/tripal/src/TripalDBX/TripalDbxSchema.php index d1653ee2b..8d4714acc 100644 --- a/tripal/src/TripalDBX/TripalDbxSchema.php +++ b/tripal/src/TripalDBX/TripalDbxSchema.php @@ -30,7 +30,7 @@ * - createTable(), dropTable() * - dropField(), dropIndex(), dropPrimaryKey(), dropUniqueKey(), * - fieldExists(), findPrimaryKeyColumns(), - * - renameTable(), tableExists() + * - renameTable() * and more from the documentation. * * @see https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Database!Driver!pgsql!Schema.php/class/Schema/9.0.x @@ -144,7 +144,7 @@ public function __construct( } /** - * + * */ public function initialize() { if (!$this->initialized) { @@ -744,7 +744,7 @@ public function foreignKeyConstraintExists( * -'name': table name * -'type': one of 'table', 'view', 'partition' and 'materialized view' for * PostgreSQL materialized views. - * -'status': either 'base' for base a table, or 'custom' for a custom table + * -'status': either 'base' for a base table, or 'custom' for a custom table * or a tripal materialized view, or 'other' for other elements. */ public function getTables( @@ -1137,4 +1137,22 @@ public function dropSchema() :void { $this->tripalDbxApi->dropSchema($this->defaultSchema, $this->connection); } + /** + * Overrides Drupal\Core\Database\Schema->tableExists(). + * + * We needed to override it because core Drupal makes some assumptions + * when building the where condition that do not match our multi-schema setup. + */ + public function tableExists($table) { + + // We can't use \Drupal::database()->select() here + // because it would prefix information_schema.tables + // and the query would fail. + // Don't use {} around information_schema.tables table. + return (bool) $this->connection + ->query('SELECT TRUE AS table_exists FROM pg_tables + WHERE schemaname=:schema AND tablename = :table', + [':table' => $table, ':schema' => $this->getSchemaName()]) + ->fetchField(); + } } diff --git a/tripal/src/TripalImporter/Annotation/TripalImporter.php b/tripal/src/TripalImporter/Annotation/TripalImporter.php index ec6023551..2567c70ce 100644 --- a/tripal/src/TripalImporter/Annotation/TripalImporter.php +++ b/tripal/src/TripalImporter/Annotation/TripalImporter.php @@ -45,8 +45,7 @@ class TripalImporter extends Plugin { * * @var array */ - public $file_types; - + public $file_types = ['txt']; /** * Provides information to the user about the file upload. @@ -77,7 +76,7 @@ class TripalImporter extends Plugin { * * @var bool */ - public $use_analysis; + public $use_analysis = TRUE; /** * If the $use_analysis value is set above then this value indicates if the @@ -85,7 +84,32 @@ class TripalImporter extends Plugin { * * @var bool */ - public $require_analysis; + public $require_analysis = FALSE; + + /** + * Indicates whether the base importer should add a submit button or not. + * This should only be used in situations were you need multiple buttons + * or control over the submit process (e.g. multi-page forms). + * + * @var bool + */ + public $use_button = TRUE; + + /** + * Indicated whether the base importer added submit button should be disabled + * when the form is first loaded (i.e when the user clicks the link for the + * importer). The form can then be programatically enabled via AJAX once + * certain criteria is set by setting the form state storage. + * + * Example of programatically enabling the button via the form state + * on an importer where this annotation is set to TRUE. + * @code + $storage = $form_state->getStorage(); + $storage['disable_TripalImporter_submit'] = FALSE; + $form_state->setStorage($storage); + * @endcode + */ + public $submit_disabled = FALSE; /** * Text that should appear on the button at the bottom of the importer form. @@ -94,29 +118,28 @@ class TripalImporter extends Plugin { * * @ingroup plugin_translatable */ - public $button_text; + public $button_text = 'Import'; /** * Indicates if the loader should provide a file upload form element. * * @var bool */ - public $file_upload; + public $file_upload = TRUE; /** * Indicates if the loader should provide a local file form element. * * @var bool */ - public $file_local; + public $file_local = TRUE; /** * Indicates if the loader should provide a remote file form element. * * @var bool */ - public $file_remote; - + public $file_remote = TRUE; /** * Indicates if the file must be provided. @@ -128,8 +151,7 @@ class TripalImporter extends Plugin { * * @var bool */ - public $file_required; - + public $file_required = TRUE; /** * The array of arguments used for this loader. @@ -142,7 +164,6 @@ class TripalImporter extends Plugin { */ public $argument_list = []; - /** * Indicates how many files are allowed to be uploaded. * @@ -151,8 +172,7 @@ class TripalImporter extends Plugin { * * @var int */ - public $cardinality; - + public $cardinality = 1; /** * Be default, all loaders are automaticlly added to the Admin > @@ -164,7 +184,6 @@ class TripalImporter extends Plugin { */ public $menu_path; - /** * If your importer requires more flexibility and advanced features than * the TripalImporter provides, you can indicate a callback function. If set, @@ -201,4 +220,4 @@ class TripalImporter extends Plugin { */ public $callback_path; -} \ No newline at end of file +} diff --git a/tripal/src/TripalImporter/TripalImporterBase.php b/tripal/src/TripalImporter/TripalImporterBase.php index 8f26b732a..d66118852 100644 --- a/tripal/src/TripalImporter/TripalImporterBase.php +++ b/tripal/src/TripalImporter/TripalImporterBase.php @@ -103,7 +103,6 @@ abstract class TripalImporterBase extends PluginBase implements TripalImporterIn */ protected $plugin_definition; - /** * {@inheritdoc} */ @@ -132,6 +131,21 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition } + /** + * Provide more informative description than is ideal in the annotation alone. + * + * NOTE: Supports full HTML. + * + * @return + * A fully formatted string describing the format of the file to be uploaded + * and providing any additional upload file information. + */ + public function describeUploadFileFormat() { + $default_description = $this->plugin_definition['upload_description']; + $file_types = $this->plugin_definition['file_types']; + return $default_description . ' The following file extensions are supported: ' . implode(', ', $file_types) . '.'; + } + /** * Associate this importer with the Tripal job that is running it. * @@ -160,7 +174,8 @@ public function setJob($job) { * -file_remote: provides the remote URL for the file. * This argument is optional if the loader does not use the built-in * file loader. - * + * @return int + * Returns the import_id. */ public function create($run_args, $file_details = []) { @@ -245,6 +260,7 @@ public function create($run_args, $file_details = []) { ->execute(); $this->import_id = $import_id; + return $import_id; } catch (\Exception $e) { throw new \Exception('Cannot create importer: ' . $e->getMessage()); @@ -317,28 +333,21 @@ public function submitJob() { */ public function prepareFiles() { - // If no file is required then just indicate that all is good to go. - if ($this->plugin_definition['file_required'] == FALSE) { - $this->is_prepared = TRUE; - return; - } - try { for ($i = 0; $i < count($this->arguments['files']); $i++) { if (!empty($this->arguments['files'][$i]['file_remote'])) { $file_remote = $this->arguments['files'][$i]['file_remote']; - $this->logMessage('Download file: !file_remote...', ['!file_remote' => $file_remote]); - + $this->logger->notice('Download file: %file_remote...', ['%file_remote' => $file_remote]); - // If this file is compressed then keepthe .gz extension so we can + // If this file is compressed then keep the .gz extension so we can // uncompress it. $ext = ''; if (preg_match('/^(.*?)\.gz$/', $file_remote)) { $ext = '.gz'; } // Create a temporary file. - $temp = tempnam("temporary://", 'import_') . $ext; - $this->logMessage("Saving as: !file", ['!file' => $temp]); + $temp = \Drupal::service('file_system')->tempnam("temporary://", 'import_') . $ext; + $this->logger->notice('Saving as: %file', ['%file' => $temp]); $url_fh = fopen($file_remote, "r"); $tmp_fh = fopen($temp, "w"); @@ -359,7 +368,7 @@ public function prepareFiles() { // Is this file compressed? If so, then uncompress it $matches = []; if (preg_match('/^(.*?)\.gz$/', $this->arguments['files'][$i]['file_path'], $matches)) { - $this->logMessage("Uncompressing: !file", ['!file' => $this->arguments['files'][$i]['file_path']]); + $this->logger->notice('Uncompressing: %file', ['%file' => $this->arguments['files'][$i]['file_path']]); $buffer_size = 4096; $new_file_path = $matches[1]; $gzfile = gzopen($this->arguments['files'][$i]['file_path'], 'rb'); @@ -389,6 +398,13 @@ public function prepareFiles() { catch (\Exception $e) { throw new \Exception('Cannot prepare the importer: ' . $e->getMessage()); } + + + // If we get here and no exception has been thrown then either + // 1) files were added but none needed to be prepared. + // 2) files were not added (check for files being required happens elsewhere). + $this->is_prepared = TRUE; + } /** @@ -403,7 +419,7 @@ public function cleanFile() { for ($i = 0; $i < count($this->arguments['files']); $i++) { if (!empty($this->arguments['files'][$i]['file_remote']) and file_exists($this->arguments['files'][$i]['file_path'])) { - $this->logMessage('Removing downloaded file...'); + $this->logger->notice('Removing downloaded file...'); unlink($this->arguments['files'][$i]['file_path']); $this->is_prepared = FALSE; } @@ -465,10 +481,6 @@ protected function setItemsHandled($total_handled) { $percent = ($this->num_handled / $this->total_items) * 100; $ipercent = (int) $percent; } - else { - $percent = 0; - $ipercent = 0; - } // If we've reached our interval then print update info. if ($ipercent > 0 and $ipercent != $this->reported and $ipercent % $this->interval == 0) { @@ -485,6 +497,22 @@ protected function setItemsHandled($total_handled) { } $this->reported = $ipercent; } + + // If we're done then indicate so. + if ($this->num_handled >= $this->total_items) { + $memory = number_format(memory_get_usage()); + $spercent = sprintf("%.2f", 100); + $this->logger->notice( + t("Percent complete: " . $spercent . " %. Memory: " . $memory . " bytes.") + . "\r" + ); + + // If we have a job the update the job progress too. + if ($this->job) { + $this->job->setProgress(100); + } + $this->reported = 100; + } } /** diff --git a/tripal/src/TripalStorage/StoragePropertyBase.php b/tripal/src/TripalStorage/StoragePropertyBase.php index 96d11a3b0..8265feaf7 100644 --- a/tripal/src/TripalStorage/StoragePropertyBase.php +++ b/tripal/src/TripalStorage/StoragePropertyBase.php @@ -63,9 +63,20 @@ public function __construct($entityType, $fieldType, $key, $term_id) { } } else { - throw new \Exception('Cannot create a StorageProperty object without a property formatted term: ' . $term_id); + throw new \Exception('Cannot create a StorageProperty object without a properly formatted term: ' . $term_id); } + // Ensure we have required values. + if (!$entityType) { + throw new \Exception('Cannot create a StorageProperty object without specifying the entity type.'); + } + if (!$fieldType) { + throw new \Exception('Cannot create a StorageProperty object without specifying the field that is using it.'); + } + if (!$key) { + throw new \Exception('Cannot create a StorageProperty object without a key.'); + } + // Drupal doesn't allow non alphanumeric characters in the key, so // remove any. $key = preg_replace('/[^\w]/', '_', $key); diff --git a/tripal/src/TripalStorage/StoragePropertyValue.php b/tripal/src/TripalStorage/StoragePropertyValue.php index 6e9a5359d..c74b190bf 100644 --- a/tripal/src/TripalStorage/StoragePropertyValue.php +++ b/tripal/src/TripalStorage/StoragePropertyValue.php @@ -32,10 +32,8 @@ class StoragePropertyValue extends StoragePropertyBase { * An optional initial value for this storage property value. */ public function __construct($entityType, $fieldType, $key, $term_id, $entityId, $value = NUll) { - if (!$key) { - throw new \Exception('Cannot create a StoragePropertyValue object without a key.'); - } parent::__construct($entityType, $fieldType, $key, $term_id); + $this->entityId = $entityId; $this->value = $value; } diff --git a/tripal/src/TripalStorage/TripalStorageBase.php b/tripal/src/TripalStorage/TripalStorageBase.php index d12e0eb1c..562806051 100644 --- a/tripal/src/TripalStorage/TripalStorageBase.php +++ b/tripal/src/TripalStorage/TripalStorageBase.php @@ -30,6 +30,16 @@ abstract class TripalStorageBase extends PluginBase implements TripalStorageInte */ protected $field_definitions = []; + /** + * An associative array that contains all of the property types that + * have been added to this object. It is indexed by entityType -> + * fieldName -> key and the value is the + * Drupal\tripal\TripalStorage\StoragePropertyValue object. + * + * @var array + */ + protected $property_types = []; + /** * Implements ContainerFactoryPluginInterface->create(). * @@ -77,9 +87,6 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition */ public function addFieldDefinition(string $field_name, object $field_definition) { - if (!array_key_exists($field_name, $this->field_definitions)) { - $this->field_definitions[$field_name] = []; - } $this->field_definitions[$field_name] = $field_definition; return TRUE; @@ -98,4 +105,70 @@ public function getFieldDefinition(string $field_name) { return FALSE; } + + /** + * @{inheritdoc} + */ + public function addTypes(string $field_name, array $types) { + + // Index the types by their entity type, field type and key. + foreach ($types as $index => $type) { + if (!is_object($type)) { + $this->logger->error('Type provided must be an object but instead index @index was a @type', + ['@index' => $index, '@type' => gettype($type)]); + return FALSE; + } + elseif(!is_subclass_of($type, 'Drupal\tripal\TripalStorage\StoragePropertyTypeBase')) { + $this->logger->error('Type provided must be an object extending StoragePropertyTypeBase. Instead index @index was of type: @type', + ['@index' => $index, '@type' => get_class($type)]); + return FALSE; + } + + $key = $type->getKey(); + + if (!array_key_exists($field_name, $this->property_types)) { + $this->property_types[$field_name] = []; + } + $this->property_types[$field_name][$key] = $type; + + } + } + + /** + * @{inheritdoc} + */ + public function getTypes() { + return $this->property_types; + } + + /** + * @{inheritdoc} + */ + public function getPropertyType(string $field_name, string $key) { + + if (array_key_exists($field_name, $this->property_types)) { + if (array_key_exists($key, $this->property_types[$field_name])) { + return $this->property_types[$field_name][$key]; + } + } + + return NULL; + } + + /** + * @{inheritdoc} + */ + public function removeTypes(string $field_name, array $types) { + + foreach ($types as $type) { + $key = $type->getKey(); + + if (array_key_exists($field_name, $this->property_types)) { + if (array_key_exists($key, $this->property_types[$field_name])) { + unset($this->property_types[$field_name][$key]); + } + } + + } + } } diff --git a/tripal/src/TripalStorage/TripalStorageUpdateException.php b/tripal/src/TripalStorage/TripalStorageUpdateException.php deleted file mode 100644 index cebca79b8..000000000 --- a/tripal/src/TripalStorage/TripalStorageUpdateException.php +++ /dev/null @@ -1,23 +0,0 @@ -maxCharacterSize = $size; } diff --git a/tripal/src/TripalVocabTerms/TripalTerm.php b/tripal/src/TripalVocabTerms/TripalTerm.php index 0d97fbecb..a2900f4e4 100644 --- a/tripal/src/TripalVocabTerms/TripalTerm.php +++ b/tripal/src/TripalVocabTerms/TripalTerm.php @@ -818,7 +818,7 @@ public function getInternalId() { * An array of synonyms. * * For easy lookup, this is an associative array where - * the key is the synonym and the value is the type term.. + * the key is the synonym and the value is the type term. * Using the synonym as the key also prevents duplication. * * @var array diff --git a/tripal/tests/fixtures/importer_whitespace_test_file.txt.gz b/tripal/tests/fixtures/importer_whitespace_test_file.txt.gz new file mode 100644 index 000000000..99689cad3 Binary files /dev/null and b/tripal/tests/fixtures/importer_whitespace_test_file.txt.gz differ diff --git a/tripal/tests/src/Functional/Services/TripalFieldColectionTest.php b/tripal/tests/src/Functional/Services/TripalFieldColectionTest.php index 9e7c3095b..b597120b5 100644 --- a/tripal/tests/src/Functional/Services/TripalFieldColectionTest.php +++ b/tripal/tests/src/Functional/Services/TripalFieldColectionTest.php @@ -76,7 +76,7 @@ public function testTripalFieldCollection() { 'cardinality' => 1, 'required' => TRUE, 'storage_settings' => [ - 'storage_plugin_id' => 'tripal_default_storage', + 'storage_plugin_id' => 'drupal_sql_storage', 'storage_plugin_settings'=> [], 'max_length' => 255, ], diff --git a/tripal/tests/src/Functional/Services/TripalTokenParserTest.php b/tripal/tests/src/Functional/Services/TripalTokenParserTest.php new file mode 100644 index 000000000..ac5dc3de1 --- /dev/null +++ b/tripal/tests/src/Functional/Services/TripalTokenParserTest.php @@ -0,0 +1,517 @@ +createCollection('OBI', "tripal_default_id_space"); + $vocab = $vmanager->createCollection('OBI', "tripal_default_vocabulary"); + $term = new TripalTerm([ + 'name' => 'organism', + 'idSpace' => 'OBI', + 'vocabulary' => 'OBI', + 'accession' => '0100026', + 'definition' => '', + ]); + $created = $idspace->saveTerm($term); + $this->assertTrue($created, 'Could not create organism term'); + + // Create the content type. + $oganism_config = [ + 'label' => 'Organism', + 'term' => $term, + 'help_text' => 'Use the organism page for an individual living system, such as animal, plant, bacteria or virus,', + 'category' => 'General', + 'id' => 'organism', + 'title_format' => "[organism_genus] [organism_species] [organism_infraspecific_type] [organism_infraspecific_name]", + 'url_format' => "organism/[TripalEntity__entity_id]", + 'synonyms' => ['bio_data_1'] + ]; + /** @var \Drupal\tripal\Services\TripalEntityTypeCollection $content_type_service **/ + $content_type_service = \Drupal::service('tripal.tripalentitytype_collection'); + $organism = $content_type_service->createContentType($oganism_config); + $this->assertTrue(!is_null($organism), "Failed to create the organism content type with a valid definition."); + + // + // Add fields to the content type. + // + /** @var \Drupal\tripal\Services\TripalFieldCollection $fields_service **/ + $fields_service = \Drupal::service('tripal.tripalfield_collection'); + + // Add TAXRANK Terms + $idspace = $idsmanager->createCollection('TAXRANK', "tripal_default_id_space"); + $vocab = $vmanager->createCollection('TAXRANK', "tripal_default_vocabulary"); + $term = new TripalTerm([ + 'name' => 'genus', + 'idSpace' => 'TAXRANK', + 'vocabulary' => 'TAXRANK', + 'accession' => '0000005', + 'definition' => '', + ]); + $idspace->saveTerm($term); + $this->assertTrue($created, 'Could not create genus term'); + $term = new TripalTerm([ + 'name' => 'species', + 'idSpace' => 'TAXRANK', + 'vocabulary' => 'TAXRANK', + 'accession' => '0000006', + 'definition' => '', + ]); + $idspace->saveTerm($term); + $this->assertTrue($created, 'Could not create species term'); + $term = new TripalTerm([ + 'name' => 'infraspecies', + 'idSpace' => 'TAXRANK', + 'vocabulary' => 'TAXRANK', + 'accession' => '0000045', + 'definition' => '', + ]); + $idspace->saveTerm($term); + $this->assertTrue($created, 'Could not create infraspecies term'); + + // Add local Terms + $idspace = $idsmanager->createCollection('local', "tripal_default_id_space"); + $vocab = $vmanager->createCollection('local', "tripal_default_vocabulary"); + $term = new TripalTerm([ + 'name' => 'infrasepecific type', + 'idSpace' => 'local', + 'vocabulary' => 'local', + 'accession' => 'infraspecific_type', + 'definition' => '', + ]); + $idspace->saveTerm($term); + $this->assertTrue($created, 'Could not create infrasepecific type term'); + $term = new TripalTerm([ + 'name' => 'abbreviation', + 'idSpace' => 'local', + 'vocabulary' => 'local', + 'accession' => 'abbreviation', + 'definition' => '', + ]); + $idspace->saveTerm($term); + $this->assertTrue($created, 'Could not create abbreviation term'); + + // Add NCBITaxon Terms + $idspace = $idsmanager->createCollection('NCBITaxon', "tripal_default_id_space"); + $vocab = $vmanager->createCollection('NCBITaxon', "tripal_default_vocabulary"); + $term = new TripalTerm([ + 'name' => 'common name', + 'idSpace' => 'NCBITaxon', + 'vocabulary' => 'NCBITaxon', + 'accession' => 'common_name', + 'definition' => '', + ]); + $idspace->saveTerm($term); + $this->assertTrue($created, 'Could not create common name term'); + + // Add schema Terms + $idspace = $idsmanager->createCollection('schema', "tripal_default_id_space"); + $vocab = $vmanager->createCollection('schema', "tripal_default_vocabulary"); + $term = new TripalTerm([ + 'name' => 'description', + 'idSpace' => 'schema', + 'vocabulary' => 'schema', + 'accession' => 'description', + 'definition' => '', + ]); + $idspace->saveTerm($term); + $this->assertTrue($created, 'Could not create description term'); + + $genus_field = [ + 'name' => 'organism_genus', + 'content_type' => 'organism', + 'label' => 'Genus', + 'type' => 'tripal_string_type', + 'description' => 'The genus name of the organism.', + 'cardinality' => 1, + 'required' => TRUE, + 'storage_settings' => [ + 'storage_plugin_id' => 'drupal_sql_storage', + 'storage_plugin_settings' => [], + 'max_length' => 255, + ], + 'settings' => [ + 'termIdSpace' => 'TAXRANK', + 'termAccession' => '0000005', + ], + 'display' => [ + 'view' => [ + 'default' => [ + 'region' => 'content', + 'label' => 'above', + 'weight' => 15, + ], + ], + 'form' => [ + 'default' => [ + 'region' => 'content', + 'weight' => 15, + ], + ], + ], + ]; + $is_valid = $fields_service->validate($genus_field); + $this->assertTrue($is_valid, "The genus field definition is invalid."); + $is_added = $fields_service->addBundleField($genus_field); + $this->assertTrue($is_added, "The genus field could not be added to the bundle."); + + $species_field = [ + 'name' => 'organism_species', + 'content_type' => 'organism', + 'label' => 'Species', + 'type' => 'tripal_string_type', + 'description' => 'The species name of the organism.', + 'cardinality' => 1, + 'required' => TRUE, + 'storage_settings' => [ + 'storage_plugin_id' => 'drupal_sql_storage', + 'storage_plugin_settings' => [], + 'max_length' => 255, + ], + 'settings' => [ + 'termIdSpace' => 'TAXRANK', + 'termAccession' => '0000006', + ], + 'display' => [ + 'view' => [ + 'default' => [ + 'region' => 'content', + 'label' => 'above', + 'weight' => 20, + ], + ], + 'form' => [ + 'default' => [ + 'region' => 'content', + 'weight' => 20, + ], + ], + ], + ]; + $is_valid = $fields_service->validate($species_field); + $this->assertTrue($is_valid, "The species field definition is invalid."); + $is_added = $fields_service->addBundleField($species_field); + $this->assertTrue($is_added, "The species field could not be added to the bundle."); + + + $infraspecific_name_field = [ + 'name' => 'organism_infraspecific_name', + 'content_type' => 'organism', + 'label' => 'Infraspecies', + 'type' => 'tripal_string_type', + 'description' => 'The infraspecfic name for the organism.', + 'cardinality' => 1, + 'required' => FALSE, + 'storage_settings' => [ + 'storage_plugin_id' => 'drupal_sql_storage', + 'storage_plugin_settings' => [], + 'max_length' => 1024, + ], + 'settings' => [ + 'termIdSpace' => 'TAXRANK', + 'termAccession' => '0000045', + ], + 'display' => [ + 'view' => [ + 'default' => [ + 'region' => 'content', + 'label' => 'above', + 'weight' => 30, + ], + ], + 'form' => [ + 'default' => [ + 'region' => 'content', + 'weight' => 30, + ], + ], + ], + ]; + $is_valid = $fields_service->validate($infraspecific_name_field); + $this->assertTrue($is_valid, "The infraspecific name field definition is invalid."); + $is_added = $fields_service->addBundleField($infraspecific_name_field); + $this->assertTrue($is_added, "The infraspecific name field could not be added to the bundle."); + + $comment_field = [ + 'name' => 'organism_comment', + 'content_type' => 'organism', + 'label' => 'Description', + 'type' => 'tripal_string_type', + 'description' => 'A description of the organism.', + 'cardinality' => 1, + 'required' => FALSE, + 'storage_settings' => [ + 'storage_plugin_id' => 'drupal_sql_storage', + 'storage_plugin_settings' => [], + 'max_length' => 255, + ], + 'settings' => [ + 'termIdSpace' => 'schema', + 'termAccession' => 'description', + ], + 'display' => [ + 'view' => [ + 'default' => [ + 'region' => 'content', + 'label' => 'above', + 'weight' => 35, + ], + ], + 'form' => [ + 'default' => [ + 'region' => 'content', + 'weight' => 35, + ], + ], + ], + ]; + $is_valid = $fields_service->validate($comment_field); + $this->assertTrue($is_valid, "The comment field definition is invalid."); + $is_added = $fields_service->addBundleField($comment_field); + $this->assertTrue($is_added, "The comment field could not be added to the bundle."); + + $infraspecific_type_field = [ + 'name' => 'organism_infraspecific_type', + 'content_type' => 'organism', + 'label' => 'Infraspecific Type', + 'type' => 'tripal_string_type', + 'description' => 'The connector type (e.g. subspecies, varietas, forma, etc.) for the infraspecific name.', + 'cardinality' => 1, + 'required' => FALSE, + 'storage_settings' => [ + 'storage_plugin_id' => 'drupal_sql_storage', + 'storage_plugin_settings' => [], + ], + 'settings' => [ + 'termIdSpace' => 'local', + 'termAccession' => 'infraspecific_type', + ], + 'display' => [ + 'view' => [ + 'default' => [ + 'region' => 'content', + 'label' => 'above', + 'weight' => 10, + ], + ], + 'form' => [ + 'default' => [ + 'region' => 'content', + 'weight' => 10, + ], + ], + ], + ]; + $is_valid = $fields_service->validate($infraspecific_type_field); + $this->assertTrue($is_valid, "The infraspecific type field definition is invalid."); + $is_added = $fields_service->addBundleField($infraspecific_type_field); + $this->assertTrue($is_added, "The infraspecific type field could not be added to the bundle."); + + $abbreviation_field = [ + 'name' => 'organism_abbreviation', + 'content_type' => 'organism', + 'label' => 'Abbreviation', + 'type' => 'tripal_string_type', + 'description' => 'A shortened name (or abbreviation) for the organism (e.g. O. sativa).', + 'cardinality' => 1, + 'required' => FALSE, + 'storage_settings' => [ + 'storage_plugin_id' => 'drupal_sql_storage', + 'storage_plugin_settings' => [], + 'max_length' => 255, + ], + 'settings' => [ + 'termIdSpace' => 'local', + 'termAccession' => 'abbreviation', + ], + 'display' => [ + 'view' => [ + 'default' => [ + 'region' => 'content', + 'label' => 'above', + 'weight' => 10, + ], + ], + 'form' => [ + 'default' => [ + 'region' => 'content', + 'weight' => 10, + ], + ], + ], + ]; + $is_valid = $fields_service->validate($abbreviation_field); + $this->assertTrue($is_valid, "The abbreviation field definition is invalid."); + $is_added = $fields_service->addBundleField($abbreviation_field); + $this->assertTrue($is_added, "The abbreviation field could not be added to the bundle."); + + $common_name_field = [ + 'name' => 'organism_common_name', + 'content_type' => 'organism', + 'label' => 'Common Name', + 'type' => 'tripal_string_type', + 'description' => 'The common name for the organism.', + 'cardinality' => 1, + 'required' => FALSE, + 'storage_settings' => [ + 'storage_plugin_id' => 'drupal_sql_storage', + 'storage_plugin_settings' => [], + 'max_length' => 255, + ], + 'settings' => [ + 'termIdSpace' => 'NCBITaxon', + 'termAccession' => 'common_name', + ], + 'display' => [ + 'view' => [ + 'default' => [ + 'region' => 'content', + 'label' => 'above', + 'weight' => 25, + ], + ], + 'form' => [ + 'default' => [ + 'region' => 'content', + 'weight' => 25, + ], + ], + ], + ]; + $is_valid = $fields_service->validate($common_name_field); + $this->assertTrue($is_valid, "The common name field definition is invalid."); + $is_added = $fields_service->addBundleField($common_name_field); + $this->assertTrue($is_added, "The common name field could not be added to the bundle."); + + // + // Test the functions of the token parser service. + // + /** @var \Drupal\tripal\Services\TripalTokenParser $token_parser **/ + $token_parser = \Drupal::service('tripal.token_parser'); + $token_parser->initParser($organism); + $this->assertTrue($token_parser->getBunde()->getId() === 'organism', "The tripal token parser didn't set the bundle properly."); + $this->assertTrue(is_null($token_parser->getEntity()), "The tripal token parser should have a null entity."); + $field_names = $token_parser->getFieldNames(); + + // Mkae sure all of the fields are present in the token parser. + // This ensures that the bundle was found and it was able to + // create instances of all the fields attached to it. + $this->assertTrue(in_array('organism_genus', $field_names), 'The tripal token parser is missing the organism_genus field.'); + $this->assertTrue(in_array('organism_species', $field_names), 'The tripal token parser is missing the organism_species field.'); + $this->assertTrue(in_array('organism_abbreviation', $field_names), 'The tripal token parser is missing the organism_abbreviation field.'); + $this->assertTrue(in_array('organism_common_name', $field_names), 'The tripal token parser is missing the organism_common_name field.'); + $this->assertTrue(in_array('organism_infraspecific_type', $field_names), 'The tripal token parser is missing the organism_infraspecific_type field.'); + $this->assertTrue(in_array('organism_infraspecific_name', $field_names), 'The tripal token parser is missing the organism_infraspecific_name field.'); + $this->assertTrue(in_array('organism_comment', $field_names), 'The tripal token parser is missing the organism_comment field.'); + $this->assertTrue(count($field_names) == 7, 'The tripal token parser has more fields than expected.'); + + // Add some values without an entity + $token_parser->addFieldValue('organism_genus', 'value', 'Oryza'); + $token_parser->addFieldValue('organism_species', 'value', 'sativa'); + $token_parser->addFieldValue('organism_abbreviation', 'value', 'O. sativa'); + $token_parser->addFieldValue('organism_common_name', 'value', 'Japonica rice'); + $token_parser->addFieldValue('organism_comment', 'value', 'rice is nice'); + $values = $token_parser->getValues(); + + $this->assertTrue(in_array('organism_genus', array_keys($values)), 'The tripal token parser is missing the organism_genus value field name.'); + $this->assertTrue(in_array('organism_species', array_keys($values)), 'The tripal token parser is missing the organism_species value field name.'); + $this->assertTrue(in_array('organism_abbreviation', array_keys($values)), 'The tripal token parser is missing the organism_abbreviation value field name.'); + $this->assertTrue(in_array('organism_common_name', array_keys($values)), 'The tripal token parser is missing the organism_common_name value field name.'); + $this->assertTrue(in_array('organism_comment', array_keys($values)), 'The tripal token parser is missing the organism_comment value field name.'); + + $this->assertTrue(array_key_exists('value', $values['organism_genus']), 'The tripal token parser is missing the organism_genus value key.'); + $this->assertTrue(array_key_exists('value', $values['organism_species']), 'The tripal token parser is missing the organism_species value key.'); + $this->assertTrue(array_key_exists('value', $values['organism_abbreviation']), 'The tripal token parser is missing the organism_abbreviation value key.'); + $this->assertTrue(array_key_exists('value', $values['organism_common_name']), 'The tripal token parser is missing the organism_common_name value key.'); + $this->assertTrue(array_key_exists('value', $values['organism_comment']), 'The tripal token parser is missing the organism_comment value key.'); + + $this->assertTrue($values['organism_genus']['value'] == 'Oryza', 'The tripal token parser is missing the organism_genus value.'); + $this->assertTrue($values['organism_species']['value'] == 'sativa', 'The tripal token parser is missing the organism_species value.'); + $this->assertTrue($values['organism_abbreviation']['value'] == 'O. sativa', 'The tripal token parser is missing the organism_abbreviation value.'); + $this->assertTrue($values['organism_common_name']['value'] == 'Japonica rice', 'The tripal token parser is missing the organism_common_name value.'); + $this->assertTrue($values['organism_comment']['value'] == 'rice is nice', 'The tripal token parser is missing the organism_comment value.'); + $this->assertTrue(count(array_keys($values)) == 5, 'The tripal token parser has more values than expected.'); + + + $replaced = $token_parser->replaceTokens(['[organism_genus] [organism_species]']); + $this->assertTrue(is_array($replaced), 'TripalTokenParser::replaceTokens() does not return an array'); + $this->assertTrue(count($replaced) == 1, 'TripalTokenParser::replaceTokens() should have returned only one replaced string'); + $this->assertTrue($replaced[0] == 'Oryza sativa', 'TripalTokenParser did not correctly replace tokens'); + + $replaced = $token_parser->replaceTokens([ + '[organism_genus] [organism_species]', + '[organism_genus] [organism_species] [organism_infraspecific_type] [organism_infraspecific_name]' + ]); + $this->assertTrue(count($replaced) == 2, 'TripalTokenParser::replaceTokens() should have returned only two replaced string'); + $this->assertTrue($replaced[0] == 'Oryza sativa', 'TripalTokenParser did not return a correctly replaced string when multiple were provided..'); + $this->assertTrue($replaced[1] == 'Oryza sativa [organism_infraspecific_type] [organism_infraspecific_name]', 'TripalTokenParser did not return unparsed tokens in the input string.'); + + $token_parser->addFieldValue('organism_infraspecific_type', 'value', ''); + $replaced = $token_parser->replaceTokens([ + '[organism_genus] [organism_species] [organism_infraspecific_type]' + ]); + $this->assertTrue($replaced[0] == 'Oryza sativa', 'TripalTokenParser did not correctly replace a token with an empty value and trim the result.'); + + $token_parser->addFieldValue('organism_infraspecific_type', 'value', 'subspecies'); + $token_parser->addFieldValue('organism_infraspecific_name', 'value', 'Japonica'); + $replaced = $token_parser->replaceTokens([ + '[organism_genus] [organism_species] [organism_infraspecific_type] [organism_infraspecific_name]' + ]); + $this->assertTrue($replaced[0] == 'Oryza sativa subspecies Japonica', 'TripalTokenParser did not correctly replace all of the tokens.'); + + // + // Test Entity / Bundle tokens. + // + /** @var \Drupal\tripal\Entity\TripalEntity $entity **/ + $values = []; + $values['title'] = 'Test ' . uniqid(); + $values['type'] = 'organism'; + $values['organism_genus'] = [['value' => 'Oryza']]; + $values['organism_species'] = [['value' => 'sativa']]; + $values['organism_infraspecific_type'] = [['value' => 'subspecies']]; + $values['organism_infraspecific_name'] = [['value' => 'Japonica']]; + $values['organism_abbreviation'] = [['value' => 'O. sativa']]; + $values['organism_common_name'] = [['value' => 'Japonica rice']]; + $values['organism_comment'] = [['value' => 'rice is nice']]; + $entity = \Drupal\tripal\Entity\TripalEntity::create($values); + $this->assertIsObject($entity, "Unable to create an organism entity."); + $entity->save(); + + $token_parser->setEntity($entity); + $this->assertTrue($token_parser->getEntity()->getID() == $entity->getId(), 'The tripal token parser did not return the entity as expected.'); + $replaced = $token_parser->replaceTokens([ + '[TripalBundle__bundle_id]', + '[TripalEntityType__label]', + '[TripalEntity__entity_id]' + ]); + $this->assertTrue($replaced[0] == 'organism', 'The [TripalBundle__bundle_id] token is not being replaced.'); + $this->assertTrue($replaced[1] == 'Organism', 'The [TripalEntityType__label] token is not being replaced.'); + $this->assertTrue($replaced[2] == '1', 'The [TripalEntity__entity_id] token is not being replaced.'); + + } +} diff --git a/tripal/tests/src/Functional/TripalImporterTest.php b/tripal/tests/src/Functional/TripalImporterTest.php deleted file mode 100644 index cd4e69eb3..000000000 --- a/tripal/tests/src/Functional/TripalImporterTest.php +++ /dev/null @@ -1,104 +0,0 @@ -assertIsObject($type, 'An importer plugin service object was not returned.'); - - // --Use the plugin manager to get a list of available implementations. - $plugin_definitions = $type->getDefinitions(); - $this->assertIsArray( - $plugin_definitions, - 'Implementations of the tripal importer plugin should be returned in an array.' - ); - - } - - /** - * Tests focusing on the Tripal Importer plugin system. - * - * Functions to test: - * - tripal_run_importer - * - tripal_run_importer_run - * - tripal_run_importer_post_run - * - * @group tripal_importer - */ - public function testTripalImporterFunctions() { - $this->markTestIncomplete( - 'This test has not been implemented yet.' - ); - } - - /** - * Tests focusing on the Tripal importer form. - * - * @group tripal_importer - */ - public function testTripalImporterForm() { - - // Build the form using the Drupal form builder. - $form = \Drupal::formBuilder()->getForm('Drupal\tripal\Form\TripalImporterForm'); - - // Ensure we are able to build the form. - $this->assertIsArray( - $form, - 'The form array was not returned by the form builder.' - ); - - // Check the form_id - $this->assertEquals( - 'tripal_admin_form_tripalimporter', - $form['#form_id'], - 'We did not get the form id we expected.' - ); - - // Since we didn't provide a Tripal Importer plugin id... - // We shouldn't get the file and submit form elements. - $this->assertArrayNotHasKey( - 'file', - $form, - "The form should not have a file fieldset if we don't provide a specific importer." - ); - $this->assertArrayNotHasKey( - 'button', - $form, - "The form should not have a submit button if we don't provide a specific importer." - ); - } - - /** - * Tests focusing on the Tripal importer base class. - * - * @group tripal_importer - */ - public function testTripalImporterBase() { - $this->markTestIncomplete( - 'This test has not been implemented yet.' - ); - } -} diff --git a/tripal/tests/src/Kernel/TripalDBX/ConnectionTest.php b/tripal/tests/src/Kernel/TripalDBX/ConnectionTest.php index 3570ef5fb..dd4f5fa2b 100644 --- a/tripal/tests/src/Kernel/TripalDBX/ConnectionTest.php +++ b/tripal/tests/src/Kernel/TripalDBX/ConnectionTest.php @@ -144,7 +144,7 @@ public function testConnectionConstructorAllDefault() { // Create a mock for the abstract class. $dbmock = $this->getMockBuilder(\Drupal\tripal\TripalDBX\TripalDbxConnection::class) ->disableOriginalConstructor() - ->onlyMethods(['setTarget', 'setKey', 'setSchemaName']) + ->onlyMethods(['setTarget', 'setKey', 'setSchemaName', 'findVersion']) ->getMockForAbstractClass() ; @@ -160,6 +160,10 @@ public function testConnectionConstructorAllDefault() { ->method('setSchemaName') ->with($this->equalTo('')) ; + $dbmock + ->expects($this->any()) + ->method('findVersion') + ->willReturn(''); // Call the constructor. $reflected_class = new \ReflectionClass(\Drupal\tripal\TripalDBX\TripalDbxConnection::class); diff --git a/tripal/tests/src/Kernel/TripalImporter/TripalImporterBaseTest.php b/tripal/tests/src/Kernel/TripalImporter/TripalImporterBaseTest.php new file mode 100644 index 000000000..6da43d779 --- /dev/null +++ b/tripal/tests/src/Kernel/TripalImporter/TripalImporterBaseTest.php @@ -0,0 +1,644 @@ + 'fakeImporterName', + 'label' => 'Gemstone Loader', + 'description' => 'Imports details on the incredible diversity of gemstones created by our earth into Chado.', + 'file_types' => ["gem", "txt"], + 'upload_description' => "Please provide a plain text, tab-delimited file of gemstone descriptions making sure to include the details which make them most unique and beautiful.", + 'upload_title' => 'Gemstone Descriptions', + 'use_analysis' => FALSE, + 'require_analysis' => FALSE, + 'button_text' => 'Import file', + 'file_upload' => FALSE, + 'file_load' => FALSE, + 'file_remote' => FALSE, + 'file_required' => FALSE, + 'cardinality' => 1, + ]; + + protected $test_file; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Ensure we see all logging in tests. + \Drupal::state()->set('is_a_test_environment', TRUE); + + // Ensure we can access file_managed related functionality from Drupal. + // ... users need access to system.action config? + $this->installConfig('system'); + // ... managed files are associated with a user. + $this->installEntitySchema('user'); + // ... Finally the file module + tables itself. + $this->installEntitySchema('file'); + $this->installSchema('file', ['file_usage']); + + // Ensure we have our tripal import tables. + $this->installSchema('tripal', ['tripal_import', 'tripal_jobs']); + + // Create and log-in a user. + $this->setUpCurrentUser(); + + // Create a managed file to use as needed. + $filepath = 'temporary://Файл для тестирования ' . $this->randomMachineName(); + $contents = "file_put_contents() doesn't seem to appreciate empty strings so let's put in some data."; + file_put_contents($filepath, $contents); + $file = \Drupal\file\Entity\File::create([ + 'uri' => $filepath, + 'uid' => 1, + ]); + $file->save(); + $this->assertFileExists($filepath); + $this->test_file = $file; + + } + + /** + * Tests focusing on the Tripal Importer plugin system. + * + * @group tripal_importer + */ + public function testTripalImporterManager() { + + // Test the Tripal Importer Plugin Manager. + // --Ensure we can instantiate the plugin manager. + $type = \Drupal::service('tripal.importer'); + // Note: If the plugin manager is not found you will get a ServiceNotFoundException. + $this->assertIsObject($type, 'An importer plugin service object was not returned.'); + + // --Use the plugin manager to get a list of available implementations. + $plugin_definitions = $type->getDefinitions(); + $this->assertIsArray( + $plugin_definitions, + 'Implementations of the tripal importer plugin should be returned in an array.' + ); + + } + + /** + * Tests focusing on the Tripal importer base class. + * Specifically, create(), load(), and getArguments() methods. + * + * @group tripal_importer + */ + public function testTripalImporterBase() { + + // CASE --- Valid + // -- Empty run args + no file. + $expected_args = ['run_args' => [], 'files' => []]; + $plugin_defn = $this->plugin_definition; + $configuration = []; + $plugin_id = 'fakeImporterName'; + $importer = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + + // Execute create, file not required so expected to succeed. + $run_args = []; + $file_details = []; + $import_id = $importer->create($run_args, $file_details); + + // Now check that a record was added to the tripal_import table. + $public = \Drupal::database(); + $query = $public->select('tripal_import', 'ti'); + $query->fields('ti', ['uid', 'class', 'fid', 'arguments']); + $query->condition('import_id', $import_id, '='); + $records = $query->execute() + ->fetchAll(); + $this->assertCount(1, $records, + "We should have a single record in the tripal_import table."); + $this->assertEquals($plugin_id, $records[0]->class, + "The class should match our fake plugin name."); + $selected_args = unserialize(base64_decode($records[0]->arguments)); + $this->assertIsArray($selected_args, + "Unable to retrieve arguments after creating tripal importer record."); + $this->assertEquals($expected_args, $selected_args, + "We did not retrieve the arguments we expected."); + + $importerTestLoad = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + $importerTestLoad->load($import_id); + $retrieved_args = $importerTestLoad->getArguments(); + $this->assertIsArray($retrieved_args, + "Unable to retrieve arguments after loading tripal importer."); + $this->assertEquals($expected_args, $retrieved_args, + "We did not retrieve the arguments we expected after loading."); + + // CASE --- Exception Expected + // -- Empty run args, no file when file required. + $plugin_defn = $this->plugin_definition; + $plugin_defn['file_required'] = TRUE; + $configuration = []; + $plugin_id = 'fakeImporterName'; + $importer = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + + // Execute create, file required so expected to FAIL. + $exception_msg = NULL; + try { + $run_args = []; + $file_details = []; + $import_id = $importer->create($run_args, $file_details); + } + catch(\Exception $e) { + $exception_msg = $e->getMessage(); + } + $this->assertNotNull($exception_msg, + "We did not recieve an exception when trying to create with no file + file_required is TRUE."); + $this->assertStringContainsString('Must provide a proper file', $exception_msg, + "We did not get the exception we expected when trying to create with no file + file_required is TRUE."); + + // CASE --- Valid + // -- run args + local file. + $test_file_path = $this->test_file->getFileUri(); + $expected_args = [ + 'run_args' => ['test' => 'single run arg'], + 'files' => [['file_local' => $test_file_path, 'file_path' => $test_file_path]] + ]; + $plugin_defn = $this->plugin_definition; + $configuration = []; + $plugin_id = 'fakeImporterName'; + $importer = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + + // Execute create, file not required so expected to succeed. + $run_args = ['test' => 'single run arg']; + $file_details = ['file_local' => $test_file_path]; + $import_id = $importer->create($run_args, $file_details); + + // Now check that a record was added to the tripal_import table. + $public = \Drupal::database(); + $query = $public->select('tripal_import', 'ti'); + $query->fields('ti', ['uid', 'class', 'fid', 'arguments']); + $query->condition('import_id', $import_id, '='); + $records = $query->execute() + ->fetchAll(); + $this->assertCount(1, $records, + "We should have a single record in the tripal_import table."); + $this->assertEquals($plugin_id, $records[0]->class, + "The class should match our fake plugin name."); + $selected_args = unserialize(base64_decode($records[0]->arguments)); + $this->assertIsArray($selected_args, + "Unable to retrieve arguments after creating tripal importer record."); + $this->assertEquals($expected_args, $selected_args, + "We did not retrieve the arguments we expected."); + + $importerTestLoad = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + $importerTestLoad->load($import_id); + $retrieved_args = $importerTestLoad->getArguments(); + $this->assertIsArray($retrieved_args, + "Unable to retrieve arguments after loading tripal importer."); + $this->assertEquals($expected_args, $retrieved_args, + "We did not retrieve the arguments we expected after loading."); + + // CASE --- Valid + // -- run args + remote file. + $test_file_path = 'https://raw.githubusercontent.com/tripal/tripal/4.x/LICENSE.txt'; + $expected_args = [ + 'run_args' => ['test' => 'single run arg'], + 'files' => [['file_remote' => $test_file_path]] + ]; + $plugin_defn = $this->plugin_definition; + $configuration = []; + $plugin_id = 'fakeImporterName'; + $importer = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + + // Execute create, file not required so expected to succeed. + $run_args = ['test' => 'single run arg']; + $file_details = ['file_remote' => $test_file_path]; + $import_id = $importer->create($run_args, $file_details); + + // Now check that a record was added to the tripal_import table. + $public = \Drupal::database(); + $query = $public->select('tripal_import', 'ti'); + $query->fields('ti', ['uid', 'class', 'fid', 'arguments']); + $query->condition('import_id', $import_id, '='); + $records = $query->execute() + ->fetchAll(); + $this->assertCount(1, $records, + "We should have a single record in the tripal_import table."); + $this->assertEquals($plugin_id, $records[0]->class, + "The class should match our fake plugin name."); + $selected_args = unserialize(base64_decode($records[0]->arguments)); + $this->assertIsArray($selected_args, + "Unable to retrieve arguments after creating tripal importer record."); + $this->assertEquals($expected_args, $selected_args, + "We did not retrieve the arguments we expected."); + + $importerTestLoad = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + $importerTestLoad->load($import_id); + $retrieved_args = $importerTestLoad->getArguments(); + $this->assertIsArray($retrieved_args, + "Unable to retrieve arguments after loading tripal importer."); + $this->assertEquals($expected_args, $retrieved_args, + "We did not retrieve the arguments we expected after loading."); + + // CASE --- Valid + // -- run args + file upload (single file). + $test_file_path = $this->test_file->getFileUri(); + $test_file_path = \Drupal::service('file_system')->realpath($test_file_path); + $test_fid = $this->test_file->Id(); + $expected_args = [ + 'run_args' => ['test' => 'single run arg'], + 'files' => [['fid' => $test_fid, 'file_path' => $test_file_path]], + 'file' => ['fid' => $test_fid, 'file_path' => $test_file_path], + ]; + $plugin_defn = $this->plugin_definition; + $configuration = []; + $plugin_id = 'fakeImporterName'; + $importer = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + + // Execute create, file not required so expected to succeed. + $run_args = ['test' => 'single run arg']; + $file_details = ['fid' => $test_fid]; + $import_id = $importer->create($run_args, $file_details); + + // Now check that a record was added to the tripal_import table. + $public = \Drupal::database(); + $query = $public->select('tripal_import', 'ti'); + $query->fields('ti', ['uid', 'class', 'fid', 'arguments']); + $query->condition('import_id', $import_id, '='); + $records = $query->execute() + ->fetchAll(); + $this->assertCount(1, $records, + "We should have a single record in the tripal_import table."); + $this->assertEquals($plugin_id, $records[0]->class, + "The class should match our fake plugin name."); + $selected_args = unserialize(base64_decode($records[0]->arguments)); + $this->assertIsArray($selected_args, + "Unable to retrieve arguments after creating tripal importer record."); + $this->assertEquals($expected_args, $selected_args, + "We did not retrieve the arguments we expected."); + + $importerTestLoad = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + $importerTestLoad->load($import_id); + $retrieved_args = $importerTestLoad->getArguments(); + $this->assertIsArray($retrieved_args, + "Unable to retrieve arguments after loading tripal importer."); + $this->assertEquals($expected_args, $retrieved_args, + "We did not retrieve the arguments we expected after loading."); + + // CASE --- Valid + // -- run args + file upload (multiple files). + $test_file_path = $this->test_file->getFileUri(); + $test_file_path = \Drupal::service('file_system')->realpath($test_file_path); + $test_fid = $this->test_file->Id(); + $expected_args = [ + 'run_args' => ['test' => 'single run arg'], + 'files' => [ + ['fid' => $test_fid, 'file_path' => $test_file_path], + ['fid' => $test_fid, 'file_path' => $test_file_path], + ['fid' => $test_fid, 'file_path' => $test_file_path] + ], + ]; + $plugin_defn = $this->plugin_definition; + $configuration = []; + $plugin_id = 'fakeImporterName'; + $importer = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + + // Execute create, file not required so expected to succeed. + $run_args = ['test' => 'single run arg']; + $file_details = ['fid' => "$test_fid|$test_fid|$test_fid"]; + $import_id = $importer->create($run_args, $file_details); + + // Now check that a record was added to the tripal_import table. + $public = \Drupal::database(); + $query = $public->select('tripal_import', 'ti'); + $query->fields('ti', ['uid', 'class', 'fid', 'arguments']); + $query->condition('import_id', $import_id, '='); + $records = $query->execute() + ->fetchAll(); + $this->assertCount(1, $records, + "We should have a single record in the tripal_import table."); + $this->assertEquals($plugin_id, $records[0]->class, + "The class should match our fake plugin name."); + $selected_args = unserialize(base64_decode($records[0]->arguments)); + $this->assertIsArray($selected_args, + "Unable to retrieve arguments after creating tripal importer record."); + $this->assertEquals($expected_args, $selected_args, + "We did not retrieve the arguments we expected."); + + $importerTestLoad = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + $importerTestLoad->load($import_id); + $retrieved_args = $importerTestLoad->getArguments(); + $this->assertIsArray($retrieved_args, + "Unable to retrieve arguments after loading tripal importer."); + $this->assertEquals($expected_args, $retrieved_args, + "We did not retrieve the arguments we expected after loading."); + + // CASE --- Exception Expected + // -- Load non-existant importer. + $plugin_defn = $this->plugin_definition; + $configuration = []; + $plugin_id = 'fakeImporterName'; + $importerTestLoad = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + + $exception_msg = NULL; + try { + $run_args = []; + $file_details = []; + $import_id = $importerTestLoad->load(999); + } + catch(\Exception $e) { + $exception_msg = $e->getMessage(); + } + $this->assertNotNull($exception_msg, + "We did not recieve an exception when trying to load a non-existant importer."); + $this->assertStringContainsString('Cannot find an importer', $exception_msg, + "We did not get the exception we expected when trying to load a non-existant."); + + // CASE --- Exception Expected + // -- Load an importer where the class doesn't match. + $plugin_defn = $this->plugin_definition; + $configuration = []; + $plugin_id = 'fakeImporterName'; + $importerTestLoad = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + + $mismatched_import_id = $public->insert('tripal_import') + ->fields([ + 'uid' => 2, + 'class' => 'SomeCompletelyDifferentImporterClass', + 'submit_date' => time(), + ]) + ->execute(); + + $exception_msg = NULL; + try { + $run_args = []; + $file_details = []; + $import_id = $importerTestLoad->load($mismatched_import_id); + } + catch(\Exception $e) { + $exception_msg = $e->getMessage(); + } + $this->assertNotNull($exception_msg, + "We did not recieve an exception when trying to load a non-existant importer."); + $this->assertStringContainsString('does not match this importer class', $exception_msg, + "We did not get the exception we expected when trying to load a non-existant."); + + } + + /** + * Tests focusing on the Tripal importer base class. + * Specifically, submitJob() and setJob() methods. + * + * @group tripal_importer + */ + public function testTripalImporterBaseJobs() { + + // CASE --- Valid + // -- Empty run args + no file. + $expected_args = ['run_args' => [], 'files' => []]; + $plugin_defn = $this->plugin_definition; + $configuration = []; + $plugin_id = 'fakeImporterName'; + $importer = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + $run_args = []; + $file_details = []; + $import_id = $importer->create($run_args, $file_details); + $job_id = $importer->submitJob(); + $this->assertIsNumeric($job_id, + "We expected to have a tripal job_id returned from submitJob()."); + + $job = \Drupal::service('tripal.job'); + $job->load($job_id); + $importer->setJob($job); + + // CASE --- Exception Expected + // submit job when import not yet created. + $configuration = []; + $plugin_id = 'fakeImporterName'; + $importer = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $this->plugin_definition] + ); + + $exception_msg = NULL; + try{ + $importer->submitJob(); + } + catch (\Exception $e) { + $exception_msg = $e->getMessage(); + } + $this->assertNotNull($exception_msg, + "We did not recieve an exception when trying to submit a job when import_id not set."); + $this->assertStringContainsString('without an import record', $exception_msg, + "We did not get the exception we expected when when import_id not set."); + + } + + /** + * Tests focusing on the Tripal importer base class. + * Specifically, prepareFiles() and cleanFile() methods. + * + * @group tripal_importer + */ + public function testTripalImporterBaseFiles() { + + // CASE --- Valid + // -- plain text remote file, files not required. + $test_file_path = 'https://raw.githubusercontent.com/tripal/tripal/4.x/LICENSE.txt'; + $expected_args = [ + 'run_args' => ['test' => 'single run arg'], + 'files' => [['file_remote' => $test_file_path]] + ]; + $plugin_defn = $this->plugin_definition; + $configuration = []; + $plugin_id = 'fakeImporterName'; + $importer = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + $run_args = []; + $file_details = ['file_remote' => $test_file_path]; + $import_id = $importer->create($run_args, $file_details); + + // Now try to prepare the file. + $importer->prepareFiles(); + $retrieved_args = $importer->getArguments(); + $this->assertIsArray($retrieved_args, + "We could not retrieve arguments after preparing files"); + $this->assertArrayHasKey('file_path', $retrieved_args['files'][0], + "The file_path should have been set during prepareFiles()."); + + // Now use cleanFile() to remove any temporary files. + $importer->cleanFile(); + $this->assertFileDoesNotExist($retrieved_args['files'][0]['file_path'], + "The temporary file created by downloading the remote file should no longer exist."); + + // CASE --- Valid + // -- gzipped remote file, files not required. + $test_file_path = __DIR__ . '/../../../fixtures/importer_whitespace_test_file.txt.gz'; + $plugin_defn = $this->plugin_definition; + $configuration = []; + $plugin_id = 'fakeImporterName'; + $importer = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_defn] + ); + $run_args = []; + $file_details = ['file_remote' => $test_file_path]; + $import_id = $importer->create($run_args, $file_details); + + // Now try to prepare the file. + $importer->prepareFiles(); + $retrieved_args = $importer->getArguments(); + $this->assertIsArray($retrieved_args, + "We could not retrieve arguments after preparing files"); + $this->assertArrayHasKey('file_path', $retrieved_args['files'][0], + "The file_path should have been set during prepareFiles()."); + + // Now use cleanFile() to remove any temporary files. + $importer->cleanFile(); + $this->assertFileDoesNotExist($retrieved_args['files'][0]['file_path'], + "The temporary file created by downloading the remote file should no longer exist."); + } + + /** + * Tests focusing on the Tripal importer base class. + * Specifically, setTotalItems(), addItemsHandled(), setItemsHandled(), setInterval(). + * + * @group tripal_importer + */ + public function testTripalImporterBaseProgress() { + + // 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']) + ->getMock(); + $mock_logger->method('notice') + ->willReturnCallback(function($message, $context, $options) { + print str_replace(array_keys($context), $context, $message); + return NULL; + }); + $container->set('tripal.logger', $mock_logger); + + $configuration = []; + $plugin_id = 'fakeImporterName'; + $importer = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $this->plugin_definition] + ); + + // Note: These methods are protected and not called elsewhere in the base + // class. Therefore, in order to test them here we need to use closures. + $that = $this; + $assertClosure = function () use ($that){ + + // Set the total and interval. + $this->setTotalItems(100); + $this->setInterval(40); + + // Let's start by setting the number of items handled to 0. + // At this point we expect a message to be logged saying 0% complete. + ob_start(); + $this->setItemsHandled(0); + $printed_output = ob_get_clean(); + $that->assertStringContainsString('Percent complete: 0%', $printed_output, + "We just started so expect to see 0% printed to the log"); + + // Now we start by handling 20 items. + // At this point we do not expect any logging as we did not reach + // the interval to be printed at. + $this->addItemsHandled(20); + + // handle another 20 items. + // At this point we should see a logged message saying we are 40% complete. + ob_start(); + $this->addItemsHandled(20); + $printed_output = ob_get_clean(); + $that->assertStringContainsString('40.00 %', $printed_output, + "We've added a total of 40/100 items so expect to see 40% printed to the log"); + + // handle another 20 items. + // At this point we do not expect any logging as we did not reach + // the interval to be printed at. + $this->addItemsHandled(20); + + // handle another 20 items. + // At this point we should see a logged message saying we are 80% complete. + ob_start(); + $this->addItemsHandled(20); + $printed_output = ob_get_clean(); + $that->assertStringContainsString('Percent complete: 80.00 %', $printed_output, + "We've added a total of 80/100 items so expect to see 80% printed to the log"); + + // handle another 20 items. + // At this point we should see a logged message saying we are 100% complete + // even though this is not the typical interval. + ob_start(); + $this->addItemsHandled(20); + $printed_output = ob_get_clean(); + $that->assertStringContainsString('Percent complete: 100.00 %', $printed_output, + "We've added a total of 100/100 items so expect to see 100% printed to the log"); + }; + $doAssertions = $assertClosure->bindTo($importer, get_class($importer)); + $doAssertions($this); + } +} diff --git a/tripal/tests/src/Kernel/TripalImporter/TripalImporterFormBuildTest.php b/tripal/tests/src/Kernel/TripalImporter/TripalImporterFormBuildTest.php new file mode 100644 index 000000000..b23917c23 --- /dev/null +++ b/tripal/tests/src/Kernel/TripalImporter/TripalImporterFormBuildTest.php @@ -0,0 +1,615 @@ + [ + 'id' => 'fakeImporterName', + 'label' => 'Gemstone Loader', + 'description' => 'Imports details on the incredible diversity of gemstones created by our earth into Chado.', + 'file_types' => ["gem", "txt"], + 'upload_description' => "Please provide a plain text, tab-delimited file of gemstone descriptions making sure to include the details which make them most unique and beautiful.", + 'upload_title' => 'Gemstone Descriptions', + 'use_analysis' => FALSE, + 'require_analysis' => FALSE, + 'use_button' => TRUE, + 'button_text' => 'Import file', + 'file_upload' => FALSE, + 'file_load' => FALSE, + 'file_remote' => FALSE, + 'file_required' => FALSE, + 'cardinality' => 1, + ], + ]; + + /** + * A selection of form elements to be provided by our fake importer. + * @var Array + */ + protected $form = [ + 'gemstone_composition' => [ + '#title' => 'Chemical Composition', + '#type' => 'select', + '#description' => 'Choose the chemical composition that all gems in your input file fall into.', + '#required' => TRUE, + '#options' => [ + 'borate' => 'Borate (e.g. Howlite)', + 'carbon' => 'Carbon (e.g. Diamond)', + 'carbonate' => 'Carbonate (e.g. Azurite, Calcite, Malachite)', + 'halide' => 'Halide (e.g. Fluorite)', + 'igneous' => 'Igneous Rock (e.g. obsidian, lava stone)', + 'organic' => 'Organic (e.g. Amber, Pearl)', + 'silicate' => 'Silicate (e.g. Amazonite, Danburite, Lepidolite)' + ], + '#empty_option' => '- Select -', + ], + ]; + + /** + * An analysis form element to be provided by our fake importer. + * @var Array + */ + protected $analysis_form = [ + 'analysis_method' => [ + '#title' => 'Gemstone Validation', + '#type' => 'select', + '#description' => 'Choose the validation methodology that proves these stones are authentic.', + '#required' => TRUE, + '#options' => [ + 'raman' => 'Raman spectroscopy', + 'luminescence' => 'Luminescence', + 'acid' => 'Acid Testing', + ], + '#empty_option' => '- Select -', + ], + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Ensure we see all logging in tests. + \Drupal::state()->set('is_a_test_environment', TRUE); + + // Ensure we can access file_managed related functionality from Drupal. + // ... users need access to system.action config? + $this->installConfig('system'); + // ... managed files are associated with a user. + $this->installEntitySchema('user'); + // ... Finally the file module + tables itself. + $this->installEntitySchema('file'); + $this->installSchema('file', ['file_usage']); + + } + + /** + * HELPER: Creates a mock plugin + plugin manager. + */ + protected function setMockManager($annotation) { + + // Mock Tripal Importer Plugin. + $configuration = []; + $plugin_id = 'fakeImporterName'; + $plugin_definition = $annotation['fakeImporterName']; + $this->mock_plugin = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $plugin_definition] + ); + $this->mock_plugin->method('form') + ->willReturn($this->form); + $this->mock_plugin->method('addAnalysis') + ->willReturn($this->analysis_form); + + // Mock Plugin Manager. + $manager = $this->createMock(\Drupal\tripal\TripalImporter\PluginManagers\TripalImporterManager::class); + $manager->method('createInstance') + ->willReturn($this->mock_plugin); + $manager->method('getDefinitions') + ->willReturn($annotation); + + return $manager; + } + + /** + * Tests focusing on the Tripal importer form. + */ + public function testTripalImporterForm() { + + $manager = $this->setMockManager($this->definitions); + $container = \Drupal::getContainer(); + $container->set('tripal.importer', $manager); + + // -- Test form with no plugin_id supplied. + // Build the form using the Drupal form builder. + $form = \Drupal::formBuilder()->getForm( + 'Drupal\tripal\Form\TripalImporterForm' + ); + + // Ensure we are able to build the form. + $this->assertIsArray($form, + 'We still expect the form builder to return a form array even without a plugin_id but it did not.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // Since we didn't provide a Tripal Importer plugin id... + // We shouldn't get the file and submit form elements. + $this->assertArrayNotHasKey('file', $form, + "The form should not have a file fieldset if we don't provide a specific importer."); + $this->assertArrayNotHasKey('button', $form, + "The form should not have a submit button if we don't provide a specific importer."); + + // -- Test form with plugin_id but no file or analysis. + $plugin_id = 'fakeImporterName'; + $expected = $this->definitions[$plugin_id]; + + // 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($expected['label'], $form['#title'], + "The title should match the label annotated for our fake 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 fake plugin_id."); + // a submit button. + $this->assertArrayHasKey('button', $form, + "The form should 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('gemstone_composition', $form, + "The form should include our plugin-specific form elements."); + foreach ($this->form['gemstone_composition'] as $expected_key => $expected_element) { + $this->assertArrayHasKey($expected_key, $form['gemstone_composition'], + "The form includes our plugin-specific form element but it does not have all they keys we expect."); + } + + // Our default annotation indicates no file or analysis elements + // should be added so let's confirm they are not. + $this->assertArrayNotHasKey('file', $form, + "Our default annotation for our fake importer indicates there should not be a file element added."); + $this->assertArrayNotHasKey('analysis_method', $form, + "Our default annotation for our fake importer indicates there should not be an analysis element added."); + } + + /** + * Confirm that the file-related form elements are added to the form + * as expected based on plugin annotation. + */ + public function testTripalImporterFormFiles() { + + $container = \Drupal::getContainer(); + $plugin_id = 'fakeImporterName'; + $expected = $this->definitions[$plugin_id]; + + // -- Include all file elements. + $expected['file_upload'] = TRUE; + $expected['file_load'] = TRUE; + $expected['file_local'] = TRUE; + $expected['file_remote'] = TRUE; + $expected['file_required'] = TRUE; + $manager = $this->setMockManager([$plugin_id => $expected]); + $container->set('tripal.importer', $manager); + + // Build the form using the Drupal form builder. + $form = \Drupal::formBuilder()->getForm( + 'Drupal\tripal\Form\TripalImporterForm', + $plugin_id, + ); + $this->assertIsArray($form, + 'We still expect the form builder to return a form array even without a plugin_id but it did not.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // Check the file fieldset and upload description are as expected. + $this->assertArrayHasKey('file', $form, + "The form should have a file key as our fake importer definition indicates we want one."); + $this->assertEquals('fieldset', $form['file']['#type'], + "The file element in the form is expected to be a fieldset."); + $this->assertArrayHasKey('upload_description', $form['file'], + "If any file element is included, there should be an upload description added to the file fieldset."); + $this->assertStringContainsString($expected['upload_description'], $form['file']['upload_description']['#markup'], + "The upload description should match the one provided in the plugin annotation."); + + // Check the Upload file element + $this->assertArrayHasKey('file_upload', $form['file'], + "The form should have a file upload form element based on our annotation."); + $this->assertEquals('html5_file', $form['file']['file_upload']['#type'], + "The file_upload element is not of the expected type."); + $this->assertEquals('tripal_importer', $form['file']['file_upload']['#usage_type'], + "The file_upload element should indicate it is associated with tripal_importer."); + $this->assertEquals($expected['file_types'], $form['file']['file_upload']['#allowed_types'], + "Only the allowed types indicated by the annotation should be indicated for the file_upload element."); + $this->assertEquals($expected['cardinality'], $form['file']['file_upload']['#cardinality'], + "The cardinality indicated in the annotation should be reflected in the file_upload element."); + // There should not be any existing files associated with this user. + // So check that form elements does not exist. + $this->assertArrayNotHasKey('file_upload_existing', $form['file'], + "The form should NOT have an element for existing files as we have not created a user or associated files."); + + // Check the local file element + $this->assertArrayHasKey('file_local', $form['file'], + "The form should have a local file form element based on our annotation."); + $this->assertEquals('textfield', $form['file']['file_local']['#type'], + "The file_local element is not of the expected type."); + + // Check the remote file element + $this->assertArrayHasKey('file_remote', $form['file'], + "The form should have a remote file form element based on our annotation."); + $this->assertEquals('textfield', $form['file']['file_remote']['#type'], + "The file_remote element is not of the expected type."); + + // -- Include file_upload only and ensure other elements are not included. + $expected = $this->definitions[$plugin_id]; + $expected['file_upload'] = TRUE; + $manager = $this->setMockManager([$plugin_id => $expected]); + $container->set('tripal.importer', $manager); + + // Build the form using the Drupal form builder. + $form = \Drupal::formBuilder()->getForm( + 'Drupal\tripal\Form\TripalImporterForm', + $plugin_id, + ); + $this->assertIsArray($form, + 'We still expect the form builder to return a form array even without a plugin_id but it did not.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // Check the file fieldset and upload description are as expected. + $this->assertArrayHasKey('file', $form, + "The form should have a file key as our fake importer definition indicates we want one."); + $this->assertEquals('fieldset', $form['file']['#type'], + "The file element in the form is expected to be a fieldset."); + $this->assertArrayHasKey('upload_description', $form['file'], + "If any file element is included, there should be an upload description added to the file fieldset."); + $this->assertStringContainsString($expected['upload_description'], $form['file']['upload_description']['#markup'], + "The upload description should match the one provided in the plugin annotation."); + + // Check the Upload file element + $this->assertArrayHasKey('file_upload', $form['file'], + "The form should have a file upload form element based on our annotation."); + // But NOT the other two. + $this->assertArrayNotHasKey('file_local', $form['file'], + "The form should NOT have a local file form element based on our annotation."); + $this->assertArrayNotHasKey('file_remote', $form['file'], + "The form should NOT have a remote file form element based on our annotation."); + + // -- Include file_local only and ensure other elements are not included. + $expected = $this->definitions[$plugin_id]; + $expected['file_local'] = TRUE; + $manager = $this->setMockManager([$plugin_id => $expected]); + $container->set('tripal.importer', $manager); + + // Build the form using the Drupal form builder. + $form = \Drupal::formBuilder()->getForm( + 'Drupal\tripal\Form\TripalImporterForm', + $plugin_id, + ); + $this->assertIsArray($form, + 'We still expect the form builder to return a form array even without a plugin_id but it did not.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // Check the file fieldset and upload description are as expected. + $this->assertArrayHasKey('file', $form, + "The form should have a file key as our fake importer definition indicates we want one."); + $this->assertEquals('fieldset', $form['file']['#type'], + "The file element in the form is expected to be a fieldset."); + $this->assertArrayHasKey('upload_description', $form['file'], + "If any file element is included, there should be an upload description added to the file fieldset."); + $this->assertStringContainsString($expected['upload_description'], $form['file']['upload_description']['#markup'], + "The upload description should match the one provided in the plugin annotation."); + + // Check the file element we should have + $this->assertArrayHasKey('file_local', $form['file'], + "The form should have a local file form element based on our annotation."); + // But NOT the other two. + $this->assertArrayNotHasKey('file_upload', $form['file'], + "The form should NOT have a file upload form element based on our annotation."); + $this->assertArrayNotHasKey('file_remote', $form['file'], + "The form should NOT have a remote file form element based on our annotation."); + + // -- Include file_upload only and ensure other elements are not included. + $expected = $this->definitions[$plugin_id]; + $expected['file_remote'] = TRUE; + $manager = $this->setMockManager([$plugin_id => $expected]); + $container->set('tripal.importer', $manager); + + // Build the form using the Drupal form builder. + $form = \Drupal::formBuilder()->getForm( + 'Drupal\tripal\Form\TripalImporterForm', + $plugin_id, + ); + $this->assertIsArray($form, + 'We still expect the form builder to return a form array even without a plugin_id but it did not.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // Check the file fieldset and upload description are as expected. + $this->assertArrayHasKey('file', $form, + "The form should have a file key as our fake importer definition indicates we want one."); + $this->assertEquals('fieldset', $form['file']['#type'], + "The file element in the form is expected to be a fieldset."); + $this->assertArrayHasKey('upload_description', $form['file'], + "If any file element is included, there should be an upload description added to the file fieldset."); + $this->assertStringContainsString($expected['upload_description'], $form['file']['upload_description']['#markup'], + "The upload description should match the one provided in the plugin annotation."); + + // Check the file element we should have + $this->assertArrayHasKey('file_remote', $form['file'], + "The form should NOT have a remote file form element based on our annotation."); + // But NOT the other two. + $this->assertArrayNotHasKey('file_upload', $form['file'], + "The form should NOT have a file upload form element based on our annotation."); + $this->assertArrayNotHasKey('file_local', $form['file'], + "The form should have a local file form element based on our annotation."); + } + + /** + * Confirm that the file-related form elements are added to the form + * as expected based on plugin annotation. + */ + public function testTripalImporterFormAnalysis() { + + $container = \Drupal::getContainer(); + $plugin_id = 'fakeImporterName'; + $expected = $this->definitions[$plugin_id]; + + // -- Indicate to use an analysis elements. + $expected['use_analysis'] = TRUE; + $manager = $this->setMockManager([$plugin_id => $expected]); + $container->set('tripal.importer', $manager); + + // Build the form using the Drupal form builder. + $form = \Drupal::formBuilder()->getForm( + 'Drupal\tripal\Form\TripalImporterForm', + $plugin_id, + ); + $this->assertIsArray($form, + 'We still expect the form builder to return a form array even without a plugin_id but it did not.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // check that our analysis element is in the form. + $this->assertArrayHasKey('analysis_method', $form, + "Our analysis form element should be included based on the annotation."); + $this->assertEquals('Gemstone Validation', $form['analysis_method']['#title'], + "The title for our analysis element did not match what we expected."); + $this->assertCount(4, $form['analysis_method']['#options'], + "There were not the expected number of options including the empty option that we expected for our analysis."); + } + + /** + * Confirm that importers whose annotation indicates they do not want a submit + * button, do not get a submit button forced on them. + */ + public function testTripalImporterFormNoButton() { + + $container = \Drupal::getContainer(); + $plugin_id = 'fakeImporterName'; + $expected = $this->definitions[$plugin_id]; + + // -- Indicate to use an analysis elements. + $expected['use_button'] = FALSE; + $manager = $this->setMockManager([$plugin_id => $expected]); + $container->set('tripal.importer', $manager); + + // Build the form using the Drupal form builder. + $form = \Drupal::formBuilder()->getForm( + 'Drupal\tripal\Form\TripalImporterForm', + $plugin_id, + ); + $this->assertIsArray($form, + 'We still expect the form builder to return a form array even without a plugin_id but it did not.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // check that our analysis element is in the form. + $this->assertArrayNotHasKey('button', $form, + "We should not have a submit button if our annotation sets use_button to FALSE but we do."); + + } + + /** + * Confirm that importers can indicate they want the form submit button + * disabled using a particular key in the form state. + */ + public function testTripalImporterFormDisableButtonFormStateOnly() { + + $container = \Drupal::getContainer(); + $form_builder = \Drupal::formBuilder(); + $plugin_id = 'fakeImporterName'; + $form_id = 'tripal_admin_form_tripalimporter'; + $form_class = 'Drupal\tripal\Form\TripalImporterForm'; + + // CASE 1: No Form State + $expected = $this->definitions[$plugin_id]; + $manager = $this->setMockManager([$plugin_id => $expected]); + $container->set('tripal.importer', $manager); + + // Build the form using the Drupal form builder. + // and confirm that when the form is built with no Form State that + // the button is enabled. + $form = $form_builder->getForm( + 'Drupal\tripal\Form\TripalImporterForm', + $plugin_id, + ); + $this->assertIsArray($form, + 'The form builder should return a form array.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // check that our button element is in the form. + $this->assertArrayHasKey('button', $form, + "We should have a submit button."); + $this->assertArrayHasKey('#disabled', $form['button'], + "The submit button should have the disabled key set."); + $this->assertFalse($form['button']['#disabled'], + "The submit button should BE ENABLED when the form is built without a specific form state."); + + // CASE 2: Form State[disable_submit] = TRUE when form rebuilt. + $form_state = new FormState(); + $form_state->addBuildInfo('args', [$plugin_id]); + $form_state->setStorage(['disable_TripalImporter_submit' => TRUE]); + $form = $form_builder->buildForm($form_class, $form_state); + + $this->assertIsArray($form, + 'The form builder should return a form array.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // check that our button element is in the form. + $this->assertArrayHasKey('button', $form, + "We should have a submit button."); + $this->assertArrayHasKey('#disabled', $form['button'], + "The submit button should have the disabled key set."); + $this->assertTrue($form['button']['#disabled'], + "The submit button should BE DISABLED when the form is built with the form state disable_TripalImporter_submit set to TRUE."); + + // CASE 3: Form State[disable_submit] = FALSE when form rebuilt. + $form_state = new FormState(); + $form_state->addBuildInfo('args', [$plugin_id]); + $form_state->setStorage(['disable_TripalImporter_submit' => FALSE]); + $form = $form_builder->buildForm($form_class, $form_state); + + $this->assertIsArray($form, + 'The form builder should return a form array.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // check that our button element is in the form. + $this->assertArrayHasKey('button', $form, + "We should have a submit button."); + $this->assertArrayHasKey('#disabled', $form['button'], + "The submit button should have the disabled key set."); + $this->assertFalse($form['button']['#disabled'], + "The submit button should BE ENABLED when the form is built with the form state disable_TripalImporter_submit set to FALSE."); + } + + /** + * Confirm that importers can indicate they want the form submit button + * disabled via annotation and change it via form state. + */ + public function testTripalImporterFormDisableButtonAnnotation() { + + $container = \Drupal::getContainer(); + $form_builder = \Drupal::formBuilder(); + $plugin_id = 'fakeImporterName'; + $form_id = 'tripal_admin_form_tripalimporter'; + $form_class = 'Drupal\tripal\Form\TripalImporterForm'; + + // CASE 1: No Form State + $expected = $this->definitions[$plugin_id]; + $expected['submit_disabled'] = TRUE; + $manager = $this->setMockManager([$plugin_id => $expected]); + $container->set('tripal.importer', $manager); + + // Build the form using the Drupal form builder. + // and confirm that when the form is built with no Form State that + // the button is enabled. + $form = $form_builder->getForm( + 'Drupal\tripal\Form\TripalImporterForm', + $plugin_id, + ); + $this->assertIsArray($form, + 'The form builder should return a form array.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // check that our button element is in the form. + $this->assertArrayHasKey('button', $form, + "We should have a submit button."); + $this->assertArrayHasKey('#disabled', $form['button'], + "The submit button should have the disabled key set."); + $this->assertTrue($form['button']['#disabled'], + "The submit button should BE DISABLED when the form is built without a specific form state but annotation says it should be."); + + // CASE 2: Form State[disable_submit] = FALSE when form rebuilt. + $form_state = new FormState(); + $form_state->addBuildInfo('args', [$plugin_id]); + $form_state->setStorage(['disable_TripalImporter_submit' => FALSE]); + $form = $form_builder->buildForm($form_class, $form_state); + + $this->assertIsArray($form, + 'The form builder should return a form array.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // check that our button element is in the form. + $this->assertArrayHasKey('button', $form, + "We should have a submit button."); + $this->assertArrayHasKey('#disabled', $form['button'], + "The submit button should have the disabled key set."); + $this->assertFalse($form['button']['#disabled'], + "The submit button should BE ENABLED when the form is built with the form state disable_TripalImporter_submit set to FALSE."); + + // CASE 3: Form State[disable_submit] = TRUE when form rebuilt. + $form_state = new FormState(); + $form_state->addBuildInfo('args', [$plugin_id]); + $form_state->setStorage(['disable_TripalImporter_submit' => TRUE]); + $form = $form_builder->buildForm($form_class, $form_state); + + $this->assertIsArray($form, + 'The form builder should return a form array.'); + $this->assertEquals('tripal_admin_form_tripalimporter', $form['#form_id'], + 'We did not get the form id we expected.'); + + // check that our button element is in the form. + $this->assertArrayHasKey('button', $form, + "We should have a submit button."); + $this->assertArrayHasKey('#disabled', $form['button'], + "The submit button should have the disabled key set."); + $this->assertTrue($form['button']['#disabled'], + "The submit button should BE DISABLED when the form is built with the form state disable_TripalImporter_submit set to TRUE."); + + } +} diff --git a/tripal/tests/src/Kernel/TripalImporter/TripalImporterFormSubmitTest.php b/tripal/tests/src/Kernel/TripalImporter/TripalImporterFormSubmitTest.php new file mode 100644 index 000000000..5715372dd --- /dev/null +++ b/tripal/tests/src/Kernel/TripalImporter/TripalImporterFormSubmitTest.php @@ -0,0 +1,396 @@ + 'fakeImporterName', + 'label' => 'Gemstone Loader', + 'description' => 'Imports details on the incredible diversity of gemstones created by our earth into Chado.', + 'file_types' => ["gem", "txt"], + 'upload_description' => "Please provide a plain text, tab-delimited file of gemstone descriptions making sure to include the details which make them most unique and beautiful.", + 'upload_title' => 'Gemstone Descriptions', + 'use_analysis' => FALSE, + 'require_analysis' => FALSE, + 'button_text' => 'Import file', + 'file_upload' => TRUE, + 'file_local' => TRUE, + 'file_remote' => TRUE, + 'file_required' => TRUE, + 'cardinality' => 1, + ]; + + /** + * A selection of form elements to be provided by our fake importer. + * @var Array + */ + protected $form = [ + 'gemstone_composition' => [ + '#title' => 'Chemical Composition', + '#type' => 'select', + '#description' => 'Choose the chemical composition that all gems in your input file fall into.', + '#required' => FALSE, + '#options' => [ + 'borate' => 'Borate (e.g. Howlite)', + 'carbon' => 'Carbon (e.g. Diamond)', + 'carbonate' => 'Carbonate (e.g. Azurite, Calcite, Malachite)', + 'halide' => 'Halide (e.g. Fluorite)', + 'igneous' => 'Igneous Rock (e.g. obsidian, lava stone)', + 'organic' => 'Organic (e.g. Amber, Pearl)', + 'silicate' => 'Silicate (e.g. Amazonite, Danburite, Lepidolite)' + ], + '#empty_option' => '- Select -', + ], + ]; + + /** + * An analysis form element to be provided by our fake importer. + * @var Array + */ + protected $analysis_form = [ + 'analysis_method' => [ + '#title' => 'Gemstone Validation', + '#type' => 'select', + '#description' => 'Choose the validation methodology that proves these stones are authentic.', + '#required' => FALSE, + '#options' => [ + 'raman' => 'Raman spectroscopy', + 'luminescence' => 'Luminescence', + 'acid' => 'Acid Testing', + ], + '#empty_option' => '- Select -', + ], + ]; + + protected $test_file; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Ensure we see all logging in tests. + \Drupal::state()->set('is_a_test_environment', TRUE); + + // Ensure we can access file_managed related functionality from Drupal. + // ... users need access to system.action config? + $this->installConfig('system'); + // ... managed files are associated with a user. + $this->installEntitySchema('user'); + // ... Finally the file module + tables itself. + $this->installEntitySchema('file'); + $this->installSchema('file', ['file_usage']); + + // Ensure we have our tripal import tables. + $this->installSchema('tripal', ['tripal_import', 'tripal_jobs']); + + // Create and log-in a user. + $this->setUpCurrentUser(); + + // Create a managed file to use as needed. + $filepath = 'temporary://Файл для тестирования ' . $this->randomMachineName(); + $contents = "file_put_contents() doesn't seem to appreciate empty strings so let's put in some data."; + file_put_contents($filepath, $contents); + $file = \Drupal\file\Entity\File::create([ + 'uri' => $filepath, + 'uid' => 1, + ]); + $file->save(); + $this->assertFileExists($filepath); + $this->test_file = $file; + } + + /** + * Tests the validation of the local file element added by the Base Tripal Importer. + * + * Specifically, + * - VALID: provide an existing local file, correct file format + * - ERROR: indicate a file which does not exist locally. + */ + public function testTripalImporterFormValidateLocalFile() { + global $DRUPAL_ROOT; + + $manager = $this->setMockManager($this->definitions); + $container = \Drupal::getContainer(); + $container->set('tripal.importer', $manager); + + $plugin_id = 'fakeImporterName'; + $expected = $this->definitions; + + // --- CASE VALID + // --- Supply a valid remote path and ensure the form works! + $test_file_path = 'modules/contrib/tripal/LICENSE.txt'; + + // Now setup the form_state. + $form_state = new \Drupal\Core\Form\FormState(); + $form_state->addBuildInfo('args', [$plugin_id]); + $form_state->setValue('file_local', $test_file_path); + + // Now try validation! + \Drupal::formBuilder()->submitForm( + 'Drupal\tripal\Form\TripalImporterForm', + $form_state + ); + // And do some basic checks to ensure there were no errors. + $this->assertTrue($form_state->isValidationComplete(), + "We expect the form state to have been updated to indicate that validation is complete."); + // Looking for form validation errors + $form_validation_messages = $form_state->getErrors(); + $helpful_output = []; + foreach ($form_validation_messages as $element => $markup) { + $helpful_output[] = $element . " => " . (string) $markup; + } + $this->assertCount(0, $form_validation_messages, + "We should not have any validation errors for '$test_file_path' but instead we have: " . implode(" AND ", $helpful_output)); + // Looking for drupal message errors. + $messages = \Drupal::messenger()->all(); + $this->assertIsArray($messages, + "We expect to have status messages to the user on submission of the form."); + $this->assertArrayNotHasKey('error', $messages, + "There should not be any error messages from this form. Instead we recieved: " . print_r($messages, TRUE)); + // Now delete drupal messages so we start the next test clean. + \Drupal::messenger()->deleteAll(); + + // --- CASE ERROR + // --- Supply a file which does not exist locally + $bad_remote_uri = [ + '/var/di/dump/doo', + 'relative/but/not/here', + 'singleWord.no', + ]; + foreach ($bad_remote_uri as $test_file_path) { + // Now setup the form_state. + $form_state = new \Drupal\Core\Form\FormState(); + $form_state->addBuildInfo('args', [$plugin_id]); + $form_state->setValue('file_local', $test_file_path); + + // Now try validation! + \Drupal::formBuilder()->submitForm( + 'Drupal\tripal\Form\TripalImporterForm', + $form_state + ); + // And do some basic checks to ensure there were no errors. + $this->assertTrue($form_state->isValidationComplete(), + "We expect the form state to have been updated to indicate that validation is complete."); + // Looking for form validation errors + $form_validation_messages = $form_state->getErrors(); + $this->assertCount(1, $form_validation_messages, + "We expect validation errors for '$test_file_path' but did not recieve them."); + $this->assertArrayHasKey('file_local', $form_validation_messages, + "There should be an entry for file_local in the validation errors for '$test_file_path'."); + // Looking for drupal message errors. + $messages = \Drupal::messenger()->all(); + $this->assertIsArray($messages, + "We expect to have status messages to the user on submission of the form."); + $this->assertArrayHasKey('error', $messages, + "There should be an error message from this form but we didn't recieve any."); + $this->assertCount(1, $messages['error'], + "There should be only one error message."); + $this->assertStringContainsString('Cannot find the file', (string) $messages['error'][0], + "The error did not match the one we expected for an file which doesn't exist for file_local."); + $this->assertArrayNotHasKey('status', $messages, + "There should not be any success/status messages from this form. Instead we recieved: " . print_r($messages, TRUE)); + // Now delete drupal messages so we start the next test clean. + \Drupal::messenger()->deleteAll(); + } + } + + /** + * Tests the validation of the remote file element added by the Base Tripal Importer. + * + * Specifically, + * - VALID: provide a valid remote URL + * - ERROR: provide a badly formatted URI + * - ERROR: provide a correctly formatted but non-existent URI + */ + public function testTripalImporterFormValidateRemoteFile() { + + $manager = $this->setMockManager($this->definitions); + $container = \Drupal::getContainer(); + $container->set('tripal.importer', $manager); + + $plugin_id = 'fakeImporterName'; + $expected = $this->definitions; + + // --- CASE VALID + // --- Supply a valid remote path and ensure the form works! + $test_file_path = 'https://raw.githubusercontent.com/tripal/tripal/4.x/LICENSE.txt'; + + // Now setup the form_state. + $form_state = new \Drupal\Core\Form\FormState(); + $form_state->addBuildInfo('args', [$plugin_id]); + $form_state->setValue('file_remote', $test_file_path); + + // Now try validation! + \Drupal::formBuilder()->submitForm( + 'Drupal\tripal\Form\TripalImporterForm', + $form_state + ); + // And do some basic checks to ensure there were no errors. + $this->assertTrue($form_state->isValidationComplete(), + "We expect the form state to have been updated to indicate that validation is complete."); + // Looking for form validation errors + $form_validation_messages = $form_state->getErrors(); + $this->assertCount(0, $form_validation_messages, + "We should not have any validation errors for '$test_file_path'."); + // Looking for drupal message errors. + $messages = \Drupal::messenger()->all(); + $this->assertIsArray($messages, + "We expect to have status messages to the user on submission of the form."); + $this->assertArrayNotHasKey('error', $messages, + "There should not be any error messages from this form. Instead we recieved: " . print_r($messages, TRUE)); + // Now delete drupal messages so we start the next test clean. + \Drupal::messenger()->deleteAll(); + + // --- CASE ERROR + // --- Supply a text string that is clearly not a URL for file_remote and ensure it fails. + $bad_remote_uri = [ + 'lalalalalalala', + 'http://.com', + 'http://...', + 'http://', + 'http://£$"%$&*.com', + ]; + foreach ($bad_remote_uri as $test_file_path) { + // Now setup the form_state. + $form_state = new \Drupal\Core\Form\FormState(); + $form_state->addBuildInfo('args', [$plugin_id]); + $form_state->setValue('file_remote', $test_file_path); + + // Now try validation! + \Drupal::formBuilder()->submitForm( + 'Drupal\tripal\Form\TripalImporterForm', + $form_state + ); + // And do some basic checks to ensure there were no errors. + $this->assertTrue($form_state->isValidationComplete(), + "We expect the form state to have been updated to indicate that validation is complete."); + // Looking for form validation errors + $form_validation_messages = $form_state->getErrors(); + $this->assertCount(1, $form_validation_messages, + "We expect validation errors for '$test_file_path' but did not recieve them."); + $this->assertArrayHasKey('file_remote', $form_validation_messages, + "There should be an entry for file_remote in the validation errors for '$test_file_path'."); + // Looking for drupal message errors. + $messages = \Drupal::messenger()->all(); + $this->assertIsArray($messages, + "We expect to have status messages to the user on submission of the form."); + $this->assertArrayHasKey('error', $messages, + "There should be an error message from this form but we didn't recieve any."); + $this->assertCount(1, $messages['error'], + "There should be only one error message."); + $this->assertStringContainsString('not a valid URI', (string) $messages['error'][0], + "The error did not match the one we expected for an invalid URL passed to file_remote."); + $this->assertArrayNotHasKey('status', $messages, + "There should not be any success/status messages from this form. Instead we recieved: " . print_r($messages, TRUE)); + // Now delete drupal messages so we start the next test clean. + \Drupal::messenger()->deleteAll(); + } + + // --- CASE ERROR + // --- Supply a URI that is valid but does not resolve to a web page. + $bad_remote_uri = [ + 'http://notreallyasite.com/', + 'https://notreallyatripalsite.github.com', + ]; + foreach ($bad_remote_uri as $test_file_path) { + // Now setup the form_state. + $form_state = new \Drupal\Core\Form\FormState(); + $form_state->addBuildInfo('args', [$plugin_id]); + $form_state->setValue('file_remote', $test_file_path); + + // Now try validation! + \Drupal::formBuilder()->submitForm( + 'Drupal\tripal\Form\TripalImporterForm', + $form_state + ); + // And do some basic checks to ensure there were no errors. + $this->assertTrue($form_state->isValidationComplete(), + "We expect the form state to have been updated to indicate that validation is complete."); + // Looking for form validation errors + $form_validation_messages = $form_state->getErrors(); + $this->assertCount(1, $form_validation_messages, + "We expect validation errors for '$test_file_path' but did not recieve them."); + $this->assertArrayHasKey('file_remote', $form_validation_messages, + "There should be an entry for file_remote in the validation errors for '$test_file_path'."); + // Looking for drupal message errors. + $messages = \Drupal::messenger()->all(); + $this->assertIsArray($messages, + "We expect to have status messages to the user on submission of the form."); + $this->assertArrayHasKey('error', $messages, + "There should be an error message from this form but we didn't recieve any."); + $this->assertCount(1, $messages['error'], + "There should be only one error message."); + $this->assertStringContainsString('cannot be accessed', (string) $messages['error'][0], + "The error did not match the one we expected for an invalid URL passed to file_remote."); + $this->assertArrayNotHasKey('status', $messages, + "There should not be any success/status messages from this form. Instead we recieved: " . print_r($messages, TRUE)); + // Now delete drupal messages so we start the next test clean. + \Drupal::messenger()->deleteAll(); + } + } + + /** + * HELPER: Creates a mock plugin + plugin manager. + */ + protected function setMockManager($annotation) { + + // Mock Tripal Importer Plugin. + $configuration = []; + $plugin_id = 'fakeImporterName'; + $this->mock_plugin = $this->getMockForAbstractClass( + '\Drupal\tripal\TripalImporter\TripalImporterBase', + [$configuration, $plugin_id, $annotation] + ); + $this->mock_plugin->method('form') + ->willReturn($this->form); + $this->mock_plugin->method('addAnalysis') + ->willReturn($this->analysis_form); + + // Mock Plugin Manager. + $manager = $this->createMock(\Drupal\tripal\TripalImporter\PluginManagers\TripalImporterManager::class); + $manager->method('createInstance') + ->willReturn($this->mock_plugin); + $manager->method('getDefinitions') + ->willReturn([$plugin_id => $annotation]); + + return $manager; + } +} diff --git a/tripal/tests/src/Kernel/TripalStorage/DrupalSqlStorageTest.php b/tripal/tests/src/Kernel/TripalStorage/DrupalSqlStorageTest.php new file mode 100644 index 000000000..26538e0f0 --- /dev/null +++ b/tripal/tests/src/Kernel/TripalStorage/DrupalSqlStorageTest.php @@ -0,0 +1,121 @@ +set('is_a_test_environment', TRUE); + + // Grab the container. + $container = \Drupal::getContainer(); + + // We need a term for property types so we will create a generic mocked one + // here which will be pulled from the container any time a term is requested. + $this->mock_term = $this->createMock(\Drupal\tripal\TripalVocabTerms\TripalTerm::class); + // Create a mock ID space to return our mock term when asked. + $mock_idspace = $this->createMock(\Drupal\tripal\TripalVocabTerms\Interfaces\TripalIdSpaceInterface::class); + $mock_idspace->method('getTerm') + ->willReturn($this->mock_term); + // Create a mock Tripal ID Space service to return our mock idspace when asked. + $mock_idspace_service = $this->createMock(\Drupal\tripal\TripalVocabTerms\PluginManagers\TripalIdSpaceManager::class); + $mock_idspace_service->method('loadCollection') + ->willReturn($mock_idspace); + $container->set('tripal.collection_plugin_manager.idspace', $mock_idspace_service); + + + // Check that the static create method is returning the above mocked objects + // as we expect it to. Note: Testing in this way is a big arbitrary as the + // create() method is used for dependency injection but we like to be thorough. + $returned_static_class = DrupalSqlStorage::create( + $container, + [], + 'test_plugin_id', + [] + ); + $this->assertEquals([], $returned_static_class->getPluginDefinition(), + "We expect to be able to retrieve the plugin definition that we passed in."); + $this->assertEquals('test_plugin_id', $returned_static_class->getPluginId(), + "We expect to be able to retrieve the plugin ID we set."); + // We also want to test the logger but it is a protected variable. + // Use closures to test. + $that = $this; + $assertClosure = function () use ($that){ + $that->assertIsObject($this->logger, + "The message logging object in our plugin was not set properly."); + $this->assertInstanceOf(\Drupal\tripal\Services\TripalLogger::class, $this->logger, + "We expect the logger to have been set and be a Tripal Logger."); + }; + $doAssertLogger = $assertClosure->bindTo($returned_static_class, get_class($returned_static_class)); + } + + /** + * Tests the add/get field definition functionality. + */ + public function testDrupalSqlStorageCRUD() { + + // To create a tripal storage object we will need the parameters required + // for the constructor. + $configuration = []; + $plugin_id = 'fakePluginName'; + $plugin_definition = []; + $logger = \Drupal::service('tripal.logger'); + // Create a new instance of Drupal SQL Storage for testing purposes. + $manager = \Drupal::service('tripal.storage'); + $drupalSqlStorage = $manager->getInstance(['plugin_id' => 'drupal_sql_storage']); + $this->assertIsObject($drupalSqlStorage, "Unable to create Drupal SQL Storage object."); + + // All crud should be handled outside of TripalStorage... + // So we are just checking that these methods are implemented. + $values = []; + // Should return TRUE. + $return_value = $drupalSqlStorage->updateValues($values); + $this->assertTrue($return_value, "drupalSqlStorage::updateValues() did not return the expected TRUE boolean."); + $return_value = $drupalSqlStorage->deleteValues($values); + $this->assertTrue($return_value, "drupalSqlStorage::deleteValues() did not return the expected TRUE boolean."); + $return_value = $drupalSqlStorage->insertValues($values); + $this->assertTrue($return_value, "drupalSqlStorage::insertValues() did not return the expected TRUE boolean."); + $return_value = $drupalSqlStorage->loadValues($values); + $this->assertTrue($return_value, "drupalSqlStorage::loadValues() did not return the expected TRUE boolean."); + // Should return empty array. + $return_value = $drupalSqlStorage->validateValues($values); + $this->assertIsArray($return_value, "drupalSqlStorage::validateValues() did not return the expected empty array."); + $this->assertCount(0, $return_value, "The returned array should be empty but is not."); + $return_value = $drupalSqlStorage->findValues($values); + $this->assertIsArray($return_value, "drupalSqlStorage::findValues() did not return the expected empty array."); + $this->assertCount(0, $return_value, "The returned array should be empty but is not."); + + } +} diff --git a/tripal/tests/src/Kernel/TripalStorage/PropertyBaseClassTest.php b/tripal/tests/src/Kernel/TripalStorage/PropertyBaseClassTest.php new file mode 100644 index 000000000..7e83d7145 --- /dev/null +++ b/tripal/tests/src/Kernel/TripalStorage/PropertyBaseClassTest.php @@ -0,0 +1,160 @@ +set('is_a_test_environment', TRUE); + + // Grab the container. + $container = \Drupal::getContainer(); + + // We need a term for property types so we will create a generic mocked one + // here which will be pulled from the container any time a term is requested. + $this->mock_term = $this->createMock(\Drupal\tripal\TripalVocabTerms\TripalTerm::class); + // Create a mock ID space to return our mock term when asked. + $this->mock_idspace = $this->createMock(\Drupal\tripal\TripalVocabTerms\Interfaces\TripalIdSpaceInterface::class); + $this->mock_idspace->method('getTerm') + ->willReturnCallback(function($accession) { + if ($accession == 'term') { + return $this->mock_term; + } + else { + return NULL; + } + }); + // Create a mock Tripal ID Space service to return our mock idspace when asked. + $mock_idspace_service = $this->createMock(\Drupal\tripal\TripalVocabTerms\PluginManagers\TripalIdSpaceManager::class); + $mock_idspace_service->method('loadCollection') + ->willReturnCallback(function($id_space) { + if ($id_space == 'mock') { + return $this->mock_idspace; + } + else { + return NULL; + } + }); + $container->set('tripal.collection_plugin_manager.idspace', $mock_idspace_service); + } + + /** + * Tests the creation of properties focusing on invalid parameters. + * + * Note: Valid parameters are checked when testing property types + values. + */ + public function testPropertyCreation() { + + // Valid Parameters. + $entityType = 'tripal_entity'; + $fieldType = 'AFakeFieldType'; + $key = 'AFakepropertyKey'; + $term_id = 'mock:term'; + + // Test passing in a badly formatted term. + $exception_message = NULL; + $bad_term = 'NoColonDelimiter'; + try { + $property = new \Drupal\tripal\TripalStorage\StoragePropertyBase($entityType, $fieldType, $key, $bad_term); + } + catch (\Exception $e) { + $exception_message = $e->getMessage(); + } + $this->assertStringContainsString('properly formatted term', $exception_message, + "We did not get the exception message we expected for passing in a badly formatted term."); + + // Test passing in a term whose ID Space doesn't exist in our mock. + $exception_message = NULL; + $bad_term = 'MissingIdSpace:term'; + try { + $property = new \Drupal\tripal\TripalStorage\StoragePropertyBase($entityType, $fieldType, $key, $bad_term); + } + catch (\Exception $e) { + $exception_message = $e->getMessage(); + } + $this->assertStringContainsString('IdSpace for the property term is not recognized', $exception_message, + "We did not get the exception message we expected for passing in a term whose id space didn't exist."); + + // Test passing in a term whose ID Space doesn't exist in our mock. + $exception_message = NULL; + $bad_term = 'mock:MissingTerm'; + try { + $property = new \Drupal\tripal\TripalStorage\StoragePropertyBase($entityType, $fieldType, $key, $bad_term); + } + catch (\Exception $e) { + $exception_message = $e->getMessage(); + } + $this->assertStringContainsString('accession for the property term is not recognized', $exception_message, + "We did not get the exception message we expected for passing in a term whose accession didn't exist."); + + // Test passing in empty Entity type. + $exception_message = NULL; + try { + $property = new \Drupal\tripal\TripalStorage\StoragePropertyBase('', $fieldType, $key, $term_id); + } + catch (\Exception $e) { + $exception_message = $e->getMessage(); + } + $this->assertStringContainsString('without specifying the entity type', $exception_message, + "We did not get the exception message we expected for passing in an empty string for entity type."); + + // Test passing in empty field type. + $exception_message = NULL; + try { + $property = new \Drupal\tripal\TripalStorage\StoragePropertyBase($entityType, '', $key, $term_id); + } + catch (\Exception $e) { + $exception_message = $e->getMessage(); + } + $this->assertStringContainsString('without specifying the field', $exception_message, + "We did not get the exception message we expected for passing in an empty string for entity type."); + + // Test passing in empty property key. + $exception_message = NULL; + try { + $property = new \Drupal\tripal\TripalStorage\StoragePropertyBase($entityType, $fieldType, '', $term_id); + } + catch (\Exception $e) { + $exception_message = $e->getMessage(); + } + $this->assertStringContainsString('without a key', $exception_message, + "We did not get the exception message we expected for passing in an empty string for entity type."); + + } +} diff --git a/tripal/tests/src/Kernel/TripalStorage/PropertyTypeClassTest.php b/tripal/tests/src/Kernel/TripalStorage/PropertyTypeClassTest.php new file mode 100644 index 000000000..83c3a5208 --- /dev/null +++ b/tripal/tests/src/Kernel/TripalStorage/PropertyTypeClassTest.php @@ -0,0 +1,277 @@ +set('is_a_test_environment', TRUE); + + // Grab the container. + $container = \Drupal::getContainer(); + + // We need a term for property types so we will create a generic mocked one + // here which will be pulled from the container any time a term is requested. + $this->mock_term = $this->createMock(\Drupal\tripal\TripalVocabTerms\TripalTerm::class); + // Create a mock ID space to return our mock term when asked. + $this->mock_idspace = $this->createMock(\Drupal\tripal\TripalVocabTerms\Interfaces\TripalIdSpaceInterface::class); + $this->mock_idspace->method('getTerm') + ->willReturnCallback(function($accession) { + if ($accession == 'term') { + return $this->mock_term; + } + else { + return NULL; + } + }); + // Create a mock Tripal ID Space service to return our mock idspace when asked. + $mock_idspace_service = $this->createMock(\Drupal\tripal\TripalVocabTerms\PluginManagers\TripalIdSpaceManager::class); + $mock_idspace_service->method('loadCollection') + ->willReturnCallback(function($id_space) { + if ($id_space == 'mock') { + return $this->mock_idspace; + } + else { + return NULL; + } + }); + $container->set('tripal.collection_plugin_manager.idspace', $mock_idspace_service); + } + + /** + * Tests the Base Classes for property types focusing on the methods. + */ + public function testPropertyTypeBaseClass() { + + $entityType = 'tripal_entity'; + $fieldType = 'AFakeFieldType'; + $key = 'AFakePropertyTypeKey'; + $term_id = 'mock:term'; + $id = 'FAKEStoragePropertyType'; + $storage_settings = ['put something in here' => 'so that we know its been retrieved']; + $propertyType = new \Drupal\tripal\TripalStorage\StoragePropertyTypeBase($entityType, $fieldType, $key, $term_id, $id, $storage_settings); + + $retrieved = $propertyType->getEntityType(); + $this->assertEquals($entityType, $retrieved, + "We were not able to retrieve the entity type that we set when creating the property type."); + + $retrieved = $propertyType->getFieldType(); + $this->assertEquals($fieldType, $retrieved, + "We were not able to retrieve the field type that we set when creating the property type."); + + $retrieved = $propertyType->getKey(); + $this->assertEquals($key, $retrieved, + "We were not able to retrieve the key that we set when creating the property type."); + + $retrieved = $propertyType->getTerm(); + $this->assertEquals($this->mock_term, $retrieved, + "We were not able to retrieve the term that we set when creating the property type."); + $retrieved = $propertyType->getTermIdSpace(); + $this->assertEquals('mock', $retrieved, + "We were not able to retrieve the idspace of the term we set when creating the property type."); + $retrieved = $propertyType->getTermAccession(); + $this->assertEquals('term', $retrieved, + "We were not able to retrieve the accession of the term we set when creating the property type."); + + $retrieved = $propertyType->getId(); + $this->assertEquals($id, $retrieved, + "We were not able to retrieve the id that we set when creating the property type."); + + $retrieved = $propertyType->getStorageSettings(); + $this->assertEquals($storage_settings, $retrieved, + "We were not able to retrieve the storage settings that we set when creating the property type."); + $new_settings = ['these are just' => 'random different words from before']; + $propertyType->setStorageSettings($new_settings); + $retrieved = $propertyType->getStorageSettings(); + $this->assertEquals($new_settings, $retrieved, + "We were not able to retrieve the storage settings that we just set."); + + // Now expand our tests to other methods that do not just access exactly what we supplied. + // -- Cardinality. + $retrieved = $propertyType->getCardinality(); + $this->assertEquals(1, $retrieved, "We were not able to retrieve the default cardinality."); + $propertyType->setCardinality(5); + $retrieved = $propertyType->getCardinality(); + $this->assertEquals(5, $retrieved, "We were not able to retrieve the cardinality we just set."); + $propertyType->setCardinality(0); + $retrieved = $propertyType->getCardinality(); + $this->assertEquals(0, $retrieved, "We were not able to retrieve the cardinality when we try to set it to unlimited."); + // -- Searchability + $retrieved = $propertyType->getSearchability(); + $this->assertEquals(TRUE, $retrieved, "We were not able to retrieve the default Searchability."); + $propertyType->setSearchability(FALSE); + $retrieved = $propertyType->getSearchability(); + $this->assertFalse($retrieved, "We were not able to retrieve the Searchability we just set."); + // -- Operations. + $retrieved = $propertyType->getOperations(); + $this->assertIsArray($retrieved, "We were not able to retrieve the default operations."); + $this->assertContains('=', $retrieved, + "We expected '=' to be included in the default operations but it was not."); + $propertyType->setOperations(['A', 'B', 'C']); + $retrieved = $propertyType->getOperations(); + $this->assertIsArray($retrieved, "We were not able to retrieve the operations we just set."); + $this->assertCount(3, $retrieved, + "We set only 3 operations so that is what we expect to be able to retrieve."); + // -- sortable + $retrieved = $propertyType->getSortable(); + $this->assertEquals(TRUE, $retrieved, "We were not able to retrieve the default sortability."); + $propertyType->setSortable(FALSE); + $retrieved = $propertyType->getSortable(); + $this->assertFalse($retrieved, "We were not able to retrieve the sortability we just set."); + // -- read only + $retrieved = $propertyType->getReadOnly(); + $this->assertEquals(FALSE, $retrieved, "We were not able to retrieve the default read only value."); + $propertyType->setReadOnly(TRUE); + $retrieved = $propertyType->getReadOnly(); + $this->assertTrue($retrieved, "We were not able to retrieve the read only we just set."); + // -- required + $retrieved = $propertyType->getRequired(); + $this->assertEquals(FALSE, $retrieved, "We were not able to retrieve the default required setting."); + $propertyType->setRequired(TRUE); + $retrieved = $propertyType->getRequired(); + $this->assertTrue($retrieved, "We were not able to retrieve the required setting we just set."); + } + + /** + * Tests the default implementation of Tripal PropertyTypes. + * + * Specifically: + * - BoolStoragePropertyType + * - DateTimeStoragePropertyType + * - IntStoragePropertyType + * - RealStoragePropertyType + * - TextStoragePropertyType + * - VarCharStoragePropertyType + */ + public function testPropertyTypes() { + + $entityType = 'tripal_entity'; + $fieldType = 'AFakeFieldType'; + $key = 'AFakePropertyTypeKey'; + $term_id = 'mock:term'; + $storage_settings = ['put something in here' => 'so that we know its been retrieved']; + + // BoolStoragePropertyType + $type = 'BoolStoragePropertyType'; + $instance = '\Drupal\tripal\TripalStorage\\' . $type; + $propertyType = new \Drupal\tripal\TripalStorage\BoolStoragePropertyType($entityType, $fieldType, $key, $term_id); + $this->assertIsObject($propertyType, "We were not able to create an object for $type."); + $this->assertInstanceOf($instance, $propertyType, + "We created an object but it was not the type we expected."); + + // DateTimeStoragePropertyType + $type = 'DateTimeStoragePropertyType'; + $instance = '\Drupal\tripal\TripalStorage\\' . $type; + $propertyType = new \Drupal\tripal\TripalStorage\DateTimeStoragePropertyType($entityType, $fieldType, $key, $term_id); + $this->assertIsObject($propertyType, "We were not able to create an object for $type."); + $this->assertInstanceOf($instance, $propertyType, + "We created an object but it was not the type we expected."); + + // IntStoragePropertyType + $type = 'IntStoragePropertyType'; + $instance = '\Drupal\tripal\TripalStorage\\' . $type; + $propertyType = new \Drupal\tripal\TripalStorage\IntStoragePropertyType($entityType, $fieldType, $key, $term_id); + $this->assertIsObject($propertyType, "We were not able to create an object for $type."); + $this->assertInstanceOf($instance, $propertyType, + "We created an object but it was not the type we expected."); + + // RealStoragePropertyType + $type = 'RealStoragePropertyType'; + $instance = '\Drupal\tripal\TripalStorage\\' . $type; + $propertyType = new \Drupal\tripal\TripalStorage\RealStoragePropertyType($entityType, $fieldType, $key, $term_id); + $this->assertIsObject($propertyType, "We were not able to create an object for $type."); + $this->assertInstanceOf($instance, $propertyType, + "We created an object but it was not the type we expected."); + + // TextStoragePropertyType + $type = 'TextStoragePropertyType'; + $instance = '\Drupal\tripal\TripalStorage\\' . $type; + $propertyType = new \Drupal\tripal\TripalStorage\TextStoragePropertyType($entityType, $fieldType, $key, $term_id); + $this->assertIsObject($propertyType, "We were not able to create an object for $type."); + $this->assertInstanceOf($instance, $propertyType, + "We created an object but it was not the type we expected."); + + // VarCharStoragePropertyType + $type = 'VarCharStoragePropertyType'; + $instance = '\Drupal\tripal\TripalStorage\\' . $type; + $propertyType = new \Drupal\tripal\TripalStorage\VarCharStoragePropertyType($entityType, $fieldType, $key, $term_id); + $this->assertIsObject($propertyType, "We were not able to create an object for $type."); + $this->assertInstanceOf($instance, $propertyType, + "We created an object but it was not the type we expected."); + } + + /** + * Tests extra functionality associated with varchar property types. + */ + public function testVarCharStoragePropertyType() { + + $entityType = 'tripal_entity'; + $fieldType = 'AFakeFieldType'; + $key = 'AFakePropertyTypeKey'; + $term_id = 'mock:term'; + $id = 'FAKEStoragePropertyType'; + + // Check the default max char size. + $type = 'VarCharStoragePropertyType'; + $instance = '\Drupal\tripal\TripalStorage\\' . $type; + $propertyType = new \Drupal\tripal\TripalStorage\VarCharStoragePropertyType($entityType, $fieldType, $key, $term_id); + $this->assertIsObject($propertyType, "We were not able to create an object for $type."); + $this->assertInstanceOf($instance, $propertyType, + "We created an object but it was not the type we expected."); + + $retrieved = $propertyType->getMaxCharacterSize(); + $this->assertIsInt($retrieved, "We did not get an integer when trying to access the default max char size."); + $this->assertEquals(255, $retrieved, + "We did not retrieve the expected default max char size."); + + // Check a non-default max char size. + $type = 'VarCharStoragePropertyType'; + $instance = '\Drupal\tripal\TripalStorage\\' . $type; + $propertyType = new \Drupal\tripal\TripalStorage\VarCharStoragePropertyType($entityType, $fieldType, $key, $term_id, 333); + $this->assertIsObject($propertyType, "We were not able to create an object for $type."); + $this->assertInstanceOf($instance, $propertyType, + "We created an object but it was not the type we expected."); + + $retrieved = $propertyType->getMaxCharacterSize(); + $this->assertIsInt($retrieved, "We did not get an integer when trying to access the default max char size."); + $this->assertEquals(333, $retrieved, + "We did not retrieve the expected max char size based on what we passed in during creation."); + + } +} diff --git a/tripal/tests/src/Kernel/TripalStorage/PropertyValueClassTest.php b/tripal/tests/src/Kernel/TripalStorage/PropertyValueClassTest.php new file mode 100644 index 000000000..fa94c3595 --- /dev/null +++ b/tripal/tests/src/Kernel/TripalStorage/PropertyValueClassTest.php @@ -0,0 +1,119 @@ +set('is_a_test_environment', TRUE); + + // Grab the container. + $container = \Drupal::getContainer(); + + // We need a term for property types so we will create a generic mocked one + // here which will be pulled from the container any time a term is requested. + $this->mock_term = $this->createMock(\Drupal\tripal\TripalVocabTerms\TripalTerm::class); + // Create a mock ID space to return our mock term when asked. + $this->mock_idspace = $this->createMock(\Drupal\tripal\TripalVocabTerms\Interfaces\TripalIdSpaceInterface::class); + $this->mock_idspace->method('getTerm') + ->willReturnCallback(function($accession) { + if ($accession == 'term') { + return $this->mock_term; + } + else { + return NULL; + } + }); + // Create a mock Tripal ID Space service to return our mock idspace when asked. + $mock_idspace_service = $this->createMock(\Drupal\tripal\TripalVocabTerms\PluginManagers\TripalIdSpaceManager::class); + $mock_idspace_service->method('loadCollection') + ->willReturnCallback(function($id_space) { + if ($id_space == 'mock') { + return $this->mock_idspace; + } + else { + return NULL; + } + }); + $container->set('tripal.collection_plugin_manager.idspace', $mock_idspace_service); + } + + public function testPropertyValueClass() { + + // Valid Parameters. + $entityType = 'tripal_entity'; + $fieldType = 'AFakeFieldType'; + $key = 'AFakePropertyTypeKey'; + $term_id = 'mock:term'; + $entityId = 5; + + // Create with default value. + $instance = '\Drupal\tripal\TripalStorage\StoragePropertyValue'; + $propertyValue = new \Drupal\tripal\TripalStorage\StoragePropertyValue($entityType, $fieldType, $key, $term_id, $entityId); + $this->assertIsObject($propertyValue, "We were not able to create an object for PropertyValue."); + $this->assertInstanceOf($instance, $propertyValue, + "We created an object but it was not the type we expected."); + + // Try getting the value when it wasn't set during creation. + $value = $propertyValue->getValue(); + $this->assertNull($value, "The value should not be set as we didn't set it on creation."); + + // We can get the Entity ID, right? + $retrieved = $propertyValue->getEntityId(); + $this->assertEquals($entityId, $retrieved, "We were not able to retrieve the entity id."); + + // Create with a set value. + $instance = '\Drupal\tripal\TripalStorage\StoragePropertyValue'; + $propertyValue = new \Drupal\tripal\TripalStorage\StoragePropertyValue($entityType, $fieldType, $key, $term_id, $entityId, 333); + $this->assertIsObject($propertyValue, "We were not able to create an object for PropertyValue."); + $this->assertInstanceOf($instance, $propertyValue, + "We created an object but it was not the type we expected."); + + // Try getting the value when it wasn't set during creation. + $value = $propertyValue->getValue(); + $this->assertEquals(333, $value, "The value should have been set to 333 on creation."); + + // Now lets set it to something else and check it changed. + $propertyValue->setValue(999); + $value = $propertyValue->getValue(); + $this->assertEquals(999, $value, "The value should have been set to 999 just now."); + } + +} diff --git a/tripal/tests/src/Kernel/TripalStorage/TripalStorageTest.php b/tripal/tests/src/Kernel/TripalStorage/TripalStorageTest.php new file mode 100644 index 000000000..eef20df99 --- /dev/null +++ b/tripal/tests/src/Kernel/TripalStorage/TripalStorageTest.php @@ -0,0 +1,332 @@ +set('is_a_test_environment', TRUE); + + // Grab the container. + $container = \Drupal::getContainer(); + + // We need a term for property types so we will create a generic mocked one + // here which will be pulled from the container any time a term is requested. + $this->mock_term = $this->createMock(\Drupal\tripal\TripalVocabTerms\TripalTerm::class); + // Create a mock ID space to return our mock term when asked. + $mock_idspace = $this->createMock(\Drupal\tripal\TripalVocabTerms\Interfaces\TripalIdSpaceInterface::class); + $mock_idspace->method('getTerm') + ->willReturn($this->mock_term); + // Create a mock Tripal ID Space service to return our mock idspace when asked. + $mock_idspace_service = $this->createMock(\Drupal\tripal\TripalVocabTerms\PluginManagers\TripalIdSpaceManager::class); + $mock_idspace_service->method('loadCollection') + ->willReturn($mock_idspace); + $container->set('tripal.collection_plugin_manager.idspace', $mock_idspace_service); + + // Some of our tests will check logged messages that would normally go to + // php error_log. PHPUnit will throw an exception if anything is added to + // error_log so we want to mock TripalLogger to ensure all errors are printed + // to the screen. + // We only need to mock the error method. Other methods will not be mocked. + $mock_logger = $this->getMockBuilder(\Drupal\tripal\Services\TripalLogger::class) + ->onlyMethods(['error']) + ->getMock(); + $mock_logger->method('error') + ->willReturnCallback(function($message, $context, $options) { + print 'ERROR: ' . str_replace(array_keys($context), $context, $message); + return NULL; + }); + $container->set('tripal.logger', $mock_logger); + + } + + /** + * Tests the add/get field definition functionality. + */ + public function testTripalStorageBaseFieldDefn() { + + // To create a tripal storage object we will need the parameters required + // for the constructor. + $configuration = []; + $plugin_id = 'fakePluginName'; + $plugin_definition = []; + $logger = \Drupal::service('tripal.logger');; + // Tripal Storage Base is an abstract class. + // Therefore, in order to test it we need to mock the abstract methods. + $tripalStorage = $this->getMockForAbstractClass( + 'Drupal\tripal\TripalStorage\TripalStorageBase', + [$configuration, $plugin_id, $plugin_definition, $logger] + ); + $this->assertIsObject($tripalStorage, "Unable to create tripal storage mock object."); + + // This will be our set of fields to test. + // We're checking there are no special assumptions about field names here. + $fields = [ + 'name_all_underscores' => NULL, + 'NameSnakeCase' => NULL, + 'Name with Spaces' => NULL, + 'name.with-slightly.special-chars' => NULL, + 'name!with+symbols' => NULL, + ]; + + // We also need a FieldConfig object for each field + foreach ($fields as $field_name => $placeholder) { + $fields[$field_name] = $this->createMock(\Drupal\field\Entity\FieldConfig::class); + $fields[$field_name]->method('getLabel') + ->willReturn($field_name); + + // Now add it to the storage + $success = $tripalStorage->addFieldDefinition($field_name, $fields[$field_name]); + $this->assertTrue($success, "add Field Definition did not return true for $field_name"); + } + + // Now that we've added all fields we want to show that we can + // retrieve each field definition back out from storage as needed. + foreach ($fields as $field_name => $expected_defn) { + $retrieved_defn = $tripalStorage->getFieldDefinition($field_name); + $this->assertIsObject($retrieved_defn, "Unable to retrieve an object when given $field_name."); + $this->assertEquals($expected_defn, $retrieved_defn, + "The retrieved definition did not match the original one we mocked for $field_name."); + } + + // We also want to test trying to get a field definition that doesn't exist. + // We expect this to return FALSE and not to throw an exception. + $retrieved_defn = $tripalStorage->getFieldDefinition('aFieldWhichDoesntExist'); + $this->assertFalse($retrieved_defn, "We should not be able to retrieve a field definition for a field which does not exist."); + + // Check that if we alter a field definition + // and reset it that we get the most recent one. + $altered_mock = $fields['NameSnakeCase']; + $altered_mock->method('getLabel') + ->willReturn('NEW LABEL'); + $success = $tripalStorage->addFieldDefinition('NameSnakeCase', $altered_mock); + $this->assertTrue($success, "add Field Definition did not return true for NameSnakeCase (second time)"); + $retrieved_defn = $tripalStorage->getFieldDefinition('NameSnakeCase'); + $this->assertIsObject($retrieved_defn, "Unable to retrieve an object when given NameSnakeCase (second time)."); + $this->assertEquals($altered_mock, $retrieved_defn, + "The retrieved definition did not match the one we altered for NameSnakeCase (second time)."); + } + + /** + * Tests the add/get property type functionality. + */ + public function testTripalStorageBasePropTypes() { + + // To create a tripal storage object we will need the parameters required + // for the constructor. + $configuration = []; + $plugin_id = 'fakePluginName'; + $plugin_definition = []; + $logger = \Drupal::service('tripal.logger'); + // Tripal Storage Base is an abstract class. + // Therefore, in order to test it we need to mock the abstract methods. + $tripalStorage = $this->getMockForAbstractClass( + 'Drupal\tripal\TripalStorage\TripalStorageBase', + [$configuration, $plugin_id, $plugin_definition, $logger] + ); + $this->assertIsObject($tripalStorage, "Unable to create tripal storage mock object."); + + // This will be our set of fields to test. + // We're checking there are no special assumptions about field names here. + $fields = [ + 'name_all_underscores', + 'NameSnakeCase', + 'Name with Spaces', + 'name.with-slightly.special-chars', + 'name!with+symbols', + ]; + + // We want to use the same set of property keys for each field to confirm that + // they will not be overridden. + $property_keys = [ + 'record_id', + 'value', + 'pkey', + 'a completely nonsense name', + 'one.with!some-symbols+special|chars', + ]; + + $propertyTyleClass_namespace = 'Drupal\tripal\TripalStorage\\'; + $propertyTypeClasses = ['BoolStoragePropertyType', 'DateTimeStoragePropertyType', + 'IntStoragePropertyType', 'RealStoragePropertyType', 'TextStoragePropertyType']; + + $expected_types = []; + $expected_class_type = []; + foreach ($fields as $field_name) { + + $propertyTypeClass = $propertyTyleClass_namespace . array_pop($propertyTypeClasses); + $expected_class_type[$field_name] = $propertyTypeClass; + + foreach ($property_keys as $key) { + + $type = new $propertyTypeClass( + 'entity_test', + $field_name, + $key, + 'rdfs:type', + [], + ); + $this->assertIsObject( + $type, + "Unable to create $field_name.$key property type: not an object." + ); + $this->assertInstanceOf( + StoragePropertyTypeBase::class, + $type, + "Unable to create $field_name.$key property type: does not inherit from StoragePropertyTypeBase." + ); + + // We expect the key to have been sanitized so lets do that quickly. + // Otherwise we won't be able to check retrieved properties + // against those we passed in. + $sanitized_key = preg_replace('/[^\w]/', '_', $key); + $expected_types[$field_name][$sanitized_key] = $type; + } + + // Types are added on a per field basis, so lets do that now. + $tripalStorage->addTypes($field_name, $expected_types[$field_name]); + } + + // Also test that if we try to add a type that is not an object + $bad_test_properties = [ + 'NotClass' => 'fred really wanted to be a property type but alas he was a string', + ]; + ob_start(); + $tripalStorage->addTypes('fieldWithBadPropertyTypes', $bad_test_properties); + $printed_output = ob_get_clean(); + $this->assertMatchesRegularExpression('/ERROR.*must be an object.*NotClass/', $printed_output, + "We expected an error to be printed saying that 'NotClass' was not an object."); + $this->assertDoesNotMatchRegularExpression('/ERROR.*ERROR/', $printed_output, + "We only expected a single error and yet we may have found multiple?"); + + // or is not a propertyType object that we get an error. + // We use Drupal\tripal\TripalStorage\StoragePropertyValue to check that it + // is really specific. + $propertyValueNotType = new \Drupal\tripal\TripalStorage\StoragePropertyValue( + 'tripal_entity', + 'name_all_underscores', + 'NotAPropertyType', + 'mock:term', + 'entity_test' + ); + $bad_test_properties = [ + 'NotAPropertyType' => $propertyValueNotType + ]; + ob_start(); + $tripalStorage->addTypes('fieldWithBadPropertyTypes', $bad_test_properties); + $printed_output = ob_get_clean(); + $this->assertMatchesRegularExpression('/ERROR.*StoragePropertyType.*NotAPropertyType/', $printed_output, + "We expected an error to be printed saying that 'NotAPropertyType' was not a property type object."); + $this->assertDoesNotMatchRegularExpression('/ERROR.*ERROR/', $printed_output, + "We only expected a single error and yet we may have found multiple?"); + + // Now we use the generic getTypes method to test that we can retrieve what we added. + // Retrieved types should be of the form: + // field_name -> property key -> property type object. + $retrieved_types = $tripalStorage->getTypes(); + $expected_number_of_fields = count($fields); + $this->assertCount($expected_number_of_fields, $retrieved_types, + "We did not have the number of fields we expected assuming that the first level of keys in the array returned by getTypes() are fields."); + foreach ($retrieved_types as $retrieved_field => $retrieved_field_proptypes) { + $this->assertArrayHasKey($retrieved_field, $expected_types, + "The field we retrieved ($retrieved_field) was not in the fields we expected."); + $expected_count = count($expected_types[$retrieved_field]); + $this->assertCount($expected_count, $retrieved_field_proptypes, + "We did not have the expected number of entries for $retrieved_field."); + + foreach ($retrieved_field_proptypes as $retrieved_key => $retrieved_proptype) { + $this->assertArrayHasKey($retrieved_key, $expected_types[$retrieved_field], + "We expected the property '$retrieved_key' (sanitized from original) to be associated with the field ($retrieved_field)"); + + $expected_object = $expected_types[$retrieved_field][$retrieved_key]; + $this->assertEquals($expected_object, $retrieved_proptype, + "The object associated with $retrieved_field.$retrieved_key did not match the one we expected."); + + // We specifically chose to create a different type of propertyType for + // each field so that we could check that properties with the same + // key were being stored properly and not overridden. + $expected_type = $expected_class_type[$retrieved_field]; + $this->assertInstanceOf($expected_type, $retrieved_proptype, + "The type of propertyType we retrieved did not match what we expected."); + } + } + + // Now we want to check retrieving a specific property type. + foreach ($expected_types as $field_name => $properties) { + foreach ($properties as $property_key => $expected_property_object) { + $retrieved_property_object = $tripalStorage->getPropertyType($field_name, $property_key); + $this->assertIsObject($retrieved_property_object, + "We should have had a property type object returned but did not."); + $expected_type = $expected_class_type[$field_name]; + $this->assertInstanceOf($expected_type, $retrieved_property_object, + "The retrieved property type object was not the type we expected."); + } + } + + // Also check that if we ask for a non-existant property type that we don't get one. + $retrieved_property_object = $tripalStorage->getPropertyType('A field that definitely doesnt exist', 'also not a property that exists'); + $this->assertIsNotObject($retrieved_property_object, + "We should not have had an object returned as the field/property type combo should not exist."); + + // Finally check that we can accurately remove property types. + // We will remove a subset of types for a single field. + $field_name = 'NameSnakeCase'; + $properties2remove = [ + 'value' => $expected_types[$field_name]['value'], + 'pkey' => $expected_types[$field_name]['pkey'], + ]; + $tripalStorage->removeTypes($field_name, $properties2remove); + // Try to retrieve a property which should have been removed. + $retrieved_property_object = $tripalStorage->getPropertyType($field_name, 'value'); + $this->assertIsNotObject($retrieved_property_object, + "We should not have had an object returned as the $field_name.value should have been removed."); + $retrieved_property_object = $tripalStorage->getPropertyType($field_name, 'pkey'); + $this->assertIsNotObject($retrieved_property_object, + "We should not have had an object returned as the $field_name.pkey should have been removed."); + // Also check that the remaining properties for that field do exist. + $retrieved_types = $tripalStorage->getTypes(); + $this->assertArrayHasKey($field_name, $retrieved_types, + "We should have had some properties remaining for $field_name after removing 2 but none were returned by getTypes()."); + $expected_count = count($expected_types[$field_name]) - 2; + $this->assertCount($expected_count, $retrieved_types[$field_name], + "We did not have the expected number of properties remaining after removing some."); + } +} diff --git a/tripal/tripal.services.yml b/tripal/tripal.services.yml index 871d4e65b..90aa36b6d 100755 --- a/tripal/tripal.services.yml +++ b/tripal/tripal.services.yml @@ -10,6 +10,8 @@ services: class: Drupal\tripal\Services\Tripaljob tripal.logger: class: Drupal\tripal\Services\TripalLogger + tripal.token_parser: + class: Drupal\tripal\Services\TripalTokenParser tripal.dbx: class: Drupal\tripal\TripalDBX\TripalDbx tripal.storage: diff --git a/tripal/tripal.views.inc b/tripal/tripal.views.inc index 1a08d44a5..9066676a2 100644 --- a/tripal/tripal.views.inc +++ b/tripal/tripal.views.inc @@ -137,7 +137,7 @@ function tripal_views_data_jobs(&$data) { // Arguments $data['tripal_jobs']['arguments'] = [ - 'title' => t('Arguements'), + 'title' => t('Arguments'), 'help' => t('Any arguments passed to the callback.'), 'field' => [ 'id' => 'standard', diff --git a/tripal_chado/config/install/tripal.tripal_content_terms.chado_content_terms.yml b/tripal_chado/config/install/tripal.tripal_content_terms.chado_content_terms.yml index 82b15a84a..80dc90908 100644 --- a/tripal_chado/config/install/tripal.tripal_content_terms.chado_content_terms.yml +++ b/tripal_chado/config/install/tripal.tripal_content_terms.chado_content_terms.yml @@ -221,7 +221,7 @@ vocabularies: description: 'A version number is an information content entity which is a sequence of characters borne by part of each of a class of manufactured products or its packaging and indicates its order within a set of other products having the same name.' - id: 'IAO:0000064' name: 'algorithm' - description: 'An algorithm is a set of instructions for performing a paticular calculation.' + description: 'An algorithm is a set of instructions for performing a particular calculation.' - name: 'null' label: 'No vocabulary' diff --git a/tripal_chado/config/install/tripal.tripalentitytype_collection.default_chado.yml b/tripal_chado/config/install/tripal.tripalentitytype_collection.default_chado.yml index 3c07c7c3b..0955fbf60 100644 --- a/tripal_chado/config/install/tripal.tripalentitytype_collection.default_chado.yml +++ b/tripal_chado/config/install/tripal.tripalentitytype_collection.default_chado.yml @@ -265,7 +265,7 @@ content_types: - label: Array Design term: EFO:0000269 - help_text: Use the array design page to describe the systematic arrangement of similar objects, usually in rows and columns, used by intstrument to perform an assay. + help_text: Use the array design page to describe the systematic arrangement of similar objects, usually in rows and columns, used by an instrument to perform an assay. category: Expression id: array_design title_format: "[array_design_name]" diff --git a/tripal_chado/config/install/tripal.tripalfield_collection.default_chado.yml b/tripal_chado/config/install/tripal.tripalfield_collection.default_chado.yml index 6f6ffb212..060ab621a 100644 --- a/tripal_chado/config/install/tripal.tripalfield_collection.default_chado.yml +++ b/tripal_chado/config/install/tripal.tripalfield_collection.default_chado.yml @@ -178,8 +178,8 @@ fields: type_table: organism type_column: type_id settings: - termIdSpace: OBI - termAccession: "0100026" + termIdSpace: local + termAccession: "infraspecific_type" display: view: default: @@ -511,6 +511,32 @@ fields: region: content weight: 15 + - name: study_contact + content_type: study + label: Contact + type: chado_contact_default + description: An individual or organization that serves as a contact for this record. + cardinality: 1 + required: true + storage_settings: + storage_plugin_id: chado_storage + storage_plugin_settings: + base_table: study + base_column: contact_id + settings: + termIdSpace: local + termAccession: contact + display: + view: + default: + region: content + label: above + weight: 20 + form: + default: + region: content + weight: 20 + - name: contact_name content_type: contact label: Name @@ -3658,6 +3684,32 @@ fields: weight: 25 + - name: array_design_manufacturer + content_type: array_design + label: Manufacturer + type: chado_contact_default + description: A manufacturer's contact details + cardinality: 1 + required: true + storage_settings: + storage_plugin_id: chado_storage + storage_plugin_settings: + base_table: arraydesign + base_column: manufacturer_id + settings: + termIdSpace: local + termAccession: contact + display: + view: + default: + region: content + label: above + weight: 10 + form: + default: + region: content + weight: 10 + - name: array_design_name content_type: array_design label: Name @@ -3678,11 +3730,11 @@ fields: default: region: content label: above - weight: 10 + weight: 15 form: default: region: content - weight: 10 + weight: 15 - name: array_design_version_num content_type: array_design @@ -3704,11 +3756,11 @@ fields: default: region: content label: above - weight: 15 + weight: 20 form: default: region: content - weight: 15 + weight: 20 - name: array_design_description content_type: array_design @@ -3730,11 +3782,11 @@ fields: default: region: content label: above - weight: 20 + weight: 25 form: default: region: content - weight: 20 + weight: 25 - name: array_design_dimensions content_type: array_design @@ -3756,11 +3808,11 @@ fields: default: region: content label: above - weight: 25 + weight: 30 form: default: region: content - weight: 25 + weight: 30 - name: array_design_element_dims content_type: array_design @@ -3782,11 +3834,11 @@ fields: default: region: content label: above - weight: 30 + weight: 35 form: default: region: content - weight: 30 + weight: 35 - name: array_design_substrate_type content_type: array_design @@ -3809,11 +3861,11 @@ fields: default: region: content label: above - weight: 10 + weight: 40 form: default: region: content - weight: 10 + weight: 40 - name: array_design_platform_type content_type: array_design @@ -3836,8 +3888,8 @@ fields: default: region: content label: above - weight: 10 + weight: 45 form: default: region: content - weight: 10 + weight: 45 diff --git a/tripal_chado/config/install/views.view.chado_mviews.yml b/tripal_chado/config/install/views.view.chado_mviews.yml index 19d994fed..04820423b 100644 --- a/tripal_chado/config/install/views.view.chado_mviews.yml +++ b/tripal_chado/config/install/views.view.chado_mviews.yml @@ -794,7 +794,7 @@ display: admin_label: '' plugin_id: text_custom empty: false - content: 'Add Materilaized View' + content: 'Add Materialized View' tokenize: false footer: { } display_extenders: { } diff --git a/tripal_chado/src/ChadoCustomTables/ChadoMview.php b/tripal_chado/src/ChadoCustomTables/ChadoMview.php index 008bb9cd1..647bf3b40 100644 --- a/tripal_chado/src/ChadoCustomTables/ChadoMview.php +++ b/tripal_chado/src/ChadoCustomTables/ChadoMview.php @@ -28,10 +28,8 @@ public function __construct($table_name, string $chado_schema = NULL) { parent::__construct($table_name, $chado_schema); - $this->custom_table = NULL; $this->mview_id = NULL; - if (!$table_name) { throw new \Exception('Please provide a value for the $table_name argument'); } diff --git a/tripal_chado/src/Database/ChadoSchema.php b/tripal_chado/src/Database/ChadoSchema.php index 60aa248b6..db7b67139 100644 --- a/tripal_chado/src/Database/ChadoSchema.php +++ b/tripal_chado/src/Database/ChadoSchema.php @@ -19,10 +19,12 @@ public function getSchemaDef(array $parameters) :array { $source = $parameters['source'] ?? 'file'; $format = strtolower($parameters['format'] ?? ''); - $version = $parameters['version'] - ?? $this->connection->getVersion() - ?? $DEFAULT_VERSION - ; + + $version = $parameters['version']; + if (!array_key_exists('version', $parameters) OR empty($parameters['version'])) { + $version = $this->connection->getVersion(); + } + if (!empty($parameters['clear'])) { $schema_structure = []; } @@ -49,6 +51,7 @@ public function getSchemaDef(array $parameters) :array { . $version . '.yml' ; + // Make sure we got a valid version format. if (!preg_match('/^\\d\\.\\d$/', $version) || !file_exists($filename) @@ -84,7 +87,7 @@ public function getSchemaDef(array $parameters) :array { * include linker tables (which link two or more base tables), property * tables, and relationship tables. These provide additional information * about primary data records and are therefore not base tables. This - * function retreives only the list of tables that are considered 'base' + * function retrieves only the list of tables that are considered 'base' * tables. * * @return diff --git a/tripal_chado/src/Plugin/Field/FieldFormatter/ChadoContactFormatterDefault.php b/tripal_chado/src/Plugin/Field/FieldFormatter/ChadoContactFormatterDefault.php new file mode 100644 index 000000000..517d9be27 --- /dev/null +++ b/tripal_chado/src/Plugin/Field/FieldFormatter/ChadoContactFormatterDefault.php @@ -0,0 +1,43 @@ + $item) { + $contact_name = $item->get('contact_name')->getString(); + // Change the non-user-friendly 'null' contact, which is spedified by chado. + if ($contact_name == 'null') { + $contact_name = 'Unknown'; + } + $elements[$delta] = [ + "#markup" => $contact_name, + ]; + } + + return $elements; + } +} diff --git a/tripal_chado/src/Plugin/Field/FieldType/ChadoContactDefault.php b/tripal_chado/src/Plugin/Field/FieldType/ChadoContactDefault.php new file mode 100644 index 000000000..fd72c8c01 --- /dev/null +++ b/tripal_chado/src/Plugin/Field/FieldType/ChadoContactDefault.php @@ -0,0 +1,171 @@ +getSetting('storage_plugin_settings'); + $base_table = $form_state->getValue(['settings', 'storage_plugin_settings', 'base_table']); + + // Only present base tables that have a foreign key to contact. + $elements['storage_plugin_settings']['base_table']['#options'] = $this->getBaseTables('contact'); + // Ugly special case for the arraydesign table where manufacturer_id is mapped to contact_id + $elements['storage_plugin_settings']['base_table']['#options']['arraydesign'] = 'arraydesign'; + + // Add a validation to make sure the base table has a foreign + // key to contact_id in the chado.contact table. + $elements['storage_plugin_settings']['base_table']['#element_validate'] = [[static::class, 'storageSettingsFormValidate']]; + return $elements; + } + + /** + * {@inheritdoc} + */ + public static function tripalTypes($field_definition) { + $entity_type_id = $field_definition->getTargetEntityTypeId(); + + // Get the Chado table and column this field maps to. + $storage_settings = $field_definition->getSetting('storage_plugin_settings'); + $base_table = $storage_settings['base_table']; + + // If we don't have a base table then we're not ready to specify the + // properties for this field. + if (!$base_table) { + $record_id_term = 'local:contact'; + return [ + new ChadoIntStoragePropertyType($entity_type_id, self::$id, 'record_id', $record_id_term, [ + 'action' => 'store_id', + 'drupal_store' => TRUE, + ]) + ]; + } + + // Get the connecting information for the foreign key from the + // base table to the contact table. + $chado = \Drupal::service('tripal_chado.database'); + $schema = $chado->schema(); + $base_table_def = $schema->getTableDef($base_table, ['format' => 'Drupal']); + $contact_table_def = $schema->getTableDef('contact', ['format' => 'Drupal']); + $base_pkey_col = $base_table_def['primary key']; + // Note that for the arraydesign table, this will be manufacturer_id instead of contact_id + $base_fkey_col = array_key_first($base_table_def['foreign keys']['contact']['columns']); + + // Create variables to store the terms for the contact. + $storage = \Drupal::entityTypeManager()->getStorage('chado_term_mapping'); + $mapping = $storage->load('core_mapping'); + $record_id_term = 'SIO:000729'; + $contact_id_term = $mapping->getColumnTermId('contact', 'contact_id'); + $contact_name_term = $mapping->getColumnTermId('contact', 'name'); + $contact_name_length = $contact_table_def['fields']['name']['size']; + + // Define field properties, this is a simple lookup in the contact table + $properties = []; + $properties[] = new ChadoIntStoragePropertyType($entity_type_id, self::$id, 'record_id', $record_id_term, [ + 'action' => 'store_id', + 'drupal_store' => TRUE, + 'chado_table' => $base_table, + 'chado_column' => $base_pkey_col, + ]); + $properties[] = new ChadoIntStoragePropertyType($entity_type_id, self::$id, 'contact_id', $contact_id_term, [ + 'action' => 'store', + 'chado_table' => $base_table, + 'chado_column' => $base_fkey_col, + ]); + $properties[] = new ChadoVarCharStoragePropertyType($entity_type_id, self::$id, 'contact_name', $contact_name_term, $contact_name_length, [ + 'action' => 'join', + 'path' => $base_table . '.' . $base_fkey_col . '>contact.contact_id', + 'chado_column' => 'name', + 'as' => 'contact_name', + ]); + + return $properties; + } + + /** + * Form element validation handler + * + * @param array $form + * The form where the settings form is being included in. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state of the (entire) configuration form. + */ + public static function storageSettingsFormValidate(array $form, FormStateInterface $form_state) { + $settings = $form_state->getValue('settings'); + if (!array_key_exists('storage_plugin_settings', $settings)) { + return; + } + + // Validation confirms that the base table has a foreign key + // to contact_id in the chado.contact table. + // This validation might be redundant, since we only present + // valid base tables to the user, but let's play it safe. + $base_table = $settings['storage_plugin_settings']['base_table']; + $chado = \Drupal::service('tripal_chado.database'); + $schema = $chado->schema(); + $base_table_def = $schema->getTableDef($base_table, ['format' => 'Drupal']); + $base_fkey_col = 'contact_id'; + $fkeys = $base_table_def['foreign keys']['contact']['columns'] ?? []; + if (!in_array($base_fkey_col, $fkeys)) { + $form_state->setErrorByName('storage_plugin_settings][base_table', + 'The selected base table does not have a foreign key to contact_id,' + . ' this field cannot be used on this content type.'); + } + } + +} diff --git a/tripal_chado/src/Plugin/Field/FieldWidget/ChadoAdditionalTypeWidgetDefault.php b/tripal_chado/src/Plugin/Field/FieldWidget/ChadoAdditionalTypeWidgetDefault.php index 9220992c0..a3b017a57 100644 --- a/tripal_chado/src/Plugin/Field/FieldWidget/ChadoAdditionalTypeWidgetDefault.php +++ b/tripal_chado/src/Plugin/Field/FieldWidget/ChadoAdditionalTypeWidgetDefault.php @@ -103,6 +103,18 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#type' => 'value', '#default_value' => $fixed_value, ]; + $elements['term_name'] = [ + '#type' => 'value', + '#default_value' => '', + ]; + $elements['id_space'] = [ + '#type' => 'value', + '#default_value' => '', + ]; + $elements['accession'] = [ + '#type' => 'value', + '#default_value' => '', + ]; // Use the element defaults. They contain the required value, title, etc. $elements['term_autoc'] = $element + [ @@ -131,14 +143,17 @@ public function massageFormValues(array $values, array $form, FormStateInterface $termIdSpace = $matches[2]; $termAccession = $matches[3]; + /** @var \Drupal\tripal\TripalVocabTerms\TripalTerm $term **/ $idSpace = $idSpace_manager->loadCollection($termIdSpace); $term = $idSpace->getTerm($termAccession); $cvterm_id = $term->getInternalId(); $values[$delta]['type_id'] = $cvterm_id; $values[$delta]['value'] = $term->getName(); + $values[$delta]['term_name'] = $term->getName(); + $values[$delta]['id_space'] = $term->getIdSpace(); + $values[$delta]['accession'] = $term->getAccession(); } } - return $values; } } diff --git a/tripal_chado/src/Plugin/Field/FieldWidget/ChadoContactWidgetDefault.php b/tripal_chado/src/Plugin/Field/FieldWidget/ChadoContactWidgetDefault.php new file mode 100644 index 000000000..8d9b96b30 --- /dev/null +++ b/tripal_chado/src/Plugin/Field/FieldWidget/ChadoContactWidgetDefault.php @@ -0,0 +1,71 @@ +query($sql, []); + + while ($contact = $results->fetchObject()) { + // Change the non-user-friendly 'null' contact, which is specified by chado. + if ($contact->name == 'null') { + $contact->name = '-- Unknown --'; // This will sort to the top. + } + $type_text = $contact->type ? ' (' . $contact->type . ')' : ''; + $contacts[$contact->contact_id] = $contact->name . $type_text; + } + natcasesort($contacts); + + $item_vals = $items[$delta]->getValue(); + $record_id = $item_vals['record_id'] ?? 0; + $contact_id = $item_vals['contact_id'] ?? 0; + + $elements = []; + $elements['record_id'] = [ + '#type' => 'value', + '#default_value' => $record_id, + ]; + $elements['contact_id'] = $element + [ + '#type' => 'select', + '#options' => $contacts, + '#default_value' => $contact_id, + '#placeholder' => $this->getSetting('placeholder'), + '#empty_option' => '-- Select --', + ]; + + return $elements; + } + +} diff --git a/tripal_chado/src/Plugin/TripalIdSpace/ChadoIdSpace.php b/tripal_chado/src/Plugin/TripalIdSpace/ChadoIdSpace.php index 212ee86a2..b9958aa1a 100644 --- a/tripal_chado/src/Plugin/TripalIdSpace/ChadoIdSpace.php +++ b/tripal_chado/src/Plugin/TripalIdSpace/ChadoIdSpace.php @@ -984,7 +984,7 @@ public function setDescription($description) { } /** - * Retrieves from the Drupal cache the default vocabulary for this space.. + * Retrieves from the Drupal cache the default vocabulary for this space. * * @return string * The deffault vocabulary name. diff --git a/tripal_chado/src/Plugin/TripalImporter/FASTAImporter.php b/tripal_chado/src/Plugin/TripalImporter/FASTAImporter.php index 196bb7100..d3188bfa8 100644 --- a/tripal_chado/src/Plugin/TripalImporter/FASTAImporter.php +++ b/tripal_chado/src/Plugin/TripalImporter/FASTAImporter.php @@ -16,84 +16,18 @@ * label = @Translation("Chado FASTA File Loader"), * description = @Translation("Import a FASTA file into Chado"), * file_types = {"fasta","txt","fa","aa","pep","nuc","faa","fna"}, - * upload_description = @Translation("Please provide the FASTA file."), + * upload_description = @Translation("Please provide a plain text file following the FASTA format specification."), * upload_title = @Translation("FASTA File"), * use_analysis = True, * require_analysis = True, * button_text = @Translation("Import FASTA file"), * file_upload = True, - * file_load = False, - * file_remote = False, - * file_required = False, - * cardinality = 1, - * menu_path = "", - * callback = "", - * callback_module = "", - * callback_path = "", + * file_remote = True, + * file_local = True, + * file_required = True, * ) */ class FASTAImporter extends ChadoImporterBase { - /** - * The name of this loader. This name will be presented to the site - * user. - */ - public static $name = 'Chado FASTA Loader'; - - /** - * The machine name for this loader. This name will be used to construct - * the URL for the loader. - */ - public static $machine_name = 'chado_fasta_loader'; - - /** - * A brief description for this loader. This description will be - * presented to the site user. - */ - public static $description = 'Load sequences from a multi-FASTA file into Chado'; - - /** - * An array containing the extensions of allowed file types. - */ - public static $file_types = [ - 'fasta', - 'txt', - 'fa', - 'aa', - 'pep', - 'nuc', - 'faa', - 'fna', - ]; - - - /** - * Provides information to the user about the file upload. Typically this - * may include a description of the file types allowed. - */ - public static $upload_description = 'Please provide the FASTA file. The file must have a .fasta extension.'; - - /** - * The title that should appear above the file upload section. - */ - public static $upload_title = 'FASTA Upload'; - - /** - * Text that should appear on the button at the bottom of the importer - * form. - */ - public static $button_text = 'Import FASTA file'; - - /** - * Indicates the methods that the file uploader will support. - */ - public static $methods = [ - // Allow the user to upload a file to the server. - 'file_upload' => TRUE, - // Allow the user to provide the path on the Tripal server for the file. - 'file_local' => TRUE, - // Allow the user to provide a remote URL for the file. - 'file_remote' => TRUE, - ]; /** * @see TripalImporter::form() @@ -144,7 +78,7 @@ public function form($form, &$form_state) { exists with the same name or unique name and type then it is skipped. Select "Update only" to only update featues that already exist in the database. Select "Insert and Update" to insert features that do - not exist and upate those that do.'), + not exist and update those that do.'), '#default_value' => 2, ]; @@ -162,9 +96,9 @@ public function form($form, &$form_state) { a human-readable name then select the "Name" button. If your features are uniquely identified using the unique name then select the "Unique name" button. If you loaded your features first using the GFF loader then the unique name of each - features were indicated by the "ID=" attribute and the name by the "Name=" attribute. + feature was indicated by the "ID=" attribute and the name by the "Name=" attribute. By default, the FASTA loader will use the first word (character string - before the first space) as the name for your feature. If + before the first space) as the name for your feature. If this does not uniquely identify your feature consider specifying a regular expression in the advanced section below. Additionally, you may import both a name and a unique name for each sequence using the advanced options.'), '#default_value' => 1, @@ -311,6 +245,8 @@ public function formValidate($form, &$form_state) { $organism_id = $form_state_values['organism_id']; $file_upload = $form_state_values['file_upload']; $file_upload_existing = $form_state_values['file_upload_existing'] ?? null; + $file_local = $form_state_values['file_local'] ?? null; + $file_remote = $form_state_values['file_remote'] ?? null; $type = trim($form_state_values['seqtype']); $method = trim($form_state_values['method']); $match_type = trim($form_state_values['match_type']); @@ -340,89 +276,59 @@ public function formValidate($form, &$form_state) { $match_type = 'Unique name'; } - $file_existing = false; - if ($file_upload_existing == "0" || $file_upload_existing == "" || $file_upload_existing == null) { - // this means no existing file - } - else { - $file_existing = true; - } - - $file_uploaded = false; - // If there is no file upload - if($file_upload == "" || $file_upload == "0") { - - } - else { - $file_uploaded = true; - } - - if ($file_uploaded == false and $file_existing == false) { - \Drupal::messenger()->addError(t("You must upload a FASTA file or choose an existing FASTA file")); - } + // The parent class will validate that a file has been specified and is valid. if ($re_name and !$re_uname and strcmp($match_type, 'Unique name') == 0) { - // form_set_error('re_uname', t("You must provide a regular expression to identify the sequence unique name")); - \Drupal::messenger()->addError(t("You must provide a regular expression to identify the sequence unique name")); + $form_state->setErrorByName('re_name', t('You should not specify a regular expression for name if you are matching to sequence unique name')); } if (!$re_name and $re_uname and strcmp($match_type, 'Name') == 0) { - // form_set_error('re_name', t("You must provide a regular expression to identify the sequence name")); - \Drupal::messenger()->addError(t("You must provide a regular expression to identify the sequence name")); + $form_state->setErrorByName('re_uname', t('You should not specify a regular expression for unique name if you are matching to sequence name')); } // make sure if a relationship is specified that all fields are provided. - if (($rel_type or $re_subject) and !$parent_type) { - // form_set_error('parent_type', t("Please provide a SO term for the parent")); - \Drupal::messenger()->addError(t("Please provide a SO term for the parent")); + $form_state->setErrorByName('parent_type', t('Please provide a Sequence Ontology (SO) term for the parent')); } if (($parent_type or $re_subject) and !$rel_type) { - // form_set_error('rel_type', t("Please select a relationship type")); - \Drupal::messenger()->addError(t("Please select a relationship type")); + $form_state->setErrorByName('rel_type', t('Please select a relationship type')); } // make sure if a database is specified that all fields are provided if ($db_id and !$re_accession) { - // form_set_error('re_accession', t("Please provide a regular expression for the accession")); - \Drupal::messenger()->addError(t("Please provide a regular expression for the accession")); + $form_state->setErrorByName('re_accession', t('Please provide a regular expression for the accession')); } if ($re_accession and !$db_id) { - // form_set_error('db_id', t("Please select a database")); - \Drupal::messenger()->addError(t("Please select a database")); + $form_state->setErrorByName('db_id', t('Please select a database')); } // Check to make sure the regexps are valid. if ($re_name && @preg_match("/$re_name/", null) === false) { - // form_set_error('re_name', t("please provide a valid regular expression for the feature name.")); - \Drupal::messenger()->addError(t("please provide a valid regular expression for the feature name.")); + $form_state->setErrorByName('re_name', t('Please provide a valid regular expression for the feature name')); } if ($re_uname && @preg_match("/$re_uname/", null) === false) { - // form_set_error('re_uname', t("please provide a valid regular expression for the feature unique name.")); - \Drupal::messenger()->addError(t("please provide a valid regular expression for the feature unique name.")); + $form_state->setErrorByName('re_uname', t('Please provide a valid regular expression for the feature unique name')); } if ($re_accession && @preg_match("/$re_accession/", null) === false) { - // form_set_error('re_accession', t("please provide a valid regular expression for the external database accession.")); - \Drupal::messenger()->addError(t("please provide a valid regular expression for the external database accession.")); + $form_state->setErrorByName('re_accession', t('Please provide a valid regular expression for the external database accession')); } if ($re_subject && @preg_match("/$re_subject/", null) === false) { - // form_set_error('re_subject', t("please provide a valid regular expression for the relationship parent.")); - \Drupal::messenger()->addError(t("please provide a valid regular expression for the relationship parent.")); + $form_state->setErrorByName('re_subject', t('Please provide a valid regular expression for the relationship parent')); } // check to make sure the types exists $cv_autocomplete = new ChadoCVTermAutocompleteController(); $cvterm_id = $cv_autocomplete->getCVtermId($type, 'sequence'); if (!$cvterm_id) { - // form_set_error('type', t("The Sequence Ontology (SO) term selected for the sequence type is not available in the database. Please check spelling or select another.")); - \Drupal::messenger()->addError(t("The Sequence Ontology (SO) term selected for the sequence type is not available in the database. Please check spelling or select another.")); + $form_state->setErrorByName('type', t('The Sequence Ontology (SO) term selected for the sequence' + . ' type is not available in the database. Please check the spelling or select another.')); } if ($rel_type) { $cvterm_id = $cv_autocomplete->getCVtermId($parent_type, 'sequence'); if (!$cvterm_id) { - // form_set_error('parent_type', t("The Sequence Ontology (SO) term selected for the parent relationship is not available in the database. Please check spelling or select another.")); - \Drupal::messenger()->addError(t("The Sequence Ontology (SO) term selected for the parent relationship is not available in the database. Please check spelling or select another.")); + $form_state->setErrorByName('parent_type', t('The Sequence Ontology (SO) term selected for the parent' + . ' relationship is not available in the database. Please check the spelling or select another.')); } } } @@ -516,9 +422,9 @@ private function loadFasta($file_path, $organism_id, $type, $re_name, $re_uname, $cv_autocomplete = new ChadoCVTermAutocompleteController(); $cvterm_id = $cv_autocomplete->getCVtermId($type, 'sequence'); if (!$cvterm_id) { - // $this->logMessage("Cannot find the term type: '!type'", ['!type' => $type], TRIPAL_ERROR); - $this->logger->error("Cannot find the term type: ':type'", - [':type' => $type]); + $this->logger->error("Cannot find the term type: '@type'", + ['@type' => $type] + ); return 0; } $cvtermsql = " @@ -532,8 +438,9 @@ private function loadFasta($file_path, $organism_id, $type, $re_name, $re_uname, ':cvterm_id' => $cvterm_id, ])->fetchObject(); if (!$cvterm) { - $this->logger->error("Cannot find the term type: ':type'", - [':type' => $type]); + $this->logger->error("Cannot find the term type: '@type'", + ['@type' => $type] + ); return 0; } @@ -542,17 +449,17 @@ private function loadFasta($file_path, $organism_id, $type, $re_name, $re_uname, if (isset($parent_type) && $parent_type != "") { $parentcvterm_id = $cv_autocomplete->getCVtermId($parent_type, 'sequence'); if (!$parentcvterm_id) { - $this->logger->error("Cannot find the parent term type: ':parent_type'", - [':parent_type' => $parent_type]); + $this->logger->error("Cannot find the parent term type: '@parent_type'", + ['@parent_type' => $parent_type] + ); } $parentcvterm = $chado->query($cvtermsql, [ ':cvterm_id' => $parentcvterm_id, ])->fetchObject(); if (!isset($parentcvterm)) { - // $this->logMessage("Cannot find the parent term type: '!type'", - // ['!type' => $parentcvterm], TRIPAL_ERROR); - $this->logger->error("Cannot find the parent term type: ':parent_type'", - [':parent_type' => $parent_type]); + $this->logger->error("Cannot find the parent term type: '@parent_type'", + ['@parent_type' => $parent_type] + ); return 0; } } @@ -572,10 +479,9 @@ private function loadFasta($file_path, $organism_id, $type, $re_name, $re_uname, ':name' => $rel_type, ])->fetchObject(); if (!$relcvterm) { - // $this->logMessage("Cannot find the relationship term type: '!type'", - // ['!type' => $relcvterm], TRIPAL_ERROR); - $this->logger->error("Cannot find the relationship term type: ':relcvterm'", - [':relcvterm' => $rel_type]); + $this->logger->error("Cannot find the relationship term type: '@type'", + ['@type' => $rel_type] + ); return 0; } } @@ -585,12 +491,13 @@ private function loadFasta($file_path, $organism_id, $type, $re_name, $re_uname, $feature_tbl = chado_get_schema('feature', $chado->getSchemaName()); $dbxref_tbl = chado_get_schema('dbxref', $chado->getSchemaName()); - // $this->logMessage(t("Step 1: Finding sequences...")); - $this->logger->notice("Step 1: Finding sequences..."); + $this->logger->notice("Step 1: Finding sequences..."); $filesize = filesize($file_path); $fh = fopen($file_path, 'r'); if (!$fh) { - throw new \Exception(t("Cannot open file: !dfile", ['!dfile' => $file_path])); + $this->logger->error("Cannot open file: @dfile", + ['@dfile' => $file_path] + ); } $num_read = 0; @@ -618,16 +525,16 @@ private function loadFasta($file_path, $organism_id, $type, $re_name, $re_uname, // if ($re_name) { // OLD T3 code that does not work as intended if (isset($re_name) && $re_name != "") { if (!preg_match("/$re_name/", $defline, $matches)) { - // $this->logMessage("Regular expression for the feature name finds nothing. Line !line.", - // ['!line' => $i], TRIPAL_ERROR); - $this->logger->error("Regular expression for the feature name finds nothing. Line $i."); + $this->logger->error("Regular expression for the feature name finds nothing. Line @line.", + ['@line' => $i] + ); } // OLD TRIPAL 3 CODE // elseif (strlen($matches[1]) > $feature_tbl['fields']['name']['length']) { - elseif (strlen($matches[1]) > $feature_tbl['fields']['name']['size']) { - // $this->logMessage("Regular expression retrieves a value too long for the feature name. Line !line.", - // ['!line' => $i], TRIPAL_WARNING); - $this->logger->warning("Regular expression retrieves a value too long for the feature name. Line $i."); + elseif (strlen($matches[1]) > $feature_tbl['fields']['name']['size']) { + $this->logger->warning("Regular expression retrieves a value too long for the feature name. Line @line.", + ['@line' => $i] + ); } else { $name = trim($matches[1]); @@ -640,18 +547,19 @@ private function loadFasta($file_path, $organism_id, $type, $re_name, $re_uname, if (preg_match("/^\s*(.*?)[\s\|].*$/", $defline, $matches)) { // OLD TRIPAL 3 CODE // if (strlen($matches[1]) > $feature_tbl['fields']['name']['length']) { - if (strlen($matches[1]) > $feature_tbl['fields']['name']['size']) { - // $this->logMessage("Regular expression retrieves a feature name too long for the feature name. Line !line.", - // ['!line' => $i], TRIPAL_WARNING); - $this->logger->warning("Regular expression retrieves a feature name too long for the feature name. Line $i."); + if (strlen($matches[1]) > $feature_tbl['fields']['name']['size']) { + $this->logger->warning("Regular expression retrieves a feature name too long for the feature name. Line @line.", + ['@line' => $i] + ); } else { $name = trim($matches[1]); } } else { - // $this->logMessage("Cannot find a feature name. Line !line.", ['!line' => $i], TRIPAL_WARNING); - $this->logger->warning("Cannot find a feature name. Line $i."); + $this->logger->warning("Cannot find a feature name. Line @line.", + ['@line' => $i] + ); } } @@ -659,9 +567,9 @@ private function loadFasta($file_path, $organism_id, $type, $re_name, $re_uname, $uname = ""; if ($re_uname) { if (!preg_match("/$re_uname/", $defline, $matches)) { - // $this->logMessage("Regular expression for the feature unique name finds nothing. Line !line.", - // ['!line' => $i], TRIPAL_ERROR); - $this->logger->error("Regular expression for the feature unique name finds nothing. Line $i."); + $this->logger->error("Regular expression for the feature unique name finds nothing. Line @line.", + ['@line' => $i] + ); } $uname = trim($matches[1]); } @@ -673,9 +581,9 @@ private function loadFasta($file_path, $organism_id, $type, $re_name, $re_uname, $uname = trim($matches[1]); } else { - // $this->logMessage("Cannot find a feature unique name. Line !line.", - // ['!line' => $i], TRIPAL_ERROR); - $this->logger->error("Cannot find a feature unique name. Line $i."); + $this->logger->error("Cannot find a feature unique name. Line @line.", + ['@line' => $i] + ); } } @@ -685,12 +593,11 @@ private function loadFasta($file_path, $organism_id, $type, $re_name, $re_uname, preg_match("/$re_accession/", $defline, $matches); // OLD TRIPAL 3 CODE // if (strlen($matches[1]) > $dbxref_tbl['fields']['accession']['length']) { - if (strlen($matches[1]) > $dbxref_tbl['fields']['accession']['size']) { - // tripal_report_error('trp-fasta', TRIPAL_WARNING, "WARNING: Regular expression retrieves an accession too long for the feature name. " . - // "Cannot add cross reference. Line %line.", [ - // '%line' => $i, - // ]); - $this->logger->error("WARNING: Regular expression retrieves an accession too long for the feature name. Cannot add cross reference. Line $i."); + if (strlen($matches[1]) > $dbxref_tbl['fields']['accession']['size']) { + $this->logger->warning("WARNING: Regular expression retrieves an accession too long for" + . " the feature name. Cannot add cross reference. Line @line.", + ['@line' => $i] + ); } else { $accession = trim($matches[1]); @@ -730,15 +637,12 @@ private function loadFasta($file_path, $organism_id, $type, $re_name, $re_uname, $seqs[$num_seqs - 1]['seq_end'] = $num_read - strlen($line); // Now that we know where the sequences are in the file we need to add them. - // $this->logMessage("Step 2: Importing sequences..."); $this->logger->notice("Step 2: Importing sequences..."); - // $this->logMessage("Found !num_seqs sequence(s).", ['!num_seqs' => $num_seqs]); - $this->logger->notice("Found $num_seqs sequence(s)."); + $this->logger->notice("Found @num_seqs sequence(s).", ['@num_seqs' => $num_seqs]); $this->setTotalItems($num_seqs); $this->setItemsHandled(0); for ($j = 0; $j < $num_seqs; $j++) { $seq = $seqs[$j]; - //$this->logMessage("Importing !seqname.", array('!seqname' => $seq['name'])); $source = NULL; $this->loadFastaFeature($fh, $seq['name'], $seq['uname'], $db_id, $seq['accession'], $seq['subject'], $rel_type, $parent_type, @@ -763,14 +667,6 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren $chado = $this->getChadoConnection(); // Check to see if this feature already exists if the match_type is 'Name'. if (strcmp($match_type, 'Name') == 0) { - // $values = [ - // 'organism_id' => $organism_id, - // 'name' => $name, - // 'type_id' => $cvterm->cvterm_id, - // ]; - // $results = chado_select_record('feature', [ - // 'feature_id', - // ], $values); $results_query = $chado->select('1:feature', 'feature') ->fields('feature') ->condition('organism_id', $organism_id) @@ -779,26 +675,18 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren $results = $results_query->execute(); $results_count = $results_query->countQuery()->execute()->fetchField(); if ($results_count > 1) { - // $this->logMessage("Multiple features exist with the name '!name' of type '!type' for the organism. skipping", - // ['!name' => $name, '!type' => $cvterm->name], TRIPAL_ERROR); - $this->logger->error("Multiple features exist with the name '$name' of type '" . - $cvterm->name . "' for the organism. skipping"); + $this->logger->error("Multiple features exist with the name '@name' of type '@type' for the organism. skipping", + ['@name' => $name, '@type' => $cvterm->name] + ); return 0; } if ($results_count == 1) { - // $feature = $results[0]; $feature = $results->fetchObject(); } } // Check if this feature already exists if the match_type is 'Unique Name'. if (strcmp($match_type, 'Unique name') == 0) { - // $values = [ - // 'organism_id' => $organism_id, - // 'uniquename' => $uname, - // 'type_id' => $cvterm->cvterm_id, - // ]; - // $results = chado_select_record('feature', ['feature_id'], $values); $results_query = $chado->select('1:feature', 'feature') ->fields('feature') ->condition('organism_id', $organism_id) @@ -807,27 +695,20 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren $results = $results_query->execute(); $results_count = $results_query->countQuery()->execute()->fetchField(); if ($results_count > 1) { - // $this->logMessage("Multiple features exist with the name '!name' of type '!type' for the organism. skipping", - // ['!name' => $name, '!type' => $cvterm->name], TRIPAL_WARNING); - $this->logger->error("Multiple features exist with the name '$name' of type '" . - $cvterm->name . "' for the organism. skipping"); + $this->logger->error("Multiple features exist with the name '@name' of type '@type' for the organism. skipping", + ['@name' => $name, '@type' => $cvterm->name] + ); return 0; } if ($results_count == 1) { - // $feature = $results[0]; $feature = $results->fetchObject(); } // If the feature exists but this is an "insert only" then skip. if (isset($feature) and (strcmp($method, 'Insert only') == 0)) { - // $this->logMessage("Feature already exists '!name' ('!uname') while matching on !type. Skipping insert.", - // [ - // '!name' => $name, - // '!uname' => $uname, - // '!type' => drupal_strtolower($match_type), - // ], TRIPAL_WARNING); - $this->logger->error("Feature already exists '$name' ('$uname') while matching on " . - strtolower($match_type) . ". Skipping insert."); + $this->logger->error("Feature already exists '@name' ('@uname') while matching on @type. Skipping insert.", + ['@name' => $name, '@uname' => $uname, '@type' => strtolower($match_type)] + ); return 0; } } @@ -850,23 +731,15 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren 'uniquename' => $uname, 'type_id' => $cvterm->cvterm_id, ]; - // $success = chado_insert_record('feature', $values); $success = $chado->insert('1:feature')->fields($values)->execute(); if (!$success) { - $this->logMessage("Failed to insert feature '!name (!uname)'", [ - '!name' => $name, - '!uname' => $uname, - ], TRIPAL_ERROR); + $this->logger->error("Failed to insert feature '@name (@uname)'", + ['@name' => $name, '@uname' => $uname] + ); return 0; } // now get the feature we just inserted - // $values = [ - // 'organism_id' => $organism_id, - // 'uniquename' => $uname, - // 'type_id' => $cvterm->cvterm_id, - // ]; - // $results = chado_select_record('feature', ['feature_id'], $values); $results_query = $chado->select('1:feature', 'feature') ->fields('feature') ->condition('organism_id', $organism_id) @@ -876,15 +749,12 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren $results_count = $results_query->countQuery()->execute()->fetchField(); if ($results_count == 1) { $inserted = 1; - // $feature = $results[0]; $feature = $results->fetchObject(); } else { - // $this->logMessage("Failed to retrieve newly inserted feature '!name (!uname)'", [ - // '!name' => $name, - // '!uname' => $uname, - // ], TRIPAL_ERRORR); - $this->logger->error("Failed to retrieve newly inserted feature '$name ($uname)'"); + $this->logger->error("Failed to retrieve newly inserted feature '@name (@uname)'", + ['@name' => $name, '@uname' => $uname] + ); return 0; } @@ -894,9 +764,9 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren // if we don't have a feature and the user wants to do an update then fail if (!isset($feature) and (strcmp($method, 'Update only') == 0 or strcmp($method, 'Insert and update') == 0)) { - // $this->logMessage("Failed to find feature '!name' ('!uname') while matching on " . drupal_strtolower($match_type) . ".", - // ['!name' => $name, '!uname' => $uname], TRIPAL_ERROR); - $this->logger->error("Failed to find feature '$name' ('$uname') while matching on " . strtolower($match_type) . "."); + $this->logger->error("Failed to find feature '@name' ('@uname') while matching on @match_type.", + ['@name' => $name, '@uname' => $uname, '@match_type' => strtolower($match_type)] + ); return 0; } @@ -914,12 +784,6 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren // First check to make sure that by changing the unique name of this // feature that we won't conflict with another existing feature of // the same name - // $values = [ - // 'organism_id' => $organism_id, - // 'uniquename' => $uname, - // 'type_id' => $cvterm->cvterm_id, - // ]; - // $results = chado_select_record('feature', ['feature_id'], $values); $results_query = $chado->select('1:feature', 'feature') ->fields('feature') ->condition('organism_id', $organism_id) @@ -928,16 +792,10 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren $results = $results_query->execute(); $results_count = $results_query->countQuery()->execute()->fetchField(); if ($results_count > 0) { - // $this->logMessage("Cannot update the feature '!name' with a uniquename of '!uname' and type of '!type' as it " . - // "conflicts with an existing feature with the same uniquename and type.", - // [ - // '!name' => $name, - // '!uname' => $uname, - // '!type' => $cvterm->name, - // ], TRIPAL_ERROR); - $this->logger->error("Cannot update the feature '$name' with a uniquename of '$uname' and type of '" . - $cvterm->name . "' as it " . - "conflicts with an existing feature with the same uniquename and type."); + $this->logger->error("Cannot update the feature '@name' with a uniquename of '@uname' and type of '@type'" + . " as it conflicts with an existing feature with the same uniquename and type.", + ['@name' => $name, '@uname' => $uname, '@type' => $cvterm->name] + ); return 0; } @@ -952,9 +810,9 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren // perform the update $success = chado_update_record('feature', $match, $values); if (!$success) { - // $this->logMessage("Failed to update feature '!name' ('!name')", - // ['!name' => $name, '!uiname' => $uname], TRIPAL_ERROR); - $this->logger->error("Failed to update feature '$name' ('$name')"); + $this->logger->error("Failed to update feature '@name' ('@uname')", + ['@name' => $name, '@uname' => $uname] + ); return 0; } } @@ -974,9 +832,9 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren ]; $success = chado_update_record('feature', $match, $values); if (!$success) { - // $this->logMessage("Failed to update feature '!name' ('!name')", - // ['!name' => $name, '!uiname' => $uname], TRIPAL_ERROR); - $this->logger->error("Failed to update feature '$name' ('$name')"); + $this->logger->error("Failed to update feature '@name' ('@uname')", + ['@name' => $name, '@uname' => $uname] + ); return 0; } } @@ -989,11 +847,6 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren // add in the analysis link if ($analysis_id) { // if the association doesn't already exist then add one - // $values = [ - // 'analysis_id' => $analysis_id, - // 'feature_id' => $feature->feature_id, - // ]; - // $results = chado_select_record('analysisfeature', ['analysisfeature_id'], $values); $results_query = $chado->select('1:analysisfeature', 'analysisfeature') ->fields('analysisfeature') ->condition('analysis_id', $analysis_id) @@ -1001,16 +854,15 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren $results = $results_query->execute(); $results_count = $results_query->countQuery()->execute()->fetchField(); if ($results_count == 0) { - // $success = chado_insert_record('analysisfeature', $values); $values = [ 'analysis_id' => $analysis_id, 'feature_id' => $feature->feature_id, ]; $success = $chado->insert('1:analysisfeature')->fields($values)->execute(); if (!$success) { - // $this->logMessage("Failed to associate analysis and feature '!name' ('!name')", - // ['!name' => $name, '!uname' => $uname], TRIPAL_ERROR); - $this->logger->error("Failed to associate analysis and feature '$name' ('$name')"); + $this->logger->error("Failed to associate analysis and feature '@name' ('@name')", + ['@name' => $name, '@uname' => $uname] + ); return 0; } } @@ -1018,12 +870,6 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren // now add the database cross reference if ($db_id) { - // check to see if this accession reference exists, if not add it - // $values = [ - // 'db_id' => $db_id, - // 'accession' => $accession, - // ]; - // $results = chado_select_record('dbxref', ['dbxref_id'], $values); $results_query = $chado->select('1:dbxref', 'dbxref') ->fields('dbxref') ->condition('db_id', $db_id) @@ -1032,33 +878,31 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren $results_count = $results_query->countQuery()->execute()->fetchField(); // if the accession doesn't exist then add it if ($results_count == 0) { - // $results = chado_insert_record('dbxref', $values); $values = [ 'db_id' => $db_id, 'accession' => $accession, ]; $success = $chado->insert('1:dbxref')->fields($values)->execute(); if (!$results) { - // $this->logMessage("Failed to add database accession '!accession'", - // ['!accession' => $accession], TRIPAL_ERROR); - $this->logger->error("Failed to add database accession '$accession'"); + $this->logger->error("Failed to add database accession '@accession'", + ['@accession' => $accession] + ); return 0; } - // $results = chado_select_record('dbxref', ['dbxref_id'], $values); + $results_query = $chado->select('1:dbxref', 'dbxref') - ->fields('dbxref') - ->condition('db_id', $db_id) - ->condition('accession', $accession); + ->fields('dbxref') + ->condition('db_id', $db_id) + ->condition('accession', $accession); $results = $results_query->execute(); $results_count = $results_query->countQuery()->execute()->fetchField(); if ($results_count == 1) { - // $dbxref = $results[0]; $dbxref = $results->fetchObject(); } else { - // $this->logMessage("Failed to retrieve newly inserted dbxref '!name (!uname)'", - // ['!name' => $name, '!uname' => $uname], TRIPAL_ERROR); - $this->logger->error("Failed to retrieve newly inserted dbxref '$name ($uname)'"); + $this->logger->error("Failed to retrieve newly inserted dbxref '@name (@uname)'", + ['@name' => $name, '@uname' => $uname] + ); return 0; } } @@ -1066,12 +910,6 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren $dbxref = $results[0]; } - // Check to see if the feature dbxref record exists. If not, then add it. - // $values = [ - // 'feature_id' => $feature->feature_id, - // 'dbxref_id' => $dbxref->dbxref_id, - // ]; - // $results = chado_select_record('feature_dbxref', ['feature_dbxref_id'], $values); $results_query = $chado->select('1:feature_dbxref', 'feature_dbxref') ->fields('feature_dbxref') ->condition('feature_id', $feature->feature_id) @@ -1079,16 +917,15 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren $results = $results_query->execute(); $results_count = $results_query->countQuery()->execute()->fetchField(); if ($results_count == 0) { - // $success = chado_insert_record('feature_dbxref', $values); $values = [ 'feature_id' => $feature->feature_id, 'dbxref_id' => $dbxref->dbxref_id, ]; $success = $chado->insert('1:feature_dbxref')->fields($values)->execute(); if (!$success) { - // $this->logMessage("Failed to add associate database accession '!accession' with feature", - // ['!accession' => $accession], TRIPAL_ERROR); - $this->logger->error("Failed to add associate database accession '$accession' with feature"); + $this->logger->error("Failed to associate database accession '@accession' with feature", + ['@accession' => $accession] + ); return 0; } } @@ -1096,12 +933,6 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren // Now add in the relationship if one exists. if ($rel_type) { - // $values = [ - // 'organism_id' => $organism_id, - // 'uniquename' => $parent, - // 'type_id' => $parentcvterm->cvterm_id, - // ]; - // $results = chado_select_record('feature', ['feature_id'], $values); $results_query = $chado->select('1:feature', 'feature') ->fields('feature') ->condition('organism_id', $organism_id) @@ -1110,21 +941,14 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren $results = $results_query->execute(); $results_count = $results_query->countQuery()->execute()->fetchField(); if ($results_count != 1) { - // $this->logMessage("Cannot find a unique feature for the parent '!parent' of type '!type' for the feature.", - // ['!parent' => $parent, '!type' => $parent_type], TRIPAL_ERROR); - $this->logger->error("Cannot find a unique feature for the parent '$parent' of type '$parent_type' for the feature."); + $this->logger->error("Cannot find a unique feature for the parent '@parent' of type '@type' for the feature.", + ['@parent' => $parent, '@type' => $parent_type] + ); return 0; } - // $parent_feature = $results[0]; $parent_feature = $results->fetchObject(); // Check to see if the relationship already exists. If not, then add it. - // $values = [ - // 'subject_id' => $feature->feature_id, - // 'object_id' => $parent_feature->feature_id, - // 'type_id' => $relcvterm->cvterm_id, - // ]; - // $results = chado_select_record('feature_relationship', ['feature_relationship_id'], $values); $results_query = $chado->select('1:feature_relationship', 'feature_relationship') ->fields('feature_relationship') ->condition('subject_id', $feature->feature_id) @@ -1133,17 +957,16 @@ private function loadFastaFeature($fh, $name, $uname, $db_id, $accession, $paren $results = $results_query->execute(); $results_count = $results_query->countQuery()->execute()->fetchField(); if ($results_count == 0) { - // $success = chado_insert_record('feature_relationship', $values); $values = [ 'subject_id' => $feature->feature_id, 'object_id' => $parent_feature->feature_id, 'type_id' => $relcvterm->cvterm_id, - ]; - $success = $chado->insert('1:feature_relationship')->fields($values)->execute(); + ]; + $success = $chado->insert('1:feature_relationship')->fields($values)->execute(); if (!$success) { - // $this->logMessage("Failed to add associate database accession '!accession' with feature", - // ['!accession' => $accession], TRIPAL_ERROR); - $this->logger->error("Failed to add associate database accession '$accession' with feature"); + $this->logger->error("Failed to associate database accession '@accession' with feature", + ['@accession' => $accession] + ); return 0; } } diff --git a/tripal_chado/src/Plugin/TripalImporter/GFF3Importer.php b/tripal_chado/src/Plugin/TripalImporter/GFF3Importer.php index d0ed730e0..990940733 100644 --- a/tripal_chado/src/Plugin/TripalImporter/GFF3Importer.php +++ b/tripal_chado/src/Plugin/TripalImporter/GFF3Importer.php @@ -16,63 +16,19 @@ * id = "chado_gff3_loader", * label = @Translation("Chado GFF3 File Loader"), * description = @Translation("Import a GFF3 file into Chado"), - * file_types = {"gff","gff3"}, - * upload_description = @Translation("Please provide the GFF3 file."), + * file_types = {"gff","gff3", "txt"}, + * upload_description = @Translation("Please provide a plain text, tab-delimited file following the GFF3 Specification. It is expected that all landmark features are associated with the same organism and that the type (column 3) are sequence ontology terms."), * upload_title = @Translation("GFF3 File"), * use_analysis = True, * require_analysis = True, * button_text = @Translation("Import GFF3 file"), * file_upload = True, - * file_load = False, - * file_remote = False, - * file_required = False, - * cardinality = 1, - * menu_path = "", - * callback = "", - * callback_module = "", - * callback_path = "", + * file_remote = True, + * file_local = True, + * file_required = True, * ) */ class GFF3Importer extends ChadoImporterBase { - /** - * The name of this loader. This name will be presented to the site - * user. - */ - public static $name = 'Chado GFF3 File Loader'; - - /** - * The machine name for this loader. This name will be used to construct - * the URL for the loader. - */ - public static $machine_name = 'chado_gff3_loader'; - - /** - * A brief description for this loader. This description will be - * presented to the site user. - */ - public static $description = 'Import a GFF3 file into Chado'; - - /** - * An array containing the extensions of allowed file types. - */ - public static $file_types = ['gff', 'gff3']; - - /** - * Provides information to the user about the file upload. Typically this - * may include a description of the file types allowed. - */ - public static $upload_description = 'Please provide the GFF3 file.'; - - /** - * The title that should appear above the upload button. - */ - public static $upload_title = 'GFF3 File'; - - /** - * Text that should appear on the button at the bottom of the importer - * form. - */ - public static $button_text = 'Import GFF3 file'; /** * A handle to a temporary file for caching the GFF features. This allows for @@ -135,13 +91,11 @@ class GFF3Importer extends ChadoImporterBase { */ private $update = TRUE; - - /** + /** * A list of features to have names updated. */ private $update_names = []; - /** * If the GFF file contains a 'Target' attribute then the feature and the * target will have an alignment created, but to find the proper target @@ -211,7 +165,6 @@ class GFF3Importer extends ChadoImporterBase { */ private $landmark_cvterm_id = NULL; - /** * Regular expression to pull out the mRNA name. */ @@ -523,39 +476,40 @@ public function formValidate($form, &$form_state) { $re_mrna = trim($form_state_values['re_mrna']); $re_protein = trim($form_state_values['re_protein']); + // The parent class will validate that a file has been specified and is valid. if ($line_number and !is_numeric($line_number) or $line_number < 0) { - \Drupal::messenger()->addError(t("Please provide an integer line number greater than zero.")); + $form_state->setErrorByName('line_number', t('Please provide an integer line number greater than zero')); } if (!($re_mrna and $re_protein) and ($re_mrna or $re_protein)) { - \Drupal::messenger()->addError(t("You must provide both a regular expression for mRNA and a replacement string for protein")); + $form_state->setErrorByName('re_mrna', t('You must provide both a regular expression for mRNA and a replacement string for protein')); } // check the regular expression to make sure it is valid set_error_handler(function () {}, E_WARNING); - $result_re = preg_match("/" . $re_mrna . "/", NULL); - $result = preg_replace("/" . $re_mrna . "/", $re_protein, NULL); + $result_re = preg_match("/" . $re_mrna . "/", ""); + $result = preg_replace("/" . $re_mrna . "/", $re_protein, ""); restore_error_handler(); if ($result_re === FALSE) { - \Drupal::messenger()->addError('Invalid regular expression.'); + $form_state->setErrorByName('re_mrna', t('Invalid regular expression')); } - else { - if ($result === FALSE) { - \Drupal::messenger()->addError('Invalid replacement string.'); - } + elseif ($result === FALSE) { + $form_state->setErrorByName('re_protein', t('Invalid replacement string')); } // check to make sure the types exists $cv_autocomplete = new ChadoCVTermAutocompleteController(); $landmark_cvterm_id = $cv_autocomplete->getCVtermId($landmark_type, 'sequence'); if (!$landmark_cvterm_id) { - \Drupal::messenger()->addError(t("The Sequence Ontology (SO) term selected for the landmark type is not available in the database. Please check spelling or select another.")); + $form_state->setErrorByName('landmark_type', t('The Sequence Ontology (SO) term selected for the landmark type is not' + . ' available in the database. Please check the spelling or select another')); } if ($target_type) { $target_type_id = $cv_autocomplete->getCVtermId($target_type, 'sequence'); if (!$target_type_id) { - \Drupal::messenger()->addError(t("The Sequence Ontology (SO) term selected for the target type is not available in the database. Please check spelling or select another.")); + $form_state->setErrorByName('target_type', t('The Sequence Ontology (SO) term selected for the target type is not' + . ' available in the database. Please check the spelling or select another')); } } @@ -701,7 +655,7 @@ public function run() { // Load the GFF3. try { - $this->logger->notice("Step 1 of 27: Caching GFF3 file... "); + $this->logger->notice("Step 1 of 27: Caching GFF3 file... "); $this->parseGFF3(); // Prep the database for necessary records. @@ -709,95 +663,99 @@ public function run() { $this->prepNullPub(); $this->prepDBs(); - $this->logger->notice("Step 2 of 27: Find existing landmarks... "); + $this->logger->notice("Step 2 of 27: Find existing landmarks... "); $this->findLandmarks(); - $this->logger->notice("Step 3 of 27: Insert new landmarks (if needed)... "); + $this->logger->notice("Step 3 of 27: Insert new landmarks (if needed)... "); $this->insertLandmarks(); if (!$this->skip_protein) { - $this->logger->notice("Step 4 of 27: Find missing proteins... "); + $this->logger->notice("Step 4 of 27: Find missing proteins... "); $this->findMissingProteins(); - $this->logger->notice("Step 5 of 27: Add missing proteins to list of features... "); + $this->logger->notice("Step 5 of 27: Add missing proteins to list of features... "); $this->addMissingProteins(); } else { - $this->logger->notice("Step 4 of 27: Find missing proteins (Skipped)... "); - $this->logger->notice("Step 5 of 27: Add missing proteins to list of features (Skipped)..."); + $this->logger->notice("Step 4 of 27: Find missing proteins (Skipped) "); + $this->logger->notice("Step 5 of 27: Add missing proteins to list of features (Skipped) "); } - $this->logger->notice("Step 6 of 27: Find existing features... "); + $this->logger->notice("Step 6 of 27: Find existing features... "); $this->findFeatures(); $this->logger->notice("Step 7 of 27: Clear attributes of existing features... "); $this->deleteFeatureData(); - $this->logger->notice("Step 8 of 27: Processing @num_features features... ", - ['@num_features' => number_format(count(array_keys($this->features)))]); + $this->logger->notice("Step 8 of 27: Processing @num_features features... ", + ['@num_features' => number_format(count(array_keys($this->features)))] + ); $this->insertFeatures(); $this->logger->notice("Step 9 of 27: Processing @num_features feature Names to update... ", - ['@num_features' => number_format(count(array_keys($this->update_names)))]); + ['@num_features' => number_format(count(array_keys($this->update_names)))] + ); $this->updateFeatureNames(); - $this->logger->notice("Step 10 of 27: Get new feature IDs... "); + $this->logger->notice("Step 10 of 27: Get new feature IDs... "); $this->findFeatures(); - $this->logger->notice("Step 11 of 27: Insert locations... "); + $this->logger->notice("Step 11 of 27: Insert locations... "); $this->insertFeatureLocs(); - $this->logger->notice("Step 12 of 27: Associate parents and children... "); + $this->logger->notice("Step 12 of 27: Associate parents and children... "); $this->associateChildren(); - $this->logger->notice("Step 13 of 27: Calculate child ranks... "); + $this->logger->notice("Step 13 of 27: Calculate child ranks... "); $this->calculateChildRanks(); - $this->logger->notice("Step 14 of 27: Add child-parent relationships... "); + $this->logger->notice("Step 14 of 27: Add child-parent relationships... "); $this->insertFeatureParents(); - $this->logger->notice("Step 15 of 27: Insert properties... "); + $this->logger->notice("Step 15 of 27: Insert properties... "); $this->insertFeatureProps(); - $this->logger->notice("Step 16 of 27: Find synonyms (aliases)... "); + $this->logger->notice("Step 16 of 27: Find synonyms (aliases)... "); $this->findSynonyms(); - $this->logger->notice("Step 17 of 27: Insert new synonyms (aliases)... "); + $this->logger->notice("Step 17 of 27: Insert new synonyms (aliases)... "); $this->insertSynonyms(); - $this->logger->notice("Step 18 of 27: Insert feature synonyms (aliases)... "); + $this->logger->notice("Step 18 of 27: Insert feature synonyms (aliases)... "); $this->insertFeatureSynonyms(); - $this->logger->notice("Step 19 of 27: Find cross references... "); + $this->logger->notice("Step 19 of 27: Find cross references... "); $this->findDbxrefs(); - $this->logger->notice("Step 20 of 27: Insert new cross references... "); + $this->logger->notice("Step 20 of 27: Insert new cross references... "); $this->insertDbxrefs(); - $this->logger->notice("Step 21 of 27: Get new cross references IDs... "); + $this->logger->notice("Step 21 of 27: Get new cross references IDs... "); $this->findDbxrefs(); - $this->logger->notice("Step 22 of 27: Insert feature cross references... "); + $this->logger->notice("Step 22 of 27: Insert feature cross references... "); $this->insertFeatureDbxrefs(); - $this->logger->notice("Step 23 of 27: Insert feature ontology terms... "); + $this->logger->notice("Step 23 of 27: Insert feature ontology terms... "); $this->insertFeatureCVterms(); - $this->logger->notice("Step 24 of 27: Insert 'derives_from' relationships... "); + $this->logger->notice("Step 24 of 27: Insert 'derives_from' relationships... "); $this->insertFeatureDerivesFrom(); - $this->logger->notice("Step 25 of 27: Insert Targets... "); + $this->logger->notice("Step 25 of 27: Insert Targets... "); $this->insertFeatureTargets(); - $this->logger->notice("Step 26 of 27: Associate features with analysis.... "); + $this->logger->notice("Step 26 of 27: Associate features with analysis... "); $this->insertFeatureAnalysis(); if (!empty($this->residue_index)) { - $this->logger->notice("Step 27 of 27: Adding sequences data... "); + $this->logger->notice("Step 27 of 27: Adding sequences data... "); $this->insertFeatureSeqs(); } - $this->logger->notice("Step 27 of 27: Adding sequences data (Skipped: none available)..."); + else { + $this->logger->notice("Step 27 of 27: Adding sequences data (Skipped: none available) "); + } } // On exception, catch the error, clean up the cache file and rethrow catch (\Exception $e) { @@ -900,9 +858,6 @@ private function prepSynonms() { 'name' => 'synonym_type', 'definition' => 'vocabulary for synonym types', ]; - // $success = chado_insert_record('cv', $values, array( - // 'skip_validation' => TRUE, - // ), $this->chado_schema_main); $success = $chado->insert('1:cv') ->fields($values) ->execute(); @@ -914,8 +869,8 @@ private function prepSynonms() { // now that we've added the cv we need to get the record // $results = chado_select_record('cv', ['*'], $select, NULL, $this->chado_schema_main); $results_query = $chado->select('1:cv', 'cv') - ->fields('cv') - ->condition('name', 'synonym_type'); + ->fields('cv') + ->condition('name', 'synonym_type'); $results = $results_query->execute()->fetchObject(); $results_count = $results_query->countQuery()->execute()->fetchField(); if ($results_count > 0) { @@ -933,7 +888,6 @@ private function prepSynonms() { 'name' => 'synonym_type', ], ]; - // $result = chado_select_record('cvterm', ['*'], $select, NULL, $this->chado_schema_main); $result_cv = $chado->select('1:cv', 'cv') ->fields('cv') ->condition('name', 'synonym_type') @@ -960,7 +914,6 @@ private function prepSynonms() { 'is_relationship' => FALSE, ]; $syntype = chado_insert_cvterm($term, ['update_existing' => TRUE], $this->chado_schema_main); - // $syntype = $this->insert_cvterm($term, ['update_existing' => TRUE]); if (!$syntype) { $this->logger->warning("Cannot add synonym type: synonym_type:exact"); return 0; @@ -980,7 +933,6 @@ private function prepNullPub(){ // Check to see if we have a NULL publication in the pub table. If not, // then add one. $select = ['uniquename' => 'null']; - // $result = chado_select_record('pub', ['*'], $select, NULL, $this->chado_schema_main); $result_query = $chado->select('1:pub', 'pub') ->fields('pub') ->condition('uniquename', 'null'); @@ -1001,21 +953,10 @@ private function prepNullPub(){ ':type_id' => 'null', ]); if (!$status) { - $this->logger->warning("Cannot prepare statement 'ins_pub_uniquename_typeid."); + $this->logger->error("Cannot add null publication needed for setup of alias."); return 0; } - // Insert the null pub. - // $result = $chado->query($pub_sql, [ - // ':uname' => 'null', - // ':type_id' => 'null', - // ])->fetchObject(); - // if (!$result) { - // $this->logger->warning("Cannot add null publication needed for setup of alias."); - // return 0; - // } - - // $result = chado_select_record('pub', ['*'], $select, NULL, $this->chado_schema_main); $result_query = $chado->select('1:pub','pub') ->fields('pub') ->condition('uniquename','null'); @@ -1089,7 +1030,9 @@ private function prepDBs() { $db = $db_query->execute()->fetchObject(); } else { - $this->logger->warning("Cannot find or add the database $dbname."); + $this->logger->warning('Cannot find or add the database "@dbname".', + ['@dbname' => $dbname] + ); return 0; } } @@ -1474,8 +1417,11 @@ private function insertFeatureSeqs() { // or landmark name. if (!(array_key_exists($uniquename, $this->features) and $this->features[$uniquename]) and !(array_key_exists($uniquename, $this->landmarks) and $this->landmarks[$uniquename])) { - $this->logger->warning('Assigning Sequence: cannot find a feature with a unique name of: "@uname". Please ensure the sequence names in the ##FASTA section use the same name as the ID in the feature in the GFF file. ', - ['@uname' => $uniquename]); + $this->logger->warning('Assigning Sequence: cannot find a feature with a unique name of: "@uname".' + . ' Please ensure the sequence names in the ##FASTA section use the same name' + . ' as the ID in the feature in the GFF file.', + ['@uname' => $uniquename] + ); $count++; continue; } @@ -1930,7 +1876,8 @@ private function openCacheFile() { $this->gff_cache_file_name = \Drupal::service('file_system')->realpath($temp_file); $this->logger->notice("Opening temporary cache file: @cfile", - ['@cfile' => $this->gff_cache_file_name]); + ['@cfile' => $this->gff_cache_file_name] + ); $this->gff_cache_file = fopen($this->gff_cache_file_name, "r+"); } @@ -1940,7 +1887,8 @@ private function openCacheFile() { private function closeCacheFile() { fclose($this->gff_cache_file); $this->logger->notice("Removing temporary cache file: @cfile", - ['@cfile' => $this->gff_cache_file_name]); + ['@cfile' => $this->gff_cache_file_name] + ); unlink($this->gff_cache_file_name); } @@ -2896,9 +2844,10 @@ private function insertFeatureDerivesFrom() { if (!$feature['skipped'] and $feature['derives_from']) { $object_id = $this->features[$feature['derives_from']]['feature_id']; if (!$object_id) { - $this->logger->warning("Skipping 'derives_from' relationship for feature @feature_name. " . - "Could not find the derives_from feature: @derives_from.", - ['@feature_name' => $feature['uniquename'], '@derives_from' => $feature['derives_from']]); + $this->logger->warning("Skipping 'derives_from' relationship for feature @feature_name. " + . "Could not find the derives_from feature: @derives_from.", + ['@feature_name' => $feature['uniquename'], '@derives_from' => $feature['derives_from']] + ); continue; } $sql .= "(:subject_id_$i, :object_id_$i, :type_id_$i, 0),\n"; diff --git a/tripal_chado/src/Plugin/TripalImporter/NewickImporter.php b/tripal_chado/src/Plugin/TripalImporter/NewickImporter.php new file mode 100644 index 000000000..2c28f47db --- /dev/null +++ b/tripal_chado/src/Plugin/TripalImporter/NewickImporter.php @@ -0,0 +1,286 @@ +getChadoConnection(); + // Always call the parent form to ensure Chado is handled properly. + $form = parent::form($form, $form_state); + + // Default values can come in the following ways: + // + // 1) as elements of the $node object. This occurs when editing an existing phylotree + // 2) in the $form_state['values'] array which occurs on a failed validation or + // ajax callbacks from non submit form elements + // 3) in the $form_state['input'] array which occurs on ajax callbacks from submit + // form elements and the form is being rebuilt + // + // set form field defaults + $phylotree = NULL; + $phylotree_id = NULL; + $tree_name = ''; + $leaf_type = ''; + $analysis_id = ''; + $dbxref = ''; + $comment = ''; + $tree_required = TRUE; + $tree_file = ''; + $name_re = ''; + $match = ''; + // $load_later = FALSE; // Default is to combine tree import with current job + + // get the sequence ontology CV ID + $cv_results = $chado->select('1:cv', 'cv') + ->fields('cv') + ->condition('name', 'sequence') + ->execute(); + $cv_id = $cv_results->fetchObject()->cv_id; + + $form_state_values = $form_state->getValues(); + $form_state_input = $form_state->getUserInput(); + // If we are re constructing the form from a failed validation or ajax callback + // then use the $form_state['values'] values. + if (isset($form_state_values['tree_name'])) { + $tree_name = $form_state_values['tree_name']; + $leaf_type = $form_state_values['leaf_type']; + $analysis_id = $form_state_values['analysis_id']; + $dbxref = $form_state_values['dbxref']; + $comment = $form_state_values['description']; + } + // If we are re building the form from after submission (from ajax call) then + // the values are in the $form_state['input'] array. + if (!empty($form_state_input)) { + $tree_name = $form_state_input['tree_name']; + $leaf_type = $form_state_input['leaf_type']; + $analysis_id = $form_state_input['analysis_id']; + $comment = $form_state_input['description']; + $dbxref = $form_state_input['dbxref']; + } + + $form['tree_name'] = [ + '#type' => 'textfield', + '#title' => t('Tree Name'), + '#required' => TRUE, + '#default_value' => $tree_name, + '#description' => t('Enter the name used to refer to this phylogenetic tree.'), + '#maxlength' => 255, + ]; + + $so_cv = chado_get_cv(['name' => 'sequence']); + $cv_id = $so_cv->cv_id; + if (!$so_cv) { + \Drupal::messenger()->addError(t("The Sequence Ontology does not appear to be imported. + Please import the Sequence Ontology before adding a tree.")); + } + + $form['leaf_type'] = [ + '#title' => t('Tree Type'), + '#type' => 'textfield', + '#description' => t("Choose the tree type. The type is + a valid Sequence Ontology (SO) term. For example, trees derived + from protein sequences should use the SO term 'polypeptide'. + Alternatively, a phylotree can be used for representing a taxonomic + tree. In this case, the word 'taxonomy' should be used."), + '#required' => TRUE, + '#default_value' => $leaf_type, + '#autocomplete_route_name' => 'tripal_chado.cvterm_autocomplete', + '#autocomplete_route_parameters' => ['count' => 5, 'cv_id' => $cv_id] + ]; + + $form['dbxref'] = [ + '#title' => t('Database Cross-Reference'), + '#type' => 'textfield', + '#description' => t("Enter a database cross-reference of the form + [DB name]:[accession]. The database name must already exist in the + database. If the accession does not exist it is automatically added."), + '#required' => FALSE, + '#default_value' => $dbxref, + ]; + + $form['description'] = [ + '#type' => 'textarea', + '#title' => t('Description'), + '#required' => TRUE, + '#default_value' => $comment, + '#description' => t('Enter a description for this tree.'), + ]; + + $form['name_re'] = [ + '#title' => t('Feature Name Regular Expression'), + '#type' => 'textfield', + '#description' => t('The tree nodes will be automatically associated with + features, or in the case of taxonomic trees, with organisms. However, + if the nodes in the tree file are not exactly as the names of features + or organisms but have enough information to uniquely identify them, + then you may provide a regular expression that the importer will use to + extract the appropriate names from the node names. For example, remove + a prefix ABC_ with ^ABC_(.*)$'), + '#default_value' => $name_re, + ]; + $form['match'] = [ + '#title' => t('Use Unique Feature Name'), + '#type' => 'checkbox', + '#description' => t('If this is a phylogenetic (non taxonomic) tree and the nodes ' . + 'should match the unique name of the feature rather than the name of the feature ' . + 'then select this box. If unselected the loader will try to match the feature ' . + 'using the feature name.'), + '#default_value' => $match, + ]; + + return $form; + } + + /** + * @see TripalImporter::formValidate() + */ + public function formValidate($form, &$form_state) { + $chado = $this->getChadoConnection(); + // $values = $form_state['values']; + $values = $form_state->getValues(); + + // TRIPAL 4 - The type option is from an autocomplete which seems to include (SO:*) part + // Temporarily, remove this part + $leaf_type = $values["leaf_type"]; + $leaf_type = explode(' ', $leaf_type)[0]; // splits the string by space and takes the first part + + $options = [ + 'name' => trim($values["tree_name"]), + 'description' => trim($values["description"]), + 'analysis_id' => $values["analysis_id"], + 'leaf_type' => $leaf_type, + 'format' => 'newick', + 'dbxref' => trim($values["dbxref"]), + 'match' => $values["match"], + 'name_re' => $values["name_re"], + // 'load_later' => $values["load_later"], + ]; + + $errors = []; + $warnings = []; + + // The parent class will validate that a file has been specified and is valid. + + // Validate DBXREF + if ($options['dbxref'] and ($options['dbxref'] != "null:local:null")) { + // The db in a dbxref must already exist, the accession can be new. + $dbxref_parts = explode(':', $options['dbxref'], 2); + $db = $dbxref_parts[0]; + if ((count($dbxref_parts) < 2) or (strlen($db) < 1) or (strlen($dbxref_parts[1]) < 1)) { + $form_state->setErrorByName('dbxref', "The dbxref must consist of a DB and an accession separated by a colon, specify a valid dbxref value."); + return; + } + // Lookup + $results = $chado->select('1:db', 'db') + ->fields('db') + ->condition('name', $db) + ->execute() + ->fetchAll(); + $count = count($results); + if ($count < 1) { + $form_state->setErrorByName('dbxref', "The DB \"$db\" in the dbxref value does not exist, specify a valid dbxref value."); + return; + } + } + + // Perform API validation. + chado_validate_phylotree('insert', $options, $errors, $warnings, $chado->getSchemaName()); + + // Now set form errors if any errors were detected. + if (count($errors) > 0) { + foreach ($errors as $field => $message) { + if ($field == 'name') { + $field = 'tree_name'; + } + $form_state->setErrorByName($field, $message); + } + $form_state->setError($form, "Please fix these errors to continue creating a job"); + } + // Add any warnings if any were detected + if (count($warnings) > 0) { + foreach ($warnings as $field => $message) { + \Drupal::messenger()->addWarning(t("$message")); + } + } + } + + /** + * @see TripalImporter::run() + */ + public function run() { + $chado = $this->getChadoConnection(); + $arguments = $this->arguments['run_args']; + + // TRIPAL 4 - The type option is from an autocomplete which seems to include (SO:*) part + // Temporarily, remove this part + $leaf_type = $arguments["leaf_type"]; + $leaf_type = explode(' ', $leaf_type)[0]; // splits the string by space and takes the first part + + $options = [ + 'name' => $arguments["tree_name"], + 'description' => $arguments["description"], + 'analysis_id' => $arguments["analysis_id"], + 'leaf_type' => $leaf_type, + 'tree_file' => $this->arguments['files'][0]['file_path'], + 'format' => 'newick', + 'dbxref' => $arguments["dbxref"], + 'match' => $arguments["match"], + 'name_re' => $arguments["name_re"], + // 'load_later' => $arguments["load_later"], + ]; + // pass through the job, needed for log output to show up on the "jobs page" + if (property_exists($this, 'job')) { + $options['job'] = $this->job; + } + $errors = []; + $warnings = []; + chado_insert_phylotree($options, $errors, $warnings, $chado->getSchemaName()); + } + + /** + * {@inheritdoc} + */ + public function postRun() { + + } + + /** + * {@inheritdoc} + */ + public function formSubmit($form, &$form_state) { + + } + + +} diff --git a/tripal_chado/src/Plugin/TripalImporter/OBOImporter.php b/tripal_chado/src/Plugin/TripalImporter/OBOImporter.php index 40092e17c..888be53cb 100644 --- a/tripal_chado/src/Plugin/TripalImporter/OBOImporter.php +++ b/tripal_chado/src/Plugin/TripalImporter/OBOImporter.php @@ -21,14 +21,8 @@ * require_analysis = True, * button_text = @Translation("Import OBO File"), * file_upload = False, - * file_load = False, * file_remote = False, * file_required = False, - * cardinality = 1, - * menu_path = "", - * callback = "", - * callback_module = "", - * callback_path = "", * ) */ class OBOImporter extends ChadoImporterBase { @@ -40,7 +34,6 @@ class OBOImporter extends ChadoImporterBase { */ private $obo_namespaces = []; - /** * Holds the list of all CVs on this site. By storing them here it saves * us query time later. @@ -68,7 +61,6 @@ class OBOImporter extends ChadoImporterBase { 'related' => NULL, ]; - // An alternative cache to the temp_obo table. private $termStanzaCache = [ 'ids' => [], @@ -101,7 +93,6 @@ class OBOImporter extends ChadoImporterBase { */ private $default_namespace = ''; - /** * Holds the idspace elements from the header. These will correspond * to the accession prefixes, or short names (e.g. GO) for the terms. For @@ -117,14 +108,12 @@ class OBOImporter extends ChadoImporterBase { */ private $default_db = ''; - /** * An array of used cvterm objects so that we don't have to look them * up repeatedly. */ private $used_terms = []; - /** * An array of base IRIs returned from the EBI OLS lookup service. We * don't want to continually query OLS for the same ontology base IRIs. @@ -157,7 +146,6 @@ class OBOImporter extends ChadoImporterBase { */ private $term_names = []; - /** * {@inheritdoc} */ @@ -2731,7 +2719,7 @@ private function insertChadoCvtermRelationship($subject_id, $type_id, $object_id * The name of the vocabulary to add. * * @return object|NULL - * The newly inserted CV object.. + * The newly inserted CV object. */ private function insertChadoCv($cvname) { $chado = $this->getChadoConnection(); diff --git a/tripal_chado/src/Plugin/TripalImporter/TaxonomyImporter.php b/tripal_chado/src/Plugin/TripalImporter/TaxonomyImporter.php index 9afb342cc..f71c42ca4 100644 --- a/tripal_chado/src/Plugin/TripalImporter/TaxonomyImporter.php +++ b/tripal_chado/src/Plugin/TripalImporter/TaxonomyImporter.php @@ -24,114 +24,12 @@ * require_analysis = False, * button_text = @Translation("Import Taxonomy file"), * file_upload = False, - * file_load = False, * file_remote = False, * file_required = False, - * cardinality = 1, - * menu_path = "", - * callback = "", - * callback_module = "", - * callback_path = "", * ) */ class TaxonomyImporter extends ChadoImporterBase { - /** - * The name of this loader. This name will be presented to the site - * user. - */ - public static $name = 'Chado NCBI Taxonomy Loader'; - - /** - * The machine name for this loader. This name will be used to construct - * the URL for the loader. - */ - public static $machine_name = 'chado_taxonomy'; - - /** - * A brief description for this loader. This description will be - * presented to the site user. - */ - public static $description = 'Imports new organisms from NCBI using taxonomy IDs, ' . - 'or loads taxonomic details about existing organisms.'; - - /** - * An array containing the extensions of allowed file types. - */ - public static $file_types = []; - - - /** - * Provides information to the user about the file upload. Typically this - * may include a description of the file types allowed. - */ - public static $upload_description = ''; - - /** - * The title that should appear above the upload button. - */ - public static $upload_title = 'File Upload'; - - /** - * If the loader should require an analysis record. To maintain provenance - * we should always indicate where the data we are uploading comes from. - * The method that Tripal attempts to use for this by associating upload files - * with an analysis record. The analysis record provides the details for - * how the file was created or obtained. Set this to FALSE if the loader - * should not require an analysis when loading. if $use_analysis is set to - * true then the form values will have an 'analysis_id' key in the $form_state - * array on submitted forms. - */ - public static $use_analysis = FALSE; - - /** - * If the $use_analysis value is set above then this value indicates if the - * analysis should be required. - */ - public static $require_analysis = FALSE; - - /** - * Text that should appear on the button at the bottom of the importer - * form. - */ - public static $button_text = 'Import from NCBI Taxonomy'; - - /** - * Indicates the methods that the file uploader will support. - */ - public static $methods = [ - // Allow the user to upload a file to the server. - 'file_upload' => FALSE, - // Allow the user to provide the path on the Tripal server for the file. - 'file_local' => FALSE, - // Allow the user to provide a remote URL for the file. - 'file_remote' => FALSE, - ]; - - /** - * Indicates if the file must be provided. An example when it may not be - * necessary to require that the user provide a file for uploading if the - * loader keeps track of previous files and makes those available for - * selection. - */ - public static $file_required = FALSE; - - - /** - * The array of arguments used for this loader. Each argument should - * be a separate array containing a machine_name, name, and description - * keys. This information is used to build the help text for the loader. - */ - public static $argument_list = []; - - - /** - * Indicates how many files are allowed to be uploaded. By default this is - * set to allow only one file. Change to any positive number. A value of - * zero indicates an unlimited number of uploaded files are allowed. - */ - public static $cardinality = 0; - /** * Holds the list of all organisms currently in Chado. This list * is needed when checking to see if an organism has already been @@ -258,12 +156,8 @@ public function formValidate($form, &$form_state) { } } if (count($bad_ids) > 0) { - // form_set_error('taxonomy_ids', - // t('Taxonomy IDs must be numeric. The following are not valid: "@ids".', - // ['@ids' => implode('", "', $bad_ids)])); - \Drupal::messenger()->addError( - t('Taxonomy IDs must be numeric. The following are not valid: "@ids".', - ['@ids' => implode('", "', $bad_ids)]) + $form_state->setErrorByName('taxonomy_ids', t('Taxonomy IDs must be numeric. The following are not valid: "@ids".'), + ['@ids' => implode('", "', $bad_ids)] ); } } @@ -666,8 +560,9 @@ private function updateExisting($root_taxon = NULL) { $rfh = fopen($search_url, "r"); // If error, delay then retry if ((!$rfh) and ($retries)) { - $this->logger->warning("Error contacting NCBI to look up %sci_name, will retry", - ['%sci_name' => $sci_name_escaped]); + $this->logger->warning("Error contacting NCBI to look up @sci_name, will retry", + ['@sci_name' => $sci_name_escaped] + ); } $retries--; $remaining_sleep = $sleep_time - ((int) (1e6 * (microtime(TRUE) - $start))); @@ -677,7 +572,9 @@ private function updateExisting($root_taxon = NULL) { } if (!$rfh) { - $this->logger->warning("Could not look up %sci_name", ['%sci_name' => $sci_name_escaped]); + $this->logger->warning("Could not look up @sci_name", + ['@sci_name' => $sci_name_escaped] + ); continue; } $xml_text = ''; @@ -701,8 +598,9 @@ private function updateExisting($root_taxon = NULL) { $taxid = (string) $xml->IdList->Id; } else { - $this->logger->warning("Partial match \"%matched\" to query \"%query\", no taxid available", - ['%matched' => $matched, '%query' => $sci_name]); + $this->logger->warning("Partial match \"@matched\" to query \"@query\", no taxid available", + ['@matched' => $matched, '@query' => $sci_name] + ); } } } @@ -724,10 +622,10 @@ private function updateExisting($root_taxon = NULL) { } if (count($omitted_organisms)) { $omitted_list = implode('", "', $omitted_organisms); - $this->logger->warning('The following %count organisms do not have an NCBI taxonomy ID, ' - . 'and have not been included in the tree: "@omitted_list"', - ['%count' => count($omitted_organisms), - '@omitted_list' => $omitted_list]); + $this->logger->warning('The following @count organisms do not have an NCBI taxonomy ID,' + . ' and have not been included in the tree: "@omitted_list"', + ['@count' => count($omitted_organisms), '@omitted_list' => $omitted_list] + ); } } @@ -923,8 +821,9 @@ private function importRecord($taxid, $root_taxon = NULL, $organism = NULL) { $xml = new \SimpleXMLElement($xml_text); } else { - $this->logger->warning("Error contacting NCBI to look up taxid %taxid, will retry", - ['%taxid' => $taxid]); + $this->logger->warning("Error contacting NCBI to look up @taxid, will retry", + ['@taxid' => $taxid] + ); } $retries--; $remaining_sleep = $sleep_time - ((int) (1e6 * (microtime(TRUE) - $start))); @@ -960,9 +859,10 @@ private function importRecord($taxid, $root_taxon = NULL, $organism = NULL) { $chado_name = chado_get_organism_scientific_name($organism, $this->chado_schema_main); if ($chado_name != $sci_name) { $this->logger->warning("Substituting site taxon \"@chado_name\" for NCBI taxon \"@sci_name\"," - . " taxid @taxid, organism_id @organism_id", + . " taxid @taxid, organism_id @organism_id", ['@chado_name' => $chado_name, '@sci_name' => $sci_name, - '@taxid' => $taxid, '@organism_id' => $organism->organism_id]); + '@taxid' => $taxid, '@organism_id' => $organism->organism_id] + ); $sci_name = $chado_name; } } @@ -1104,8 +1004,9 @@ private function importRecord($taxid, $root_taxon = NULL, $organism = NULL) { return TRUE; } else { - $this->logger->warning("Error contacting NCBI to look up taxid %taxid", - ['%taxid' => $taxid]); + $this->logger->warning("Error contacting NCBI to look up taxid @taxid", + ['@taxid' => $taxid] + ); return FALSE; } } diff --git a/tripal_chado/src/Plugin/TripalStorage/ChadoStorage.php b/tripal_chado/src/Plugin/TripalStorage/ChadoStorage.php index 845545a7d..a215605cd 100644 --- a/tripal_chado/src/Plugin/TripalStorage/ChadoStorage.php +++ b/tripal_chado/src/Plugin/TripalStorage/ChadoStorage.php @@ -22,16 +22,6 @@ */ class ChadoStorage extends TripalStorageBase implements TripalStorageInterface { - /** - * An associative array that contains all of the property types that - * have been added to this object. It is indexed by entityType -> - * fieldName -> key and the value is the - * Drupal\tripal\TripalStoreage\StoragePropertyValue object. - * - * @var array - */ - protected $property_types = []; - /** * An associative array that holds the data for mapping an * entityTypes to Chado tables. It is indexed by entityType and the @@ -111,67 +101,6 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition $this->field_debugger = $field_debugger; } - /** - * @{inheritdoc} - */ - public function addTypes(string $field_name, array $types) { - - // Index the types by their entity type, field type and key. - foreach ($types as $index => $type) { - if (!is_object($type) OR !is_subclass_of($type, 'Drupal\tripal\TripalStorage\StoragePropertyTypeBase')) { - $this->logger->error('Type provided must be an object extending StoragePropertyTypeBase. Instead index @index was this: @type', - ['@index' => $index, '@type' => print_r($type, TRUE)]); - return FALSE; - } - - $key = $type->getKey(); - - if (!array_key_exists($field_name, $this->property_types)) { - $this->property_types[$field_name] = []; - } - $this->property_types[$field_name][$key] = $type; - - } - } - - /** - * @{inheritdoc} - */ - public function getTypes() { - return $this->property_types; - } - - /** - * @{inheritdoc} - */ - public function getPropertyType(string $field_name, string $key) { - - if (array_key_exists($field_name, $this->property_types)) { - if (array_key_exists($key, $this->property_types[$field_name])) { - return $this->property_types[$field_name][$key]; - } - } - - return NULL; - } - - /** - * @{inheritdoc} - */ - public function removeTypes(string $field_name, array $types) { - - foreach ($types as $type) { - $key = $type->getKey(); - - if (array_key_exists($field_name, $this->property_types)) { - if (array_key_exists($key, $this->property_types[$field_name])) { - unset($this->property_types[$field_name][$key]); - } - } - - } - } - /** * @{inheritdoc} */ @@ -422,7 +351,7 @@ public function updateValues(&$values) : bool { } } - // Now insert all new values for nor the non-base table records. + // Now insert all new values for the non-base table records. foreach ($records as $chado_table => $deltas) { foreach ($deltas as $delta => $record) { @@ -464,7 +393,7 @@ public function selectChadoRecord(&$records, $base_tables, $chado_table, $delta, } // If we are selecting on the base table and we don't have a proper - // condition then throw and error. + // condition then throw an error. if (!$this->hasValidConditions($record)) { throw new \Exception($this->t('Cannot select record in the Chado "@table" table due to unset conditions. Record: @record', ['@table' => $chado_table, '@record' => print_r($record, TRUE)])); @@ -610,14 +539,18 @@ public function findValues($match) { /** - * Sets the record_id properties after an insert. + * Sets the Record ID Properties after an insert or update. * - * @param array $values - * Array of \Drupal\tripal\TripalStorage\StoragePropertyValue objects. - * - * @param array $records - * The set of Chado records. - */ + * Specifically, this sets the value of any properties with an action of + * store_id, store_pkey and store_link. The value of the ID will be pulled + * from the record conditions for that table. + * + * @param array $values + * Array of \Drupal\tripal\TripalStorage\StoragePropertyValue objects. + * + * @param array $records + * The set of Chado records. + */ protected function setRecordIds(&$values, $records) { $schema = $this->connection->schema(); @@ -890,19 +823,21 @@ protected function buildChadoRecords($values, bool $is_store) { } $action = $prop_storage_settings['action']; - // Check that the chado table is set. + // Check that the base table for the field is set. if (!array_key_exists('base_table', $storage_plugin_settings)) { $this->logger->error($this->t('Cannot store the property, @field.@prop, in Chado. The field is missing the chado base table name.', ['@field' => $field_name, '@prop' => $key])); continue; } - // Get the base table definitions. + // Get the base table definitions for the field. $base_table = $storage_plugin_settings['base_table']; $base_table_def = $schema->getTableDef($base_table, ['format' => 'drupal']); $base_table_pkey = $base_table_def['primary key']; - // Get the Chado table. Use the base table if one is not provided. + // Get the Chado table this specific property works with. + // Use the base table as a default for properties which do not specify + // the chado table (e.g. single value fields). $chado_table = $base_table; $chado_table_def = $base_table_def; $chado_table_pkey = $base_table_pkey; @@ -911,36 +846,113 @@ protected function buildChadoRecords($values, bool $is_store) { $chado_table_def = $schema->getTableDef($chado_table, ['format' => 'drupal']); $chado_table_pkey = $chado_table_def['primary key']; } + // Properties with a store_link action use left/right table notation. + // The left table.left_table_id include the information for the value + // to be set in this property so use them as equalivalent to the + // chado table used in other actions. This also allows us to use + // the base table of the field as a default for this. + if (array_key_exists('left_table', $prop_storage_settings)) { + $chado_table = $prop_storage_settings['left_table']; + $chado_table_pkey = $prop_storage_settings['left_table_id']; + } - // This action is to store the base record primary key value. + // Now for each action type, set the conditions and fields for + // selecting chado records based on the other properties supplied. + // ---------------------------------------------------------------- + // STORE ID: stores the primary key value for a core table in chado + // Note: There may be more core tables in properties for this field + // then just the base table. For example, a field involving a two-join + // linker table will include two core tables. + // ................................................................ if ($action == 'store_id') { $record_id = $prop_value->getValue(); // If the record_id is zero then this is a brand-new value for - // this property. Let's set it to be replaced in the hopes that + // this property. Let's set it to be replaced in the hopes that // some other property has already been inserted and has the ID. if ($record_id == 0) { $records[$chado_table][0]['conditions'][$chado_table_pkey] = ['value' => ['REPLACE_BASE_RECORD_ID', $base_table], 'operation' => $operation]; - if (!array_key_exists($base_table, $base_record_ids)) { - $base_record_ids[$base_table] = $record_id; + // Now we add the chado table to our array of core tables + // so that we can replace it with the value for the record later. + if (!array_key_exists($chado_table, $base_record_ids)) { + $base_record_ids[$chado_table] = $record_id; } } + // However, if the record_id was set when the values were passed in, + // then we want to set it here and add it to the array of core ids + // for use later when replacing base record ids. else { $records[$chado_table][0]['conditions'][$chado_table_pkey] = ['value' => $record_id, 'operation' => $operation]; - $base_record_ids[$base_table] = $record_id; + $base_record_ids[$chado_table] = $record_id; } } - // This action is to store the linked table primary key value. + // STORE PKEY: stores the primary key value of a linking table. + // NOTE: A linking table is not a core table. This is important because + // during insert and update, the core tables are handled first and then + // linking tables are handled after. + // ................................................................ if ($action == 'store_pkey') { $link_record_id = $prop_value->getValue(); $records[$chado_table][$delta]['conditions'][$chado_table_pkey] = ['value' => $link_record_id, 'operation' => $operation]; } - // The link action will connect a linking table to the base table. + // STORE LINK: performs a join between two tables, one of which is a + // core table and one of which is a linking table. The value which is saved + // in this property is the left_table_id indicated in other key/value pairs. + // ................................................................ if ($action == 'store_link') { - $chado_column = $prop_storage_settings['chado_column']; - $records[$chado_table][$delta]['fields'][$chado_column] = ['REPLACE_BASE_RECORD_ID', $base_table]; + // The old implementation of store_link used chado_table/column notation + // only for the right side of the relationship. + // This meant we could not reliably determine the left side of the + // relationship... Confirm this field uses the new method. + if (array_key_exists('right_table', $prop_storage_settings)) { + // Using the tables with a store_id, determine which side of this + // relationship is a base/core table. This will be used for the + // fields below to ensure the ID is replaced. + // Start by assuming the left table is the base/core table + // (e.g. feature.feature_id = featureprop.feature_id). + $link_base = $chado_table; + $link_base_id = $chado_table_pkey; + $linker = $prop_storage_settings['right_table']; + $linker_id = $prop_storage_settings['right_table_id']; + // Then check if the right table has a store_id and if so, use it instead. + // (e.g. analysisfeature.analysis_id = analysis.analysis_id) + if (array_key_exists($prop_storage_settings['right_table'], $base_record_ids)) { + $link_base = $prop_storage_settings['right_table']; + $link_base_id = $prop_storage_settings['right_table_id']; + $linker = $chado_table; + $linker_id = $chado_table_pkey; + } + // @debug print "We decided it should be BASE $link_base.$link_base_id => LINKER $linker.$linker_id.\n"; + // We want to ensure that the linker table has a field added with + // the link to replace the ID once it's available. + $records[$linker] = $records[$linker] ?? [$delta => ['fields' => []]]; + $records[$linker][$delta] = $records[$linker][$delta] ?? ['fields' => []]; + $records[$linker][$delta]['fields'] = $records[$linker][$delta]['fields'] ?? []; + if (!array_key_exists($linker_id, $records[$linker][$delta]['fields'])) { + if ($prop_storage_settings['left_table'] !== NULL) { + $records[$linker][$delta]['fields'][$linker_id] = ['REPLACE_BASE_RECORD_ID', $link_base]; + // @debug print "Adding a note to replace $linker.$linker_id with $link_base record_id\n"; + } + } + } + else { + // Otherwise this field is using the old method for store_link. + // We will enter backwards compatibility mode here to do our best... + // It will work to handle CRUD for direct connections (e.g. props) + // but will cause problems with double-hops and all token replacement. + $bc_chado_table = $prop_storage_settings['chado_table']; + $bc_chado_column = $prop_storage_settings['chado_column']; + $records[$bc_chado_table][$delta]['fields'][$bc_chado_column] = ['REPLACE_BASE_RECORD_ID', $base_table]; + $this->logger->warning( + 'We had to use backwards compatible mode for :name.:key property type with an action of store_link.' + .' Please update the code for this field to use left/right table notation for the store_link property.' + .' Backwards compatible mode should allow this field to save/load data but may result in errors with token replacement and publishing.', + [':name' => $field_name, ':key' => $key] + ); + } } - // An action of "store" means that this value can be loaded/stored - // in the Chado table for the field. + // STORE: indicates that the value of this property can be loaded and + // stored in the Chado table indicated by this property. + // ................................................................ if ($action == 'store') { $chado_column = $prop_storage_settings['chado_column']; $value = $prop_value->getValue(); @@ -956,6 +968,10 @@ protected function buildChadoRecords($values, bool $is_store) { $records[$chado_table][$delta]['delete_if_empty'][] = $chado_column; } } + // JOIN: performs a join across multiple tables for the purposes of + // selecting a single column. This cannot be used for inserting or + // updating values. Instead we use store_link and store actions for that. + // ................................................................ if ($action == 'join') { $path = $prop_storage_settings['path']; $chado_column = $prop_storage_settings['chado_column']; @@ -963,9 +979,16 @@ protected function buildChadoRecords($values, bool $is_store) { $path_arr = explode(";", $path); $this->addChadoRecordJoins($records, $chado_column, $as, $delta, $path_arr); } + // REPLACE: replace a tokenized string with the values from other + // properties. As such we do not need to worry about adding this + // property to the chado queries. + // ................................................................ if ($action == 'replace') { // Do nothing here for properties that need replacement. } + // FUNCTION: use a function to determine the value of this property. + // As such, we do not need to add this property to the chado queries. + // ................................................................ if ($action == 'function') { // Do nothing here for properties that require post-processing // with a function. @@ -973,13 +996,24 @@ protected function buildChadoRecords($values, bool $is_store) { } } } - // Iterate through the records and set any record IDs for FK relationships. + + // Now we want to iterate through the records and set any record IDs + // for FK relationships based off the values set in the propertyValues + // before chado storage was called. + // Note: We have not yet done any querying ;-p + // ----------------------------------------------------------------------- foreach ($records as $table_name => $deltas) { foreach ($deltas as $delta => $record) { + // First for all the fields... foreach ($record['fields'] as $chado_column => $val) { if (is_array($val) and $val[0] == 'REPLACE_BASE_RECORD_ID') { - $base_table = $val[1]; + $core_table = $val[1]; + // If the core table is set in the base record ids array and the + // value is not 0 then we can set this chado field now! + if (array_key_exists($core_table, $base_record_ids) and $base_record_ids[$core_table] != 0) { + $records[$table_name][$delta]['fields'][$chado_column] = $base_record_ids[$core_table]; + } // If the base record ID is 0 then this is an insert and we // don't yet have the base record ID. So, leave in the message // to replace the ID so we can do so later. @@ -992,14 +1026,20 @@ protected function buildChadoRecords($values, bool $is_store) { if (!array_key_exists('conditions', $record)) print_r($record); foreach ($record['conditions'] as $chado_column => $val) { if (is_array($val['value']) and $val['value'][0] == 'REPLACE_BASE_RECORD_ID') { - $base_table = $val['value'][1]; + $core_table = $val['value'][1]; + // If the core table is set in the base record ids array and the + // value is not 0 then we can set this condition now! + if (array_key_exists($core_table, $base_record_ids) and $base_record_ids[$core_table] != 0) { + $records[$table_name][$delta]['conditions'][$chado_column] = $base_record_ids[$core_table]; + } // If the base record ID is 0 then this is an insert and we // don't yet have the base record ID. So, leave in the message // to replace the ID so we can do so later. if (array_key_exists($base_table, $base_record_ids) and $base_record_ids[$base_table] != 0) { $records[$table_name][$delta]['conditions'][$chado_column]['value'] = $base_record_ids[$base_table]; } + } } } @@ -1054,7 +1094,7 @@ protected function addChadoRecordJoins(array &$records, string $chado_column, st ]; } - // Get then current number of joins to the right table. + // Get the current number of joins to the right table. $num_left = 0; if (array_key_exists($left_table,$records[$parent_table][$delta]['joins'])) { $num_left = count($records[$parent_table][$delta]['joins'][$left_table]) - 1; diff --git a/tripal_chado/src/Services/ChadoFieldDebugger.php b/tripal_chado/src/Services/ChadoFieldDebugger.php index a126e571e..c0bdba5b1 100644 --- a/tripal_chado/src/Services/ChadoFieldDebugger.php +++ b/tripal_chado/src/Services/ChadoFieldDebugger.php @@ -182,7 +182,7 @@ public function reportQuery(object $query, $message) { * We would like to complete print out the query with subbed in parameters * but it's driving me crazy. * - * Usually we would use $query->arguments() to get the arguements with + * Usually we would use $query->arguments() to get the arguments with * placeholders but there are a number of bugs here: * - in Drupal 10 $insertQuery->arguments() provides a scope error. * - $updateQuery->arguments() only provides the conditional arguments, diff --git a/tripal_chado/src/TripalImporter/ChadoImporterBase.php b/tripal_chado/src/TripalImporter/ChadoImporterBase.php index 3d0452648..fffb32a8c 100644 --- a/tripal_chado/src/TripalImporter/ChadoImporterBase.php +++ b/tripal_chado/src/TripalImporter/ChadoImporterBase.php @@ -101,7 +101,7 @@ public function addAnalysis($form, &$form_state) { $chado = \Drupal::service('tripal_chado.database'); // Get the list of analyses. - $query = $chado->select('analysis', 'A'); + $query = $chado->select('1:analysis', 'A'); $query->fields('A', ['analysis_id', 'name']); $query->orderBy('A.name'); $analyses = []; diff --git a/tripal_chado/src/api/ChadoSchema.php b/tripal_chado/src/api/ChadoSchema.php index 40feba01a..32b1ff8e2 100644 --- a/tripal_chado/src/api/ChadoSchema.php +++ b/tripal_chado/src/api/ChadoSchema.php @@ -324,7 +324,7 @@ public function getCustomTableSchema($table) { * include linker tables (which link two or more base tables), property * tables, and relationship tables. These provide additional information * about primary data records and are therefore not base tables. This - * function retreives only the list of tables that are considered 'base' + * function retrieves only the list of tables that are considered 'base' * tables. * * @return @@ -531,7 +531,7 @@ public function checkTableExists($table) { * Check that any given column in a Chado table exists. * * This function is necessary because Drupal's db_field_exists() will not - * look in any other schema but the one were Drupal is installed + * look in any other schema but the one where Drupal is installed * * @param $table * The name of the chado table. @@ -611,7 +611,7 @@ public function checkColumnExists($table, $column) { * Check that any given column in a Chado table exists. * * This function is necessary because Drupal's db_field_exists() will not - * look in any other schema but the one were Drupal is installed + * look in any other schema but the one where Drupal is installed * * @param $table * The name of the chado table. diff --git a/tripal_chado/src/api/tripal_chado.db.api.php b/tripal_chado/src/api/tripal_chado.db.api.php index 37fd71d12..9908d0e61 100644 --- a/tripal_chado/src/api/tripal_chado.db.api.php +++ b/tripal_chado/src/api/tripal_chado.db.api.php @@ -161,7 +161,7 @@ function chado_get_db_select_options($schema_name = NULL) { $dbs = chado_query( "SELECT db_id, name FROM {db} ORDER BY name", - [], // Arguements. + [], // Arguments. [], // Options. $schema_name ); diff --git a/tripal_chado/src/api/tripal_chado.organism.api.php b/tripal_chado/src/api/tripal_chado.organism.api.php index 3f6016223..70337c88b 100755 --- a/tripal_chado/src/api/tripal_chado.organism.api.php +++ b/tripal_chado/src/api/tripal_chado.organism.api.php @@ -494,6 +494,7 @@ function chado_autocomplete_organism($text) { */ function chado_abbreviate_infraspecific_rank($rank) { $abb = ''; + $rank = strtolower($rank); switch ($rank) { case 'no_rank': $abb = ''; @@ -513,9 +514,15 @@ function chado_abbreviate_infraspecific_rank($rank) { case 'subvariety': $abb = 'subvar.'; break; + case 'convariety': + $abb = 'convar.'; + break; case 'cultivar': $abb = 'cv.'; break; + case 'cultivar group': + $abb = 'Group'; + break; case 'forma': $abb = 'f.'; break; @@ -541,25 +548,31 @@ function chado_abbreviate_infraspecific_rank($rank) { * @ingroup tripal_organism_api */ function chado_unabbreviate_infraspecific_rank($rank) { - if (preg_match('/^subsp\.?$/', $rank)) { + if (preg_match('/^subsp\.?$/i', $rank)) { $rank = 'subspecies'; } - elseif (preg_match('/^ssp\.?$/', $rank)) { + elseif (preg_match('/^ssp\.?$/i', $rank)) { $rank = 'subspecies'; } - elseif (preg_match('/^var\.?$/', $rank)) { + elseif (preg_match('/^var\.?$/i', $rank)) { $rank = 'varietas'; } - elseif (preg_match('/^subvar\.?$/', $rank)) { + elseif (preg_match('/^subvar\.?$/i', $rank)) { $rank = 'subvarietas'; } - elseif (preg_match('/^cv\.?$/', $rank)) { + elseif (preg_match('/^convar\.?$/i', $rank)) { + $rank = 'convariety'; + } + elseif (preg_match('/^cv\.?$/i', $rank)) { $rank = 'cultivar'; } - elseif (preg_match('/^f\.?$/', $rank)) { + elseif (preg_match('/^group$/i', $rank)) { + $rank = 'cultivar group'; + } + elseif (preg_match('/^f\.?$/i', $rank)) { $rank = 'forma'; } - elseif (preg_match('/^subf\.?$/', $rank)) { + elseif (preg_match('/^subf\.?$/i', $rank)) { $rank = 'subforma'; } // if none of the above matched, rank is returned unchanged diff --git a/tripal_chado/src/api/tripal_chado.phylotree.api.php b/tripal_chado/src/api/tripal_chado.phylotree.api.php index e5ebca296..825306d48 100644 --- a/tripal_chado/src/api/tripal_chado.phylotree.api.php +++ b/tripal_chado/src/api/tripal_chado.phylotree.api.php @@ -52,11 +52,11 @@ * @ingroup tripal_phylotree_api */ function chado_validate_phylotree($val_type, &$options, &$errors, &$warnings, $schema_name = 'chado') { - + $chado = \Drupal::service('tripal_chado.database'); + $chado->setSchemaName($schema_name); if ($val_type != 'insert' and $val_type != 'update') { - // tripal_report_error('tripal_phylogeny', TRIPAL_ERROR, - // "The $val_type argument must be either 'update or 'insert'."); - \Drupal::service('tripal.logger')->error("The \$val_type argument must be either 'update or 'insert'."); + \Drupal::service('tripal.logger')->error("The \$val_type argument to" + . " chado_validate_phylotree() must be either 'update or 'insert'."); } // Set Defaults. @@ -69,8 +69,8 @@ function chado_validate_phylotree($val_type, &$options, &$errors, &$warnings, $s if (!array_key_exists('name_re', $options) or (!$options['name_re'])) { $options['name_re'] = '^(.*)$'; } - // A dbxref is not required by Tripal but is required by the database - // field in the phylotree table. Therefore, if the dbxref is not provided + // A dbxref is not required by Tripal, but is required by the database + // field in the phylotree table. Therefore, if the dbxref is not provided, // we can set this to be the null database and null dbxref which // is represented as 'null:local:null' if (!array_key_exists('dbxref', $options)) { @@ -100,20 +100,23 @@ function chado_validate_phylotree($val_type, &$options, &$errors, &$warnings, $s return FALSE; } } + // else $val_type == 'update' else { // Does the phylotree ID exist and is it valid. if (!array_key_exists('phylotree_id', $options)) { $errors['phylotree_id'] = t('Please provide the ID for the tree.'); return FALSE; } - $exists = chado_select_record('phylotree', ['phylotree_id'], - ['phylotree_id' => $options['phylotree_id']], ['has_record' => 1], $schema_name); + $exists = $chado->select('1:phylotree', 'phylotree') + ->fields('phylotree') + ->condition('phylotree_id', $options['phylotree_id']) + ->execute() + ->fetchObject(); if (!$exists) { $errors['phylotree_id'] = t('The phylotree_id "%id" does not exist.', - [ '%id' => $options['phylotree_id']]); + ['%id' => $options['phylotree_id']]); return FALSE; } - } // Make sure the file exists if one is specified. @@ -126,6 +129,7 @@ function chado_validate_phylotree($val_type, &$options, &$errors, &$warnings, $s return FALSE; } } + // Make sure the file format is correct. if (!array_key_exists('format', $options) or ($options['format'] != 'newick' and $options['format'] != 'taxonomy')) { @@ -134,30 +138,41 @@ function chado_validate_phylotree($val_type, &$options, &$errors, &$warnings, $s return FALSE; } - // If no leaf type is provided then use the polypeptide term. + // If no leaf type is provided then assume a taxonomic tree. if (!array_key_exists('leaf_type', $options) or !$options['leaf_type']) { - $options['leaf_type'] = 'polypeptide'; + $options['leaf_type'] = 'taxonomy'; } } // Make sure the analysis exists. $analysis = NULL; if (array_key_exists('analysis_id', $options) and $options['analysis_id']) { - $analysis = chado_select_record('analysis', - ['analysis_id'], ['analysis_id' => $options['analysis_id']], NULL, $schema_name); + $analysis = $chado->select('1:analysis', 'analysis') + ->fields('analysis') + ->condition('analysis_id', $options['analysis_id']) + ->execute() + ->fetchObject(); if (!$analysis) { - $errors['analysis_id'] = t('The analysis name provided does not exist.'); + $errors['analysis_id'] = t( + 'The analysis ID provided "%id" does not exist.', + ['%id' => $options['analysis_id']]); return FALSE; } - $options['analysis_id'] = $analysis[0]->analysis_id; + $options['analysis_id'] = $analysis->analysis_id; } - if (array_key_exists('analysis', $options) and $options['analysis']) { - $analysis = chado_select_record('analysis', ['analysis_id'], ['name' => $options['analysis']], NULL, $schema_name); + elseif (array_key_exists('analysis', $options) and $options['analysis']) { + $analysis = $chado->select('1:analysis', 'analysis') + ->fields('analysis') + ->condition('name', $options['analysis']) + ->execute() + ->fetchObject(); if (!$analysis) { - $errors['analysis'] = t('The analysis ID provided does not exist.'); + $errors['analysis'] = t( + 'The analysis name provided "%name" does not exist.', + ['%name' => $options['analysis']]); return FALSE; } - $options['analysis_id'] = $analysis[0]->analysis_id; + $options['analysis_id'] = $analysis->analysis_id; } // Make sure the leaf type exists. @@ -170,7 +185,21 @@ function chado_validate_phylotree($val_type, &$options, &$errors, &$warnings, $s ], 'name' => 'Species tree', ]; - $type = chado_select_record('cvterm', ['cvterm_id'], $values, NULL, $schema_name); + + // Find the cv_id for EDAM + $cv_id = $chado->select('1:cv', 'cv') + ->fields('cv') + ->condition('name', 'EDAM') + ->execute() + ->fetchObject() + ->cv_id; + + $type = $chado->select('1:cvterm', 'cvterm') + ->fields('cvterm') + ->condition('name', 'Species tree') + ->condition('cv_id', $cv_id) + ->execute() + ->fetchObject(); } else { $values = [ @@ -179,14 +208,30 @@ function chado_validate_phylotree($val_type, &$options, &$errors, &$warnings, $s ], 'name' => $options['leaf_type'], ]; - $type = chado_select_record('cvterm', ['cvterm_id'], $values, NULL, $schema_name); + // $type = chado_select_record('cvterm', ['cvterm_id'], $values, NULL, $schema_name); + + // Find the cv_id for sequence + $cv_id = $chado->select('1:cv', 'cv') + ->fields('cv') + ->condition('name', 'sequence') + ->execute() + ->fetchObject() + ->cv_id; + + $type = $chado->select('1:cvterm', 'cvterm') + ->fields('cvterm') + ->condition('name', $options['leaf_type']) + ->condition('cv_id', $cv_id) + ->execute() + ->fetchObject(); + if (!$type) { $errors['leaf_type'] = t('The leaf_type provided "%term" is not a valid Sequence Ontology term.', ['%term' => $options['leaf_type']]); return FALSE; } } - $options['type_id'] = $type[0]->cvterm_id; + $options['type_id'] = $type->cvterm_id; } // A Dbxref is required by the phylotree module, but if the @@ -212,25 +257,34 @@ function chado_validate_phylotree($val_type, &$options, &$errors, &$warnings, $s if (!$dbxref) { $db = chado_generate_var('db', ['name' => $db_name], [], $schema_name); - if (!$db) { - $errors['dbxref'] = t(' - dbxref could not be created for db: %dbxref.', ['%dbxref' => $dbxref]); + $errors['dbxref'] = t( + 'dbxref could not be created for %dbname:%dbxref, this DB does not exist.', + ['%dbname' => $db_name, '%dbxref' => $dbxref]); return FALSE; } - $dbxref = chado_insert_record('dbxref', $values, [], $schema_name); + // Here we create the new dbxref for the specified new accession. + $dbxref = $chado->insert('1:dbxref')->fields([ + 'accession' => $values['accession'], + 'db_id' => $db->db_id + ])->execute(); if (!$dbxref) { - $errors['dbxref'] = t(' - dbxref could not be created for db: %dbxref.', ['%dbxref' => $dbxref]); + $errors['dbxref'] = t( + 'dbxref could not be created for %dbname:%dbxref.', + ['%dbname' => $db_name, '%dbxref' => $dbxref]); return FALSE; } } - if (is_array($dbxref)) { + + if (is_object($dbxref)) { + $options['dbxref_id'] = $dbxref->dbxref_id; + } + elseif (is_array($dbxref)) { $options['dbxref_id'] = $dbxref['dbxref_id']; } else { - $options['dbxref_id'] = $dbxref->dbxref_id; + $options['dbxref_id'] = $dbxref; } } @@ -238,7 +292,7 @@ function chado_validate_phylotree($val_type, &$options, &$errors, &$warnings, $s if (array_key_exists('name', $options) and $options['name']) { $sql = " SELECT * - FROM {phylotree} P + FROM {1:phylotree} P WHERE P.name = :name "; @@ -247,9 +301,12 @@ function chado_validate_phylotree($val_type, &$options, &$errors, &$warnings, $s $sql .= " AND NOT P.phylotree_id = :phylotree_id"; $args[':phylotree_id'] = $options['phylotree_id']; } - $result = chado_query($sql, $args, [], $schema_name)->fetchObject(); + $result = $chado->query($sql, $args)->fetchObject(); if ($result) { - $errors['name'] = t("The tree name is in use by another tree. Please provide a different unique name for this tree."); + $errors['name'] = t('The tree name "%name" is in use by another tree.' + . ' Please provide a different unique name for this tree.', + ['%name' => $options['name']]); + return FALSE; } } @@ -275,8 +332,8 @@ function chado_validate_phylotree($val_type, &$options, &$errors, &$warnings, $s * should be associated. * 'leaf_type': A sequence ontology term or the word 'taxonomy'. If the * type is 'taxonomy' then this tree represents a - * taxonomic tree. The default, if not specified, is the - * term 'polypeptide'. + * taxonomic tree. The default, if not specified, is a + * taxonomic tree. * 'tree_file': The path of the file containing the phylogenetic tree to * import or a Drupal managed_file numeric ID. * 'format': The file format. Currently only 'newick' is supported. @@ -313,8 +370,16 @@ function chado_validate_phylotree($val_type, &$options, &$errors, &$warnings, $s * @ingroup tripal_phylotree_api */ function chado_insert_phylotree(&$options, &$errors, &$warnings, $schema_name = 'chado') { + $chado = \Drupal::service('tripal_chado.database'); + $chado->setSchemaName($schema_name); + global $user; + // If no leaf type is provided then assume a taxonomic tree. + if (!array_key_exists('leaf_type', $options) or !$options['leaf_type']) { + $options['leaf_type'] = 'taxonomy'; + } + $options['name_re'] = isset($options['name_re']) ? trim($options['name_re']) : NULL; $options['leaf_type'] = trim($options['leaf_type']); $options['name'] = trim($options['name']); @@ -336,7 +401,6 @@ function chado_insert_phylotree(&$options, &$errors, &$warnings, $schema_name = $success = chado_validate_phylotree('insert', $options, $errors, $warnings, $schema_name); if (!$success) { foreach ($errors as $field => $message) { - // tripal_report_error($options['message_type'], TRIPAL_ERROR, $message, [], $options['message_opts']); \Drupal::service('tripal.logger')->error($message); } return FALSE; @@ -351,20 +415,25 @@ function chado_insert_phylotree(&$options, &$errors, &$warnings, $schema_name = 'type_id' => $options['type_id'], ]; - $phylotree = chado_insert_record('phylotree', $values, [], $schema_name); + $phylotree = $chado->insert('1:phylotree') + ->fields($values) + ->execute(); + $phylotree = $chado->select('1:phylotree', 'phylotree') + ->fields('phylotree') + ->condition('phylotree_id', $phylotree) + ->execute() + ->fetchAssoc(); if (!$phylotree) { drupal_set_message(t('Unable to add phylotree.'), 'warning'); - // tripal_report_error($options['message_type'], TRIPAL_WARNING, - // 'Insert phylotree: Unable to create phylotree where values: %values', - // ['%values' => print_r($values, TRUE)], $options['message_opts']); - \Drupal::service('tripal.logger')->warning('Insert phylotree: Unable to create phylotree where values:' . print_r($values, TRUE)); + \Drupal::service('tripal.logger')->warning( + 'Insert phylotree: Unable to create phylotree where values: %values', + ['%values' => print_r($values, TRUE)]); return FALSE; } $phylotree_id = $phylotree['phylotree_id']; - // tripal_report_error($options['message_type'], TRIPAL_INFO, - // 'Insert phylotree: Created phylotree with phylotree_id: %phylotree_id', - // ['%phylotree_id' => $phylotree_id], $options['message_opts']); - \Drupal::service('tripal.logger')->info("Insert phylotree: Created phylotree with phylotree_id: $phylotree_id"); + \Drupal::service('tripal.logger')->info( + 'Insert phylotree: Created phylotree with phylotree_id: %phylotree_id', + ['%phylotree_id' => $phylotree_id]); $options['phylotree_id'] = $phylotree_id; // If the tree_file is numeric then it is a Drupal managed file and @@ -381,37 +450,15 @@ function chado_insert_phylotree(&$options, &$errors, &$warnings, $schema_name = $real_file_path = $options['tree_file']; } - // If caller has requested to load the file now then do so, otherwise - // submit using a Tripal job. - if (!array_key_exists('no_load', $options) or !$options['no_load']) { - if (array_key_exists('load_later', $options) and $options['load_later']) { - $args = [ - $real_file_path, - 'newick', - [ - 'phylotree_id' => $phylotree_id, - 'leaf_type' => $options['leaf_type'], - 'match' => $options['match'] ? 'uniquename' : 'name', - 'name_re' => $options['name_re'], - ], - ]; - if (tripal_add_job("Import Tree File: " . basename($real_file_path), $options['message_type'], - 'chado_phylogeny_import_tree_file', $args, $user->uid)) { - drupal_set_message(t('The tree visualizations will appear once the tree is fully imported.')); - } - } - else { - $args = [ - 'phylotree_id' => $phylotree_id, - 'leaf_type' => $options['leaf_type'], - 'match' => $options['match'] ? 'uniquename' : 'name', - 'name_re' => $options['name_re'], - 'message_type' => $options['message_type'], - 'message_opts' => $options['message_opts'], - ]; - chado_phylogeny_import_tree_file($real_file_path, $options['format'], $args, NULL, $schema_name); - } - } + $args = [ + 'phylotree_id' => $phylotree_id, + 'leaf_type' => $options['leaf_type'], + 'match' => $options['match'] ? 'uniquename' : 'name', + 'name_re' => $options['name_re'], + 'message_type' => $options['message_type'], + 'message_opts' => $options['message_opts'], + ]; + chado_phylogeny_import_tree_file($real_file_path, $options['format'], $args, NULL, $schema_name); return TRUE; } @@ -440,8 +487,8 @@ function chado_insert_phylotree(&$options, &$errors, &$warnings, $schema_name = * should be associated. * 'leaf_type': A sequence ontology term or the word 'taxonomy'. If the * type is 'taxonomy' then this tree represents a - * taxonomic tree. The default, if not specified, is the - * term 'polypeptide'. + * taxonomic tree. The default, if not specified, is a + * taxonomic tree. * 'tree_file': The path of the file containing the phylogenetic tree to * import or a Drupal managed_file numeric ID. * 'format': The file format. Currently only 'newick' is supported @@ -461,7 +508,10 @@ function chado_insert_phylotree(&$options, &$errors, &$warnings, $schema_name = * @ingroup tripal_phylotree_api */ function chado_update_phylotree($phylotree_id, &$options, $schema_name = 'chado') { - global $user; + // global $user; // Unused variable detected by RISH VSCODE IDE [8/27/2023] + + $chado = \Drupal::service('tripal_chado.database'); + $chado->setSchemaName($schema_name); // These are options for the tripal_report_error function. We do not // want to log messages to the watchdog but we do for the job and to @@ -479,7 +529,6 @@ function chado_update_phylotree($phylotree_id, &$options, $schema_name = 'chado' $success = chado_validate_phylotree('update', $options, $errors, $warnings, $schema_name); if (!$success) { foreach ($errors as $field => $message) { - // tripal_report_error($options['message_type'], TRIPAL_ERROR, $message, [], $options['message_opts']); \Drupal::service('tripal.logger')->error($message); } return FALSE; @@ -508,18 +557,18 @@ function chado_update_phylotree($phylotree_id, &$options, $schema_name = 'chado' $phylotree = chado_update_record('phylotree', $match, $values, ['return_record' => TRUE], $schema_name); if (!$phylotree) { drupal_set_message(t('Unable to update phylotree.'), 'warning'); - // tripal_report_error('tripal_phylogeny', TRIPAL_WARNING, - // 'Update phylotree: Unable to update phylotree where values: %values', - // ['%values' => print_r($values, TRUE)], $options['message_opts'] - // ); - \Drupal::service('tripal.logger')->warning('Update phylotree: Unable to update phylotree where values: ' . print_r($values, TRUE)); + \Drupal::service('tripal.logger')->warning( + 'Update phylotree: Unable to update phylotree where values: %values', + ['%values' => print_r($values, TRUE)]); } // If we have a tree file, then import the tree. if (array_key_exists('tree_file', $options) and $options['tree_file']) { // Remove any existing nodes - chado_delete_record('phylonode', ['phylotree_id' => $options['phylotree_id']], NULL, $schema_name); + $chado->delete('1:phylonode') + ->condition('phylotree_id', $options['phylotree_id']) + ->execute(); // Make sure if we already have a file that we remove the old one. $sql = " @@ -549,35 +598,15 @@ function chado_update_phylotree($phylotree_id, &$options, $schema_name = 'chado' $real_file_path = $options['tree_file']; } - // If caller has requested to load the file now then do so, otherwise - // submit using a Tripal job. - if (array_key_exists('load_later', $options) and $options['load_later']) { - $args = [ - $real_file_path, - 'newick', - [ - 'phylotree_id' => $options['phylotree_id'], - 'leaf_type' => $options['leaf_type'], - 'match' => $options['match'] ? 'uniquename' : 'name', - 'name_re' => $options['name_re'], - ], - ]; - if (tripal_add_job("Import Tree File: " . basename($real_file_path), 'tripal_phylogeny', - 'chado_phylogeny_import_tree_file', $args, $user->uid)) { - drupal_set_message(t('The tree visualizations will appear once the tree is fully imported.')); - } - } - else { - $args = [ - 'phylotree_id' => $options['phylotree_id'], - 'leaf_type' => $options['leaf_type'], - 'match' => $options['match'] ? 'uniquename' : 'name', - 'name_re' => $options['name_re'], - 'message_type' => $options['message_type'], - 'message_opts' => $options['message_opts'], - ]; - chado_phylogeny_import_tree_file($real_file_path, $options['format'], $args, NULL, $schema_name); - } + $args = [ + 'phylotree_id' => $options['phylotree_id'], + 'leaf_type' => $options['leaf_type'], + 'match' => $options['match'] ? 'uniquename' : 'name', + 'name_re' => $options['name_re'], + 'message_type' => $options['message_type'], + 'message_opts' => $options['message_opts'], + ]; + chado_phylogeny_import_tree_file($real_file_path, $options['format'], $args, NULL, $schema_name); } return TRUE; @@ -595,18 +624,27 @@ function chado_update_phylotree($phylotree_id, &$options, $schema_name = 'chado' */ function chado_delete_phylotree($phylotree_id, $schema_name = 'chado') { + $chado = \Drupal::service('tripal_chado.database'); + $chado->setSchemaName($schema_name); + // If we don't have a phylotree id for this node then this isn't a node of // type chado_phylotree or the entry in the chado_phylotree table was lost. if (!$phylotree_id) { - // tripal_report_error('tripal_phylogeny', TRIPAL_ERROR, - // 'Please provide a phylotree_id to delete a tree.', [], []); \Drupal::service('tripal.logger')->error('Please provide a phylotree_id to delete a tree'); return FALSE; } // Remove the tree $values = ['phylotree_id' => $phylotree_id]; - return chado_delete_record('phylotree', $values, NULL, $schema_name); + // return chado_delete_record('phylotree', $values, NULL, $schema_name); + $status = false; + // RISH [8/27/2023] The below $num_deleted concept is taken from the Drupal Database API for 9.x + // https://www.drupal.org/docs/drupal-apis/database-api/delete-queries + $num_deleted = $chado->delete('1:phylotree') + ->condition('phylotree_id', $phylotree_id) + ->execute(); + return TRUE; + } /** @@ -693,6 +731,8 @@ function chado_assign_phylogeny_tree_indices(&$tree, &$index = 1) { * @ingroup tripal_phylotree_api */ function chado_phylogeny_import_tree(&$tree, $phylotree, $options, $vocab = [], $parent = NULL, $schema_name = 'chado') { + $chado = \Drupal::service('tripal_chado.database'); + $chado->setSchemaName($schema_name); // Used for final summary message at end of recursion. static $n_associated = 0; @@ -718,11 +758,13 @@ function chado_phylogeny_import_tree(&$tree, $phylotree, $options, $vocab = [], $values['distance'] = $tree['length']; } // Set the type of node. - if ($tree['is_root']) { + // echo "DEBUG Check is_root\n"; + if (isset($tree['is_root']) && $tree['is_root'] == true) { $values['type_id'] = $vocab['root']->cvterm_id; } else { - if ($tree['is_internal']) { + // echo "DEBUG Check is_internal\n"; + if (isset($tree['is_internal']) && $tree['is_internal'] == true) { $values['type_id'] = $vocab['internal']->cvterm_id; $values['parent_phylonode_id'] = $parent['phylonode_id']; // TODO: a feature may be associated here but it is recommended that it @@ -730,7 +772,7 @@ function chado_phylogeny_import_tree(&$tree, $phylotree, $options, $vocab = [], // all features beneath it. } else { - if ($tree['is_leaf']) { + if (isset($tree['is_leaf']) && $tree['is_leaf']) { $values['type_id'] = $vocab['leaf']->cvterm_id; $values['parent_phylonode_id'] = $parent['phylonode_id']; @@ -765,41 +807,53 @@ function chado_phylogeny_import_tree(&$tree, $phylotree, $options, $vocab = [], ], ]; $sel_columns = ['feature_id']; - $feature = chado_select_record('feature', $sel_columns, $sel_values, NULL, $schema_name); + + // Find the cv_id for sequence + $cv_id = $chado->select('1:cv', 'cv')->fields('cv')->condition('name', 'sequence')->execute()->fetchObject()->cv_id; + + // Find the cvterm_id for $options['leaf_type'] + $cvterm_id = $chado->select('1:cvterm', 'cvterm')->fields('cvterm') + ->condition('name', $options['leaf_type']) + ->condition('cv_id', $cv_id) + ->execute() + ->fetchObject()->cvterm_id; + + $feature = $chado->select('1:feature', 'feature') + ->fields('feature') + ->condition('type_id', $cvterm_id); + if (isset($sel_values['name'])) { + $feature = $feature->condition('name', $sel_values['name']); + } + else if (isset($sel_values['uniquename'])) { + $feature = $feature->condition('uniquename', $sel_values['uniquename']); + } + + $feature = $feature->execute() + ->fetchAll(); + if (count($feature) > 1) { // Found multiple features, cannot make an association. - // tripal_report_error($options['message_type'], TRIPAL_WARNING, - // 'Import phylotree: Warning, unable to associate to a feature, more than one feature matches the %matchtype: %value', - // ['%matchtype' => $options['match'], '%value' => $sel_values[$options['match']] ], - // $options['message_opts'] - // ); - \Drupal::service('tripal.logger')->warning('Import phylotree: Warning, unable to associate to a feature, - more than one feature matches the ' . $options['match'] . ': ' . $sel_values[$options['match']]); + \Drupal::service('tripal.logger')->warning('Import phylotree: Warning, unable to associate to a feature,' + . ' more than one feature matches the %matchtype: %value', + ['%matchtype' => $options['match'], '%value' => $sel_values[$options['match']] ]); } else { if (count($feature) == 1) { $values['feature_id'] = $feature[0]->feature_id; $n_associated++; - // tripal_report_error($options['message_type'], TRIPAL_INFO, - // 'Import phylotree: Associated %value by %matchtype to feature_id: %fid', - // ['%matchtype' => $options['match'], '%value' => $sel_values[$options['match']], - // '%fid' => $values['feature_id'] ], - // $options['message_opts'] - // ); - \Drupal::service('tripal.logger')->info('Import phylotree: Associated ' . - $sel_values[$options['match']] . ' by ' . $options['match'] . - ' to feature_id: ' . $values['feature_id']); + \Drupal::service('tripal.logger')->info('Import phylotree: Associated' + . ' %value by %matchtype to feature_id: %fid', + ['%matchtype' => $options['match'], + '%value' => $sel_values[$options['match']], + '%fid' => $values['feature_id'] ]); } else { // Could not find a feature that matches the name or uniquename $n_not_associated++; - // tripal_report_error($options['message_type'], TRIPAL_WARNING, - // 'Import phylotree: Warning, unable to associate to a feature that matches the %matchtype: %value', - // ['%matchtype' => $options['match'], '%value' => $sel_values[$options['match']] ], - // $options['message_opts'] - // ); - \Drupal::service('tripal.logger')->warning('Import phylotree: Warning, unable to associate to a - feature that matches the ' . $options['match'] . ': ' . $sel_values[$options['match']]); + \Drupal::service('tripal.logger')->warning('Import phylotree: Warning, unable to associate to a' + . ' feature that matches the %matchtype: %value', + ['%matchtype' => $options['match'], + '%value' => $sel_values[$options['match']] ]); } } } @@ -814,23 +868,15 @@ function chado_phylogeny_import_tree(&$tree, $phylotree, $options, $vocab = [], if ($organism_id) { $tree['organism_id'] = $organism_id; $n_associated++; - // tripal_report_error($options['message_type'], TRIPAL_INFO, - // 'Import phylotree: Associated %name to organism_id: %organism_id', - // ['%name' => $tree['name'], '%organism_id' => $organism_id], - // $options['message_opts'] - // ); - \Drupal::service('tripal.logger')->info('Import phylotree: Associated ' . $tree['name'] . - ' to organism_id: ' . $organism_id); + \Drupal::service('tripal.logger')->info( + 'Import phylotree: Associated %name to organism_id: %organism_id', + ['%name' => $tree['name'], '%organism_id' => $organism_id]); } else { $n_not_associated++; - // tripal_report_error($options['message_type'], TRIPAL_WARNING, - // 'Import phylotree: Warning, unable to associate to an organism that matches %name', - // ['%name' => $tree['name']], - // $options['message_opts'] - // ); - \Drupal::service('tripal.logger')->warning('Import phylotree: Warning, - unable to associate to an organism that matches ' . $tree['name']); + \Drupal::service('tripal.logger')->warning('Import phylotree: Warning, unable to' + . ' associate to an organism that matches %name', + ['%name' => $tree['name']]); } } } @@ -839,7 +885,11 @@ function chado_phylogeny_import_tree(&$tree, $phylotree, $options, $vocab = [], } // Insert the new node and then add its assigned phylonode_id to the node. - $phylonode = chado_insert_record('phylonode', $values, [], $schema_name); + // $phylonode = chado_insert_record('phylonode', $values, [], $schema_name); + $phylonode = $chado->insert('1:phylonode')->fields($values)->execute(); + // Get the phylonode record + $phylonode = $chado->select('1:phylonode', 'p')->fields('p')->condition('phylonode_id', $phylonode)->execute()->fetchAssoc(); + $tree['phylonode_id'] = $phylonode['phylonode_id']; // This is a taxonomic tree, so associate this node with an @@ -849,7 +899,7 @@ function chado_phylogeny_import_tree(&$tree, $phylotree, $options, $vocab = [], 'phylonode_id' => $tree['phylonode_id'], 'organism_id' => $tree['organism_id'], ]; - $pylonode_organism = chado_insert_record('phylonode_organism', $values, [], $schema_name); + $phylonode_organism = $chado->insert('1:phylonode_organism')->fields($values)->execute(); } // Associate any properties. @@ -860,7 +910,7 @@ function chado_phylogeny_import_tree(&$tree, $phylotree, $options, $vocab = [], 'type_id' => $type_id, 'value' => $value, ]; - $pylonode_organism = chado_insert_record('phylonodeprop', $values, [], $schema_name); + $phylonode_organism = $chado->insert('1:phylonodeprop')->fields($values)->execute(); } } } @@ -871,15 +921,9 @@ function chado_phylogeny_import_tree(&$tree, $phylotree, $options, $vocab = [], } // Report summary status of association of leaf nodes at end of recursion. if (!$parent) { - // tripal_report_error($options['message_type'], TRIPAL_INFO, - // 'Import phylotree summary: %n_associated nodes were successfully associated to ' - // . 'content, %n_not_associated nodes could not be associated', - // ['%n_associated' => $n_associated, '%n_not_associated' => $n_not_associated], - // $options['message_opts'] - // ); - \Drupal::service('tripal.logger')->info('Import phylotree summary: ' . $n_associated . - ' were successfully associated to' . - ' content, ' . $n_not_associated . ' nodes could not be associated'); + \Drupal::service('tripal.logger')->info('Import phylotree summary: %n_associated nodes' + . ' were successfully associated to content, %n_not_associated nodes could not be associated', + ['%n_associated' => $n_associated, '%n_not_associated' => $n_not_associated]); } } @@ -960,8 +1004,8 @@ function chado_phylogeny_get_node_types_vocab($options, $schema_name = 'chado') * 'phylotree_id': The imported nodes will be associated with this tree. * 'leaf_type': A sequence ontology term or the word 'taxonomy'. If the * type is 'taxonomy' then this tree represents a - * taxonomic tree. The default, if not specified, is the - * term 'polypeptide'. + * taxonomic tree. The default, if not specified, is a + * taxonomic tree. * 'name_re': The value of this field can be a regular expression to pull * out the name of the feature or organism from the node label * in the input tree. If no value is provided the entire label @@ -975,7 +1019,7 @@ function chado_phylogeny_import_tree_file($file_name, $format, $options = [], $j // Set some option details. if (!array_key_exists('leaf_type', $options)) { - $options['leaf_type'] = 'polypeptide'; + $options['leaf_type'] = 'taxonomy'; } if (!array_key_exists('match', $options)) { $options['match'] = 'name'; @@ -1007,11 +1051,9 @@ function chado_phylogeny_import_tree_file($file_name, $format, $options = [], $j // required fields for creating a tree. if (!array_key_exists('phylotree_id', $options)) { if (!array_key_exists('name', $options)) { - // tripal_report_error($options['message_type'], TRIPAL_ERROR, - // 'The phylotree_id is required for importing the tree.', [], $options['message_opts']); \Drupal::service('tripal.logger')->error( 'The phylotree_id is required for importing the tree.' - ); + ); return FALSE; } } @@ -1021,12 +1063,9 @@ function chado_phylogeny_import_tree_file($file_name, $format, $options = [], $j $phylotree = chado_generate_var('phylotree', $values, [], $schema_name); if (!$phylotree) { - // tripal_report_error($options['message_type'], TRIPAL_ERROR, - // 'Could not find the phylotree using the ID provided: %phylotree_id.', - // ['%phylotree_id' => $options['phylotree_id']], $options['message_opts']); \Drupal::service('tripal.logger')->error( - 'Could not find the phylotree using the ID provided: ' . $options['phylotree_id'] . '.' - ); + 'Could not find the phylotree using the ID provided: %phylotree_id.', + ['%phylotree_id' => $options['phylotree_id']], $options['message_opts']); return FALSE; } @@ -1034,9 +1073,9 @@ function chado_phylogeny_import_tree_file($file_name, $format, $options = [], $j $chado = \Drupal::service('tripal_chado.database'); $chado->setSchemaName($schema_name); $transaction_chado = $chado->startTransaction(); - print "\nNOTE: Loading of this tree file is performed using a database transaction. \n" . - "If the load fails or is terminated prematurely then the entire set of \n" . - "insertions/updates is rolled back and will not be found in the database\n\n"; + // print "\nNOTE: Loading of this tree file is performed using a database transaction. \n" . + // "If the load fails or is terminated prematurely then the entire set of \n" . + // "insertions/updates is rolled back and will not be found in the database\n\n"; try { @@ -1046,11 +1085,13 @@ function chado_phylogeny_import_tree_file($file_name, $format, $options = [], $j // To be upgraded at a later time // // Parse the tree into the expected nested node format. + // echo "Parsing newick file...\n"; + // T3 OLD CODE // module_load_include('inc', 'tripal_chado', 'includes/loaders/tripal_chado.phylotree_newick'); - // $tree = tripal_phylogeny_parse_newick_file($file_name); - + // T4 - this file is added as an API file + $tree = tripal_phylogeny_parse_newick_file($file_name); // // Assign the right and left indices to the tree nodes. - // chado_assign_phylogeny_tree_indices($tree); + chado_assign_phylogeny_tree_indices($tree); } // Iterate through the tree nodes and add them to Chado in accordance diff --git a/tripal_chado/src/api/tripal_chado.phylotree_newick.api.php b/tripal_chado/src/api/tripal_chado.phylotree_newick.api.php new file mode 100644 index 000000000..e82dbb082 --- /dev/null +++ b/tripal_chado/src/api/tripal_chado.phylotree_newick.api.php @@ -0,0 +1,344 @@ + Subtree ";" | Branch ";" + * Subtree --> Leaf | Internal + * Leaf --> Name + * Internal --> "(" BranchSet ")" Name + * BranchSet --> Branch | BranchSet "," Branch + * Branch --> Subtree Length + * Name --> empty | string + * Length --> empty | ":" number + * + */ + +/** + * + * @param unknown $file_name + */ +function tripal_phylogeny_parse_newick_file($file_name) { + + // Initialize the bootstrap value and index + global $tripal_phylogeny_bootstrap; + + $tripal_phylogeny_bootstrap = 1; + $tripal_phylogeny_index = 1; + + $tree = []; + + $fp = fopen($file_name, 'r'); + if ($fp) { + $tree = tripal_phylogeny_parse_newick_tree($fp); + } + else { + // ERROR + } + return $tree; +} + +/** + * + * @param unknown $fp + * @param number $depth + * + * @return boolean + */ +function tripal_phylogeny_parse_newick_tree($fp, $depth = 0) { + + $subtree = tripal_phylogeny_parse_newick_subtree($fp, $depth); + $subtree['is_root'] = 1; + + // this subtree may also be a branch. A branch is a subtree with a length, + // so see if there is a length + $token = tripal_phylogeny_parse_newick_get_token($fp); + if ($token == ";") { + // we're done! + return $subtree; + } + tripal_phylogeny_parse_newick_replace_token($fp); + + // Get the length. + $length = tripal_phylogeny_parse_newick_length($fp, $depth); + $subtree['length'] = $length; + + // Now if we're missing the semicolon we have a syntax error. + $token = tripal_phylogeny_parse_newick_get_token($fp); + if ($token != ';') { + print "Syntax Error: missing trailing semicolon.\n"; + exit; + } + + return $subtree; +} + +/** + * + * @param unknown $fp + * @param unknown $depth + * + * @return Ambigous|unknown + */ +function tripal_phylogeny_parse_newick_subtree($fp, $depth) { + + $internal = tripal_phylogeny_parse_newick_internal($fp, $depth + 1); + if (!is_array($internal)) { + $leaf_node = tripal_phylogeny_parse_newick_leaf($fp, $depth); + return [ + 'name' => $leaf_node, + 'depth' => $depth, + 'is_leaf' => TRUE, + 'descendents' => 0, + ]; + } + else { + $internal['depth'] = $depth; + } + return $internal; +} + +/** + * + * @param unknown $fp + * @param unknown $depth + * + * @return boolean|multitype:unknown Ambigous + */ +function tripal_phylogeny_parse_newick_branch($fp, $depth) { + + $subtree = tripal_phylogeny_parse_newick_subtree($fp, $depth); + $length = tripal_phylogeny_parse_newick_length($fp, $depth); + + $subtree['length'] = $length; + return $subtree; +} + +/** + * + * @param unknown $fp + * @param unknown $parent + * @param unknown $depth + */ +function tripal_phylogeny_parse_newick_internal($fp, $depth) { + + // If the next character is not an open paren then this is an internal node + if (tripal_phylogeny_parse_newick_get_token($fp) != '(') { + tripal_phylogeny_parse_newick_replace_token($fp); + return FALSE; + } + + $branches = tripal_phylogeny_parse_newick_branchset($fp, $depth); + if (!is_array($branches)) { + return FALSE; + } + // If we don't have a closing parent then this is a syntax error. + if (tripal_phylogeny_parse_newick_get_token($fp) != ')') { + tripal_phylogeny_parse_newick_replace_token($fp); + return FALSE; + } + $internal_node = tripal_phylogeny_parse_newick_name($fp, $depth); + $descendent_count = 0; + for ($i = 0; $i < count($branches); $i++) { + $branches[$i]['parent'] = $internal_node; + $descendent_count += 1 + $branches[$i]['descendents']; + } + + return [ + 'name' => $internal_node, + 'depth' => $depth, + 'branch_set' => $branches, + 'is_internal' => TRUE, + 'descendents' => $descendent_count, + ]; +} + +/** + * + * @param unknown $fp + * @param unknown $parent + * @param unknown $depth + */ +function tripal_phylogeny_parse_newick_branchset($fp, $depth) { + $branches = []; + + $num_read = 0; + $branch = tripal_phylogeny_parse_newick_branch($fp, $depth); + $branches[] = $branch; + + // If it's not a branch then return false, a branchset will + // always appear as a branch. + if (!is_array($branch)) { + return FALSE; + } + + // If we have a comma as the next token then this is + // a branchset and we should recurse. + $token = tripal_phylogeny_parse_newick_get_token($fp); + if ($token == ',') { + $rbranches = tripal_phylogeny_parse_newick_branchset($fp, $depth); + foreach ($rbranches as $branch) { + $branches[] = $branch; + } + } + else { + tripal_phylogeny_parse_newick_replace_token($fp); + } + + return $branches; +} + +/** + * + * @param unknown $fp + * @param unknown $depth + * + * @return Ambigous + */ +function tripal_phylogeny_parse_newick_leaf($fp, $depth) { + return tripal_phylogeny_parse_newick_name($fp, $depth); +} + +/** + * + * @param unknown $fp + * @param unknown $depth + * + * @return string|boolean|Ambigous + */ +function tripal_phylogeny_parse_newick_name($fp, $depth) { + global $tripal_phylogeny_bootstrap; + + $token = tripal_phylogeny_parse_newick_get_token($fp); + + // If the next token is a colon, semicolon, close paren, or comma + // then the name is empty. + if ($token == ':' or $token == ',' or $token == ';' or $token == ')') { + tripal_phylogeny_parse_newick_replace_token($fp); + // create a bootstrap value + return $tripal_phylogeny_bootstrap++; + } + + // If the next token is an open paren then this is a syntax error: + if ($token == '(') { + tripal_phylogeny_parse_newick_replace_token($fp); + return FALSE; + } + return $token; +} + +/** + * + * @param unknown $fp + * @param unknown $depth + * + * @return string|boolean|unknown + */ +function tripal_phylogeny_parse_newick_length($fp, $depth) { + $length = ''; + + $token = tripal_phylogeny_parse_newick_get_token($fp); + + // If the next token is a semicolon, close paren, or comma + // then the length is empty. + if ($token == ',' or $token == ';' or $token == ')') { + tripal_phylogeny_parse_newick_replace_token($fp); + return ''; + } + + // If the next token is a colon then we are parsing the length. + // Otherwise we are not. + if ($token != ':') { + tripal_phylogeny_parse_newick_replace_token($fp); + return FALSE; + } + + // Now get the length. + $token = tripal_phylogeny_parse_newick_get_token($fp); + + // If the next token is an open paren then this is a syntax error: + if ($token == '(') { + exit(); + } + + return $token; +} + +/** + * + * @param unknown $fp + * + * @return string + */ +function tripal_phylogeny_parse_newick_get_token($fp) { + + // Keep track of the file position that we start with + global $tripal_phylogeny_fp_pos; + $tripal_phylogeny_fp_pos = ftell($fp); + + $token = ''; + $in_quote = FALSE; + $num_read = 0; + + $c = fgetc($fp); + while (!feof($fp)) { + $num_read++; + + switch ($c) { + // If the first character is a reserved character and we + // we have not encountered any other charcters then return + // it as the token. Otherwise, return the collected token. + case ';': + case '(': + case ')': + case ',': + case ':': + if (!$token) { + return $c; + } + else { + // put the character back and return the token + fseek($fp, $tripal_phylogeny_fp_pos + $num_read - 1); + return $token; + } + + break; + // Quotes are allowed around names and if a name is in + // quotes then allow spaces. Otherwise, spaces are ignored. + case '\'': + case '"': + if (!$in_quote) { + $in_quote = TRUE; + } + else { + $in_quote = FALSE; + } + break; + case " ": + case "\t": + case "\r": + case "\n": + if ($in_quote) { + $token .= $c; + } + break; + // All other characters get saved as the token + default: + $token .= $c; + } + $c = fgetc($fp); + } + return $token; +} + +/** + * + * @param unknown $fp + */ +function tripal_phylogeny_parse_newick_replace_token($fp) { + global $tripal_phylogeny_fp_pos; + + fseek($fp, $tripal_phylogeny_fp_pos); + $tripal_phylogeny_fp_pos = ftell($fp); +} \ No newline at end of file diff --git a/tripal_chado/src/api/tripal_chado.query.api.php b/tripal_chado/src/api/tripal_chado.query.api.php index 4b5e6f43c..46b29469b 100755 --- a/tripal_chado/src/api/tripal_chado.query.api.php +++ b/tripal_chado/src/api/tripal_chado.query.api.php @@ -402,7 +402,7 @@ function chado_set_active($dbname = 'default', $chado_schema_name = NULL) { * * Use this function to insert a record into any Chado table. The first * argument specifies the table for inserting and the second is an array - * of values to be inserted. The array is mutli-dimensional such that + * of values to be inserted. The array is multi-dimensional such that * foreign key lookup values can be specified. * * @param $table @@ -651,7 +651,7 @@ function chado_insert_record($table, $values, $options = [], $chado_schema_name $ivalues = []; // Contains the values of the fields. foreach ($insert_values as $field => $value) { $ifields[] = $field; - if (strcmp($value, '__NULL__') == 0) { + if (is_string($value) and (strcmp($value, '__NULL__') == 0)) { $itypes[] = "NULL"; } else { @@ -706,7 +706,7 @@ function chado_insert_record($table, $values, $options = [], $chado_schema_name * Use this function to update a record in any Chado table. The first * argument specifies the table for inserting, the second is an array * of values to matched for locating the record for updating, and the third - * argument give the values to update. The arrays are mutli-dimensional such + * argument give the values to update. The arrays are multi-dimensional such * that foreign key lookup values can be specified. * * @param string $table @@ -917,7 +917,7 @@ function chado_update_record($table, $match, $values, $options = NULL, $chado_sc $sql = 'UPDATE {' . $table . '} SET '; $args = []; // Arguments passed to chado_query. foreach ($update_values as $field => $value) { - if (strcmp($value, '__NULL__') == 0) { + if (is_string($value) and (strcmp($value, '__NULL__') == 0)) { $sql .= " $field = NULL, "; } else { @@ -929,7 +929,7 @@ function chado_update_record($table, $match, $values, $options = NULL, $chado_sc $sql .= " WHERE "; foreach ($update_matches as $field => $value) { - if (strcmp($value, '__NULL__') == 0) { + if (is_string($value) and (strcmp($value, '__NULL__') == 0)) { $sql .= " $field = NULL AND "; } else { @@ -980,7 +980,7 @@ function chado_update_record($table, $match, $values, $options = NULL, $chado_sc * Use this function to delete a record(s) in any Chado table. The first * argument specifies the table to delete from and the second is an array * of values to match for locating the record(s) to be deleted. The arrays - * are mutli-dimensional such that foreign key lookup values can be specified. + * are multi-dimensional such that foreign key lookup values can be specified. * * @param string $table * The name of the chado table for inserting. @@ -1115,7 +1115,7 @@ function chado_delete_record($table, $match, $options = NULL, $chado_schema_name $sql .= ") AND "; } else { - if (strcmp($value, '__NULL__') == 0) { + if (is_string($value) and (strcmp($value, '__NULL__') == 0)) { $sql .= " $field = NULL AND "; } else { @@ -1127,6 +1127,7 @@ function chado_delete_record($table, $match, $options = NULL, $chado_schema_name $sql = mb_substr($sql, 0, -4); // Get rid of the trailing 'AND'. // Finally perform the delete. If successful, return the updated record. + // RISH [8/27/2023] - I think the above comment is incorrect, it returns status only ie. TRUE OR FALSE $result = chado_query($sql, $args, [], $chado_schema_name); if ($result) { return TRUE; @@ -1765,7 +1766,7 @@ function chado_query($sql, $args = [], $options = [], $chado_schema_name = NULL) } // -- Args should be an array. if (!is_array($args)) { - $msg = t('chado_query; Arguements should be an array. Query: @query; Arguements: @values', + $msg = t('chado_query; Arguments should be an array. Query: @query; Arguments: @values', ['@values' => print_r($args, TRUE), '@query' => $sql]); \Drupal::logger('tripal_chado')->error($msg); return FALSE; @@ -1776,7 +1777,7 @@ function chado_query($sql, $args = [], $options = [], $chado_schema_name = NULL) $tokens_in_sql = $matches[0]; $tokens_in_args = array_keys($args); if (count($tokens_in_sql) !== count($tokens_in_args)) { - $msg = t('chado_query; There should be the same number of tokens in the arguements as in the SQL. Tokens provided: @args, Tokens in SQL: @sql', + $msg = t('chado_query; There should be the same number of tokens in the arguments as in the SQL. Tokens provided: @args, Tokens in SQL: @sql', ['@args' => print_r($tokens_in_args,TRUE), '@sql' => print_r($tokens_in_sql,TRUE)]); \Drupal::logger('tripal_chado')->error($msg); return FALSE; diff --git a/tripal_chado/src/api/tripal_chado.schema.api.php b/tripal_chado/src/api/tripal_chado.schema.api.php index aa27bae5a..94fab0245 100644 --- a/tripal_chado/src/api/tripal_chado.schema.api.php +++ b/tripal_chado/src/api/tripal_chado.schema.api.php @@ -39,7 +39,7 @@ * Check that any given Chado table exists. * * This function is necessary because Drupal's db_table_exists() function will - * not look in any other schema but the one were Drupal is installed + * not look in any other schema but the one where Drupal is installed * * @param string $table * The name of the chado table whose existence should be checked. @@ -69,7 +69,7 @@ function chado_table_exists($table, $chado_schema = NULL) { * Check that any given column in a Chado table exists. * * This function is necessary because Drupal's db_field_exists() will not - * look in any other schema but the one were Drupal is installed + * look in any other schema but the one where Drupal is installed * * @param string $table * The name of the chado table. @@ -102,7 +102,7 @@ function chado_column_exists($table, $column, $chado_schema = NULL) { * Check that any given column in a Chado table exists. * * This function is necessary because Drupal's db_field_exists() will not - * look in any other schema but the one were Drupal is installed + * look in any other schema but the one where Drupal is installed * * @param string $sequence * The name of the sequence. @@ -557,7 +557,7 @@ function chado_get_custom_table_schema($table, $chado_schema = NULL) { * include linker tables (which link two or more base tables), property tables, * and relationship tables. These provide additional information about * primary data records and are therefore not base tables. This function - * retreives only the list of tables that are considered 'base' tables. + * retrieves only the list of tables that are considered 'base' tables. * * @param string $chado_schema * The chado schema you are interested in. @@ -605,6 +605,7 @@ function chado_get_base_tables($chado_schema = NULL) { * * @upgrade * + */ function chado_get_cvterm_mapping($params) { $cvterm_id = array_key_exists('cvterm_id', $params) ? $params['cvterm_id'] : NULL; $vocabulary = array_key_exists('vocabulary', $params) ? $params['vocabulary'] : NULL; @@ -641,4 +642,3 @@ function chado_get_cvterm_mapping($params) { } return NULL; } -*/ diff --git a/tripal_chado/src/api/tripal_chado.variables.api.php b/tripal_chado/src/api/tripal_chado.variables.api.php index 96f0b0d46..15938e90a 100644 --- a/tripal_chado/src/api/tripal_chado.variables.api.php +++ b/tripal_chado/src/api/tripal_chado.variables.api.php @@ -1027,7 +1027,7 @@ function chado_expand_var($object, $type, $to_expand, $table_options = [], $sche if (property_exists($object, 'expanded')) { // If so, then remove the expanded identifier from the correct expandable - // array.. + // array. $expandable_name = 'expandable_' . $type . 's'; if (property_exists($object, $expandable_name) and $object->{$expandable_name}) { $key_to_remove = array_search($object->expanded, $object->{$expandable_name}); diff --git a/tripal_chado/tests/fixtures/fill_chado_test_prepare.sql b/tripal_chado/tests/fixtures/fill_chado_test_prepare.sql index 02c403100..05f14b268 100644 --- a/tripal_chado/tests/fixtures/fill_chado_test_prepare.sql +++ b/tripal_chado/tests/fixtures/fill_chado_test_prepare.sql @@ -3603,7 +3603,7 @@ INSERT INTO chado.cvterm VALUES (42, 11, 'assay', 'A planned process with the ob INSERT INTO chado.cvterm VALUES (43, 12, 'location on map', '', 43, 0, 0); INSERT INTO chado.cvterm VALUES (44, 13, 'definition', 'The official OBI definition, explaining the meaning of a class or property. Shall be Aristotelian, formalized and normalized. Can be augmented with colloquial definitions.', 44, 0, 0); INSERT INTO chado.cvterm VALUES (45, 13, 'version number', 'A version number is an information content entity which is a sequence of characters borne by part of each of a class of manufactured products or its packaging and indicates its order within a set of other products having the same name.', 45, 0, 0); -INSERT INTO chado.cvterm VALUES (46, 13, 'algorithm', 'An algorithm is a set of instructions for performing a paticular calculation.', 46, 0, 0); +INSERT INTO chado.cvterm VALUES (46, 13, 'algorithm', 'An algorithm is a set of instructions for performing a particular calculation.', 46, 0, 0); INSERT INTO chado.cvterm VALUES (47, 2, 'property', 'A generic term indicating that represents an attribute, quality or characteristic of something.', 47, 0, 0); INSERT INTO chado.cvterm VALUES (48, 2, 'time_last_modified', 'The time at which the record was last modified.', 48, 0, 0); INSERT INTO chado.cvterm VALUES (49, 2, 'time_accessioned', 'The time at which the record was first added.', 49, 0, 0); diff --git a/tripal_chado/tests/fixtures/newick_loader/mrna_mini.fasta.tree b/tripal_chado/tests/fixtures/newick_loader/mrna_mini.fasta.tree new file mode 100644 index 000000000..775f12348 --- /dev/null +++ b/tripal_chado/tests/fixtures/newick_loader/mrna_mini.fasta.tree @@ -0,0 +1,401 @@ +(((((((((((((((((((((( +1_FRAEX38873_v2_000000010_1 +:0.00000, +2_FRAEX38873_v2_000000010_2 +:0.00000):0.90014,((( +37_FRAEX38873_v2_000000330_1 +:0.85712, +96_FRAEX38873_v2_000000820_1 +:0.85712):0.01673, +94_FRAEX38873_v2_000000800_1 +:0.87385):0.01036,( +98_FRAEX38873_v2_000000850_1 +:0.00336, +99_FRAEX38873_v2_000000850_2 +:0.00336):0.88084):0.01594):0.01266,(( +55_FRAEX38873_v2_000000480_1 +:0.02838, +56_FRAEX38873_v2_000000480_2 +:0.02838):0.88196,( +69_FRAEX38873_v2_000000600_1 +:0.89583,( +80_FRAEX38873_v2_000000690_1 +:0.87371, +179_FRAEX38873_v2_000001500_1 +:0.87371):0.02213):0.01451):0.00246):0.00491, +124_FRAEX38873_v2_000001050_1 +:0.91771):0.00306,((((((((((((((((( +3_FRAEX38873_v2_000000020_1 +:0.86428,( +140_FRAEX38873_v2_000001180_1 +:0.80559, +143_FRAEX38873_v2_000001210_1 +:0.80559):0.05869):0.00347,( +142_FRAEX38873_v2_000001200_1 +:0.83521,( +161_FRAEX38873_v2_000001360_1 +:0.00000, +162_FRAEX38873_v2_000001360_2 +:0.00000):0.83521):0.03254):0.00724,( +75_FRAEX38873_v2_000000660_1 +:0.07186, +76_FRAEX38873_v2_000000660_2 +:0.07186):0.80313):0.00725,( +153_FRAEX38873_v2_000001300_1 +:0.07102, +154_FRAEX38873_v2_000001300_2 +:0.07102):0.81122):0.00601, +110_FRAEX38873_v2_000000910_1 +:0.88824):0.00439,( +127_FRAEX38873_v2_000001080_1 +:0.00000, +128_FRAEX38873_v2_000001080_2 +:0.00000):0.89264):0.00192,(((((((((( +4_FRAEX38873_v2_000000030_1 +:0.83582, +187_FRAEX38873_v2_000001580_1 +:0.83582):0.02208, +181_FRAEX38873_v2_000001520_1 +:0.85790):0.01732,( +60_FRAEX38873_v2_000000520_1 +:0.81872, +133_FRAEX38873_v2_000001110_1 +:0.81872):0.05650):0.00419, +126_FRAEX38873_v2_000001070_1 +:0.87940):0.00192,(( +66_FRAEX38873_v2_000000570_1 +:0.84863,((( +84_FRAEX38873_v2_000000740_1 +:0.00000, +85_FRAEX38873_v2_000000740_2 +:0.00000):0.00000, +86_FRAEX38873_v2_000000740_3 +:0.00000):0.00000, +87_FRAEX38873_v2_000000740_4 +:0.00000):0.84863):0.02904, +100_FRAEX38873_v2_000000860_1 +:0.87767):0.00366):0.00380,( +30_FRAEX38873_v2_000000260_1 +:0.88298,( +184_FRAEX38873_v2_000001560_1 +:0.87897, +188_FRAEX38873_v2_000001600_1 +:0.87897):0.00401):0.00214):0.00426,(( +10_FRAEX38873_v2_000000090_1 +:0.88371,((( +33_FRAEX38873_v2_000000290_1 +:0.44856, +34_FRAEX38873_v2_000000300_1 +:0.44856):0.40044, +81_FRAEX38873_v2_000000700_1 +:0.84900):0.02484, +95_FRAEX38873_v2_000000810_1 +:0.87384):0.00987):0.00196, +191_FRAEX38873_v2_000001630_1 +:0.88567):0.00372):0.00110,((( +6_FRAEX38873_v2_000000050_1 +:0.81426, +160_FRAEX38873_v2_000001350_1 +:0.81426):0.05414, +43_FRAEX38873_v2_000000380_1 +:0.86840):0.01900,((( +8_FRAEX38873_v2_000000070_1 +:0.87587, +122_FRAEX38873_v2_000001030_1 +:0.87587):0.00957,(((((((((((((((((((((((((((((((((((( +16_FRAEX38873_v2_000000130_1 +:0.51885, +27_FRAEX38873_v2_000000240_1 +:0.51885):0.02236, +63_FRAEX38873_v2_000000550_1 +:0.54121):0.01613, +22_FRAEX38873_v2_000000190_1 +:0.55733):0.04423, +97_FRAEX38873_v2_000000830_1 +:0.60156):0.01795, +192_FRAEX38873_v2_000001640_1 +:0.61951):0.02593, +45_FRAEX38873_v2_000000400_1 +:0.64544):0.02229,((((( +17_FRAEX38873_v2_000000140_1 +:0.37273, +18_FRAEX38873_v2_000000150_1 +:0.37273):0.09780, +21_FRAEX38873_v2_000000180_1 +:0.47052):0.00245, +19_FRAEX38873_v2_000000160_1 +:0.47298):0.08709, +20_FRAEX38873_v2_000000170_1 +:0.56007):0.08768, +139_FRAEX38873_v2_000001170_1 +:0.64775):0.01998):0.01250, +111_FRAEX38873_v2_000000920_1 +:0.68023):0.01195,(( +163_FRAEX38873_v2_000001370_1 +:0.60715, +196_FRAEX38873_v2_000001680_1 +:0.60715):0.06009, +193_FRAEX38873_v2_000001650_1 +:0.66724):0.02493):0.03076, +36_FRAEX38873_v2_000000320_1 +:0.72293):0.01024,( +28_FRAEX38873_v2_000000250_1 +:0.00000, +29_FRAEX38873_v2_000000250_2 +:0.00000):0.73317):0.01360,( +105_FRAEX38873_v2_000000890_1 +:0.02584, +106_FRAEX38873_v2_000000890_2 +:0.02584):0.72093):0.00637,( +166_FRAEX38873_v2_000001390_1 +:0.74274,( +170_FRAEX38873_v2_000001420_1 +:0.00257, +171_FRAEX38873_v2_000001420_2 +:0.00257):0.74017):0.01040):0.00343, +113_FRAEX38873_v2_000000940_1 +:0.75658):0.00742, +74_FRAEX38873_v2_000000650_1 +:0.76399):0.01038, +135_FRAEX38873_v2_000001130_1 +:0.77437):0.01316, +104_FRAEX38873_v2_000000880_1 +:0.78753):0.00870, +149_FRAEX38873_v2_000001260_1 +:0.79623):0.00769, +23_FRAEX38873_v2_000000200_1 +:0.80392):0.00673, +26_FRAEX38873_v2_000000230_1 +:0.81065):0.00566, +48_FRAEX38873_v2_000000420_1 +:0.81631):0.00505, +197_FRAEX38873_v2_000001690_1 +:0.82136):0.01007, +51_FRAEX38873_v2_000000450_1 +:0.83143):0.00764,( +49_FRAEX38873_v2_000000430_1 +:0.83376, +176_FRAEX38873_v2_000001460_1 +:0.83376):0.00531):0.00427,((( +77_FRAEX38873_v2_000000670_1 +:0.00000, +78_FRAEX38873_v2_000000670_2 +:0.00000):0.80178,(( +101_FRAEX38873_v2_000000870_1 +:0.00000, +102_FRAEX38873_v2_000000870_2 +:0.00000):0.00000, +103_FRAEX38873_v2_000000870_3 +:0.00000):0.80178):0.03397,( +88_FRAEX38873_v2_000000760_1 +:0.00521,( +89_FRAEX38873_v2_000000760_2 +:0.00283, +90_FRAEX38873_v2_000000760_3 +:0.00283):0.00238):0.83054):0.00759):0.00404, +167_FRAEX38873_v2_000001400_1 +:0.84738):0.00470,( +67_FRAEX38873_v2_000000580_1 +:0.79862, +68_FRAEX38873_v2_000000590_1 +:0.79862):0.05346):0.00442, +200_FRAEX38873_v2_000001730_1 +:0.85650):0.00607, +58_FRAEX38873_v2_000000500_1 +:0.86257):0.00323,( +168_FRAEX38873_v2_000001410_1 +:0.00856, +169_FRAEX38873_v2_000001410_2 +:0.00856):0.85723):0.00291,( +116_FRAEX38873_v2_000000970_1 +:0.86471, +198_FRAEX38873_v2_000001700_1 +:0.86471):0.00400):0.00373,( +157_FRAEX38873_v2_000001330_1 +:0.00000, +158_FRAEX38873_v2_000001330_2 +:0.00000):0.87244):0.00258,(( +114_FRAEX38873_v2_000000950_1 +:0.85388,( +120_FRAEX38873_v2_000001010_1 +:0.83733, +137_FRAEX38873_v2_000001150_1 +:0.83733):0.01656):0.01867, +175_FRAEX38873_v2_000001450_1 +:0.87255):0.00246):0.00077,((( +144_FRAEX38873_v2_000001220_1 +:0.00000, +145_FRAEX38873_v2_000001220_2 +:0.00000):0.86919,( +164_FRAEX38873_v2_000001380_1 +:0.03022, +165_FRAEX38873_v2_000001380_2 +:0.03022):0.83897):0.00358, +194_FRAEX38873_v2_000001660_1 +:0.87277):0.00301):0.00301, +35_FRAEX38873_v2_000000310_1 +:0.87879):0.00414,(( +107_FRAEX38873_v2_000000900_1 +:0.00000, +108_FRAEX38873_v2_000000900_2 +:0.00000):0.00000, +109_FRAEX38873_v2_000000900_3 +:0.00000):0.88293):0.00251):0.00089,(( +39_FRAEX38873_v2_000000350_1 +:0.76765,( +40_FRAEX38873_v2_000000360_1 +:0.05224, +41_FRAEX38873_v2_000000360_2 +:0.05224):0.71541):0.02483, +42_FRAEX38873_v2_000000370_1 +:0.79248):0.09385):0.00107):0.00308):0.00086,(( +46_FRAEX38873_v2_000000410_1 +:0.00000, +47_FRAEX38873_v2_000000410_2 +:0.00000):0.86210, +148_FRAEX38873_v2_000001250_1 +:0.86210):0.02925):0.00255,( +62_FRAEX38873_v2_000000540_1 +:0.88143, +123_FRAEX38873_v2_000001040_1 +:0.88143):0.01247):0.00066):0.00108, +156_FRAEX38873_v2_000001320_1 +:0.89564):0.00306, +9_FRAEX38873_v2_000000080_1 +:0.89869):0.00069,(( +64_FRAEX38873_v2_000000560_1 +:0.00000, +65_FRAEX38873_v2_000000560_2 +:0.00000):0.89777,( +72_FRAEX38873_v2_000000640_1 +:0.00000, +73_FRAEX38873_v2_000000640_2 +:0.00000):0.89777):0.00162):0.00247,(( +7_FRAEX38873_v2_000000060_1 +:0.88798, +146_FRAEX38873_v2_000001230_1 +:0.88798):0.00563, +92_FRAEX38873_v2_000000780_1 +:0.89361):0.00825):0.00263,(((( +117_FRAEX38873_v2_000000980_1 +:0.87786, +178_FRAEX38873_v2_000001480_1 +:0.87786):0.01623,(( +138_FRAEX38873_v2_000001160_1 +:0.88194, +172_FRAEX38873_v2_000001430_1 +:0.88194):0.00718, +189_FRAEX38873_v2_000001610_1 +:0.88912):0.00496):0.00196, +177_FRAEX38873_v2_000001470_1 +:0.89604):0.00529, +190_FRAEX38873_v2_000001620_1 +:0.90133):0.00316):0.00290, +134_FRAEX38873_v2_000001120_1 +:0.90739):0.00351, +5_FRAEX38873_v2_000000040_1 +:0.91090):0.00198,( +112_FRAEX38873_v2_000000930_1 +:0.90383,( +185_FRAEX38873_v2_000001570_1 +:0.00000, +186_FRAEX38873_v2_000001570_2 +:0.00000):0.90383):0.00905):0.00290, +141_FRAEX38873_v2_000001190_1 +:0.91578):0.00286,( +13_FRAEX38873_v2_000000110_1 +:0.00000, +14_FRAEX38873_v2_000000110_2 +:0.00000):0.91863):0.00214):0.00046,( +61_FRAEX38873_v2_000000530_1 +:0.90311, +195_FRAEX38873_v2_000001670_1 +:0.90311):0.01813):0.00045,( +59_FRAEX38873_v2_000000510_1 +:0.91518, +121_FRAEX38873_v2_000001020_1 +:0.91518):0.00650):0.00315,(((( +57_FRAEX38873_v2_000000490_1 +:0.89185, +183_FRAEX38873_v2_000001550_1 +:0.89185):0.01060, +115_FRAEX38873_v2_000000960_1 +:0.90245):0.01559,( +173_FRAEX38873_v2_000001440_1 +:0.07981, +174_FRAEX38873_v2_000001440_2 +:0.07981):0.83823):0.00334, +155_FRAEX38873_v2_000001310_1 +:0.92139):0.00345):0.00272,( +82_FRAEX38873_v2_000000720_1 +:0.28765, +83_FRAEX38873_v2_000000730_1 +:0.28765):0.63991):0.00165,( +25_FRAEX38873_v2_000000220_1 +:0.89675,(( +129_FRAEX38873_v2_000001090_1 +:0.00000, +130_FRAEX38873_v2_000001090_2 +:0.00000):0.00000, +131_FRAEX38873_v2_000001090_3 +:0.00000):0.89675):0.03246):0.00299,( +118_FRAEX38873_v2_000000990_1 +:0.70993, +119_FRAEX38873_v2_000001000_1 +:0.70993):0.22227):0.00088,(((( +11_FRAEX38873_v2_000000100_1 +:0.00000, +12_FRAEX38873_v2_000000100_2 +:0.00000):0.92809,( +93_FRAEX38873_v2_000000790_1 +:0.92573,(( +125_FRAEX38873_v2_000001060_1 +:0.89135, +159_FRAEX38873_v2_000001340_1 +:0.89135):0.02220, +199_FRAEX38873_v2_000001710_1 +:0.91355):0.01219):0.00235):0.00292,( +24_FRAEX38873_v2_000000210_1 +:0.92631, +152_FRAEX38873_v2_000001290_1 +:0.92631):0.00470):0.00064, +151_FRAEX38873_v2_000001280_1 +:0.93165):0.00142):0.00435,( +54_FRAEX38873_v2_000000470_1 +:0.93639, +70_FRAEX38873_v2_000000610_1 +:0.93639):0.00104):0.00522, +132_FRAEX38873_v2_000001100_1 +:0.94264):0.00343, +182_FRAEX38873_v2_000001530_1 +:0.94607):0.00493, +50_FRAEX38873_v2_000000440_1 +:0.95101):0.00270,( +38_FRAEX38873_v2_000000340_1 +:0.94767,(( +71_FRAEX38873_v2_000000630_1 +:0.92179, +91_FRAEX38873_v2_000000770_1 +:0.92179):0.02092, +136_FRAEX38873_v2_000001140_1 +:0.94271):0.00496):0.00604):0.00258,( +15_FRAEX38873_v2_000000120_1 +:0.95194, +150_FRAEX38873_v2_000001270_1 +:0.95194):0.00435):0.00506, +79_FRAEX38873_v2_000000680_1 +:0.96135):0.00385,( +31_FRAEX38873_v2_000000270_1 +:0.95488,(( +52_FRAEX38873_v2_000000460_1 +:0.00518, +53_FRAEX38873_v2_000000460_2 +:0.00518):0.93948, +180_FRAEX38873_v2_000001510_1 +:0.94466):0.01022):0.01033):0.00444,( +32_FRAEX38873_v2_000000280_1 +:0.93405, +147_FRAEX38873_v2_000001240_1 +:0.93405):0.03560):0.01889, +44_FRAEX38873_v2_000000390_1 +:0.98854); diff --git a/tripal_chado/tests/fixtures/newick_loader/newick_T92076.tree b/tripal_chado/tests/fixtures/newick_loader/newick_T92076.tree new file mode 100644 index 000000000..a5ed0fc73 --- /dev/null +++ b/tripal_chado/tests/fixtures/newick_loader/newick_T92076.tree @@ -0,0 +1 @@ +(S18540_Klotzschia_glaziovii:0.0179925,((S18540_Asteriscium_chilense:0.003417,S18540_Gymnophyton_polycephalum:0.005845,S18540_Pozoa_volcanica:0.003741):0.009093,(S18540_Oschatzia_cuneifolia:0.014932,((S18540_Domeykoa_oppositifolia:0.001578,(S18540_Eremocharis_fruticosa:3.05E-4,S18540_Domeykoa_perennis:0.001399):6.96E-4):0.00291,((S18540_Domeykoa_amplexicaulis:0.001964,S18540_Domeykoa_andina:0.003436):0.001517,(S18540_Eremocharis_longiramea:2.94E-4,S18540_Eremocharis_tripartita:2.93E-4,S18540_Eremocharis_triradiata:6.93E-4):0.002017):0.002247):0.008915):0.002895):0.0179925); diff --git a/tripal_chado/tests/src/Functional/ChadoTestBrowserBase.php b/tripal_chado/tests/src/Functional/ChadoTestBrowserBase.php index 0967a60ab..6de44f671 100644 --- a/tripal_chado/tests/src/Functional/ChadoTestBrowserBase.php +++ b/tripal_chado/tests/src/Functional/ChadoTestBrowserBase.php @@ -6,7 +6,7 @@ use Drupal\tripal_chado\Database\ChadoConnection; /** - * This is a base class for Chado tests that need a full Drupal install.. + * This is a base class for Chado tests that need a full Drupal install. * * It enables Chado tests schemas and helper functions to efficiently perform * tests. diff --git a/tripal_chado/tests/src/Functional/Plugin/ChadoStorageTest.php b/tripal_chado/tests/src/Functional/Plugin/ChadoStorageTest.php index 2d918d102..ec701b6b1 100644 --- a/tripal_chado/tests/src/Functional/Plugin/ChadoStorageTest.php +++ b/tripal_chado/tests/src/Functional/Plugin/ChadoStorageTest.php @@ -14,7 +14,7 @@ * * @group Tripal * @group Tripal Chado - * @group Tripal Chado ChadoStorage + * @group ChadoStorage */ class ChadoStorageTest extends ChadoTestBrowserBase { @@ -295,11 +295,10 @@ public function testChadoStorage() { $chado_column = 'value'; $chado_table = 'featureprop'; $base_table = 'feature'; - $chado_column = 'organism_id'; $storage_settings = [ 'storage_plugin_id' => 'chado_storage', 'storage_plugin_settings' => [ - 'base_table' => $chado_table, + 'base_table' => $base_table, ], ]; @@ -317,8 +316,10 @@ public function testChadoStorage() { ]), 'fk_feature_id' => new ChadoIntStoragePropertyType($this->content_type, $field_name, 'fk_feature_id', 'SO:0000110', [ 'action' => 'store_link', - 'chado_table' => 'featureprop', - 'chado_column' => 'feature_id', + 'left_table' => 'feature', + 'left_table_id' => 'feature_id', + 'right_table' => 'featureprop', + 'right_table_id' => 'feature_id', ]), 'type_id' => new ChadoIntStoragePropertyType($this->content_type, $field_name, 'type_id', 'schema:additionalType', [ 'action' => 'store', @@ -380,12 +381,15 @@ public function testChadoStorage() { $values[$field_name][0][$key] = [ 'value' => clone $propertyValues[$key], ]; - $values[$field_name][1][$key] = [ - 'value' => clone $propertyValues[$key], - ]; - $values[$field_name][2][$key] = [ - 'value' => clone $propertyValues[$key], - ]; + + if ($key != 'feature_id') { + $values[$field_name][1][$key] = [ + 'value' => clone $propertyValues[$key], + ]; + $values[$field_name][2][$key] = [ + 'value' => clone $propertyValues[$key], + ]; + } } // We also need to set the featureprop_id for each. $values[$field_name][0]['featureprop_id']['value']->setValue($this->featureprop_id[0]); diff --git a/tripal_chado/tests/src/Functional/Plugin/FASTAImporterTest.php b/tripal_chado/tests/src/Functional/Plugin/FASTAImporterTest.php index d739b78f2..1df4d9bc7 100644 --- a/tripal_chado/tests/src/Functional/Plugin/FASTAImporterTest.php +++ b/tripal_chado/tests/src/Functional/Plugin/FASTAImporterTest.php @@ -18,11 +18,9 @@ /** * Tests for the FASTAImporter class * - * @group Tripal - * @group Tripal Chado - * @group Tripal Chado ChadoStorage - * @group Tripal Chado ChadoStorage Importer - * @group Tripal Chado ChadoStorage Importer FASTAImporter + * @group TripalImporter + * @group ChadoImporter + * @group FASTAImporter */ class FASTAImporterTest extends ChadoTestBrowserBase { @@ -66,7 +64,7 @@ public function testFASTAImporterSimpleTest() 'programversion' => '1.0', ]) ->execute(); - + // Perform the FASTA test by creating an instance of the FASTA loader $importer_manager = \Drupal::service('tripal.importer'); @@ -109,24 +107,24 @@ public function testFASTAImporterSimpleTest() $results_object = $results->fetchObject(); $this->assertEquals($results_object->c1, 1, 'There should have been one feature named orange1.1g017341m'); - + $results = $chado->query("SELECT count(*) as c1 FROM {1:feature} WHERE name = :name", [ ':name' => 'orange1.1g022797m' ]); $results_object = $results->fetchObject(); $this->assertEquals($results_object->c1, 1, 'There should have been one feature named orange1.1g017341m'); - + $results = $chado->query("SELECT count(*) as c1 FROM {1:feature} WHERE name = :name", [ ':name' => 'orange1.1g022799m' ]); $results_object = $results->fetchObject(); $this->assertEquals($results_object->c1, 1, 'There should have been one feature named orange1.1g017341m'); - + // Check the type_id - $results = $chado->query("SELECT * FROM {1:feature} as f - LEFT JOIN {1:cvterm} as cvterm ON (f.type_id = cvterm.cvterm_id) + $results = $chado->query("SELECT * FROM {1:feature} as f + LEFT JOIN {1:cvterm} as cvterm ON (f.type_id = cvterm.cvterm_id) WHERE f.name = :name", [ ':name' => 'orange1.1g022799m' ]); @@ -181,8 +179,8 @@ public function testFASTAImporterSimpleTest() $results_object = $results->fetchObject(); $this->assertEquals($results_object->c1, 1, 'There should have been one feature named scaffold00001'); - - + + } } diff --git a/tripal_chado/tests/src/Functional/Plugin/GFF3ImporterTest.php b/tripal_chado/tests/src/Functional/Plugin/GFF3ImporterTest.php index 70e1285ad..7baac8221 100644 --- a/tripal_chado/tests/src/Functional/Plugin/GFF3ImporterTest.php +++ b/tripal_chado/tests/src/Functional/Plugin/GFF3ImporterTest.php @@ -19,9 +19,9 @@ /** * Tests for the GFF3Importer class * - * @group Tripal - * @group Tripal Chado - * @group Tripal Chado ChadoStorage + * @group TripalImporter + * @group ChadoImporter + * @group GFF3Importer */ class GFF3ImporterTest extends ChadoTestBrowserBase { @@ -34,7 +34,7 @@ class GFF3ImporterTest extends ChadoTestBrowserBase public function testGFFImporterSimpleTest() { // GFF3 Specifications document: http://gmod.org/wiki/GFF3 - + // Public schema connection $public = \Drupal::database(); @@ -70,7 +70,7 @@ public function testGFFImporterSimpleTest() // Verify that gene is now in the cvterm table (which gets imported from SO obo) - $result_gene_cvterm = $chado->query("SELECT * FROM {1:cvterm} + $result_gene_cvterm = $chado->query("SELECT * FROM {1:cvterm} WHERE name = 'gene' LIMIT 1;"); $cvterm_object = null; $cvterm_object = $result_gene_cvterm->fetchObject(); @@ -131,7 +131,7 @@ public function testGFFImporterSimpleTest() unset($results_object); // This checks to ensure the test_gene_001 (gene) feature was inserted into the feature table - $results = $chado->query("SELECT * FROM {1:feature} + $results = $chado->query("SELECT * FROM {1:feature} WHERE uniquename='test_gene_001';"); $results_object = $results->fetchObject(); $gene_feature_id = $results_object->feature_id; @@ -140,7 +140,7 @@ public function testGFFImporterSimpleTest() unset($results_object); // This checks to see whether the test_mrna_001.1 (mrna) feature got inserted into the feature table - $results = $chado->query("SELECT * FROM {1:feature} + $results = $chado->query("SELECT * FROM {1:feature} WHERE uniquename='test_mrna_001.1';"); $results_object = $results->fetchObject(); $mrna_feature_id = $results_object->feature_id; @@ -149,7 +149,7 @@ public function testGFFImporterSimpleTest() unset($results_object); // This checks to see whether the test_protein_001.1 (polypeptide) feature got inserted into the feature table - $results = $chado->query("SELECT * FROM {1:feature} + $results = $chado->query("SELECT * FROM {1:feature} WHERE uniquename='test_protein_001.1';"); $results_object = $results->fetchObject(); $polypeptide_feature_id = $results_object->feature_id; @@ -158,7 +158,7 @@ public function testGFFImporterSimpleTest() unset($results_object); // Do checks on the featureprop table as well. Ensures the bio type value got added - $results = $chado->query("SELECT * FROM {1:featureprop} + $results = $chado->query("SELECT * FROM {1:featureprop} WHERE feature_id = :feature_id AND value LIKE :value;", [ ':feature_id' => $gene_feature_id, ':value' => 'protein_coding' @@ -170,7 +170,7 @@ public function testGFFImporterSimpleTest() // Ensures the GAP value got added - $results = $chado->query("SELECT * FROM {1:featureprop} + $results = $chado->query("SELECT * FROM {1:featureprop} WHERE feature_id = :feature_id AND value LIKE :value;", [ ':feature_id' => $gene_feature_id, ':value' => 'test_gap_1' @@ -181,7 +181,7 @@ public function testGFFImporterSimpleTest() unset($results_object); // Ensures the NOTE value got added - $results = $chado->query("SELECT * FROM {1:featureprop} + $results = $chado->query("SELECT * FROM {1:featureprop} WHERE feature_id = :feature_id AND value LIKE :value;", [ ':feature_id' => $gene_feature_id, ':value' => 'test_gene_001_note' @@ -195,10 +195,10 @@ public function testGFFImporterSimpleTest() /** * Run the GFF loader on gff_duplicate_ids.gff for testing. * - * This tests whether the GFF loader detects duplicate IDs which makes a - * GFF file invalid since IDs should be unique. The GFF loader should throw + * This tests whether the GFF loader detects duplicate IDs which makes a + * GFF file invalid since IDs should be unique. The GFF loader should throw * and exception which this test checks for - */ + */ $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ 'files' => [ @@ -231,14 +231,14 @@ public function testGFFImporterSimpleTest() 'file_local' => __DIR__ . '/../../../fixtures/gff3_loader/gff_duplicate_ids.gff', ]; - + $has_exception = false; try { $gff3_importer->create($run_args, $file_details); $gff3_importer->prepareFiles(); $gff3_importer->run(); $gff3_importer->postRun(); - } + } catch (\Exception $ex) { $message = $ex->getMessage(); $has_exception = true; @@ -249,9 +249,9 @@ public function testGFFImporterSimpleTest() /** * Run the GFF loader on gff_tag_unescaped_character.gff for testing. * - * This tests whether the GFF loader adds IDs that contain a comma. + * This tests whether the GFF loader adds IDs that contain a comma. * The GFF loader should allow it - */ + */ // BEGIN NEW FILE: Perform import on gff_tag_unescaped_character $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ @@ -291,7 +291,7 @@ public function testGFFImporterSimpleTest() $gff3_importer->prepareFiles(); $gff3_importer->run(); $gff3_importer->postRun(); - } + } catch (\Exception $ex) { $message = $ex->getMessage(); $has_exception = true; @@ -301,8 +301,8 @@ public function testGFFImporterSimpleTest() /** * Run the GFF loader on gff_invalidstartend.gff for testing. * - * This tests whether the GFF loader fixes start end values - */ + * This tests whether the GFF loader fixes start end values + */ $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ 'files' => [ @@ -341,7 +341,7 @@ public function testGFFImporterSimpleTest() $gff3_importer->prepareFiles(); $gff3_importer->run(); $gff3_importer->postRun(); - } + } catch (\Exception $ex) { $message = $ex->getMessage(); $has_exception = true; @@ -395,12 +395,12 @@ public function testGFFImporterSimpleTest() $gff3_importer->prepareFiles(); $gff3_importer->run(); $gff3_importer->postRun(); - } + } catch (\Exception $ex) { $message = $ex->getMessage(); $has_exception = true; } - $this->assertEquals($has_exception, true, "Should not complete when there + $this->assertEquals($has_exception, true, "Should not complete when there is invalid phase value (in this case character a) but did throw error."); /** @@ -408,7 +408,7 @@ public function testGFFImporterSimpleTest() * * This tests whether the GFF loader interprets the phase values correctly * for CDS rows when a number outside of the range 0,1,2 is specified. - */ + */ $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ 'files' => [ @@ -447,16 +447,16 @@ public function testGFFImporterSimpleTest() $gff3_importer->prepareFiles(); $gff3_importer->run(); $gff3_importer->postRun(); - } + } catch (\Exception $ex) { $message = $ex->getMessage(); $has_exception = true; } - $this->assertEquals($has_exception, true, "Should not complete when there - is invalid phase value (in this case a number > 2) but did not throw + $this->assertEquals($has_exception, true, "Should not complete when there + is invalid phase value (in this case a number > 2) but did not throw error which should have happened."); - + /** * Test that when checked, when phase is specified for CDS */ @@ -491,7 +491,7 @@ public function testGFFImporterSimpleTest() $file_details = [ 'file_local' => __DIR__ . '/../../../fixtures/gff3_loader/gff_phase.gff', ]; - + $gff3_importer->create($run_args, $file_details); $gff3_importer->prepareFiles(); $gff3_importer->run(); @@ -557,7 +557,7 @@ public function testGFFImporterSimpleTest() $gff3_importer->prepareFiles(); $gff3_importer->run(); $gff3_importer->postRun(); - } + } catch (\Exception $ex) { $message = $ex->getMessage(); // print_r($ex->__toString()); @@ -567,9 +567,9 @@ public function testGFFImporterSimpleTest() /** * Run the GFF loader on gff_rightarrow_ids.gff for testing. * - * This tests whether the GFF loader fails if ID contains + * This tests whether the GFF loader fails if ID contains * arrow >. It should not fail. - */ + */ $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ 'files' => [ @@ -609,14 +609,14 @@ public function testGFFImporterSimpleTest() $gff3_importer->run(); $gff3_importer->postRun(); - $results = $chado->query("SELECT count(*) as c1 FROM {1:feature} + $results = $chado->query("SELECT count(*) as c1 FROM {1:feature} WHERE uniquename = '>FRAEX38873_v2_000000010';"); - + foreach($results as $row) { - $this->assertEquals($row->c1, 1, 'A feature with uniquename + $this->assertEquals($row->c1, 1, 'A feature with uniquename >FRAEX38873_v2_000000010 should have been added but was not found.'); } - } + } catch (\Exception $ex) { $message = $ex->getMessage(); // echo $message . "\n"; @@ -624,7 +624,7 @@ public function testGFFImporterSimpleTest() $has_exception = true; } - $this->assertEquals($has_exception, false, "This should not fail and the + $this->assertEquals($has_exception, false, "This should not fail and the right arrow should be added."); /** @@ -669,34 +669,34 @@ public function testGFFImporterSimpleTest() $gff3_importer->run(); $gff3_importer->postRun(); - $results = $chado->query("SELECT * FROM {1:analysisfeature} + $results = $chado->query("SELECT * FROM {1:analysisfeature} WHERE significance = :significance LIMIT 1", [ ':significance' => 2 ]); foreach ($results as $row) { // print_r($row); - $this->assertEquals($row->significance,2, 'No significance value of 2 + $this->assertEquals($row->significance,2, 'No significance value of 2 could be found in the db. Import failed.'); } unset($results); - $results = $chado->query("SELECT * FROM {1:analysisfeature} + $results = $chado->query("SELECT * FROM {1:analysisfeature} WHERE significance = :significance LIMIT 1", [ ':significance' => 2.5 ]); foreach ($results as $row) { - $this->assertEquals($row->significance,2.5, 'No significance value of 2.5 + $this->assertEquals($row->significance,2.5, 'No significance value of 2.5 could be found in the db. Import failed.'); } unset($results); - $results = $chado->query("SELECT * FROM {1:analysisfeature} + $results = $chado->query("SELECT * FROM {1:analysisfeature} WHERE significance = :significance LIMIT 1", [ ':significance' => -2.5 ]); foreach ($results as $row) { // print_r($row); - $this->assertEquals($row->significance,-2.5, 'No significance value of + $this->assertEquals($row->significance,-2.5, 'No significance value of -2.5 could be found in the db. Import failed.'); } unset($results); @@ -704,9 +704,9 @@ public function testGFFImporterSimpleTest() /** * Run the GFF loader on gff_seqid_invalid_character.gff for testing. * Seqids seem to also be called landmarks within GFF loader. - * This tests whether the GFF loader has any issues with characters like + * This tests whether the GFF loader has any issues with characters like * single quotes. - */ + */ $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ 'files' => [ @@ -745,20 +745,20 @@ public function testGFFImporterSimpleTest() $gff3_importer->prepareFiles(); $gff3_importer->run(); $gff3_importer->postRun(); - } + } catch (\Exception $ex) { $message = $ex->getMessage(); $has_exception = true; } - $this->assertEquals($has_exception, true, 'An invalid seqid in the - gff_seqid_invalid_character should have caused an + $this->assertEquals($has_exception, true, 'An invalid seqid in the + gff_seqid_invalid_character should have caused an exception but did not.'); /** * Run the GFF loader on gff_strand_invalid.gff for testing. * * This tests whether the GFF loader interprets the strand values - */ + */ $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ 'files' => [ @@ -797,13 +797,13 @@ public function testGFFImporterSimpleTest() $gff3_importer->prepareFiles(); $gff3_importer->run(); $gff3_importer->postRun(); - } + } catch (\Exception $ex) { $message = $ex->getMessage(); $has_exception = true; } - $this->assertEquals($has_exception, true, 'An invalid strand in the - gff_strand_invalid.gff file should have caused an + $this->assertEquals($has_exception, true, 'An invalid strand in the + gff_strand_invalid.gff file should have caused an exception but did not.'); @@ -811,7 +811,7 @@ public function testGFFImporterSimpleTest() * Run the GFF loader on gff_strand.gff for testing. * * This tests whether the GFF loader interprets the strand values - */ + */ $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ 'files' => [ @@ -848,12 +848,12 @@ public function testGFFImporterSimpleTest() $gff3_importer->prepareFiles(); $gff3_importer->run(); $gff3_importer->postRun(); - + // Test that integer values for strand that get placed in the db // Strand data gets saved in {1:featureloc} - $results = $chado->query('SELECT * FROM {1:featureloc} fl + $results = $chado->query('SELECT * FROM {1:featureloc} fl LEFT JOIN {1:feature} f ON (fl.feature_id = f.feature_id) - WHERE uniquename = :uniquename LIMIT 1', + WHERE uniquename = :uniquename LIMIT 1', array( ':uniquename' => 'FRAEX38873_v2_000000010' ) @@ -863,9 +863,9 @@ public function testGFFImporterSimpleTest() $this->assertEquals($row->strand, 1); // + } - $results = $chado->query('SELECT * FROM {1:featureloc} fl + $results = $chado->query('SELECT * FROM {1:featureloc} fl LEFT JOIN {1:feature} f ON (fl.feature_id = f.feature_id) - WHERE uniquename = :uniquename LIMIT 1', + WHERE uniquename = :uniquename LIMIT 1', array( ':uniquename' => 'FRAEX38873_v2_000000010.1' ) @@ -873,11 +873,11 @@ public function testGFFImporterSimpleTest() foreach ($results as $row) { $this->assertEquals($row->strand,-1); // - - } - - $results = $chado->query('SELECT * FROM {1:featureloc} fl + } + + $results = $chado->query('SELECT * FROM {1:featureloc} fl LEFT JOIN {1:feature} f ON (fl.feature_id = f.feature_id) - WHERE uniquename = :uniquename LIMIT 1', + WHERE uniquename = :uniquename LIMIT 1', array( ':uniquename' => 'FRAEX38873_v2_000000010.2' ) @@ -886,10 +886,10 @@ public function testGFFImporterSimpleTest() foreach ($results as $row) { $this->assertEquals($row->strand, 0); // ? } - - $results = $chado->query('SELECT * FROM {1:featureloc} fl + + $results = $chado->query('SELECT * FROM {1:featureloc} fl LEFT JOIN {1:feature} f ON (fl.feature_id = f.feature_id) - WHERE uniquename = :uniquename LIMIT 1', + WHERE uniquename = :uniquename LIMIT 1', array( ':uniquename' => 'FRAEX38873_v2_000000010.3' ) @@ -897,14 +897,14 @@ public function testGFFImporterSimpleTest() foreach ($results as $row) { $this->assertEquals($row->strand, 0); // . - } + } /** * Run the GFF loader on gff_tag_parent_verification.gff for testing. * * This tests whether the GFF loader adds Parent attributes * The GFF loader should allow it - */ + */ $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ 'files' => [ @@ -941,7 +941,7 @@ public function testGFFImporterSimpleTest() $gff3_importer->prepareFiles(); $gff3_importer->run(); $gff3_importer->postRun(); - $results = $chado->query("SELECT COUNT(*) as c1 FROM + $results = $chado->query("SELECT COUNT(*) as c1 FROM (SELECT * FROM {1:feature_relationship} fr LEFT JOIN {1:feature} f ON (fr.object_id = f.feature_id) WHERE f.uniquename = 'FRAEX38873_v2_000000010' LIMIT 1 @@ -954,9 +954,9 @@ public function testGFFImporterSimpleTest() /** * Run the GFF loader on gff_tagvalue_encoded_character.gff for testing. * - * This tests whether the GFF loader adds IDs that contain encoded character. + * This tests whether the GFF loader adds IDs that contain encoded character. * The GFF loader should allow it - */ + */ $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ 'files' => [ @@ -994,20 +994,20 @@ public function testGFFImporterSimpleTest() $gff3_importer->run(); $gff3_importer->postRun(); - $results = $chado->query("SELECT COUNT(*) as c1 FROM {1:feature} + $results = $chado->query("SELECT COUNT(*) as c1 FROM {1:feature} WHERE uniquename = 'FRAEX38873_v2_000000010,20';",[]); foreach ($results as $row) { $this->assertEquals($row->c1, 1); } - + /** * Run the GFF loader on gff_tagvalue_comma_character.gff for testing. * - * This tests whether the GFF loader adds tag values contain comma seperation - * character. + * This tests whether the GFF loader adds tag values contain comma seperation + * character. * The GFF loader should allow it - */ + */ $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ 'files' => [ @@ -1045,7 +1045,7 @@ public function testGFFImporterSimpleTest() $gff3_importer->run(); $gff3_importer->postRun(); - $results = $chado->query("SELECT COUNT(*) as c1 FROM {1:featureprop} + $results = $chado->query("SELECT COUNT(*) as c1 FROM {1:featureprop} WHERE value ILIKE :value",[ ':value' => 'T' ]); @@ -1053,7 +1053,7 @@ public function testGFFImporterSimpleTest() $this->assertEquals($row->c1, 1); } - $results = $chado->query("SELECT COUNT(*) as c1 FROM {1:featureprop} + $results = $chado->query("SELECT COUNT(*) as c1 FROM {1:featureprop} WHERE value ILIKE :value",[ ':value' => 'EST' ]); @@ -1065,9 +1065,9 @@ public function testGFFImporterSimpleTest() * Run the GFF loader on gff_tagvalue_comma_character.gff for testing. * * This tests whether the GFF loader adds tag values containing encoded comma - * character. + * character. * The GFF loader should allow it - */ + */ $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ 'files' => [ @@ -1106,22 +1106,22 @@ public function testGFFImporterSimpleTest() $gff3_importer->run(); $gff3_importer->postRun(); - $results = $chado->query("SELECT COUNT(*) as c1 FROM {1:featureprop} + $results = $chado->query("SELECT COUNT(*) as c1 FROM {1:featureprop} WHERE value ILIKE :value",[ ':value' => 'T,EST' ]); foreach ($results as $row) { $this->assertEquals($row->c1, 1); } - - + + /** * Run the GFF loader on gff_1380_landmark_test.gff for testing. * * This tests whether the GFF loader adds landmarks directly from the GFF file - * character. + * character. * The GFF loader should allow it - */ + */ $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ 'files' => [ @@ -1161,7 +1161,7 @@ public function testGFFImporterSimpleTest() $gff3_importer->postRun(); // Check that the chr1_h1 feature (which is a landmark was added to feature table) - $results = $chado->query("SELECT count(*) as c1 FROM {1:feature} + $results = $chado->query("SELECT count(*) as c1 FROM {1:feature} WHERE uniquename ILIKE :value",[ ':value' => 'chr1_h1' ]); @@ -1177,7 +1177,7 @@ public function testGFFImporterSimpleTest() } // Check if the same chr1_h1 has type_id of chromosome_type_id - $results = $chado->query("SELECT count(*) as c1 FROM {1:feature} + $results = $chado->query("SELECT count(*) as c1 FROM {1:feature} WHERE uniquename ILIKE :value AND type_id = :type_id",[ ':value' => 'chr1_h1', ':type_id' => $chromosome_type_id @@ -1185,57 +1185,57 @@ public function testGFFImporterSimpleTest() foreach ($results as $row) { $this->assertEquals($row->c1, 1); - } - + } + // Check to make sure landmark exists in featureloc table - $results = $chado->query("SELECT count(*) as c1 FROM {1:featureloc} fl - LEFT JOIN {1:feature} f + $results = $chado->query("SELECT count(*) as c1 FROM {1:featureloc} fl + LEFT JOIN {1:feature} f ON fl.feature_id = f.feature_id - WHERE uniquename = :landmark_name", + WHERE uniquename = :landmark_name", [':landmark_name' => 'chr1_h1']); foreach ($results as $row) { $this->assertEquals($row->c1, 1); } // Check to make sure landmark exists in featureloc table - $results = $chado->query("SELECT count(*) as c1 FROM {1:featureloc} fl - LEFT JOIN {1:feature} f + $results = $chado->query("SELECT count(*) as c1 FROM {1:featureloc} fl + LEFT JOIN {1:feature} f ON fl.feature_id = f.feature_id - WHERE uniquename = :landmark_name", + WHERE uniquename = :landmark_name", [':landmark_name' => 'chr2_h1']); foreach ($results as $row) { $this->assertEquals($row->c1, 1); } // Check to make sure landmark exists in featureloc table - $results = $chado->query("SELECT count(*) as c1 FROM {1:featureloc} fl - LEFT JOIN {1:feature} f + $results = $chado->query("SELECT count(*) as c1 FROM {1:featureloc} fl + LEFT JOIN {1:feature} f ON fl.feature_id = f.feature_id - WHERE uniquename = :landmark_name", + WHERE uniquename = :landmark_name", [':landmark_name' => 'chr3_h1']); foreach ($results as $row) { $this->assertEquals($row->c1, 1); - } - + } + // Check to make sure landmark exists in featureloc table - $results = $chado->query("SELECT count(*) as c1 FROM {1:featureloc} fl - LEFT JOIN {1:feature} f + $results = $chado->query("SELECT count(*) as c1 FROM {1:featureloc} fl + LEFT JOIN {1:feature} f ON fl.feature_id = f.feature_id - WHERE uniquename = :landmark_name", + WHERE uniquename = :landmark_name", [':landmark_name' => 'chr4_h1']); foreach ($results as $row) { $this->assertEquals($row->c1, 1); } - - - + + + /** * Run the GFF loader on Citrus GFF3 for testing. * * This tests whether the GFF loader adds Citrus data - * character. + * character. * The GFF loader should allow it - */ + */ $gff3_importer = $importer_manager->createInstance('chado_gff3_loader'); $run_args = [ 'files' => [ @@ -1275,41 +1275,41 @@ public function testGFFImporterSimpleTest() $gff3_importer->postRun(); // Check to make sure landmark (scaffold0001) exists in featureloc table - $results = $chado->query("SELECT count(*) as c1 FROM {1:featureloc} fl - LEFT JOIN {1:feature} f + $results = $chado->query("SELECT count(*) as c1 FROM {1:featureloc} fl + LEFT JOIN {1:feature} f ON fl.feature_id = f.feature_id LEFT JOIN {1:cvterm} c ON c.cvterm_id = f.type_id - WHERE uniquename = :landmark_name", + WHERE uniquename = :landmark_name", [':landmark_name' => 'scaffold00001']); foreach ($results as $row) { $this->assertEquals($row->c1, 1); - } + } // Check to make sure feature orange1.1g015632m.g of type name gene - $results = $chado->query("SELECT count(*) as c1 FROM - (SELECT * FROM {1:feature} f + $results = $chado->query("SELECT count(*) as c1 FROM + (SELECT * FROM {1:feature} f LEFT JOIN {1:cvterm} c ON c.cvterm_id = f.type_id WHERE uniquename = :landmark_name AND c.name = :name - ) as table1", + ) as table1", [ ':landmark_name' => 'orange1.1g015632m.g', ':name' => 'gene' ]); foreach ($results as $row) { $this->assertEquals($row->c1, 1); - } + } // Check to make sure feature orange1.1g015632m.g of type name mRNA - $results = $chado->query("SELECT count(*) as c1 FROM - (SELECT * FROM {1:feature} f + $results = $chado->query("SELECT count(*) as c1 FROM + (SELECT * FROM {1:feature} f LEFT JOIN {1:cvterm} c ON c.cvterm_id = f.type_id WHERE uniquename = :unique_name AND c.name = :name - ) as table1", + ) as table1", [ ':unique_name' => 'PAC:18136217', ':name' => 'mRNA' @@ -1320,13 +1320,13 @@ public function testGFFImporterSimpleTest() } // Check to make sure feature PAC:18136217-cds of type name CDS - $results = $chado->query("SELECT count(*) as c1 FROM - (SELECT * FROM {1:feature} f + $results = $chado->query("SELECT count(*) as c1 FROM + (SELECT * FROM {1:feature} f LEFT JOIN {1:cvterm} c ON c.cvterm_id = f.type_id WHERE f.name = :feature_name AND c.name = :name - ) as table1", + ) as table1", [ ':feature_name' => 'PAC:18136217-cds', ':name' => 'CDS' diff --git a/tripal_chado/tests/src/Functional/Plugin/NewickImporterTest.php b/tripal_chado/tests/src/Functional/Plugin/NewickImporterTest.php new file mode 100644 index 000000000..0b5f0df6f --- /dev/null +++ b/tripal_chado/tests/src/Functional/Plugin/NewickImporterTest.php @@ -0,0 +1,296 @@ +getTestSchema(ChadoTestBrowserBase::PREPARE_TEST_CHADO); + + // Keep track of the schema name in case we need it + $schema_name = $chado->getSchemaName(); + + // Test to ensure cvterms are found in the cvterms table + $cvterms_count_query = $chado->query("SELECT count(*) as c1 FROM {1:cvterm}"); + $cvterms_count_object = $cvterms_count_query->fetchObject(); + $this->assertNotEquals($cvterms_count_object->c1, 0); + + // Insert organism + $organism_id = $chado->insert('1:organism') + ->fields([ + 'genus' => 'Citrus', + 'species' => 'sinensis', + 'common_name' => 'Sweet Orange', + ]) + ->execute(); + + // Insert Analysis + $analysis_id = $chado->insert('1:analysis') + ->fields([ + 'name' => 'Test Analysis', + 'description' => 'Test Analysis', + 'program' => 'PROGRAM', + 'programversion' => '1.0', + ]) + ->execute(); + + // Check to make sure polypeptide is in cvterms table + $results = $chado->query('SELECT COUNT(*) as c1 FROM {1:cvterm} WHERE name = :name', [ + 'name' => 'polypeptide' + ]); + $count = 0; + foreach ($results as $row) { + $count = $row->c1; + } + $this->assertEquals($count, 1, "Polypeptide was not found in cvterms"); + + // Verify that polypeptide is now in the cvterm table + $result_polypeptide_cvterm = $chado->query("SELECT * FROM {1:cvterm} + WHERE name = 'polypeptide' LIMIT 1;"); + $polypeptide_object = null; + $polypeptide_object = $result_polypeptide_cvterm->fetchObject(); + $this->assertNotEquals($polypeptide_object, null); + + $polypeptide_cvterm_id = $polypeptide_object->cvterm_id; + + + // Perform the Newick test by creating an instance of the Newick loader + $importer_manager = \Drupal::service('tripal.importer'); + $newick_importer = $importer_manager->createInstance('chado_newick_tree_loader'); + + // This tree file takes 13 minutes so replaced it with Douglas' tree file + // $run_args = [ + // 'files' => [ + // 0 => [ + // 'file_path' => __DIR__ . '/../../../fixtures/newick_loader/mrna_mini.fasta.tree' + // ] + // ], + // 'analysis_id' => $analysis_id, + // 'schema_name' => $schema_name, + // 'tree_name' => 'Tree 2', + // 'leaf_type' => 'polypeptide (SO:0000104)', + // 'dbxref' => NULL, + // 'description' => 'No description', + // 'name_re' => NULL, + // 'match' => 0, + // 'load_later' => 0 + // ]; + + // $file_details = [ + // 'file_local' => __DIR__ . '/../../../fixtures/fasta_loader/mrna_mini.fasta.tree', + // ]; + + + // Create feature S18540_Klotzschia_glaziovii + $values = [ + 'organism_id' => $organism_id, + 'name' => 'S18540_Klotzschia_glaziovii', + 'uniquename' => 'S18540_Klotzschia_glaziovii', + 'type_id' => $polypeptide_cvterm_id, + 'dbxref_id' => 1, + 'seqlen' => 0, + ]; + $chado->insert('1:feature')->fields($values)->execute(); + + // Check to see if the feature now exists + $results = $chado->query('SELECT * FROM {1:feature} WHERE name = :name', [ + 'name' => 'S18540_Klotzschia_glaziovii' + ]); + $feature_object = $results->fetchObject(); + $this->assertEquals('S18540_Klotzschia_glaziovii', $feature_object->name, 'Feature could not be found'); + + + $run_args = [ + 'files' => [ + 0 => [ + 'file_path' => __DIR__ . '/../../../fixtures/newick_loader/newick_T92076.tree' + ] + ], + 'analysis_id' => $analysis_id, + 'schema_name' => $schema_name, + 'tree_name' => 'Tree 2', + 'leaf_type' => 'polypeptide (SO:0000104)', + 'dbxref' => NULL, + 'description' => 'No description', + 'name_re' => "", + 'match' => 0, + // 'load_later' => 0 + ]; + + $file_details = [ + 'file_local' => __DIR__ . '/../../../fixtures/newick_loader/newick_T92076.tree', + ]; + + $newick_importer->create($run_args, $file_details); + $newick_importer->prepareFiles(); + $newick_importer->run(); + $newick_importer->postRun(); + + // Check for phylotree based on name + $results = $chado->query('SELECT COUNT(*) as c1 FROM {1:phylotree} WHERE name = :name', [ + ':name' => 'Tree 2' + ]); + $count = 0; + foreach ($results as $row) { + $count = $row->c1; + } + $this->assertGreaterThan(0, $count, "Should have created at least one phylotree but did not."); + + + // We need to get the type id - polypeptide + $results = $chado->query('SELECT * FROM {1:cvterm} WHERE name = :name', [ + 'name' => 'polypeptide' + ]); + $type_id = NULL; + foreach ($results as $row) { + $type_id = $row->cvterm_id; + } + + + // Check for phylotree based on name and also type_id which should be there as well + $results = $chado->query('SELECT COUNT(*) as c1 FROM {1:phylotree} WHERE name = :name and type_id = :type_id', [ + ':name' => 'Tree 2', + ':type_id' => $type_id + ]); + $count = 0; + foreach ($results as $row) { + $count = $row->c1; + } + $this->assertGreaterThan(0, $count, "Should have created at least one phylotree with associated type_id but did not."); + + // Check phylonode table - count should be 23 for this specific tree using this test + $results = $chado->query('SELECT COUNT(*) as c1 FROM {1:phylonode}'); + $count = 0; + foreach ($results as $row) { + $count = $row->c1; + } + // print_r($row->c1); + $this->assertEquals($count, 23, "Should have created 23 phylonode records but didn't."); + + // Check to see if there's a phylonode with a feature_id attached to it since we added S18540_Klotzschia_glaziovii + // as a feature + $results = $chado->query('SELECT * FROM {1:phylonode} WHERE label = :label AND feature_id IS NOT NULL', [ + ':label' => 'S18540_Klotzschia_glaziovii' + ]); + $count = 0; + foreach ($results as $row) { + $count++; + //$count = $row->c1; + } + // print_r($row->c1); + $this->assertGreaterThan(0, $count, "Should have created at least one phylonode record with a feature not being null."); + + // Check to see if there's a phylonode without connected features, this happens if the feature names do not exist + // in the features table. There should be 22 out of 23 phylonodes like this + $results = $chado->query('SELECT COUNT(*) as c1 FROM {1:phylonode} WHERE feature_id IS NULL'); + $count = 0; + foreach ($results as $row) { + $count = $row->c1; + } + // print_r($row->c1); + $this->assertGreaterThan(20, $count, "Should have created at least 20 phylonode records with a features being null."); + + + // Let's check to see if one phylotree exists + $results = $chado->query('SELECT COUNT(*) as c1 FROM {1:phylotree}'); + $count = 0; + foreach ($results as $row) { + $count = $row->c1; + } + $this->assertEquals(1, $count, "There should be one phylotree created"); + + // Get a phylotree_id for further testing + $results = $chado->query('SELECT phylotree_id FROM {1:phylotree} LIMIT 1'); + $phylotree_id = NULL; + foreach ($results as $row) { + $phylotree_id = $row->phylotree_id; + } + + // Let's try to cover more code for phylotree.api.php by testing chado_update_phylotree + $options = [ + 'job' => NULL, + 'name' => 'New name', + 'analysis_id' => 1, + 'dbxref_id' => 'null:local:null', + 'comment' => 'test comment', + ]; + chado_update_phylotree($phylotree_id, $options, $schema_name); + + + // Let's try to cover more code for phylotree.api.php + // This will DELETE ALL TEST PHYLOTREES SO DO THIS LAST + // TO AVOID OTHER TESTS FAILING + $results = $chado->query('SELECT phylotree_id FROM {1:phylotree}'); + foreach ($results as $row) { + $phylotree_id = $row->phylotree_id; + chado_delete_phylotree($phylotree_id, $schema_name); + } + + + // Initialize a drupal user with the following permissions + $account = $this->drupalCreateUser([ + // 'administer rules', + 'access administration pages', + 'administer tripal', + 'administer users', + 'administer permissions', + 'access tripal content overview', + 'allow tripal import', + 'administer tripal content', + 'admin tripal files', + 'add tripal content entities', + 'manage tripal jobs', + 'use chado_newick_tree_loader importer', + 'view tripal content entities', + 'upload files' + ]); + // Login the drupal user + $this->drupalLogin($account); + + // Check if the tripal loaders page is loadable / viewable + $this->drupalGet('admin/tripal/loaders'); + $this->assertSession()->statusCodeEquals(200); + + // Check if the newick tree loader form is viewable + $this->drupalGet('admin/tripal/loaders/chado_newick_tree_loader'); + $this->assertSession()->statusCodeEquals(200); + + // $this->drupalGet('admin/config/workflow/rules'); + // $this->assertSession()->statusCodeEquals(200); + + // Test that there is an empty reaction rule listing. + // $this->assertSession()->pageTextContains('There is no Reaction Rule yet.'); + } + +} \ No newline at end of file diff --git a/tripal_chado/tests/src/Functional/Plugin/OBOImporterTest.php b/tripal_chado/tests/src/Functional/Plugin/OBOImporterTest.php index ac82c2da1..e80fa6fbe 100644 --- a/tripal_chado/tests/src/Functional/Plugin/OBOImporterTest.php +++ b/tripal_chado/tests/src/Functional/Plugin/OBOImporterTest.php @@ -12,9 +12,10 @@ /** * Tests for the ChadoCVTerm classes * - * @group Tripal - * @group Tripal Chado - * @group Tripal Chado ChadoStorage + * @group TripalImporter + * @group ChadoImporter + * @group OntologyImporter + * @group OBOImporter */ class OBOImporterTest extends ChadoTestBrowserBase { diff --git a/tripal_chado/tests/src/Functional/Plugin/TaxonomyImporterTest.php b/tripal_chado/tests/src/Functional/Plugin/TaxonomyImporterTest.php index 682779d76..8041503eb 100644 --- a/tripal_chado/tests/src/Functional/Plugin/TaxonomyImporterTest.php +++ b/tripal_chado/tests/src/Functional/Plugin/TaxonomyImporterTest.php @@ -19,9 +19,9 @@ /** * Tests for the TaxonomyImporter class * - * @group Tripal - * @group Tripal Chado - * @group Tripal Chado ChadoStorage + * @group TripalImporter + * @group ChadoImporter + * @group TaxonomyImporter */ class TaxonomyImporterTest extends ChadoTestBrowserBase { @@ -68,9 +68,9 @@ public function testTaxonomyImporterSimpleTest() $taxonomy_importer->prepareFiles(); $taxonomy_importer->run(); $taxonomy_importer->postRun(); - + // Check if Arabidopsis thaliana - $results = $chado->query("SELECT count(*) as c1 FROM {1:organism} + $results = $chado->query("SELECT count(*) as c1 FROM {1:organism} where genus = 'Arabidopsis' AND species = 'thaliana';"); $results_object = $results->fetchObject(); $this->assertEquals(1, $results_object->c1, @@ -82,7 +82,7 @@ public function testTaxonomyImporterSimpleTest() "); $this->assertEquals(1, $results_object->c1, 'A phylotree named Taxonomy Tree should have been created but wasnt. TaxonomyImporter test failed'); - + // Check if phylonode organism was created $results = $chado->query("SELECT count(*) as c1 FROM {1:phylonode_organism}"); $this->assertEquals(1, $results_object->c1, @@ -90,54 +90,54 @@ public function testTaxonomyImporterSimpleTest() // Check if more than 5 phylonodes were created $results = $chado->query("SELECT count(*) as c1 FROM {1:phylonode}"); - $this->assertGreaterThan(0, $results_object->c1, + $this->assertGreaterThan(0, $results_object->c1, 'Phylonodes count should be more than 0. TaxonomyImporter test failed.'); - // Check if there are phylonodeprops like + // Check if there are phylonodeprops like $results = $chado->query("SELECT count(*) as c1 FROM {1:phylonodeprop}"); - $this->assertGreaterThan(0, $results_object->c1, - 'Phylonodeprop count should be more than 5. TaxonomyImporter test failed.'); - + $this->assertGreaterThan(0, $results_object->c1, + 'Phylonodeprop count should be more than 5. TaxonomyImporter test failed.'); + // Check more specifics in phylonodeprop - genus $results = $chado->query("SELECT count(*) as c1 FROM {1:phylonodeprop} WHERE value='genus'"); - $this->assertGreaterThan(0, $results_object->c1, + $this->assertGreaterThan(0, $results_object->c1, 'Phylonodeprop genus should exist. TaxonomyImporter test failed.'); // Check more specifics in phylonodeprop - species $results = $chado->query("SELECT count(*) as c1 FROM {1:phylonodeprop} WHERE value='species'"); - $this->assertGreaterThan(0, $results_object->c1, + $this->assertGreaterThan(0, $results_object->c1, 'Phylonodeprop species should exist. TaxonomyImporter test failed.'); // Check more specifics in phylonodeprop - family $results = $chado->query("SELECT count(*) as c1 FROM {1:phylonodeprop} WHERE value='family'"); - $this->assertGreaterThan(0, $results_object->c1, + $this->assertGreaterThan(0, $results_object->c1, 'Phylonodeprop family should exist. TaxonomyImporter test failed.'); - + // Check more specifics in phylonodeprop - kingdom $results = $chado->query("SELECT count(*) as c1 FROM {1:phylonodeprop} WHERE value='kingdom'"); - $this->assertGreaterThan(0, $results_object->c1, + $this->assertGreaterThan(0, $results_object->c1, 'Phylonodeprop kingdom should exist. TaxonomyImporter test failed.'); - + // Check more specifics in phylonodeprop - order $results = $chado->query("SELECT count(*) as c1 FROM {1:phylonodeprop} WHERE value='order'"); - $this->assertGreaterThan(0, $results_object->c1, - 'Phylonodeprop order should exist. TaxonomyImporter test failed.'); - + $this->assertGreaterThan(0, $results_object->c1, + 'Phylonodeprop order should exist. TaxonomyImporter test failed.'); + // Check more specifics in phylonodeprop - superkingdom $results = $chado->query("SELECT count(*) as c1 FROM {1:phylonodeprop} WHERE value='superkingdom'"); - $this->assertGreaterThan(0, $results_object->c1, - 'Phylonodeprop superkingdom should exist. TaxonomyImporter test failed.'); - + $this->assertGreaterThan(0, $results_object->c1, + 'Phylonodeprop superkingdom should exist. TaxonomyImporter test failed.'); + // Check more specifics in phylonodeprop - clade $results = $chado->query("SELECT count(*) as c1 FROM {1:phylonodeprop} WHERE value='clade'"); - $this->assertGreaterThan(0, $results_object->c1, + $this->assertGreaterThan(0, $results_object->c1, 'Phylonodeprop clade should exist. TaxonomyImporter test failed.'); // Check more specifics in phylonodeprop - class $results = $chado->query("SELECT count(*) as c1 FROM {1:phylonodeprop} WHERE value='class'"); - $this->assertGreaterThan(0, $results_object->c1, + $this->assertGreaterThan(0, $results_object->c1, 'Phylonodeprop class should exist. TaxonomyImporter test failed.'); } } -?> \ No newline at end of file +?> diff --git a/tripal_chado/tests/src/Functional/api/ChadoQueryAPITest.php b/tripal_chado/tests/src/Functional/api/ChadoQueryAPITest.php index a7041d48a..e0e55811d 100644 --- a/tripal_chado/tests/src/Functional/api/ChadoQueryAPITest.php +++ b/tripal_chado/tests/src/Functional/api/ChadoQueryAPITest.php @@ -53,12 +53,12 @@ public function testChadoQuery() { $dbq = chado_query($sql, $args); $this->assertEquals(FALSE, $dbq); - // -- Arguements must be an array. + // -- Arguments must be an array. $sql = $args = 'SELECT * FROM {organism}'; $dbq = chado_query($sql, $args); $this->assertEquals(FALSE, $dbq); - // -- Arguements should be in the SQL string. + // -- Arguments should be in the SQL string. $sql = 'SELECT * FROM {organism} WHERE genus=:genus'; $args = [':genus' => 'Tripalus', ':species' => 'databasica']; $dbq = chado_query($sql, $args); diff --git a/tripal_chado/tests/src/Kernel/ChadoTestKernelBase.php b/tripal_chado/tests/src/Kernel/ChadoTestKernelBase.php index 2399c760e..af72eeee5 100644 --- a/tripal_chado/tests/src/Kernel/ChadoTestKernelBase.php +++ b/tripal_chado/tests/src/Kernel/ChadoTestKernelBase.php @@ -80,6 +80,17 @@ protected function setUp() :void { $this->getRealConfig(); $this->initTripalDbx(); $this->allowTestSchemas(); + + // We also lose the tripaldbx.settings config in Kernel tests + // This is needed when getting available schema, for example. + // As such we are going to manually set some needed ones within + // the test config based on the real config. + $fromReal = $this->realConfigFactory->get('tripaldbx.settings') + ->get('test_schema_base_names', []); + \Drupal::configFactory() + ->getEditable('tripaldbx.settings') + ->set('test_schema_base_names', $fromReal) + ->save(); } } diff --git a/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoAnalysisDefaultTest.php b/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoAnalysisDefaultTest.php index 39e4c60dd..80339c422 100644 --- a/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoAnalysisDefaultTest.php +++ b/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoAnalysisDefaultTest.php @@ -37,6 +37,8 @@ * * @group Tripal * @group Tripal Chado + * @group Tripal Chado Fields + * @group Tripal Chado Fields Analysis * @group ChadoStorage */ class ChadoAnalysisDefaultTest extends ChadoTestKernelBase { diff --git a/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoContactDefaultTest.php b/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoContactDefaultTest.php new file mode 100644 index 000000000..82e945214 --- /dev/null +++ b/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoContactDefaultTest.php @@ -0,0 +1,432 @@ + 'store_id', + 'drupal_store' => TRUE, + 'chado_table' => $base_table, + 'chado_column' => $base_pkey_col, + ]); + $properties[] = new ChadoIntStoragePropertyType($entity_type_id, self::$id, 'contact_id', $contact_id_term, [ + 'action' => 'store', + 'chado_table' => $base_table, + 'chado_column' => $base_fkey_col, + ]); + $properties[] = new ChadoVarCharStoragePropertyType($entity_type_id, self::$id, 'contact_name', $contact_name_term, $contact_name_length, [ + 'action' => 'join', + 'path' => $base_table . '.' . $base_fkey_col . '>contact.contact_id', + 'chado_column' => 'name', + 'as' => 'contact_name', + ]); + * @endcode + * + * These will be repeated in the testContactFieldStudy and + * testContactFieldArrayDesign properties array below for testing. + */ + protected $fields = [ + 'testContactFieldStudy' => [ + 'field_name' => 'testContactFieldStudy', + 'base_table' => 'study', + 'properties' => [ + 'record_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_id', + 'drupal_store' => TRUE, + 'chado_table' => 'study', + 'chado_column' => 'study_id' + ], + 'contact_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'study', + 'chado_column' => 'contact_id' + ], + 'name' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoVarCharStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'study', + 'chado_column' => 'name', + ], + 'contact_name' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoVarCharStoragePropertyType', + 'action' => 'join', + 'path' => 'study.contact_id>contact.contact_id', + 'chado_column' => 'name', + 'as' => 'contact_name', + ], + ], + ], + 'testContactFieldArrayDesign' => [ + 'field_name' => 'testContactFieldArrayDesign', + 'base_table' => 'arraydesign', + 'properties' => [ + 'record_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_id', + 'drupal_store' => TRUE, + 'chado_table' => 'arraydesign', + 'chado_column' => 'arraydesign_id' + ], + 'manufacturer_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'arraydesign', + 'chado_column' => 'manufacturer_id' + ], + 'name' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoVarCharStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'arraydesign', + 'chado_column' => 'name', + ], + 'contact_name' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoVarCharStoragePropertyType', + 'action' => 'join', + 'path' => 'arraydesign.manufacturer_id>contact.contact_id', + 'chado_column' => 'name', + 'as' => 'contact_name', + ], + // platformtype_id corresponds to a cvterm.cvterm_id + 'platformtype_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'arraydesign', + 'chado_column' => 'platformtype_id' + ], + ], + ], + ]; + + protected array $contact_id; + + /** + * {@inheritdoc} + */ + protected function setUp() :void { + parent::setUp(); + $this->setUpChadoStorageTestEnviro(); + + // Create basic contact records for use with these fields. + // This field does not create a contact but rather just links to one. + foreach (range(0, 1) as $id) { + $query = $this->chado_connection->insert('1:contact'); + $query->fields([ + 'name' => 'Contact name for testing #' . $id, + ]); + $this->contact_id[$id] = $query->execute(); + } + } + + + + /** + * Testing ChadoStorage with the ChadoContactDefault field on a study content type. + * + * Test Cases: + * - Create Values in Chado using ChadoStorage when they don't yet exist. + * - Load values in Chado using ChadoStorage after we just inserted them. + * - Update values in Chado using ChadoStorage after we just inserted them. + */ + public function testStudyBaseTableFieldCRUD() { + + // Test Case: Insert valid values when they do not yet exist in Chado. + // --------------------------------------------------------- + $insert_values = [ + 'testContactFieldStudy' => [ + [ + // No value for record_id as we do not yet have a Study record. + 'contact_id' => $this->contact_id[0], + 'name' => 'ChadoContactDefaultTest study #1', + ], + ], + ]; + $this->chadoStorageTestInsertValues($insert_values); + + // Uncomment the following if you want to check that the arrays + // for chado storage are being formed as we expect. This is very + // useful for debugging. + // @debug $this->debugChadoStorageTestTraitArrays(); + + // Check that the Study record was created as expected. + $query = $this->chado_connection->select('1:study', 'base') + ->fields('base', ['study_id', 'name', 'contact_id']); + $query->join('1:contact', 'linked', 'base.contact_id = linked.contact_id'); + $query->addField('linked', 'name', 'linked_name'); + $base_records = $query->execute()->fetchAll(); + $this->assertCount( + 1, + $base_records, + 'Only one Study record should have been created.'); + $base_dbrecord = $base_records[0]; + $this->assertEquals( + $this->contact_id[0], + $base_dbrecord->contact_id, + "The contact_id should be the one we set."); + $this->assertEquals( + 'Contact name for testing #0', + $base_dbrecord->linked_name, + "Failing the extra more readable check that the contact is the one we expect."); + $base_id = $base_dbrecord->study_id; + + // Test Case: Load values existing in Chado. + // --------------------------------------------------------- + // First we want to reset all the chado storage arrays to ensure we are + // doing a clean test. The values will purposefully remain in Chado but the + // Property Types, Property Values and Data Values will be built from scratch. + $this->cleanChadoStorageValues(); + + // For loading only the store id/pkey/link items should be populated. + $load_values = [ + 'testContactFieldStudy' => [ + [ + 'record_id' => $base_id, + ], + ], + ]; + $retrieved_values = $this->chadoStorageTestLoadValues($load_values); + + // @debug Uncomment the following line if the asserts below fail. + // @debug $this->debugChadoStorageTestTraitArrays(); + + // Now test that the values have been loaded. + // We want to test only our field + // and retrieved values will be keyed by field name + delta. + $retrieved = $retrieved_values['testContactFieldStudy'][0]; + $this->assertEquals( + $base_id, + $retrieved['record_id']['value']->getValue(), + "The Study ID did not match the one we retrieved from chado after insert." + ); + $this->assertEquals( + $this->contact_id[0], + $retrieved['contact_id']['value']->getValue(), + "The contact_id did not match the one we retrieved from chado after insert." + ); + $this->assertEquals( + 'Contact name for testing #0', + $retrieved['contact_name']['value']->getValue(), + "The contact name did not match the one we retrieved from chado after insert." + ); + + // Test Case: Update values in Chado using ChadoStorage. + // --------------------------------------------------------- + // When updating we need all the store id/pkey/link records + // and all values of the other properties. + // array_merge alone seems not to be sufficient + + $update_values = [ + 'testContactFieldStudy' => [ + [ + 'record_id' => $base_id, + 'name' => 'ChadoContactDefaultTest study #1', + 'contact_id' => $this->contact_id[1], // This is the change! + ], + ], + ]; + $this->chadoStorageTestUpdateValues($update_values); + + // Now we check chado to see if these values were changed... + $query = $this->chado_connection->select('1:study', 'base') + ->fields('base', ['study_id', 'name', 'contact_id']); + $query->join('1:contact', 'linked', 'base.contact_id = linked.contact_id'); + $query->addField('linked', 'name', 'linked_name'); + $base_records = $query->execute()->fetchAll(); + $this->assertCount( + 1, + $base_records, + 'Only one study record should be present as we should have updated the existing one.'); + + $base_dbrecord = $base_records[0]; + $this->assertEquals( + $base_id, + $base_dbrecord->study_id, + "The study primary key should remain unchanged through update."); + $this->assertEquals( + $this->contact_id[1], + $base_dbrecord->contact_id, + "The contact_id should be updated to the second one inserted."); + $this->assertEquals( + 'Contact name for testing #1', + $base_dbrecord->linked_name, + "Failing the extra more readable check that the updated contact is the one we expect."); + } + + + + /** + * Testing ChadoStorage with the ChadoContactDefault field on an arraydesign content type. + * + * Test Cases: + * - Create Values in Chado using ChadoStorage when they don't yet exist. + * - Load values in Chado using ChadoStorage after we just inserted them. + * - Update values in Chado using ChadoStorage after we just inserted them. + */ + public function testArrayDesignBaseTableFieldCRUD() { + + // ArrayDesign requires a platformtype_id, however it has no impact on our field. + // As such, we will just use the "null" CV term (cvterm_id = 1). + $null_platformtype_id = 1; + + // Test Case: Insert valid values when they do not yet exist in Chado. + // --------------------------------------------------------- + $insert_values = [ + 'testContactFieldArrayDesign' => [ + [ + // No value for record_id as we do not yet have an arraydesign record. + 'manufacturer_id' => $this->contact_id[0], + 'name' => 'ChadoContactDefaultTest arraydesign #1', + 'platformtype_id' => $null_platformtype_id, + ], + ], + ]; + $this->chadoStorageTestInsertValues($insert_values); + + // Uncomment the following if you want to check that the arrays + // for chado storage are being formed as we expect. This is very + // useful for debugging. + // @debug $this->debugChadoStorageTestTraitArrays(); + + // Check that the arraydesign record was created as expected. + $query = $this->chado_connection->select('1:arraydesign', 'base') + ->fields('base', ['arraydesign_id', 'platformtype_id', 'manufacturer_id']); + $query->join('1:contact', 'linked', 'base.manufacturer_id = linked.contact_id'); + $query->addField('linked', 'name', 'linked_name'); + $records = $query->execute()->fetchAll(); + $this->assertCount( + 1, + $records, + 'Only one arraydesign record should have been created.'); + $base_dbrecord = $records[0]; + $this->assertEquals( + $this->contact_id[0], + $base_dbrecord->manufacturer_id, + "The manufacturer_id should be the one we set."); + $this->assertEquals( + 'Contact name for testing #0', + $base_dbrecord->linked_name, + "Failing the extra more readable check that the manufacturer is the one we expect."); + $base_id = $base_dbrecord->arraydesign_id; + + // Test Case: Load values existing in Chado. + // --------------------------------------------------------- + // First we want to reset all the chado storage arrays to ensure we are + // doing a clean test. The values will purposefully remain in Chado but the + // Property Types, Property Values and Data Values will be built from scratch. + $this->cleanChadoStorageValues(); + + // For loading only the store id/pkey/link items should be populated. + $load_values = [ + 'testContactFieldArrayDesign' => [ + [ + 'record_id' => $base_id, + ], + ], + ]; + $retrieved_values = $this->chadoStorageTestLoadValues($load_values); + + // @debug Uncomment the following line if the asserts below fail. + // @debug $this->debugChadoStorageTestTraitArrays(); + + // Now test that the values have been loaded. + // We want to test only our field + // and retrieved values will be keyed by field name + delta. + $retrieved = $retrieved_values['testContactFieldArrayDesign'][0]; + $this->assertEquals( + $base_id, + $retrieved['record_id']['value']->getValue(), + "The arraydesign ID did not match the one we retrieved from chado after insert." + ); + $this->assertEquals( + $this->contact_id[0], + $retrieved['manufacturer_id']['value']->getValue(), + "The manufacturer_id did not match the one we retrieved from chado after insert." + ); + $this->assertEquals( + 'Contact name for testing #0', + $retrieved['contact_name']['value']->getValue(), + "The contact name did not match the one we retrieved from chado after insert." + ); + + // Test Case: Update values in Chado using ChadoStorage. + // --------------------------------------------------------- + // When updating we need all the store id/pkey/link records + // and all values of the other properties. + // array_merge alone seems not to be sufficient + + $update_values = [ + 'testContactFieldArrayDesign' => [ + [ + 'record_id' => $base_id, + 'name' => 'ChadoContactDefaultTest arraydesign #1', + 'manufacturer_id' => $this->contact_id[1], // This is the change! + 'platformtype_id' => $null_platformtype_id, + ], + ], + ]; + $this->chadoStorageTestUpdateValues($update_values); + + // Now we check chado to see if these values were changed... + $query = $this->chado_connection->select('1:arraydesign', 'base') + ->fields('base', ['arraydesign_id', 'platformtype_id', 'manufacturer_id']); + $query->join('1:contact', 'linked', 'base.manufacturer_id = linked.contact_id'); + $query->addField('linked', 'name', 'linked_name'); + $records = $query->execute()->fetchAll(); + $this->assertCount( + 1, + $records, + 'Only one arraydesign record should be present as we should have updated the existing one.'); + + $base_dbrecord = $records[0]; + $this->assertEquals($base_id, + $base_dbrecord->arraydesign_id, + "The arraydesign primary key should remain unchanged through update."); + $this->assertEquals( + $this->contact_id[1], + $base_dbrecord->manufacturer_id, + "The manufacturer_id should be updated to the second one inserted."); + $this->assertEquals( + 'Contact name for testing #1', + $base_dbrecord->linked_name, + "Failing the extra more readable check that the updated manufacturer is the one we expect."); + } +} diff --git a/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoLinkerPropertyDefaultTest.php b/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoLinkerPropertyDefaultTest.php index 8621cd965..12f5f2a32 100644 --- a/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoLinkerPropertyDefaultTest.php +++ b/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoLinkerPropertyDefaultTest.php @@ -65,8 +65,10 @@ class ChadoLinkerPropertyDefaultTest extends ChadoTestKernelBase { 'A_linker_id' => [ 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', 'action' => 'store_link', - 'chado_table' => 'featureprop', - 'chado_column' => 'feature_id' + 'left_table' => 'feature', + 'left_table_id' => 'feature_id', + 'right_table' => 'featureprop', + 'right_table_id' => 'feature_id' ], // Now we are going to store all the core columns of the featureprop table to // ensure we can meet the unique and not null requirements of the table. @@ -167,6 +169,51 @@ class ChadoLinkerPropertyDefaultTest extends ChadoTestKernelBase { ], ], ], + 'testBackwardsCompatiblePropertyField' => [ + 'field_name' => 'testpropertyfieldABackwardsCompatible', + 'base_table' => 'feature', + 'properties' => [ + 'A_record_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_id', + 'drupal_store' => TRUE, + 'chado_table' => 'feature', + 'chado_column' => 'feature_id' + ], + 'A_prop_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_pkey', + 'chado_table' => 'featureprop', + 'chado_column' => 'featureprop_id', + ], + 'A_linker_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_link', + 'chado_table' => 'featureprop', + 'chado_column' => 'feature_id' + ], + 'A_type_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'featureprop', + 'chado_column' => 'type_id' + ], + 'A_value' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoTextStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'featureprop', + 'chado_column' => 'value', + 'delete_if_empty' => TRUE, + 'empty_value' => '' + ], + 'A_rank' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'featureprop', + 'chado_column' => 'rank' + ], + ], + ], ]; protected int $organism_id; @@ -176,6 +223,19 @@ class ChadoLinkerPropertyDefaultTest extends ChadoTestKernelBase { */ protected function setUp() :void { parent::setUp(); + + // We need to mock the logger to test the progress reporting. + $container = \Drupal::getContainer(); + $mock_logger = $this->getMockBuilder(\Drupal\tripal\Services\TripalLogger::class) + ->onlyMethods(['warning']) + ->getMock(); + $mock_logger->method('warning') + ->willReturnCallback(function($message, $context, $options) { + print str_replace(array_keys($context), $context, $message); + return NULL; + }); + $container->set('tripal.logger', $mock_logger); + $this->setUpChadoStorageTestEnviro(); // Create the organism record for use with the feature table. @@ -193,9 +253,20 @@ protected function setUp() :void { $this->organism_id = $query->execute(); } + /** + * Data Provider: Test both the current store_link model and the old one. + */ + public function provideSinglePropFieldNames() { + return [ + ['testBackwardsCompatiblePropertyField'], + ['testpropertyfieldA'] + ]; + } /** * Testing ChadoStorage on single property field with multiple values. * + * @dataProvider provideSinglePropFieldNames + * * Test Cases: * - Create Values in Chado using ChadoStorage when they don't yet exist. * - Load values in Chado using ChadoStorage after we just inserted them. @@ -203,7 +274,7 @@ protected function setUp() :void { * - [NOT IMPLEMENTED] Delete values in Chado using ChadoStorage. * - [NOT IMPLEMENTED] Ensure property field picks up records in Chado not added through field. */ - public function testInsertValuesForSingleField() { + public function testInsertValuesForSingleField($prop_field_name) { $rdfs_comment_cvtermID = $this->getCvtermID('rdfs', 'comment'); $gene_cvtermID = $this->getCvtermID('SO', '0000704'); @@ -212,18 +283,27 @@ public function testInsertValuesForSingleField() { // Test Case: Insert valid values when they do not yet exist in Chado. // --------------------------------------------------------- $insert_values = [ - 'testpropertyfieldA' => [ + $prop_field_name => [ [ + 'A_record_id' => NULL, + 'A_prop_id' => NULL, + 'A_linker_id' => NULL, 'A_type_id' => $rdfs_comment_cvtermID, 'A_value' => 'Note 1', 'A_rank' => 0, ], [ + 'A_record_id' => NULL, + 'A_prop_id' => NULL, + 'A_linker_id' => NULL, 'A_type_id' => $rdfs_comment_cvtermID, 'A_value' => 'Note 2', 'A_rank' => 1, ], [ + 'A_record_id' => NULL, + 'A_prop_id' => NULL, + 'A_linker_id' => NULL, 'A_type_id' => $rdfs_comment_cvtermID, 'A_value' => 'Note 3', 'A_rank' => 2, @@ -237,8 +317,16 @@ public function testInsertValuesForSingleField() { ] ], ]; + ob_start(); $this->chadoStorageTestInsertValues($insert_values); - + $printed_output = ob_get_clean(); + if ($prop_field_name == 'testBackwardsCompatiblePropertyField') { + $this->assertStringContainsString('backwards compatible mode', $printed_output, + "We expect this field to be in backwards compatible mode and should have been informed during insert."); + } + else { + $this->assertEmpty($printed_output, "There should not be any messages logged."); + } // @debug $this->debugChadoStorageTestTraitArrays(); // Check that the base feature record was created in the database as expected. @@ -275,7 +363,7 @@ public function testInsertValuesForSingleField() { // Check that the featureprop records were created in the database as expected. // We use the unique key to select this particular value in order to // ensure it is here and there is one one. - foreach ($insert_values['testpropertyfieldA'] as $delta => $expected) { + foreach ($insert_values[$prop_field_name] as $delta => $expected) { $query = $this->chado_connection->select('1:featureprop', 'prop') ->fields('prop', ['featureprop_id', 'feature_id', 'type_id', 'value', 'rank']) ->condition('feature_id', $feature_id, '=') @@ -299,7 +387,7 @@ public function testInsertValuesForSingleField() { // For loading only the store id/pkey/link items should be populated. $load_values = [ - 'testpropertyfieldA' => [ + $prop_field_name => [ [ 'A_record_id' => $feature_id, 'A_prop_id' => $prop0->featureprop_id, @@ -322,12 +410,21 @@ public function testInsertValuesForSingleField() { ] ], ]; + ob_start(); $retrieved_values = $this->chadoStorageTestLoadValues($load_values); + $printed_output = ob_get_clean(); + if ($prop_field_name == 'testBackwardsCompatiblePropertyField') { + $this->assertStringContainsString('backwards compatible mode', $printed_output, + "We expect this field to be in backwards compatible mode and should have been informed during load."); + } + else { + $this->assertEmpty($printed_output, "There should not be any messages logged."); + } // Now test that the additional values have been loaded. // @debug $this->debugChadoStorageTestTraitArrays(); foreach([0,1,2] as $delta) { - $retrieved = $retrieved_values['testpropertyfieldA'][$delta]; + $retrieved = $retrieved_values[$prop_field_name][$delta]; $varname = 'prop' . $delta; $expected = $$varname; $this->assertEquals( @@ -356,14 +453,25 @@ public function testInsertValuesForSingleField() { $update_values = $insert_values; foreach ($load_values as $field_name => $tmp) { foreach ($tmp as $delta => $id_values) { - $update_values[$field_name][$delta] += $id_values; + foreach ($id_values as $key => $value) { + $update_values[$field_name][$delta][$key] = $value; + } } } // We then change a few non key related values... - $update_values['testpropertyfieldA'][1]['A_value'] = 'Changed Note to be more informative.'; - $update_values['testpropertyfieldA'][2]['A_value'] = 'Something completely different. Not even a note at all.'; + $update_values[$prop_field_name][1]['A_value'] = 'Changed Note to be more informative.'; + $update_values[$prop_field_name][2]['A_value'] = 'Something completely different. Not even a note at all.'; + ob_start(); $this->chadoStorageTestUpdateValues($update_values); + $printed_output = ob_get_clean(); + if ($prop_field_name == 'testBackwardsCompatiblePropertyField') { + $this->assertStringContainsString('backwards compatible mode', $printed_output, + "We expect this field to be in backwards compatible mode and should have been informed during update."); + } + else { + $this->assertEmpty($printed_output, "There should not be any messages logged."); + } // Now we check chado to see if these values were changed... // Still the expected number of records in the featureprop table? @@ -377,7 +485,7 @@ public function testInsertValuesForSingleField() { // Check that the featureprop records were created in the database as expected. // We use the unique key to select this particular value in order to // ensure it is here and there is one one. - foreach ($update_values['testpropertyfieldA'] as $delta => $expected) { + foreach ($update_values[$prop_field_name] as $delta => $expected) { $query = $this->chado_connection->select('1:featureprop', 'prop') ->fields('prop', ['featureprop_id', 'feature_id', 'type_id', 'value', 'rank']) ->condition('feature_id', $feature_id, '=') diff --git a/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoLinkerTableTest.php b/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoLinkerTableTest.php new file mode 100644 index 000000000..cbaa0618f --- /dev/null +++ b/tripal_chado/tests/src/Kernel/Plugin/ChadoStorage/ChadoLinkerTableTest.php @@ -0,0 +1,544 @@ + feature_synonym + * - analysisfield: feature > analysisfeature + * - contactfield: feature > feature_contact + * - relationshipfield: feature > feature_relationship + * + * Note: testotherfeaturefield is added to ensure we meet the unique constraint + * on the base table and also to ensure we are testing multi-field functionality. + * + * Note: We do not need to test invalid conditions for createValues() and + * updateValues() as these are only called after the entity has validated + * the system using validateValues(). Instead we test all invalid conditions + * are caught by validateValues(). + * + * Specific test cases + * Test the following for both single and multiple property fields: + * - [SINGLE FIELD ONLY] Create Values in Chado using ChadoStorage when they don't yet exist. + * - [SINGLE FIELD ONLY] Load values in Chado using ChadoStorage after we just inserted them. + * - [SINGLE FIELD ONLY] Update values in Chado using ChadoStorage after we just inserted them. + * - [NOT IMPLEMENTED] Delete values in Chado using ChadoStorage. + * + * @group Tripal + * @group Tripal Chado + * @group ChadoStorage + */ +class ChadoLinkerTableTest extends ChadoTestKernelBase { + + use ChadoStorageTestTrait; + + protected $fields = [ + 'synonymfield' => [ + 'field_name' => 'synonymfield', + 'base_table' => 'feature', + 'properties' => [ + 'record_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_id', + 'drupal_store' => TRUE, + 'chado_table' => 'feature', + 'chado_column' => 'feature_id' + ], + 'linker_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_pkey', + 'chado_table' => 'feature_synonym', + 'chado_column' => 'feature_synonym_id', + ], + 'link' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_link', + 'left_table' => 'feature', + 'left_table_id' => 'feature_id', + 'right_table' => 'feature_synonym', + 'right_table_id' => 'feature_id' + ], + 'right_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'feature_synonym', + 'chado_column' => 'synonym_id' + ], + // Other columns in feature_synonym. These are set by the widget. + 'pub_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'feature_synonym', + 'chado_column' => 'pub_id' + ], + 'is_current' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoBoolStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'feature_synonym', + 'chado_column' => 'is_current' + ], + 'is_internal' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoBoolStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'feature_synonym', + 'chado_column' => 'is_internal' + ], + ], + ], + 'analysisfield' => [ + 'field_name' => 'analysisfield', + 'base_table' => 'feature', + 'properties' => [ + 'record_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_id', + 'drupal_store' => TRUE, + 'chado_table' => 'feature', + 'chado_column' => 'feature_id' + ], + 'linker_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_pkey', + 'chado_table' => 'analysisfeature', + 'chado_column' => 'analysisfeature_id', + ], + 'link' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_link', + 'left_table' => 'feature', + 'left_table_id' => 'feature_id', + 'right_table' => 'analysisfeature', + 'right_table_id' => 'feature_id' + ], + 'right_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'analysisfeature', + 'chado_column' => 'analysis_id' + ], + ], + ], + 'contactfield' => [ + 'field_name' => 'contactfield', + 'base_table' => 'feature', + 'properties' => [ + 'record_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_id', + 'drupal_store' => TRUE, + 'chado_table' => 'feature', + 'chado_column' => 'feature_id' + ], + 'linker_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_pkey', + 'chado_table' => 'feature_contact', + 'chado_column' => 'feature_contact_id', + ], + 'link' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_link', + 'left_table' => 'feature', + 'left_table_id' => 'feature_id', + 'right_table' => 'feature_contact', + 'right_table_id' => 'feature_id' + ], + 'right_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'feature_contact', + 'chado_column' => 'contact_id' + ], + ], + ], + 'testotherfeaturefield' => [ + 'field_name' => 'testotherfeaturefield', + 'base_table' => 'feature', + 'properties' => [ + 'record_id' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store_id', + 'chado_table' => 'feature', + 'chado_column' => 'feature_id' + ], + 'feature_type' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'feature', + 'chado_column' => 'type_id' + ], + 'feature_organism' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoIntStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'feature', + 'chado_column' => 'organism_id' + ], + 'feature_uname' => [ + 'propertyType class' => 'Drupal\tripal_chado\TripalStorage\ChadoVarCharStoragePropertyType', + 'action' => 'store', + 'chado_table' => 'feature', + 'chado_column' => 'uniquename' + ], + ], + ], + ]; + + protected int $organism_id; + protected int $cvterm_id; + protected array $right_id; + + /** + * {@inheritdoc} + */ + protected function setUp() :void { + parent::setUp(); + + // We need to mock the logger to test the progress reporting. + $container = \Drupal::getContainer(); + $mock_logger = $this->getMockBuilder(\Drupal\tripal\Services\TripalLogger::class) + ->onlyMethods(['warning']) + ->getMock(); + $mock_logger->method('warning') + ->willReturnCallback(function($message, $context, $options) { + print str_replace(array_keys($context), $context, $message); + return NULL; + }); + $container->set('tripal.logger', $mock_logger); + + $this->setUpChadoStorageTestEnviro(); + + // Create the organism record for use with the feature table. + $infra_type_id = $this->getCvtermID('TAXRANK', '0000010'); + $query = $this->chado_connection->insert('1:organism'); + $query->fields([ + 'genus' => 'Tripalus', + 'species' => 'databasica', + 'common_name' => 'Tripal', + 'abbreviation' => 'T. databasica', + 'infraspecific_name' => 'postgresql', + 'type_id' => $infra_type_id, + 'comment' => 'This is fake organism specifically for testing purposes.' + ]); + $this->organism_id = $query->execute(); + + $this->cvterm_id = $this->getCvtermID('rdfs', 'type'); + + // Pub + $query = $this->chado_connection->insert('1:pub'); + $query->fields([ + 'uniquename' => 'test' . uniqid() . 'PUB', + 'type_id' => $this->cvterm_id, + ]); + $query->execute(); + // Don't need to save as it will be 1 since the table is empty. + // We just need it to exist. + + // Synonym. + $this->right_id['synonymfield'] = []; + foreach([1,2,3,4,5] as $delta) { + $query = $this->chado_connection->insert('1:synonym'); + $query->fields([ + 'name' => 'test' . uniqid() . '-' . $delta, + 'synonym_sgml' => 'test-' . $delta, + 'type_id' => $this->cvterm_id, + ]); + $this->right_id['synonymfield'][] = $query->execute(); + } + + // Analysis. + $this->right_id['analysisfield'] = []; + foreach([1,2,3,4,5] as $delta) { + $query = $this->chado_connection->insert('1:analysis'); + $query->fields([ + 'program' => 'test' . uniqid() . '-' . $delta, + 'programversion' => 'test-' . $delta, + ]); + $this->right_id['analysisfield'][] = $query->execute(); + } + + // Contact. + $this->right_id['contactfield'] = []; + foreach([1,2,3,4,5] as $delta) { + $query = $this->chado_connection->insert('1:contact'); + $query->fields([ + 'name' => 'test' . uniqid() . '-' . $delta, + 'type_id' => $this->cvterm_id, + ]); + $this->right_id['contactfield'][] = $query->execute(); + } + } + + /** + * Data Provider: define each test case. + */ + public function provideTestCases() { + return [ + // synonymfield: feature > feature_synonym + [ + 'synonymfield', + 'feature_synonym', + 'synonym_id', + [ + 'pub_id' => 1, + 'is_current' => TRUE, + 'is_internal' => TRUE, + ] + ], + // analysisfield: feature > analysisfeature + [ + 'analysisfield', + 'analysisfeature', + 'analysis_id', + [] + ], + // contactfield: feature > feature_contact + [ + 'contactfield', + 'feature_contact', + 'contact_id', + [] + ], + // relationshipfield: feature > feature_relationship + /* + [ + 'relationshipfield', + 'feature_relationship', + 'synonym_id', + [ + 'pub_id' => 1, + 'is_current' => TRUE, + 'is_internal' => TRUE, + ] + ], + */ + ]; + } + + /** + * Testing ChadoStorage on linker fields. + * + * @dataProvider provideTestCases + * + * Test Cases: + * - Create Values in Chado using ChadoStorage when they don't yet exist. + * - Load values in Chado using ChadoStorage after we just inserted them. + * - Update values in Chado using ChadoStorage after we just inserted them. + * - [NOT IMPLEMENTED] Delete values in Chado using ChadoStorage. + * - [NOT IMPLEMENTED] Ensure property field picks up records in Chado not added through field. + * + * Parameters provided by provideTestCases(). + * @params $linker_field_name + * The specific field in $fields to be used for the current test case. + * @param $linker_table_name + * The name of the chado linker table we are testing in the field specified by $linker_field_name + * @param $right_table_id + * The primary key for the right table in the link. + * @param $extra_values + * The values for the extra fields specific to each linker table + * where the key is the property key and the value is the value we should set it to. + */ + public function testLinkerTableField($linker_field_name, $linker_table_name, $right_table_id, $extra_values) { + $linker_pkey = $linker_table_name . '_id'; + + // Test Case: Insert valid values when they do not yet exist in Chado. + // --------------------------------------------------------- + $insert_values = [ + $linker_field_name => [ + [ + 'record_id' => NULL, + 'linker_id' => NULL, + 'link' => NULL, + 'right_id' => $this->right_id[$linker_field_name][0], + ] + $extra_values, + [ + 'record_id' => NULL, + 'linker_id' => NULL, + 'link' => NULL, + 'right_id' => $this->right_id[$linker_field_name][1], + ] + $extra_values, + ], + 'testotherfeaturefield' => [ + [ + 'feature_type' => $this->cvterm_id, + 'feature_organism' => $this->organism_id, + 'feature_uname' => 'testGene4' . $linker_field_name . 'Test', + ] + ], + ]; + $this->chadoStorageTestInsertValues($insert_values); + + // @debug $this->debugChadoStorageTestTraitArrays(); + + // Check that the base feature record was created in the database as expected. + // Note: makes some assumptions based on knowing the data provider for + // better readability of the tests. + $field_name = 'testotherfeaturefield'; + $query = $this->chado_connection->select('1:feature', 'f') + ->fields('f', ['feature_id', 'type_id', 'organism_id', 'uniquename']) + ->execute(); + $records = $query->fetchAll(); + $this->assertCount(1, $records, + "There should only be a single feature record created by our storage properties."); + $record = $records[0]; + $record_expect = $insert_values[$field_name][0]; + $this->assertIsObject($record, + "The returned feature record should be an object."); + $this->assertEquals($record_expect['feature_type'], $record->type_id, + "The feature record should have the type we set in our storage properties."); + $this->assertEquals($record_expect['feature_organism'], $record->organism_id, + "The feature record should have the organism we set in our storage properties."); + $this->assertEquals($record_expect['feature_uname'], $record->uniquename, + "The feature record should have the unique name we set in our storage properties."); + $feature_id = $record->feature_id; + + // Also check that there are only the expected number of records + // in the linker table. + $query = $this->chado_connection->select('1:' . $linker_table_name, 'linker') + ->fields('linker') + ->execute(); + $all_linker_records = $query->fetchAll(); + $this->assertCount(2, $all_linker_records, + "There were more records then we were expecting in the $linker_table_name table: " . print_r($all_linker_records, TRUE)); + + // Check that the linker table records were created in the database as expected. + // We use the unique key to select this particular value in order to + // ensure it is here and there is one one. + foreach ($insert_values[$linker_field_name] as $delta => $expected) { + $query = $this->chado_connection->select('1:' . $linker_table_name, 'linker') + ->fields('linker') + ->condition('feature_id', $feature_id, '=') + ->condition($right_table_id, $expected['right_id']) + ->execute(); + $records = $query->fetchAll(); + $this->assertCount(1, $records, "We expected to get exactly one record for:" . print_r($expected, TRUE)); + + $varname = 'link' . $delta; + $$varname = $records[0]; + } + + // Test Case: Load values existing in Chado. + // --------------------------------------------------------- + // First we want to reset all the chado storage arrays to ensure we are + // doing a clean test. The values will purposefully remain in Chado but the + // Property Types, Property Values and Data Values will be built from scratch. + $this->cleanChadoStorageValues(); + + // For loading only the store id/pkey/link items should be populated. + $load_values = [ + $linker_field_name => [ + [ + 'record_id' => $feature_id, + 'linker_id' => $link0->$linker_pkey, + 'link' => $feature_id, + ], + [ + 'record_id' => $feature_id, + 'linker_id' => $link1->$linker_pkey, + 'link' => $feature_id, + ], + ], + 'testotherfeaturefield' => [ + [ + 'record_id' => $feature_id, + ] + ], + ]; + $retrieved_values = $this->chadoStorageTestLoadValues($load_values); + + // Now test that the additional values have been loaded. + // @debug $this->debugChadoStorageTestTraitArrays(); + foreach([0,1] as $delta) { + $retrieved = $retrieved_values[$linker_field_name][$delta]; + $varname = 'link' . $delta; + $expected = $$varname; + $this->assertEquals( + $expected->$right_table_id, + $retrieved['right_id']['value']->getValue(), + "The $right_table_id for delta $delta did not match the one we retrieved from chado after insert." + ); + // These two should match for sure as we actually set the above in our load + // arrays but let's check anyway to make sure there isn't any funny business. + $this->assertEquals( + $expected->$linker_pkey, + $retrieved['linker_id']['value']->getValue(), + "The pkey for the linker table, $linker_pkey, for delta $delta did not match the one we retrieved from chado after insert." + ); + $this->assertEquals( + $expected->feature_id, + $retrieved['record_id']['value']->getValue(), + "The record_id/feature_id for delta $delta did not match the one we retrieved from chado after insert." + ); + } + + // Test Case: Update values in Chado using ChadoStorage. + // --------------------------------------------------------- + // When updating we need all the store id/pkey/link records + // and all values of the other properties. + // array_merge alone seems not to be sufficient + $update_values = $insert_values; + foreach ($load_values as $field_name => $tmp) { + foreach ($tmp as $delta => $id_values) { + foreach ($id_values as $key => $value) { + $update_values[$field_name][$delta][$key] = $value; + } + } + } + // We also want to add the right_id in for each delta + // which we didn't include in the load to ensure we had something + // new to check ;-). + foreach($update_values[$linker_field_name] as $delta => $values) { + $cur_link_record = 'link' . $delta; + $update_values[$linker_field_name][$delta]['right_id'] = $$cur_link_record->$right_table_id; + } + // Finally we want to add the 3rd record which will be new + // (an insert rather than an update). + $update_values[$linker_field_name][3] = [ + 'record_id' => $feature_id, + 'linker_id' => NULL, + 'link' => $feature_id, + 'right_id' => $this->right_id[$linker_field_name][4], + ] + $extra_values; + + // We then change the right table selected to one we haven't used yet. + $update_values[$linker_field_name][1]['right_id'] = $this->right_id[$linker_field_name][3]; + $this->chadoStorageTestUpdateValues($update_values); + + // Now we check chado to see if these values were changed... + // Still the expected number of records in the featureprop table? + $query = $this->chado_connection->select('1:' . $linker_table_name, 'linker') + ->fields('linker') + ->execute(); + $all_linker_records = $query->fetchAll(); + $this->assertCount(3, $all_linker_records, + "There were more records then we were expecting in the $linker_table_name table: " . print_r($all_linker_records, TRUE)); + + // Check that the linker table records were updated/created in the database as expected. + // We use the unique key to select this particular value in order to + // ensure it is here and there is one one. + foreach ($update_values[$linker_field_name] as $delta => $expected) { + $query = $this->chado_connection->select('1:' . $linker_table_name, 'linker') + ->fields('linker') + ->condition('feature_id', $feature_id, '=') + ->condition($right_table_id, $expected['right_id']) + ->execute(); + $records = $query->fetchAll(); + $this->assertCount(1, $records, "We expected to get exactly one record for:" . print_r($expected, TRUE)); + } + + // Test Case: Delete values in Chado using ChadoStorage. + // --------------------------------------------------------- + + // NOT YET IMPLEMENTED IN CHADOSTORAGE. + } +} diff --git a/tripal_chado/tests/src/Kernel/Plugin/TripalImporter/FASTAImporterFormTest.php b/tripal_chado/tests/src/Kernel/Plugin/TripalImporter/FASTAImporterFormTest.php new file mode 100644 index 000000000..1393036c2 --- /dev/null +++ b/tripal_chado/tests/src/Kernel/Plugin/TripalImporter/FASTAImporterFormTest.php @@ -0,0 +1,89 @@ +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'); + // ... managed files are associated with a user. + $this->installEntitySchema('user'); + // ... Finally the file module + tables itself. + $this->installEntitySchema('file'); + $this->installSchema('file', ['file_usage']); + + } + + /** + * Tests focusing on the importer form. + */ + public function testImporterForm() { + + $plugin_id = 'chado_fasta_loader'; + $importer_label = 'Chado FASTA File Loader'; + + // 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($importer_label, $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 have a submit button since we indicated a specific importer."); + // This importer requires an analysis. + $this->assertArrayHasKey('analysis_id', $form, + "The from should not include analysis element, yet one exists."); + + // 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."); + + } + +} diff --git a/tripal_chado/tests/src/Kernel/Plugin/TripalImporter/GFF3ImporterFormTest.php b/tripal_chado/tests/src/Kernel/Plugin/TripalImporter/GFF3ImporterFormTest.php new file mode 100644 index 000000000..ea50d034b --- /dev/null +++ b/tripal_chado/tests/src/Kernel/Plugin/TripalImporter/GFF3ImporterFormTest.php @@ -0,0 +1,89 @@ +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'); + // ... managed files are associated with a user. + $this->installEntitySchema('user'); + // ... Finally the file module + tables itself. + $this->installEntitySchema('file'); + $this->installSchema('file', ['file_usage']); + + } + + /** + * Tests focusing on the importer form. + */ + public function testImporterForm() { + + $plugin_id = 'chado_gff3_loader'; + $importer_label = 'Chado GFF3 File Loader'; + + // 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($importer_label, $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 have a submit button since we indicated a specific importer."); + // This importer requires an analysis. + $this->assertArrayHasKey('analysis_id', $form, + "The from should not include analysis element, yet one exists."); + + // 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."); + + } + +} diff --git a/tripal_chado/tests/src/Kernel/Plugin/TripalImporter/OBOImporterFormTest.php b/tripal_chado/tests/src/Kernel/Plugin/TripalImporter/OBOImporterFormTest.php new file mode 100644 index 000000000..090343914 --- /dev/null +++ b/tripal_chado/tests/src/Kernel/Plugin/TripalImporter/OBOImporterFormTest.php @@ -0,0 +1,87 @@ +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'); + // ... 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_cv_obo']); + + } + + /** + * Tests focusing on the importer form. + */ + public function testImporterForm() { + + $plugin_id = 'chado_obo_loader'; + $importer_label = 'OBO Vocabulary Loader'; + + // 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($importer_label, $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 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."); + + } + +} diff --git a/tripal_chado/tests/src/Kernel/Plugin/TripalImporter/TaxonomyImporterFormTest.php b/tripal_chado/tests/src/Kernel/Plugin/TripalImporter/TaxonomyImporterFormTest.php new file mode 100644 index 000000000..e4f4f68de --- /dev/null +++ b/tripal_chado/tests/src/Kernel/Plugin/TripalImporter/TaxonomyImporterFormTest.php @@ -0,0 +1,86 @@ +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'); + // ... managed files are associated with a user. + $this->installEntitySchema('user'); + // ... Finally the file module + tables itself. + $this->installEntitySchema('file'); + $this->installSchema('file', ['file_usage']); + + } + + /** + * Tests focusing on the importer form. + */ + public function testImporterForm() { + + $plugin_id = 'chado_taxonomy_loader'; + $importer_label = 'Taxonomy Loader'; + + // 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($importer_label, $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 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."); + + } + +} diff --git a/tripal_chado/tests/src/Traits/ChadoStorageTestTrait.php b/tripal_chado/tests/src/Traits/ChadoStorageTestTrait.php index b55c21a38..7c40d1388 100644 --- a/tripal_chado/tests/src/Traits/ChadoStorageTestTrait.php +++ b/tripal_chado/tests/src/Traits/ChadoStorageTestTrait.php @@ -240,6 +240,8 @@ protected function chadoStorageTestInsertValues(array $values) { // Set the values in the propertyValue objects. $this->setExpectedValues($field_names, $values); + // $this->debugChadoStorageTestTraitArrays(); + $success = $this->chadoStorage->insertValues($this->dataStoreValues); $this->assertTrue($success, 'We were not able to insert the data.'); } @@ -650,6 +652,9 @@ public function setExpectedValues($field_names, $values) { foreach($values[$field_name] as $delta => $current_values) { foreach($current_values as $property_key => $val) { + $this->assertArrayHasKey($property_key, $this->dataStoreValues[$field_name][$delta], + "The key $property_key does not exist in the data store values for $field_name[$delta], it may be missing from your \$fields definition"); + $this->dataStoreValues[$field_name][$delta][$property_key]['value']->setValue($val); // @debug print "SETTING $field_name [ $delta ] $property_key: $val\n"; diff --git a/tripal_chado/tripal_chado.module b/tripal_chado/tripal_chado.module index 35b7b8caf..f78ce8cab 100644 --- a/tripal_chado/tripal_chado.module +++ b/tripal_chado/tripal_chado.module @@ -18,6 +18,7 @@ require_once 'src/api/tripal_chado.db.api.php'; require_once 'src/api/tripal_chado.cv.api.php'; require_once 'src/api/tripal_chado.property.api.php'; require_once 'src/api/tripal_chado.phylotree.api.php'; +require_once 'src/api/tripal_chado.phylotree_newick.api.php'; require_once 'src/legacyFunctions.php'; /**