'example_property', // the name of the table linking additional properties to this node 'chado_id_field' => 'example_id', // key to link to the chado content created by this node 'chado_id' => $example_id, // the value of the above key 'cv_name' => 'example_prop_cv', // the name of the cv governing the _prop.type_id 'fieldset_title' => 'Additional References', // the non-translated title for this fieldset 'additional_instructions' => '' // a non-stranslated string providing additional instructions ); // Finally, and add the additional form elements to the form chado_add_node_form_properties($form, $form_state, $details); return $form; } function chado_example_insert($node) { // if there is an example_id in the $node object then this must be a sync so // we can skip adding the chado_example as it is already there, although // we do need to proceed with the rest of the insert if (!property_exists($node, 'example_id')) { // Add record to chado example table // Add to any other tables needed // Add all properties // Existing _property links will be cleared and then re-added tripal_api_chado_node_properties_form_update_properties( $node, // the node object passed in via hook_insert() 'example_property', // the name of the _property linking table 'example', // the name of the base chado table for the node 'example_id', // key to link to the chado content created by this node $node->example_id // value of the above key ); } // Add record to chado_example linking example_id to new node } function chado_example_update($node) { // Update record in chado example table // Update any other tables needed // Update all properties // Existing _property links will be cleared and then re-added tripal_api_chado_node_properties_form_update_properties( $node, // the node object passed in via hook_insert() 'example_property', // the name of the _property linking table 'example', // the name of the base chado table for the node 'example_id', // key to link to the chado content created by this node $node->example_id // value of the above key ); // Don't need to update chado_example linking table since niether example_id or nid can be changed in update } * @endcode * * @ingroup tripal_chado_node_api */ /** * Provides a form for adding to BASEprop table * * @param $form * The Drupal form array into which the property form elements will be added * @param $form_state * The corresponding form_state array for the form * @param $details * An array defining details used by this form. * Required keys that are always required: * - property_table: the name of the property table (e.g.: featureprop, stockprop, etc.) * Required keys for forms that update a record. * - chado_id: the id of the record to which properties will be associated (e.g.: if a * feature has a feature_id of 999 and we want to associate properties for that feature * then the chado_id option should be 999) * Require ONE of the following to identify the controlled vocabulary containing the properties to use: * -cv_id: the unique key from the cv table * -cv_name: the cv.name field uniquely identifying the controlled vocabulary * Optional keys include: * - chado_id_field: the foreign key field that links properties to the * chado_id record. If this value is not specified it is determined using the * traditional Chado naming scheme for property tables. * * @ingroup tripal_chado_node_api */ function chado_add_node_form_properties(&$form, &$form_state, $details) { // make sure the property table exists before proceeding. if (!chado_table_exists($details['property_table'])) { drupal_set_message("Cannot add property elements to the form. The property table, '" . $details['property_table'] . "', does not exists", "error"); tripal_report_error('tcprops_form', TRIPAL_ERROR, "Cannot add property elements to the form. The property table, '%name', cannot be found.", array('%name' => $details['property_table'])); return; } // if the chado_id_field is not specified then set it using the // typical chado naming scheme if (!array_key_exists('chado_id_field', $details)) { $chado_id_table = preg_replace('/prop$/', '', $details['property_table']); $chado_id_field = $chado_id_table . '_id'; $details['chado_id_field'] = $chado_id_field; } // make sure the specified cv exists if (isset($details['cv_name'])) { // make sure the cv_name is real $result = chado_select_record('cv',array('cv_id'),array('name' => $details['cv_name'])); if (count($result) == 0) { drupal_set_message("Cannot add property elements to the form. The CV name, '" . $details['cv_name'] . "', does not exists", "error"); tripal_report_error('tcprops_form', TRIPAL_ERROR, "Cannot add property elements to the form. The CV named, '%name', cannot be found.", array('%name' => $details['cv_name'])); return; } // add the cv_id option to the details array $details['cv_id'] = $result[0]->cv_id; } elseif (isset($details['cv_id'])) { // make sure the cv_id is real $result = chado_select_record('cv', array('name'), array('cv_id' => $details['cv_id'])); if (count($result) == 0) { drupal_set_message("Cannot add property elements to the form. The CV ID, '" . $details['cv_id'] . "', does not exist", "error"); tripal_report_error('tcprops_form', TRIPAL_ERROR, "Cannot add property elements to the form. The CV ID, '%id', cannot be found.", array('%id' => $details['cv_id'])); return; } // add the cv_name option to the details array $details['cv_name'] = $result[0]->name; } else { drupal_set_message("Please provide either a 'cv_name' or 'cv_id' as an option for adding properties to the form", "error"); tripal_report_error('tcprops_form', TRIPAL_ERROR, "Please provide either a 'cv_name' or 'cv_id' as an option for adding properties to the form", array()); return; } // Set Defaults for optional fields $details['fieldset_title'] = 'Properties'; $details['additional_instructions'] = ''; // Get Property Types for the Select List if (isset($details['select_options'])) { $property_options = $details['select_options']; } // if the select options are not provided then try to get them on our own else { // if the vocabulary name is provided in the details then use that to // get the terms if (isset($details['cv_name'])) { $property_options = array(); $property_options[] = 'Select a Property'; $sql = " SELECT DISTINCT CVT.cvterm_id, CVT.name, CVT.definition, CV.cv_id as cv_id FROM {cvterm} CVT INNER JOIN {cv} CV ON CVT.cv_id = CV.cv_id WHERE CV.name = :cv_name AND NOT CVT.is_obsolete = 1 ORDER BY CVT.name ASC "; $prop_types = chado_query($sql, array(':cv_name' => $details['cv_name'])); while ($prop = $prop_types->fetchObject()) { $property_options[$prop->cvterm_id] = $prop->name; } } // if the cv_id is set in the $details array then use that to get the terms elseif (isset($details['cv_id'])) { $property_options = array(); $property_options[] = 'Select a Property'; $sql = " SELECT DISTINCT CVT.cvterm_id, CVT.name, CVT.definition, CV.name as cv_name FROM {cvterm} CVT INNER JOIN {cv} CV ON CVT.cv_id = CV.cv_id WHERE CV.cv_id = :cv_id AND NOT CVT.is_obsolete = 1 ORDER BY CVT.name ASC "; $prop_types = chado_query($sql, array(':cv_id' => $details['cv_id'])); while ($prop = $prop_types->fetchObject()) { $property_options[$prop->cvterm_id] = $prop->name; } } } // Tell tripal administrators how to add terms to the property types drop down. if (empty($property_options)) { $tripal_message = tripal_set_message( t('There are currently no proeprty types! To add additional properties to the drop down list, you need to add a controlled vocabulary term to the %cv_name controlled vocabulary.', array( '%cv_name' => $details['cv_name'], '@cvtermlink' => url('admin/tripal/chado/tripal_cv/cv/' . $details['cv_id'] . '/cvterm/add') ) ), TRIPAL_WARNING, array('return_html' => TRUE) ); } else { $tripal_message = tripal_set_message( t('To add additional properties to the drop down list, you need to add a controlled vocabulary term to the %cv_name controlled vocabulary.', array( '%cv_name' => $details['cv_name'], '@cvtermlink' => url('admin/tripal/chado/tripal_cv/cv/' . $details['cv_id'] . '/cvterm/add') ) ), TRIPAL_INFO, array('return_html' => TRUE) ); } // the fieldset of the property elements $form['properties'] = array( '#type' => 'fieldset', '#title' => t($details['fieldset_title']), '#description' => t('You may add additional properties by selecting a property type from the dropdown and adding text. You may add as many properties as desired by clicking the add button on the right. To remove a property, click the remove button.' . $details['additional_instructions']), '#prefix' => "
", '#suffix' => '
', '#weight' => 8 ); $form['properties']['admin_message'] = array( '#type' => 'markup', '#markup' => $tripal_message ); // this form element is a tree, so that we don't puke all of the values into then node variable // it is set as a tree, and keeps them in the $form_state['values']['property_table'] heading. $form['properties']['property_table'] = array( '#type' => 'markup', '#tree' => TRUE, '#prefix' => '
', '#suffix' => '
', '#theme' => 'chado_node_properties_form_table' ); // Add defaults into form_state to be used elsewhere $form['properties']['property_table']['details'] = array( '#type' => 'hidden', '#value' => serialize($details) ); /* Properties can come to us in two ways: * * 1) In the form state in the $form_state['chado_properties']. Data is in this field * when an AJAX call updates the form state or a validation error. * * 2) Directly from the database if the record already has properties associated. This * data is only used the first time the form is loaded. On AJAX calls or validation * errors the fields on the form are populated from the $form_state['chado_properties'] * entry. */ if (isset($form_state['chado_properties'])) { $existing_properties = $form_state['chado_properties']; } else { if (isset($details['cv_name'])) { $existing_properties = chado_query( "SELECT PP.".$details['property_table']."_id property_id, CVT.cvterm_id as type_id, CVT.name as type_name, CVT.definition, PP.value, PP.rank FROM {" . $details['property_table'] . "} PP INNER JOIN {cvterm} CVT ON CVT.cvterm_id = PP.type_id INNER JOIN {cv} CV ON CVT.cv_id = CV.cv_id WHERE PP." . $details['chado_id_field'] . " = :chado_id AND CV.name = '" .$details['cv_name']. "' ORDER BY CVT.name, PP.rank", array(':chado_id' => $details['chado_id']) ); } elseif (isset($details['cv_id'])) { $existing_properties = chado_query( "SELECT PP.".$details['property_table']."_id property_id, CVT.cvterm_id as type_id, CVT.name as type_name, CVT.definition, PP.value, PP.rank FROM {" . $details['property_table'] . "} PP INNER JOIN {cvterm} CVT ON CVT.cvterm_id = PP.type_id INNER JOIN {cv} CV ON CVT.cv_id = CV.cv_id WHERE PP." . $details['chado_id_field'] . " = :chado_id AND CV.cv_id = '" .$details['cv_id']. "' ORDER BY CVT.name, PP.rank", array(':chado_id' => $details['chado_id']) ); } } /* The format of the $existing_properties array is either: * * From the chado_properties array: * $form_state['chado_properties'] = array( * '[type_id]-[rank]' => array( * 'type_id' => [the cvterm.cvterm_id value] * 'type_name' => [the cvterm.name value] * 'property_id' => [the property.property_id value, or NULL if it doesn't yet exist], * 'value' => [the BASEprop.value value], * 'rank' => [the BASEprop.rank value], * ), * ); * * OR * Populated from the database: * $existing_property = array( * 0 => array( * 'property_id' => [the property.property_id value, or NULL if it doesn't yet exist], * 'type_id' => [the cvterm.cvterm_id value] * 'type_name' => [the cvterm.name value] * 'value' => [the BASEprop.value value], * 'rank' => [the BASEprop.rank value], * ), * ); * * NOTE: The main difference is the key * * Loop on the array elements of the $existing_properties array and add * an element to the form for each one as long as it's also in the * $properties_options array. */ foreach ($existing_properties as $property) { if (array_key_exists($property->type_id, $property_options)) { $form['properties']['property_table'][$property->type_id]['#type'] = 'markup'; $form['properties']['property_table'][$property->type_id]['#value'] = ''; $form['properties']['property_table'][$property->type_id][$property->rank]['#type'] = 'markup'; $form['properties']['property_table'][$property->type_id][$property->rank]['#value'] = ''; $form['properties']['property_table'][$property->type_id][$property->rank]['prop_type_id'] = array( '#type' => 'hidden', '#value' => $property->type_id ); $form['properties']['property_table'][$property->type_id][$property->rank]['prop_value'] = array( '#type' => 'hidden', '#value' => $property->value ); $form['properties']['property_table'][$property->type_id][$property->rank]['property_id'] = array( '#type' => 'hidden', '#value' => $property->property_id ); $form['properties']['property_table'][$property->type_id][$property->rank]['type'] = array( '#type' => 'markup', '#markup' => $property->type_name ); $form['properties']['property_table'][$property->type_id][$property->rank]['value'] = array( '#type' => 'markup', '#markup' => $property->value ); $form['properties']['property_table'][$property->type_id][$property->rank]['rank'] = array( '#type' => 'markup', '#markup' => $property->rank ); // remove button $form['properties']['property_table'][$property->type_id][$property->rank]['property_action'] = array( '#type' => 'submit', '#value' => t('Remove'), '#name' => "property_remove-".$property->type_id.'-'.$property->rank, '#ajax' => array( 'callback' => "chado_add_node_form_properties_ajax_update", 'wrapper' => 'tripal-generic-edit-properties-table', 'effect' => 'fade', 'method' => 'replace', 'prevent' => 'click' ), // When this button is clicked, the form will be validated and submitted. // Therefore, we set custom submit and validate functions to override the // default node form submit. In the validate function we validate only the // property fields and in the submit we remove the indicated property // from the chado_properties array. In order to keep validate errors // from the node form validate and Drupal required errors for non-property fields // preventing the user from removing properties we set the #limit_validation_errors below '#validate' => array('chado_add_node_form_properties_remove_button_validate'), '#submit' => array('chado_add_node_form_properties_remove_button_submit'), // Limit the validation of the form upon clicking this button to the property_table tree // No other fields will be validated (ie: no fields from the main form or any other api // added form). '#limit_validation_errors' => array( array('property_table') // Validate all fields within $form_state['values']['property_table'] ) ); } } // Form elements for adding a new property //--------------------------------------------- $form['properties']['property_table']['new'] = array( '#type' => 'markup', '#prefix' => '', '#suffix' => '' ); $form['properties']['property_table']['new']['type'] = array( '#type' => 'select', '#options' => $property_options, // Set at top of form ); $form['properties']['property_table']['new']['value'] = array( '#type' => 'textarea', '#rows' => 1, ); // add button $form['properties']['property_table']['new']['property_action'] = array( '#type' => 'submit', '#value' => t('Add'), '#name' => "property-add", '#ajax' => array( 'callback' => "chado_add_node_form_properties_ajax_update", 'wrapper' => 'tripal-generic-edit-properties-table', 'effect' => 'fade', 'method' => 'replace', 'prevent' => 'click' ), // When this button is clicked, the form will be validated and submitted. // Therefore, we set custom submit and validate functions to override the // default node form submit. In the validate function we validate only the // additional property fields and in the submit we add them to the chado_properties // array. In order to keep validate errors from the node form validate and Drupal // required errors for non-property fields preventing the user from adding properties we // set the #limit_validation_errors below '#validate' => array('chado_update_node_form_properties_add_button_validate'), '#submit' => array('chado_add_node_form_properties_add_button_submit'), // Limit the validation of the form upon clicking this button to the property_table tree // No other fields will be validated (ie: no fields from the main form or any other api // added form). '#limit_validation_errors' => array( array('property_table') // Validate all fields within $form_state['values']['property_table'] ) ); } /** * Validate the user input for creating a new property * Called by the add button in chado_add_node_form_properties * * @ingroup tripal_core */ function chado_update_node_form_properties_add_button_validate($form, &$form_state) { // Ensure the type_id is supplied & Valid $cvterm = chado_select_record( 'cvterm', array('cvterm_id', 'name'), array('cvterm_id' => $form_state['values']['property_table']['new']['type']) ); if (!isset($cvterm[0])) { form_set_error('property_table][new][cvterm', 'Please select a property type before attempting to add a new property.'); } else { $form_state['values']['property_table']['new']['type_name'] = $cvterm[0]->name; } // Ensure value is supplied if (empty($form_state['values']['property_table']['new']['value'])) { form_set_error('property_table][new][value','You must enter the property value before attempting to add a new property.'); } } /** * Called by the add button in chado_add_node_form_properties * * Create an array of properties in the form state. This array will then be * used to rebuild the form in subsequent builds * * @ingroup tripal_core */ function chado_add_node_form_properties_add_button_submit(&$form, &$form_state) { $details = unserialize($form_state['values']['property_table']['details']); // if the chado_additional_properties array is not set then this is the first time modifying the // property table. this means we need to include all the properties from the db if (!isset($form_state['chado_properties'])) { chado_add_node_form_properties_create_property_formstate_array($form, $form_state); } // get details for the new property $property = array( 'type_id' => $form_state['values']['property_table']['new']['type'], 'type_name' => $form_state['values']['property_table']['new']['type_name'], 'property_id' => NULL, 'value' => $form_state['values']['property_table']['new']['value'], 'rank' => '0', ); // get max rank $rank = chado_get_table_max_rank( $details['property_table'], array( $details['chado_id_field'] => $details['chado_id'], 'type_id' => $property['type_id'] ) ); $property['rank'] = strval($rank + 1); $key = $property['type_id'] . '-' . $property['rank']; $form_state['chado_properties'][$key] = (object) $property; $form_state['rebuild'] = TRUE; } /** * There is no user input for the remove buttons so there is no need to validate * However, both a submit & validate need to be specified so this is just a placeholder * * Called by the many remove buttons in chado_add_node_form_properties * * @ingroup tripal_core */ function chado_add_node_form_properties_remove_button_validate($form, $form_state) { // No Validation needed for remove } /** * Remove the correct property from the form * Called by the many remove buttons in chado_add_node_form_properties * * @ingroup tripal_core */ function chado_add_node_form_properties_remove_button_submit(&$form, &$form_state) { // if the chado_properties array is not set then this is the first time modifying the // property table. this means we need to include all the properties from the db if (!isset($form_state['chado_properties'])) { chado_add_node_form_properties_create_property_formstate_array($form, $form_state); } // remove the specified property from the form property table if(preg_match('/property_remove-([^-]+-[^-]+)/',$form_state['triggering_element']['#name'],$match)) { $key = $match[1]; if (array_key_exists($key, $form_state['chado_properties'])) { unset($form_state['chado_properties'][$key]); } } $form_state['rebuild'] = TRUE; } /** * Ajax function which returns the section of the form to be re-rendered * * @ingroup tripal_core */ function chado_add_node_form_properties_ajax_update($form, $form_state) { return $form['properties']['property_table']; } /** * Creates an array in form_state containing the existing properties. This array is * then modified by the add/remove buttons and used as a source for rebuilding the form. * This function get's called at each button (add and remove) button submits the first * time one of the button's is clicked to instantiates the $form_state['chado_properties'] array * * $form_state['chado_properties'] = array( * '[type_id]-[rank]' => array( * 'type_id' => [the cvterm.cvterm_id value] * 'type_name' => [the cvterm.name value] * 'property_id' => [the property.property_id value, or NULL if it doesn't yet exist], * 'value' => [the BASEprop.value value], * 'rank' => [the BASEprop.rank value], * ), * ); * * @ingroup tripal_core */ function chado_add_node_form_properties_create_property_formstate_array($form, &$form_state) { $form_state['chado_properties'] = array(); foreach (element_children($form['properties']['property_table']) as $type_id) { if ($type_id != 'new') { foreach (element_children($form['properties']['property_table'][$type_id]) as $rank) { $element = $form['properties']['property_table'][$type_id][$rank]; $property = array( 'type_id' => $element['prop_type_id']['#value'], 'type_name' => $element['type']['#markup'], 'property_id' => $element['property_id']['#value'], 'value' => $element['value']['#markup'], 'rank' => $element['rank']['#markup'] ); $key = $property['type_id'] . '-' . $property['rank']; $form_state['chado_properties'][$key] = (object) $property; } } } } /** * Function to theme the add/remove properties form into a table * * @ingroup tripal_chado_node_api */ function theme_chado_add_node_form_properties($variables) { $element = $variables['element']; $header = array( 'type' => t('Type'), 'value' => t('Value'), 'property_action' => t('Actions'), ); $rows = array(); foreach (element_children($element) as $type_id) { if ($type_id == 'new') { $row = array(); $row['data'] = array(); foreach ($header as $fieldname => $title) { $row['data'][] = drupal_render($element[$type_id][$fieldname]); } $rows[] = $row; } else { foreach (element_children($element[$type_id]) as $version) { $row = array(); $row['data'] = array(); foreach ($header as $fieldname => $title) { $row['data'][] = drupal_render($element[$type_id][$version][$fieldname]); } $rows[] = $row; } } } return theme('table', array( 'header' => $header, 'rows' => $rows )); } /** * This function is used in a hook_insert, hook_update for a node form * when the chado node properties form has been added to the form. It retrieves all of the properties * and returns them in an array of the format: * * $dbxefs[][] = * * This array can then be used for inserting or updating properties * * @param $node * * @return * A property array * * @ingroup tripal_chado_node_api */ function chado_retrieve_node_form_properties($node) { $properties = array(); if (isset($node->property_table)) { foreach ($node->property_table as $type_id => $elements) { if ($type_id != 'new' AND $type_id != 'details') { foreach ($elements as $rank => $element) { $properties[$type_id][$rank] = $element['prop_value']; } } } } return $properties; } /** * This function is used in hook_insert or hook_update and handles inserting of any new * properties * * @param $node * The node passed into hook_insert & hook_update * @param $details * - property_table: the name of the _property linking table (ie: feature_property) * - base_table: the name of the base table (ie: feature) * - foreignkey_name: the name of the foreign key used to link to the node content (ie: feature_id) * - foreignkey_value: the value of the foreign key (ie: 445, if there exists a feature where feature_id=445) * @param $retrieved_properties * An array of properties from chado_retrieve_node_form_properties($node). This can be used if you need * special handling for some of the properties (See FeatureMap chado_featuremap_insert for an example) * * @ingroup tripal_chado_node_api */ function chado_update_node_form_properties($node, $details, $retrieved_properties = FALSE) { $details['foreignkey_value'] = (isset($details['foreignkey_value'])) ? $details['foreignkey_value'] : 0; if (isset($node->property_table) AND ($details['foreignkey_value'] > 0)) { // First remove existing property links chado_delete_record($details['property_table'], array($details['foreignkey_name'] => $details['foreignkey_value'])); // Add back in property links and insert properties as needed if ($retrieved_properties) { $properties = $retrieved_properties; } else { $properties = chado_retrieve_node_form_properties($node); } foreach ($properties as $type_id => $ranks) { foreach ($ranks as $rank => $value) { $success = chado_insert_record( $details['property_table'], array( $details['foreignkey_name'] => $details['foreignkey_value'], 'type_id' => $type_id, 'value' => $value, 'rank' => $rank ) ); if (!$success) { tripal_report_error('tripal_' . $details['base_table'], TRIPAL_ERROR, $details['base_table'] . ' Insert: Unable to insert property type_id %cvterm with value %value.', array('%cvterm' => $type_id, '%value' => $value)); } } } } }