Index: modules/taxonomy/taxonomy.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v
retrieving revision 1.484
diff -u -p -r1.484 taxonomy.module
--- modules/taxonomy/taxonomy.module	5 Jul 2009 18:00:10 -0000	1.484
+++ modules/taxonomy/taxonomy.module	6 Jul 2009 06:39:48 -0000
@@ -62,6 +62,12 @@ function taxonomy_theme() {
     'taxonomy_overview_terms' => array(
       'arguments' => array('form' => array()),
     ),
+    'field_formatter_term_default' => array(
+      'arguments' => array('element' => NULL),
+    ),
+    'field_formatter_term_plain' => array(
+      'arguments' => array('element' => NULL),
+    ),
   );
 }
 
@@ -476,6 +482,7 @@ function taxonomy_term_save($term) {
 
   cache_clear_all();
   taxonomy_terms_static_reset();
+  _taxonomy_clean_field_cache($term);
 
   return $status;
 }
@@ -526,6 +533,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);
     }
 
@@ -1840,3 +1848,197 @@ function taxonomy_hook_info() {
     ),
   );
 }
+
+/**
+ * Implement hook_field_info().
+ */
+function taxonomy_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_widget_info_alter().
+ */
+function taxonomy_field_widget_info_alter(&$info) {
+  $info['options_select']['field types'][] = 'term';
+  $info['options_buttons']['field types'][] = 'term';
+}
+
+/**
+ * Implement hook_field_schema().
+ */
+function taxonomy_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 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 (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 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(
+    '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 taxonomy_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 taxonomy_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]);
+        }
+      }
+    }
+  }
+}
+
+function _taxonomy_clean_field_cache($term) {
+  $fields = field_read_fields(array('type' => 'term'));
+  foreach ($fields as $field) {
+    if (in_array($term->vid, $field['settings']['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/taxonomy.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.test,v
retrieving revision 1.38
diff -u -p -r1.38 taxonomy.test
--- modules/taxonomy/taxonomy.test	27 Jun 2009 19:49:07 -0000	1.38
+++ modules/taxonomy/taxonomy.test	6 Jul 2009 06:39:49 -0000
@@ -727,3 +727,137 @@ class TaxonomyHooksTestCase extends Taxo
     $this->assertFalse($antonyms, t('The antonyms were deleted from the database.'));
   }
 }
+
+class TaxonomyFieldTestCase extends TaxonomyWebTestCase {
+  protected $instance;
+  protected $vocabulary;
+
+  public static function getInfo() {
+    return array(
+      'name'  => t('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 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.
+  /**
+   *
+   */
+}
