Index: modules/field/modules/options/options.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/options/options.module,v retrieving revision 1.7 diff -u -p -r1.7 options.module --- modules/field/modules/options/options.module 28 May 2009 16:44:06 -0000 1.7 +++ modules/field/modules/options/options.module 21 Jun 2009 17:06:27 -0000 @@ -44,7 +44,7 @@ function options_field_widget_info() { return array( 'options_select' => array( 'label' => t('Select list'), - 'field types' => array('list', 'list_boolean', 'list_text', 'list_number'), + 'field types' => array('list', 'list_boolean', 'list_text', 'list_number', 'term'), 'behaviors' => array( 'multiple values' => FIELD_BEHAVIOR_CUSTOM, 'default value' => FIELD_BEHAVIOR_DEFAULT, @@ -52,7 +52,7 @@ function options_field_widget_info() { ), 'options_buttons' => array( 'label' => t('Check boxes/radio buttons'), - 'field types' => array('list', 'list_boolean', 'list_text', 'list_number'), + 'field types' => array('list', 'list_boolean', 'list_text', 'list_number', 'term'), 'behaviors' => array( 'multiple values' => FIELD_BEHAVIOR_CUSTOM, 'default value' => FIELD_BEHAVIOR_DEFAULT, Index: modules/taxonomy/term.info =================================================================== RCS file: modules/taxonomy/term.info diff -N modules/taxonomy/term.info --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/taxonomy/term.info 21 Jun 2009 17:06:29 -0000 @@ -0,0 +1,8 @@ +; $Id$ +name = Term +description = Defines taxonomy term field types. Use with Options to create selection lists. +dependencies[] = taxonomy +package = Core - fields +core = 7.x +files[]=term.module +files[]=term.test \ No newline at end of file Index: modules/taxonomy/term.module =================================================================== RCS file: modules/taxonomy/term.module diff -N modules/taxonomy/term.module --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/taxonomy/term.module 21 Jun 2009 17:06:29 -0000 @@ -0,0 +1,228 @@ + array( + 'arguments' => array('element' => NULL), + ), + 'field_formatter_term_plain' => array( + 'arguments' => array('element' => NULL), + ), + ); +} + +/** + * Implement hook_field_info(). + */ +function term_field_info() { + return array( + 'term' => array( + 'label' => t('Term'), + 'description' => t('This field represents a taxonomy term reference.'), + 'default_widget' => 'options_select', + 'default_formatter' => 'term_default', + 'settings' => array('vid' => array(0)), + ), + ); +} + +/** + * Implement hook_field_schema(). + */ +function term_field_schema($field) { + switch ($field['type']) { + default: + $columns = array( + 'value' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), + ); + break; + } + return array( + 'columns' => $columns, + 'indexes' => array( + 'value' => array('value'), + ), + ); +} + +/** + * Implement hook_field_validate(). + * + * Possible error codes: + * - 'term_illegal_value': The value is not part of the list of allowed values. + */ +function term_field_validate($obj_type, $object, $field, $instance, $items, &$errors) { + $allowed_values = term_allowed_values($field); + foreach ($items as $delta => $item) { + if (!empty($item['value'])) { + if (count($allowed_values) && !array_key_exists($item['value'], $allowed_values)) { + $errors[$field['field_name']][$delta][] = array( + 'error' => 'term_illegal_value', + 'message' => t('%name: illegal value.', array('%name' => t($instance['label']))), + ); + } + } + } +} + +/** + * Implement hook_field_is_empty(). + */ +function term_field_is_empty($item, $field) { + if (empty($item['value']) && (string)$item['value'] !== '0') { + return TRUE; + } + return FALSE; +} + +/** + * Implement hook_field_formatter_info(). + */ +function term_field_formatter_info() { + return array( + 'term_default' => array( + 'label' => t('Link'), + 'field types' => array('term'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + 'term_plain' => array( + 'label' => t('Plain text'), + 'field types' => array('term'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + ); +} + +/** + * Theme function for 'default' term field formatter. + */ +function theme_field_formatter_term_default($element) { + $term = $element['#item']['term']; + return l($term->name, taxonomy_term_path($term)); +} + +/** + * Theme function for 'plain' term field formatter. + */ +function theme_field_formatter_term_plain($element) { + $term = $element['#item']['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. + * + * This function should imitate the features of _taxonomy_term_select + * + * TODO deal with excluded tids? + * TODO support scope limiting to a particular subtree of the vocabulary + * TODO support multiple subtrees of the same or different vocabularies in one field + * TODO allow values that aren't in a vocabulary if the field/widget/vocabulary (which?) is for + * tagging. + * TODO the field settings could also enforce some constraints on the user's choosing behavior. + * e.g. force user to choose a term with no children, etc. + */ +function term_allowed_values($field) { + $options = array(); + foreach ($field['settings']['vid'] as $vid) { + $tree = taxonomy_get_tree($vid); + if ($tree) { + foreach ($tree as $term) { + $options[$term->tid] = str_repeat('-', $term->depth) . $term->name; + } + } + } + return $options; +} + +/* + * Implement hook_field_load(). + * + * This preloads all taxonomy terms for a given object at once using taxonomy_term_load_multiple + * and unsets values for invalid terms which don't exist. + * + * @return + * array with 'term' object at that index + */ +function term_field_load($obj_type, $objects, $field, $instances, &$items, $age) { + $tids = array(); + + foreach ($objects as $id => $object) { + foreach ($items[$id] as $delta => $item) { + $tids[$item['value']] = $item['value']; + } + } + if (count($tids)) { + $terms = array(); + $query = db_select('taxonomy_term_data', 't'); + $taxonomy_term_data = drupal_schema_fields_sql('taxonomy_term_data'); + $query->fields('t', $taxonomy_term_data); + $query->condition('t.tid', $tids, 'IN'); + $terms = $query->execute()->fetchAllAssoc('tid'); + foreach ($objects as $id => $object) { + foreach ($items[$id] as $delta => $item) { + if (isset($terms[$item['value']])) { + $items[$id][$delta]['term'] = $terms[$item['value']]; + } + else { + unset($items[$id][$delta]); + } + } + } + } +} + +/* + * Implement hook_taxonomy_term_insert(). + */ +function term_taxonomy_term_insert($term) { + _term_clean_field_cache($term); +} + +/* + * Implement hook_taxonomy_term_update(). + */ +function term_taxonomy_term_update($term) { + _term_clean_field_cache($term); +} + +/* + * Implement hook_taxonomy_term_delete(). + */ +function term_taxonomy_term_delete($term) { + _term_clean_field_cache($term); +} + +function _term_clean_field_cache($term) { + $fields = field_read_fields(array('type' => 'term')); + foreach ($fields as $field) { + if ($field['settings']['vid'] == $term->vid) { + $objects = field_attach_query($field['field_name'], array(array('value', $term->tid))); + foreach ($objects as $obj_type => $ids) { + foreach ($ids as $id) { + cache_clear_all("field:$obj_type:$id", 'cache_field'); + } + } + } + } +} \ No newline at end of file Index: modules/taxonomy/term.test =================================================================== RCS file: modules/taxonomy/term.test diff -N modules/taxonomy/term.test --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/taxonomy/term.test 21 Jun 2009 17:06:29 -0000 @@ -0,0 +1,135 @@ + t('Term Field'), + 'description' => t("Test the creation of term fields."), + 'group' => t('Field') + ); + } + + function setUp() { + parent::setUp('term', '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 fields. + + /** + * Test term field validation. + */ + function testTermFieldValidation() { + // Create a field with settings to validate. + $this->field = array( + 'field_name' => drupal_strtolower($this->randomName()), + 'type' => 'term', + 'settings' => array( + 'vid' => array($this->vocabulary->vid), + ) + ); + field_create_field($this->field); + $this->instance = array( + 'field_name' => $this->field['field_name'], + 'bundle' => FIELD_TEST_BUNDLE, + 'widget' => array( + 'type' => 'options_select', + ), + 'display' => array( + 'full' => array( + 'type' => 'term_default', + ), + ), + ); + 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['field_name']}[0]['value'] = $term->tid; + field_attach_validate('test_entity', $entity); + try { + $this->assertTrue($entity->{$this->field['field_name']}[0]['value'] == $term->tid, "Term $term->tid does not cause validation error"); + } + catch (FieldValidationException $e) { + $this->assertTrue($entity->{$this->field['field_name']}[0]['value'] != $term->tid, "Term $term->tid doesn't cause validation error even though it is in $this->vocabulary->vid"); + } + + $entity = field_test_create_stub_entity(0, 0, FIELD_TEST_BUNDLE); + $bad_term = $this->createTerm($this->createVocabulary()); + $entity->{$this->field['field_name']}[0]['value'] = $bad_term->tid; + try { + field_attach_validate('test_entity', $entity); + } + catch (FieldValidationException $e) { + $this->assertTrue($this->field['settings']['vid'] != $bad_term->vid, "Term $bad_term->tid does cause validation error"); + } + } + + /** + * Test widgets. + */ + function testTermfieldWidgets() { + $this->_testTermfieldWidgets('term', 'options_select'); + } + + /** + * Helper function for testTermfieldWidgets(). + */ + function _testTermfieldWidgets($field_type, $widget_type) { + // 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' => $field_type, + 'settings' => array( + 'vid' => array($this->vocabulary->vid), + ) + ); + field_create_field($this->field); + $this->instance = array( + 'field_name' => $this->field_name, + 'bundle' => FIELD_TEST_BUNDLE, + 'label' => $this->randomName() . '_label', + 'widget' => array( + 'type' => $widget_type, + ) + ); + 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, 'Term name is displayed'); + } + + // Test formatters. + /** + * + */ +}