From 48eaadd24e79c91f3a991a6eb614105eca7a75a6 Mon Sep 17 00:00:00 2001 From: Dries Buytaert <dries@buytaert.net> Date: Fri, 31 Jul 2009 07:43:33 +0000 Subject: [PATCH] - Patch #491190 by bangpound et al: provide a taxonomy term field type. Woot. Woot. --- modules/taxonomy/taxonomy.module | 239 +++++++++++++++++++++++++++++++ modules/taxonomy/taxonomy.test | 135 +++++++++++++++++ 2 files changed, 374 insertions(+) diff --git a/modules/taxonomy/taxonomy.module b/modules/taxonomy/taxonomy.module index 450757f4d776..4c003cbb026d 100644 --- a/modules/taxonomy/taxonomy.module +++ b/modules/taxonomy/taxonomy.module @@ -78,6 +78,12 @@ function taxonomy_theme() { 'taxonomy_overview_terms' => array( 'arguments' => array('form' => array()), ), + 'field_formatter_taxonomy_term_link' => array( + 'arguments' => array('element' => NULL), + ), + 'field_formatter_taxonomy_term_plain' => array( + 'arguments' => array('element' => NULL), + ), ); } @@ -425,6 +431,7 @@ function taxonomy_term_save($term) { } else { $status = drupal_write_record('taxonomy_term_data', $term); + _taxonomy_clean_field_cache($term); field_attach_insert('taxonomy_term', $term); module_invoke_all('taxonomy_term_update', $term); } @@ -552,6 +559,7 @@ function taxonomy_term_delete($tid) { ->execute(); field_attach_delete('taxonomy_term', $term); + _taxonomy_clean_field_cache($term); module_invoke_all('taxonomy_term_delete', $term); } @@ -1831,6 +1839,237 @@ function taxonomy_hook_info() { ); } +/** + * Implement hook_field_info(). + * + * Field settings: + * - allowed_values: a list array of one or more vocabulary trees: + * - vid: a vocabulary ID. + * - parent: a term ID of a term whose children are allowed. This should be + * '0' if all terms in a vocabulary are allowed. The allowed values do not + * include the parent term. + * + */ +function taxonomy_field_info() { + return array( + 'taxonomy_term' => array( + 'label' => t('Taxonomy term'), + 'description' => t('This field stores a reference to a taxonomy term.'), + 'default_widget' => 'options_select', + 'default_formatter' => 'taxonomy_term_link', + 'settings' => array( + 'allowed_values' => array( + array( + 'vid' => '0', + 'parent' => '0', + ), + ), + ), + ), + ); +} + +/** + * Implement hook_field_widget_info_alter(). + */ +function taxonomy_field_widget_info_alter(&$info) { + $info['options_select']['field types'][] = 'taxonomy_term'; + $info['options_buttons']['field types'][] = 'taxonomy_term'; +} + +/** + * Implement hook_field_schema(). + */ +function taxonomy_field_schema($field) { + return array( + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), + ), + 'indexes' => array( + 'value' => array('value'), + ), + ); +} + +/** + * Implement hook_field_validate(). + * + * Possible error codes: + * - 'taxonomy_term_illegal_value': The value is not part of the list of allowed values. + */ +function taxonomy_field_validate($obj_type, $object, $field, $instance, $items, &$errors) { + $allowed_values = taxonomy_allowed_values($field); + foreach ($items as $delta => $item) { + if (!empty($item['value'])) { + if (!isset($allowed_values[$item['value']])) { + $errors[$field['field_name']][$delta][] = array( + 'error' => 'taxonomy_term_illegal_value', + 'message' => t('%name: illegal value.', array('%name' => t($instance['label']))), + ); + } + } + } +} + +/** + * Implement hook_field_is_empty(). + */ +function taxonomy_field_is_empty($item, $field) { + if (empty($item['value']) && (string) $item['value'] !== '0') { + return TRUE; + } + return FALSE; +} + +/** + * Implement hook_field_formatter_info(). + */ +function taxonomy_field_formatter_info() { + return array( + 'taxonomy_term_link' => array( + 'label' => t('Link'), + 'field types' => array('taxonomy_term'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + 'taxonomy_term_plain' => array( + 'label' => t('Plain text'), + 'field types' => array('taxonomy_term'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + ); +} + +/** + * Theme function for 'link' term field formatter. + */ +function theme_field_formatter_taxonomy_term_link($element) { + $term = $element['#item']['taxonomy_term']; + return l($term->name, taxonomy_term_path($term)); +} + +/** + * Theme function for 'plain' term field formatter. + */ +function theme_field_formatter_taxonomy_term_plain($element) { + $term = $element['#item']['taxonomy_term']; + return $term->name; +} + +/** + * Create an array of the allowed values for this field. + * + * Call the field's allowed_values function to retrieve the allowed + * values array. + * + * @see _taxonomy_term_select() + */ +function taxonomy_allowed_values($field) { + $options = array(); + foreach ($field['settings']['allowed_values'] as $tree) { + $terms = taxonomy_get_tree($tree['vid'], $tree['parent']); + if ($terms) { + foreach ($terms as $term) { + $options[$term->tid] = str_repeat('-', $term->depth) . $term->name; + } + } + } + return $options; +} + +/** + * Implement hook_field_load(). + * + * This preloads all taxonomy terms for multiple loaded objects at once and + * unsets values for invalid terms that do not exist. + */ +function taxonomy_field_load($obj_type, $objects, $field, $instances, &$items, $age) { + $tids = array(); + + // Collect every possible term attached to any of the fieldable entities. + foreach ($objects as $id => $object) { + foreach ($items[$id] as $delta => $item) { + // Force the array key to prevent duplicates. + $tids[$item['value']] = $item['value']; + } + } + if ($tids) { + $terms = array(); + + // Avoid calling taxonomy_term_load_multiple because it could lead to + // circular references. + $query = db_select('taxonomy_term_data', 't'); + $query->fields('t'); + $query->condition('t.tid', $tids, 'IN'); + $query->addTag('term_access'); + $terms = $query->execute()->fetchAllAssoc('tid'); + + // Iterate through the fieldable entities again to attach the loaded term data. + foreach ($objects as $id => $object) { + foreach ($items[$id] as $delta => $item) { + // Check whether the taxonomy term field instance value could be loaded. + if (isset($terms[$item['value']])) { + // Replace the instance value with the term data. + $items[$id][$delta]['taxonomy_term'] = $terms[$item['value']]; + } + // Otherwise, unset the instance value, since the term does not exist. + else { + unset($items[$id][$delta]); + } + } + } + } +} + +/** + * Helper function that clears field cache when terms are updated or deleted + */ +function _taxonomy_clean_field_cache($term) { + $cids = array(); + + // Determine object types that are not cacheable. + $obj_types = array(); + foreach (field_info_fieldable_types() as $obj_type => $info) { + if (!$info['cacheable']) { + $obj_types[] = $obj_type; + } + } + + // Load info for all taxonomy term fields. + $fields = field_read_fields(array('type' => 'taxonomy_term')); + foreach ($fields as $field_name => $field) { + + // Assemble an array of vocabulary IDs that are used in this field. + foreach ($field['settings']['allowed_values'] as $tree) { + $vids[$tree['vid']] = $tree['vid']; + } + + // Check this term's vocabulary against those used for the field's options. + if (in_array($term->vid, $vids)) { + $conditions = array(array('value', $term->tid)); + if ($obj_types) { + $conditions[] = array('type', $obj_types, 'NOT IN'); + } + $results = field_attach_query($field['field_name'], $conditions, FIELD_QUERY_NO_LIMIT); + foreach ($results as $obj_type => $objects) { + foreach (array_keys($objects) as $id) { + $cids[] = "field:$obj_type:$id"; + } + } + } + } + if ($cids) { + cache_clear_all($cids, 'cache_field'); + } +} + /** * Title callback for term pages. * diff --git a/modules/taxonomy/taxonomy.test b/modules/taxonomy/taxonomy.test index 522323c79301..8c97a1f9675d 100644 --- a/modules/taxonomy/taxonomy.test +++ b/modules/taxonomy/taxonomy.test @@ -727,3 +727,138 @@ class TaxonomyHooksTestCase extends TaxonomyWebTestCase { $this->assertFalse($antonyms, t('The antonyms were deleted from the database.')); } } + +/** + * Tests for taxonomy term field and formatter. + */ +class TaxonomyTermFieldTestCase extends TaxonomyWebTestCase { + protected $instance; + protected $vocabulary; + + public static function getInfo() { + return array( + 'name' => t('Taxonomy term field'), + 'description' => t('Test the creation of term fields.'), + 'group' => t('Taxonomy') + ); + } + + function setUp() { + parent::setUp('field_test'); + + $web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content', 'administer taxonomy')); + $this->drupalLogin($web_user); + + $this->vocabulary = $this->createVocabulary(); + } + + /** + * Test term field validation. + */ + function testTaxonomyTermFieldValidation() { + $this->field_name = drupal_strtolower($this->randomName()); + + // Create a field with settings to validate. + $this->field = array( + 'field_name' => $this->field_name, + 'type' => 'taxonomy_term', + 'settings' => array( + 'allowed_values' => array( + array( + 'vid' => $this->vocabulary->vid, + 'parent' => '0', + ), + ), + ) + ); + field_create_field($this->field); + $this->instance = array( + 'field_name' => $this->field_name, + 'bundle' => FIELD_TEST_BUNDLE, + 'widget' => array( + 'type' => 'options_select', + ), + 'display' => array( + 'full' => array( + 'type' => 'taxonomy_term_link', + ), + ), + ); + field_create_instance($this->instance); + + // Test valid and invalid values with field_attach_validate(). + $entity = field_test_create_stub_entity(0, 0, FIELD_TEST_BUNDLE); + $term = $this->createTerm($this->vocabulary); + $entity->{$this->field_name}[0]['value'] = $term->tid; + field_attach_validate('test_entity', $entity); + try { + $this->assertTrue($entity->{$this->field_name}[0]['value'] == $term->tid, t('Correct term does not cause validation error')); + } + catch (FieldValidationException $e) { + $this->assertTrue($entity->{$this->field_name}[0]['value'] != $term->tid, t('Term from wrong vocabulary does not cause validation error')); + } + + $entity = field_test_create_stub_entity(0, 0, FIELD_TEST_BUNDLE); + $bad_term = $this->createTerm($this->createVocabulary()); + $entity->{$this->field_name}[0]['value'] = $bad_term->tid; + try { + field_attach_validate('test_entity', $entity); + } + catch (FieldValidationException $e) { + $this->assertTrue($this->field['settings']['allowed_values'][0]['vid'] != $bad_term->vid, t('Wrong term causes validation error')); + } + } + + /** + * Test widgets. + */ + function testTaxonomyTermFieldWidgets() { + // Setup a field and instance. + $entity_type = 'test_entity'; + $this->field_name = drupal_strtolower($this->randomName()); + $this->field = array( + 'field_name' => $this->field_name, + 'type' => 'taxonomy_term', + 'settings' => array( + 'allowed_values' => array( + array( + 'vid' => $this->vocabulary->vid, + 'parent' => '0', + ), + ), + ) + ); + field_create_field($this->field); + $this->instance = array( + 'field_name' => $this->field_name, + 'bundle' => FIELD_TEST_BUNDLE, + 'label' => $this->randomName() . '_label', + 'widget' => array( + 'type' => 'options_select', + ) + ); + field_create_instance($this->instance); + + // Create a term in the vocabulary. + $term = $this->createTerm($this->vocabulary); + + // Display creation form. + $this->drupalGet('test-entity/add/test-bundle'); + $this->assertFieldByName($this->field_name . '[value]', '', t('Widget is displayed')); + + // Submit with some value. + $edit = array( + $this->field_name . '[value]' => array($term->tid), + ); + $this->drupalPost(NULL, $edit, t('Save')); + preg_match('|test-entity/(\d+)/edit|', $this->url, $match); + $id = $match[1]; + $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), t('Entity was created')); + + // Display the object. + $entity = field_test_entity_load($id); + $entity->content = field_attach_view($entity_type, $entity); + $this->content = drupal_render($entity->content); + $this->assertText($term->name, t('Term name is displayed')); + } +} -- GitLab