Index: modules/forum/forum-topic-list.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/modules/forum/forum-topic-list.tpl.php,v retrieving revision 1.7 diff -u -p -r1.7 forum-topic-list.tpl.php --- modules/forum/forum-topic-list.tpl.php 28 Jul 2009 10:41:20 -0000 1.7 +++ modules/forum/forum-topic-list.tpl.php 6 Oct 2009 16:40:39 -0000 @@ -20,7 +20,7 @@ * - $topic->message: If the topic has been moved, this contains an * explanation and a link. * - $topic->zebra: 'even' or 'odd' string used for row class. - * - $topic->num_comments: The number of replies on this topic. + * - $topic->comment_count: The number of replies on this topic. * - $topic->new_replies: A flag to indicate whether there are unread comments. * - $topic->new_url: If there are unread replies, this is a link to them. * - $topic->new_text: Text containing the translated, properly pluralized count. @@ -53,7 +53,7 @@ message; ?> - num_comments; ?> + comment_count; ?> new_replies): ?>
new_text; ?> Index: modules/forum/forum.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/forum/forum.admin.inc,v retrieving revision 1.25 diff -u -p -r1.25 forum.admin.inc --- modules/forum/forum.admin.inc 18 Sep 2009 00:12:46 -0000 1.25 +++ modules/forum/forum.admin.inc 6 Oct 2009 16:40:39 -0000 @@ -5,8 +5,8 @@ * @file * Administrative page callbacks for the forum module. */ - function forum_form_main($type, $edit = array()) { + $edit = (array) $edit; if ((isset($_POST['op']) && $_POST['op'] == t('Delete')) || !empty($_POST['confirm'])) { return drupal_get_form('forum_confirm_delete', $edit['tid']); } Index: modules/forum/forum.install =================================================================== RCS file: /cvs/drupal/drupal/modules/forum/forum.install,v retrieving revision 1.35 diff -u -p -r1.35 forum.install --- modules/forum/forum.install 29 Sep 2009 15:13:55 -0000 1.35 +++ modules/forum/forum.install 6 Oct 2009 16:40:39 -0000 @@ -22,9 +22,7 @@ function forum_install() { function forum_enable() { if ($vocabulary = taxonomy_vocabulary_load(variable_get('forum_nav_vocabulary', 0))) { - // Existing install. Add back forum node type, if the forums - // vocabulary still exists. Keep all other node types intact there. - $vocabulary->nodes['forum'] = 1; + // Save the vocabulary to create the default field instance. taxonomy_vocabulary_save($vocabulary); } else { @@ -33,17 +31,26 @@ function forum_enable() { // forms. $edit = array( 'name' => t('Forums'), - 'multiple' => 0, - 'required' => 0, + 'machine_name' => 'forums', + 'description' => t('Forum navigation vocabulary'), 'hierarchy' => 1, 'relations' => 0, 'module' => 'forum', 'weight' => -10, - 'nodes' => array('forum' => 1), ); $vocabulary = (object) $edit; taxonomy_vocabulary_save($vocabulary); + $instance = array( + 'field_name' => 'taxonomy_' . $vocabulary->machine_name, + 'label' => $vocabulary->name, + 'bundle' => 'forum', + 'widget' => array( + 'type' => 'options_select', + ), + ); + field_create_instance($instance); + variable_set('forum_nav_vocabulary', $vocabulary->vid); } } @@ -109,6 +116,68 @@ function forum_schema() { ), ); + $schema['forum_index'] = array( + 'description' => 'Maintains denormalized information about node/term relationships.', + 'fields' => array( + 'nid' => array( + 'description' => 'The {node}.nid this record tracks.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'title' => array( + 'description' => 'The title of this node, always treated as non-markup plain text.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'tid' => array( + 'description' => 'The term ID.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'sticky' => array( + 'description' => 'Boolean indicating whether the node is sticky.', + 'type' => 'int', + 'not null' => FALSE, + 'default' => 0, + 'size' => 'tiny', + ), + 'created' => array( + 'description' => 'The Unix timestamp when the node was created.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default'=> 0, + ), + 'last_comment_timestamp' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.timestamp.', + ), + 'comment_count' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The total number of comments on this node.', + ), + ), + 'indexes' => array( + 'forum_topics' => array('tid', 'sticky', 'last_comment_timestamp'), + ), + 'foreign keys' => array( + 'node' => 'nid', + 'taxonomy_term_data' => 'tid', + ), + ); + + return $schema; } @@ -119,3 +188,74 @@ function forum_update_7000() { db_drop_index('forum', 'nid'); db_add_index('forum', 'forum_topic', array('nid', 'tid')); } + +/** + * Create new {forum_index} table. + */ +function forum_update_7001() { + $forum_index = array( + 'description' => 'Maintains denormalized information about node/term relationships.', + 'fields' => array( + 'nid' => array( + 'description' => 'The {node}.nid this record tracks.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'title' => array( + 'description' => 'The title of this node, always treated as non-markup plain text.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'tid' => array( + 'description' => 'The term ID.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'sticky' => array( + 'description' => 'Boolean indicating whether the node is sticky.', + 'type' => 'int', + 'not null' => FALSE, + 'default' => 0, + 'size' => 'tiny', + ), + 'created' => array( + 'description' => 'The Unix timestamp when the node was created.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default'=> 0, + ), + 'last_comment_timestamp' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.timestamp.', + ), + 'comment_count' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The total number of comments on this node.', + ), + ), + 'indexes' => array( + 'forum_topics' => array('tid', 'sticky', 'last_comment_timestamp'), + ), + 'foreign keys' => array( + 'node' => 'nid', + 'taxonomy_term_data' => 'tid', + ), + ); + db_create_table($ret, 'forum_index', $forum_index); + + db_query('INSERT INTO {forum_index} (SELECT n.nid, n.title, f.tid, n.sticky, n.created, ncs.last_comment_timestamp, ncs.comment_count FROM {node} n INNER JOIN {forum} f on n.vid = f.vid INNER JOIN {node_comment_statistics} ncs ON n.nid = ncs.nid)'); + + return $ret; +} Index: modules/forum/forum.module =================================================================== RCS file: /cvs/drupal/drupal/modules/forum/forum.module,v retrieving revision 1.519 diff -u -p -r1.519 forum.module --- modules/forum/forum.module 18 Sep 2009 00:04:22 -0000 1.519 +++ modules/forum/forum.module 6 Oct 2009 16:40:39 -0000 @@ -61,25 +61,6 @@ function forum_theme() { } /** - * Fetch a forum term. - * - * @param $tid - * The ID of the term which should be loaded. - * - * @return - * An associative array containing the term data or FALSE if the term cannot be loaded, or is not part of the forum vocabulary. - */ -function forum_term_load($tid) { - return db_select('taxonomy_term_data', 't') - ->fields('t', array('tid', 'vid', 'name', 'description', 'weight')) - ->condition('tid', $tid) - ->condition('vid', variable_get('forum_nav_vocabulary', 0)) - ->addTag('term_access') - ->execute() - ->fetchAssoc(); -} - -/** * Implement hook_menu(). */ function forum_menu() { @@ -130,13 +111,13 @@ function forum_menu() { 'parent' => 'admin/structure/forum', 'file' => 'forum.admin.inc', ); - $items['admin/structure/forum/edit/%forum_term'] = array( + $items['admin/structure/forum/edit/%taxonomy_term'] = array( 'page callback' => 'forum_form_main', 'access arguments' => array('administer forums'), 'type' => MENU_CALLBACK, 'file' => 'forum.admin.inc', ); - $items['admin/structure/forum/edit/container/%forum_term'] = array( + $items['admin/structure/forum/edit/container/%taxonomy_term'] = array( 'title' => 'Edit container', 'page callback' => 'forum_form_main', 'page arguments' => array('container', 5), @@ -144,7 +125,7 @@ function forum_menu() { 'type' => MENU_CALLBACK, 'file' => 'forum.admin.inc', ); - $items['admin/structure/forum/edit/forum/%forum_term'] = array( + $items['admin/structure/forum/edit/forum/%taxonomy_term'] = array( 'title' => 'Edit forum', 'page callback' => 'forum_form_main', 'page arguments' => array('forum', 5), @@ -164,28 +145,19 @@ function forum_init() { } /** - * _forum_node_check_node_type + * Check whether a content type can be used in a forum. + * + * @param $node + * A node object. * - * @param mixed $node - * @param mixed $vocabulary - * @access protected - * @return bool + * @return + * Boolean indicating if the node can be assigned to a forum. */ -function _forum_node_check_node_type($node, $vocabulary) { - // We are going to return if $node->type is not one of the node - // types assigned to the forum vocabulary. If forum_nav_vocabulary - // is undefined or the vocabulary does not exist, it clearly cannot - // be assigned to $node->type, so return to avoid E_ALL warnings. - if (empty($vocabulary)) { - return FALSE; - } +function _forum_node_check_node_type($node) { + // Fetch information about the forum field. + $field = field_info_instance('taxonomy_forums', $node->type); - // Operate only on node types assigned for the forum vocabulary. - if (!in_array($node->type, $vocabulary->nodes)) { - return FALSE; - } - - return TRUE; + return is_array($field); } /** @@ -194,24 +166,15 @@ function _forum_node_check_node_type($no function forum_node_view($node, $build_mode) { $vid = variable_get('forum_nav_vocabulary', 0); $vocabulary = taxonomy_vocabulary_load($vid); - if (_forum_node_check_node_type($node, $vocabulary)) { - if ((bool)menu_get_object() && taxonomy_node_get_terms_by_vocabulary($node, $vid) && $tree = taxonomy_get_tree($vid)) { - // Get the forum terms from the (cached) tree - foreach ($tree as $term) { - $forum_terms[] = $term->tid; - } - foreach ($node->taxonomy as $term_id => $term) { - if (in_array($term_id, $forum_terms)) { - $node->tid = $term_id; - } - } + if (_forum_node_check_node_type($node)) { + if ((bool)menu_get_object()) { // Breadcrumb navigation $breadcrumb[] = l(t('Home'), NULL); $breadcrumb[] = l($vocabulary->name, 'forum'); - if ($parents = taxonomy_get_parents_all($node->tid)) { + if ($parents = taxonomy_get_parents_all($node->forum_tid)) { $parents = array_reverse($parents); - foreach ($parents as $p) { - $breadcrumb[] = l($p->name, 'forum/' . $p->tid); + foreach ($parents as $parent) { + $breadcrumb[] = l($parent->name, 'forum/' . $parent->tid); } } drupal_set_breadcrumb($breadcrumb); @@ -224,15 +187,10 @@ function forum_node_view($node, $build_m * Implement hook_node_prepare(). */ function forum_node_prepare($node) { - $vid = variable_get('forum_nav_vocabulary', 0); - $vocabulary = taxonomy_vocabulary_load($vid); - if (_forum_node_check_node_type($node, $vocabulary)) { + if (_forum_node_check_node_type($node)) { if (empty($node->nid)) { // New topic - $node->taxonomy[arg(3)] = (object) array( - 'vid' => $vid, - 'tid' => arg(3), - ); + $node->taxonomy_forums[0]['value'] = arg(3); } } } @@ -243,22 +201,20 @@ function forum_node_prepare($node) { * Check in particular that only a "leaf" term in the associated taxonomy. */ function forum_node_validate($node, $form) { - $vid = variable_get('forum_nav_vocabulary', 0); - $vocabulary = taxonomy_vocabulary_load($vid); - if (_forum_node_check_node_type($node, $vocabulary)) { + if (_forum_node_check_node_type($node)) { + $langcode = $form['taxonomy_forums']['#language']; // vocabulary is selected, not a "container" term. - if ($node->taxonomy) { + if (!empty($node->taxonomy_forums[$langcode])) { // Extract the node's proper topic ID. - $vocabulary = $vid; $containers = variable_get('forum_containers', array()); - foreach ($node->taxonomy as $term) { - $used = db_query_range('SELECT 1 FROM {taxonomy_term_data} WHERE tid = :tid AND vid = :vid', 0, 1, array( - ':tid' => $term, - ':vid' => $vocabulary, + foreach ($node->taxonomy_forums[$langcode] as $tid) { + $term = taxonomy_term_load($tid['value']); + $used = db_query_range('SELECT 1 FROM {taxonomy_term_data} WHERE tid = :tid AND vid = :vid',0 , 1, array( + ':tid' => $term->tid, + ':vid' => $term->vid, ))->fetchField(); - if ($used && in_array($term, $containers)) { - $term = taxonomy_term_load($term); - form_set_error('taxonomy', t('The item %forum is only a container for forums. Please select one of the forums below it.', array('%forum' => $term->name))); + if ($used && in_array($term->tid, $containers)) { + form_set_error('taxonomy_forums', t('The item %forum is only a container for forums. Please select one of the forums below it.', array('%forum' => $term->name))); } } } @@ -271,26 +227,16 @@ function forum_node_validate($node, $for * Assign forum taxonomy when adding a topic from within a forum. */ function forum_node_presave($node) { - $vid = variable_get('forum_nav_vocabulary', 0); - $vocabulary = taxonomy_vocabulary_load($vid); - if (_forum_node_check_node_type($node, $vocabulary)) { + if (_forum_node_check_node_type($node)) { // Make sure all fields are set properly: $node->icon = !empty($node->icon) ? $node->icon : ''; - - if ($node->taxonomy && $tree = taxonomy_get_tree($vid)) { - // Get the forum terms from the (cached) tree if we have a taxonomy. - foreach ($tree as $term) { - $forum_terms[] = $term->tid; - } - foreach ($node->taxonomy as $term_id) { - if (in_array($term_id, $forum_terms)) { - $node->tid = $term_id; - } - } + $langcode = array_shift(array_keys($node->taxonomy_forums)); + if (!empty($node->taxonomy_forums[$langcode])) { + $node->forum_tid = $node->taxonomy_forums[$langcode][0]['value']; $old_tid = db_query_range("SELECT f.tid FROM {forum} f INNER JOIN {node} n ON f.vid = n.vid WHERE n.nid = :nid ORDER BY f.vid DESC", 0, 1, array(':nid' => $node->nid))->fetchField(); - if ($old_tid && isset($node->tid) && ($node->tid != $old_tid) && !empty($node->shadow)) { + if ($old_tid && isset($node->forum_tid) && ($node->forum_tid != $old_tid) && !empty($node->shadow)) { // A shadow copy needs to be created. Retain new term and add old term. - $node->taxonomy[] = $old_tid; + $node->taxonomy_forums[$langcode][] = array('value' => $old_tid); } } } @@ -300,13 +246,11 @@ function forum_node_presave($node) { * Implement hook_node_update(). */ function forum_node_update($node) { - $vid = variable_get('forum_nav_vocabulary', 0); - $vocabulary = taxonomy_vocabulary_load($vid); - if (_forum_node_check_node_type($node, $vocabulary)) { + if (_forum_node_check_node_type($node)) { if (empty($node->revision) && db_query('SELECT tid FROM {forum} WHERE nid=:nid', array(':nid' => $node->nid))->fetchField()) { - if (!empty($node->tid)) { + if (!empty($node->forum_tid)) { db_update('forum') - ->fields(array('tid' => $node->tid)) + ->fields(array('tid' => $node->forum_tid)) ->condition('vid', $node->vid) ->execute(); } @@ -318,16 +262,31 @@ function forum_node_update($node) { } } else { - if (!empty($node->tid)) { + if (!empty($node->forum_tid)) { db_insert('forum') ->fields(array( - 'tid' => $node->tid, + 'tid' => $node->forum_tid, 'vid' => $node->vid, 'nid' => $node->nid, )) ->execute(); } } + // If the node has a shadow forum topic, update the record for this + // revision. + if ($node->shadow) { + db_delete('forum') + ->condition('nid', $node->nid) + ->condition('vid', $node->vid) + ->execute(); + db_insert('forum') + ->fields(array( + 'nid' => $node->nid, + 'vid' => $node->vid, + 'tid' => $node->forum_tid, + )) + ->execute(); + } } } @@ -335,13 +294,11 @@ function forum_node_update($node) { * Implement hook_node_insert(). */ function forum_node_insert($node) { - $vid = variable_get('forum_nav_vocabulary', 0); - $vocabulary = taxonomy_vocabulary_load($vid); - if (_forum_node_check_node_type($node, $vocabulary)) { - if (!empty($node->tid)) { + if (_forum_node_check_node_type($node)) { + if (!empty($node->forum_tid)) { $nid = db_insert('forum') ->fields(array( - 'tid' => $node->tid, + 'tid' => $node->forum_tid, 'vid' => $node->vid, 'nid' => $node->nid, )) @@ -354,34 +311,32 @@ function forum_node_insert($node) { * Implement hook_node_delete(). */ function forum_node_delete($node) { - $vid = variable_get('forum_nav_vocabulary', 0); - $vocabulary = taxonomy_vocabulary_load($vid); - if (_forum_node_check_node_type($node, $vocabulary)) { + if (_forum_node_check_node_type($node)) { db_delete('forum') ->condition('nid', $node->nid) ->execute(); + db_delete('forum_index') + ->condition('nid', $node->nid) + ->execute(); } } /** * Implement hook_node_load(). */ -function forum_node_load($nodes, $types) { - $vid = variable_get('forum_nav_vocabulary', 0); - // If no forum vocabulary is set up, return. - if ($vid == '') { - return; - } - $vocabulary = taxonomy_vocabulary_load($vid); - +function forum_node_load($nodes) { $node_vids = array(); foreach ($nodes as $node) { - if (isset($vocabulary->nodes[$node->type])) { + if (_forum_node_check_node_type($node)) { $node_vids[] = $node->vid; } } if (!empty($node_vids)) { - $result = db_query('SELECT nid, tid FROM {forum} WHERE vid IN(:node_vids)', array(':node_vids' => $node_vids)); + $query = db_select('forum', 'f'); + $query + ->fields('f', array('nid', 'tid')) + ->condition('f.vid', $node_vids); + $result = $query->execute(); foreach ($result as $record) { $nodes[$record->nid]->forum_tid = $record->tid; } @@ -418,26 +373,114 @@ function forum_permission() { /** * Implement hook_taxonomy(). */ -function forum_taxonomy($op, $type, $term = NULL) { - if ($op == 'delete' && $term['vid'] == variable_get('forum_nav_vocabulary', 0)) { - switch ($type) { - case 'term': - $result = db_query('SELECT f.nid FROM {forum} f WHERE f.tid = :tid', array(':tid' => $term['tid'])); - foreach ($result as $node) { - // node_delete will also remove any association with non-forum vocabularies. - node_delete($node->nid); - } +function forum_taxonomy_term_delete($tid) { + // For containers, remove the tid from the forum_containers variable. + $containers = variable_get('forum_containers', array()); + $key = array_search($tid, $containers); + if ($key !== FALSE) { + unset($containers[$key]); + } + variable_set('forum_containers', $containers); +} + +/** + * Update the taxonomy index with the most recent o - // For containers, remove the tid from the forum_containers variable. - $containers = variable_get('forum_containers', array()); - $key = array_search($term['tid'], $containers); - if ($key !== FALSE) { - unset($containers[$key]); +/** + * Implement hook_comment_publish(). + * + * This actually handles the insert and update of published nodes since + * comment_save() calls hook_comment_publish() for all published comments. + */ +function forum_comment_publish($comment) { + _forum_update_forum_index($comment->nid); +} + +/** + * Implement forum_comment_update(). + * + * Comment module doesn't call hook_comment_unpublish() when saving individual + * comments so we need to check for those here. + */ +function forum_comment_update($comment) { + // comment_save() calls hook_comment_publish() for all published comments + // so we to handle all other values here. + if (!$comment->status) { + _forum_update_forum_index($comment->nid); + } +} + +/** + * Implement forum_comment_unpublish() { + */ +function forum_comment_unpublish($comment) { + _forum_update_forum_index($comment->nid); +} + +/** + * Implement forum_comment_delete(). + */ +function forum_comment_delete($comment) { + _forum_update_forum_index($comment->nid); +} + +/** + * Implement hook_field_attach_pre_insert(). + */ +function forum_field_attach_pre_insert($obj_type, $object, $skip_fields) { + if ($obj_type == 'node' && $object->status && _forum_node_check_node_type($object)) { + $query = db_insert('forum_index')->fields(array('nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp')); + foreach ($object->taxonomy_forums as $language) { + foreach ($language as $delta) { + $query->values(array( + 'nid' => $object->nid, + 'title' => $object->title, + 'tid' => $delta['value'], + 'sticky' => $object->sticky, + 'created' => $object->created, + 'comment_count' => 0, + 'last_comment_timestamp' => $object->created, + )); + } + } + $query->execute(); + } +} + +/** + * Implement hook_field_attach_pre_update(). + */ +function forum_field_attach_pre_update($obj_type, $object, $skip_fields) { + $first_call = &drupal_static(__FUNCTION__, array()); + + if ($obj_type == 'node' && $object->status && _forum_node_check_node_type($object)) { + // We don't maintain data for old revisions, so clear all previous values + // from the table. Since this hook runs once per field, per object, make + // sure we only wipe values once. + if (!isset($first_call[$object->nid])) { + $first_call[$object->nid] = FALSE; + db_delete('forum_index')->condition('nid', $object->nid)->execute(); + } + // Only save data to the table if the node is published. + if ($object->status) { + $query = db_insert('forum_index')->fields(array('nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp')); + foreach ($object->taxonomy_forums as $language) { + foreach ($language as $delta) { + $query->values(array( + 'nid' => $object->nid, + 'title' => $object->title, + 'tid' => $delta['value'], + 'sticky' => $object->sticky, + 'created' => $object->created, + 'comment_count' => 0, + 'last_comment_timestamp' => $object->created, + )); } - variable_set('forum_containers', $containers); - break; - case 'vocabulary': - variable_del('forum_nav_vocabulary'); + } + $query->execute(); + // The logic for determining last_comment_count is fairly complex, so + // call _forum_update_forum_index() too. + _forum_update_forum_index($object->nid); } } } @@ -454,13 +497,8 @@ function forum_form_alter(&$form, $form_ '#markup' => t('This is the designated forum vocabulary. Some of the normal vocabulary options have been removed.'), '#weight' => -1, ); - $form['content_types']['nodes']['#required'] = TRUE; $form['hierarchy'] = array('#type' => 'value', '#value' => 1); - $form['settings']['required'] = array('#type' => 'value', '#value' => FALSE); - $form['settings']['relations'] = array('#type' => 'value', '#value' => FALSE); - $form['settings']['tags'] = array('#type' => 'value', '#value' => FALSE); - $form['settings']['multiple'] = array('#type' => 'value', '#value' => FALSE); - unset($form['delete']); + $form['delete']['#access'] = FALSE; } // Hide multiple parents select from forum terms. elseif ($form_id == 'taxonomy_form_term') { @@ -468,10 +506,10 @@ function forum_form_alter(&$form, $form_ } } if ($form_id == 'forum_node_form') { + $langcode = $form['taxonomy_forums']['#language']; // Make the vocabulary required for 'real' forum-nodes. - $vid = variable_get('forum_nav_vocabulary', 0); - $form['taxonomy'][$vid]['#required'] = TRUE; - $form['taxonomy'][$vid]['#options'][''] = t('- Please choose -'); + $form['taxonomy_forums'][$langcode]['#required'] = TRUE; + $form['taxonomy_forums'][$langcode]['#multiple'] = FALSE; } } @@ -512,28 +550,21 @@ function forum_block_save($delta = '', $ * most recently added forum topics. */ function forum_block_view($delta = '') { - $query = db_select('node', 'n'); - $query->join('forum', 'f', 'f.vid = n.vid'); - $query->join('taxonomy_term_data', 'td', 'td.tid = f.tid'); - $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid'); - $query - ->fields('n', array('nid', 'title')) - ->fields('ncs', array('comment_count', 'last_comment_timestamp')) - ->condition('n.status', 1) - ->condition('td.vid', variable_get('forum_nav_vocabulary', 0)) + $query = db_select('forum_index', 'f') + ->fields('f') ->addTag('node_access'); switch ($delta) { case 'active': $title = t('Active forum topics'); $query - ->orderBy('ncs.last_comment_timestamp', 'DESC') + ->orderBy('f.last_comment_timestamp', 'DESC') ->range(0, variable_get('forum_block_num_active', '5')); break; case 'new': $title = t('New forum topics'); $query - ->orderBy('n.nid', 'DESC') + ->orderBy('f.created', 'DESC') ->range(0, variable_get('forum_block_num_new', '5')); break; } @@ -581,11 +612,11 @@ function forum_form($node, $form_state) $form['title'] = array('#type' => 'textfield', '#title' => check_plain($type->title_label), '#default_value' => !empty($node->title) ? $node->title : '', '#required' => TRUE, '#weight' => -5); if (!empty($node->nid)) { - $vid = variable_get('forum_nav_vocabulary', 0); - $forum_terms = taxonomy_node_get_terms_by_vocabulary($node, $vid); - // if editing, give option to leave shadows + $forum_terms = $node->taxonomy_forums; + // If editing, give option to leave shadows $shadow = (count($forum_terms) > 1); $form['shadow'] = array('#type' => 'checkbox', '#title' => t('Leave shadow copy'), '#default_value' => $shadow, '#description' => t('If you move this topic, you can leave a link in the old forum to the new forum.')); + $form['forum_tid'] = array('#type' => 'value', '#value' => $node->forum_tid); } $form['#submit'][] = 'forum_submit'; @@ -667,7 +698,7 @@ function forum_get_forums($tid = 0) { $last_post = new stdClass(); if (!empty($topic->last_comment_timestamp)) { - $last_post->timestamp = $topic->last_comment_timestamp; + $last_post->created = $topic->last_comment_timestamp; $last_post->name = $topic->last_comment_name; $last_post->uid = $topic->last_comment_uid; } @@ -702,9 +733,9 @@ function forum_get_topics($tid, $sortby, $forum_topic_list_header = array( NULL, - array('data' => t('Topic'), 'field' => 'n.title'), - array('data' => t('Replies'), 'field' => 'ncs.comment_count'), - array('data' => t('Last reply'), 'field' => 'ncs.last_comment_timestamp'), + array('data' => t('Topic'), 'field' => 'f.title'), + array('data' => t('Replies'), 'field' => 'f.comment_count'), + array('data' => t('Last reply'), 'field' => 'f.last_comment_timestamp'), ); $order = _forum_get_topic_order($sortby); @@ -714,48 +745,45 @@ function forum_get_topics($tid, $sortby, } } - $query = db_select('node_comment_statistics', 'ncs')->extend('PagerDefault')->extend('TableSort'); - $query->join('node', 'n', 'n.nid = ncs.nid'); - $query->join('users', 'cu', 'ncs.last_comment_uid = cu.uid'); - $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $tid)); - $query->join('users', 'u', 'n.uid = u.uid'); - $query->addExpression('IF(ncs.last_comment_uid != 0, cu.name, ncs.last_comment_name)', 'last_comment_name'); - $query->addField('n', 'created', 'timestamp'); - $query->addField('n', 'comment', 'comment_mode'); - $query->addField('ncs', 'comment_count', 'num_comments'); - $query->addField('f', 'tid', 'forum_tid'); + $query = db_select('forum_index', 'f')->extend('PagerDefault')->extend('TableSort'); + $query->fields('f'); $query + ->condition('f.tid', $tid) ->addTag('node_access') - ->fields('n', array('nid', 'title', 'type', 'sticky')) - ->fields('f', array('tid')) - ->fields('u', array('name', 'uid')) - ->fields('ncs', array('last_comment_timestamp', 'last_comment_uid')) - ->condition('n.status', 1) - ->orderBy('n.sticky', 'DESC') + ->orderBy('f.sticky', 'DESC') ->orderByHeader($forum_topic_list_header) - ->orderBy('n.created', 'DESC') + ->orderBy('f.last_comment_timestamp', 'DESC') ->limit($forum_per_page); - $count_query = db_select('node', 'n'); - $count_query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $tid)); + $count_query = db_select('forum_index', 'f'); + $count_query->condition('f.tid', $tid); $count_query->addExpression('COUNT(*)'); - $count_query - ->condition('n.status', 1) - ->addTag('node_access'); + $count_query->addTag('node_access'); $query->setCountQuery($count_query); $result = $query->execute(); + $nids = array(); + foreach ($result as $record) { + $nids[] = $record->nid; + } + if ($nids) { + $result = db_query("SELECT n.title, n.nid, n.sticky, n.created, n.uid, n.comment AS comment_mode, ncs.*, f.tid AS forum_tid, u.name, IF (ncs.last_comment_uid != 0, u2.name, ncs.last_comment_name) AS last_comment_name FROM {node} n INNER JOIN {node_comment_statistics} ncs ON n.nid = ncs.nid INNER JOIN {forum} f ON n.vid = f.vid INNER JOIN {users} u ON n.uid = u.uid INNER JOIN {users} u2 ON ncs.last_comment_uid = u2.uid WHERE n.nid IN (:nids)", array(':nids' => $nids)); + } + else { + $result = array(); + } + $topics = array(); foreach ($result as $topic) { if ($user->uid) { // folder is new if topic is new or there are new comments since last visit - if ($topic->tid != $tid) { + if ($topic->forum_tid != $tid) { $topic->new = 0; } else { $history = _forum_user_last_visit($topic->nid); $topic->new_replies = comment_num_new($topic->nid, $history); - $topic->new = $topic->new_replies || ($topic->timestamp > $history); + $topic->new = $topic->new_replies || ($topic->last_comment_timestamp > $history); } } else { @@ -764,9 +792,9 @@ function forum_get_topics($tid, $sortby, $topic->new = 0; } - if ($topic->num_comments > 0) { + if ($topic->comment_count > 0) { $last_reply = new stdClass(); - $last_reply->timestamp = $topic->last_comment_timestamp; + $last_reply->created = $topic->last_comment_timestamp; $last_reply->name = $topic->last_comment_name; $last_reply->uid = $topic->last_comment_uid; $topic->last_reply = $last_reply; @@ -820,8 +848,9 @@ function template_preprocess_forums(&$va // Format the "post new content" links listing. $forum_types = array(); - // Loop through all node types for forum vocabulary. - foreach ($vocabulary->nodes as $type) { + // Loop through all bundles for forum taxonomy vocabulary field. + $field = field_info_field('taxonomy_tags'); + foreach ($field['bundles'] as $type) { // Check if the current user has the 'create' permission for this node type. if (node_access('create', $type)) { // Fetch the "General" name of the content type; @@ -951,14 +980,14 @@ function template_preprocess_forum_topic if (!empty($variables['topics'])) { $row = 0; foreach ($variables['topics'] as $id => $topic) { - $variables['topics'][$id]->icon = theme('forum_icon', $topic->new, $topic->num_comments, $topic->comment_mode, $topic->sticky); + $variables['topics'][$id]->icon = theme('forum_icon', $topic->new, $topic->comment_count, $topic->comment_mode, $topic->sticky); $variables['topics'][$id]->zebra = $row % 2 == 0 ? 'odd' : 'even'; $row++; // We keep the actual tid in forum table, if it's different from the // current tid then it means the topic appears in two forums, one of // them is a shadow copy. - if ($topic->forum_tid != $variables['tid']) { + if ($variables['tid'] != $topic->forum_tid) { $variables['topics'][$id]->moved = TRUE; $variables['topics'][$id]->title = check_plain($topic->title); $variables['topics'][$id]->message = l(t('This topic has been moved'), "forum/$topic->forum_tid"); @@ -968,6 +997,7 @@ function template_preprocess_forum_topic $variables['topics'][$id]->title = l($topic->title, "node/$topic->nid"); $variables['topics'][$id]->message = ''; } + $topic->uid = $topic->last_comment_uid ? $topic->last_comment_uid : $topic->uid; $variables['topics'][$id]->created = theme('forum_submitted', $topic); $variables['topics'][$id]->last_reply = theme('forum_submitted', isset($topic->last_reply) ? $topic->last_reply : NULL); @@ -1031,7 +1061,7 @@ function template_preprocess_forum_icon( */ function template_preprocess_forum_submitted(&$variables) { $variables['author'] = isset($variables['topic']->uid) ? theme('username', $variables['topic']) : ''; - $variables['time'] = isset($variables['topic']->timestamp) ? format_interval(REQUEST_TIME - $variables['topic']->timestamp) : ''; + $variables['time'] = isset($variables['topic']->created) ? format_interval(REQUEST_TIME - $variables['topic']->created) : ''; } function _forum_user_last_visit($nid) { @@ -1063,3 +1093,42 @@ function _forum_get_topic_order($sortby) break; } } + +/** + * Updates the taxonomy index for a given node. + * + * @param $nid + * The ID of the node to update. + */ +function _forum_update_forum_index($nid) { + $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array( + ':nid' => $nid, + ':status' => COMMENT_PUBLISHED, + ))->fetchField(); + + if ($count > 0) { + // Comments exist. + $last_reply = db_query_range('SELECT cid, name, timestamp, uid FROM {comment} WHERE nid = :nid AND status = :status ORDER BY cid DESC', array( + ':nid' => $nid, + ':status' => COMMENT_PUBLISHED, + ), 0, 1)->fetchObject(); + db_update('forum_index') + ->fields( array( + 'comment_count' => $count, + 'last_comment_timestamp' => $last_reply->timestamp, + )) + ->condition('nid', $nid) + ->execute(); + } + else { + // Comments do not exist. + $node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject(); + db_update('forum_index') + ->fields( array( + 'comment_count' => 0, + 'last_comment_timestamp' => $node->created, + )) + ->condition('nid', $nid) + ->execute(); + } +} Index: modules/forum/forum.test =================================================================== RCS file: /cvs/drupal/drupal/modules/forum/forum.test,v retrieving revision 1.32 diff -u -p -r1.32 forum.test --- modules/forum/forum.test 28 Sep 2009 22:14:30 -0000 1.32 +++ modules/forum/forum.test 6 Oct 2009 16:40:39 -0000 @@ -131,8 +131,7 @@ class ForumTestCase extends DrupalWebTes $edit = array( 'name' => $title, 'description' => $description, - 'machine_name' => drupal_strtolower($this->randomName()), - 'help' => '', + 'machine_name' => drupal_strtolower(drupal_substr($this->randomName(), 3, 9)), ); // Edit the vocabulary. @@ -251,7 +250,7 @@ class ForumTestCase extends DrupalWebTes $edit = array( 'title' => $title, "body[$langcode][0][value]" => $body, - 'taxonomy[1]' => $tid + "taxonomy_forums[$langcode][value]" => $tid, ); // TODO The taxonomy select value is set by drupal code when the tid is part @@ -341,7 +340,7 @@ class ForumTestCase extends DrupalWebTes $langcode = FIELD_LANGUAGE_NONE; $edit["body[$langcode][0][value]"] = $this->randomName(256); // Assume the topic is initially associated with $forum. - $edit['taxonomy[1]'] = $this->root_forum['tid']; + $edit["taxonomy_forums[$langcode][value]"] = $this->root_forum['tid']; // Assumes the topic is initially associated with $forum. $edit['shadow'] = TRUE; $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); $this->assertRaw(t('Forum topic %title has been updated.', array('%title' => $edit['title'])), t('Forum node was edited')); Index: modules/node/node.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.pages.inc,v retrieving revision 1.84 diff -u -p -r1.84 node.pages.inc --- modules/node/node.pages.inc 3 Oct 2009 17:45:26 -0000 1.84 +++ modules/node/node.pages.inc 6 Oct 2009 16:40:39 -0000 @@ -85,7 +85,7 @@ function node_object_prepare($node) { // If this is a new node, fill in the default values. if (!isset($node->nid)) { foreach (array('status', 'promote', 'sticky') as $key) { - $node->$key = in_array($key, $node_options); + $node->$key = (int) in_array($key, $node_options); } global $user; $node->uid = $user->uid; @@ -543,9 +543,6 @@ function node_revision_revert_confirm_su $node_revision = $form['#node_revision']; $node_revision->revision = 1; $node_revision->log = t('Copy of the revision from %date.', array('%date' => format_date($node_revision->revision_timestamp))); - if (module_exists('taxonomy')) { - $node_revision->taxonomy = array_keys($node_revision->taxonomy); - } node_save($node_revision); Index: modules/simpletest/tests/common.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v retrieving revision 1.80 diff -u -p -r1.80 common.test --- modules/simpletest/tests/common.test 5 Oct 2009 14:56:43 -0000 1.80 +++ modules/simpletest/tests/common.test 6 Oct 2009 16:40:39 -0000 @@ -1355,14 +1355,15 @@ class DrupalDataApiTest extends DrupalWe $this->assertTrue($update_result == SAVED_UPDATED, t('Correct value returned when a record updated with drupal_write_record() for table with single-field primary key.')); // Insert an object record for a table with a multi-field primary key. - $vocabulary_node_type = new stdClass(); - $vocabulary_node_type->vid = $vocabulary->vid; - $vocabulary_node_type->type = 'page'; - $insert_result = drupal_write_record('taxonomy_vocabulary_node_type', $vocabulary_node_type); + $node_access = new stdClass(); + $node_access->nid = mt_rand(); + $node_access->gid = mt_rand(); + $node_access->realm = $this->randomName(); + $insert_result = drupal_write_record('node_access', $node_access); $this->assertTrue($insert_result == SAVED_NEW, t('Correct value returned when a record is inserted with drupal_write_record() for a table with a multi-field primary key.')); // Update the record. - $update_result = drupal_write_record('taxonomy_vocabulary_node_type', $vocabulary_node_type, array('vid', 'type')); + $update_result = drupal_write_record('node_access', $node_access, array('nid', 'gid', 'realm')); $this->assertTrue($update_result == SAVED_UPDATED, t('Correct value returned when a record is updated with drupal_write_record() for a table with a multi-field primary key.')); } Index: modules/taxonomy/taxonomy.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.admin.inc,v retrieving revision 1.70 diff -u -p -r1.70 taxonomy.admin.inc --- modules/taxonomy/taxonomy.admin.inc 21 Sep 2009 06:44:14 -0000 1.70 +++ modules/taxonomy/taxonomy.admin.inc 6 Oct 2009 16:40:39 -0000 @@ -17,14 +17,8 @@ function taxonomy_overview_vocabularies( $vocabularies = taxonomy_get_vocabularies(); $form['#tree'] = TRUE; foreach ($vocabularies as $vocabulary) { - $types = array(); - foreach ($vocabulary->nodes as $type) { - $node_type = node_type_get_name($type); - $types[] = $node_type ? check_plain($node_type) : check_plain($type); - } $form[$vocabulary->vid]['#vocabulary'] = $vocabulary; $form[$vocabulary->vid]['name'] = array('#markup' => check_plain($vocabulary->name)); - $form[$vocabulary->vid]['types'] = array('#markup' => implode(', ', $types)); $form[$vocabulary->vid]['weight'] = array('#type' => 'weight', '#delta' => 10, '#default_value' => $vocabulary->weight); $form[$vocabulary->vid]['edit'] = array('#markup' => l(t('edit vocabulary'), "admin/structure/taxonomy/$vocabulary->vid")); $form[$vocabulary->vid]['list'] = array('#markup' => l(t('list terms'), "admin/structure/taxonomy/$vocabulary->vid/list")); @@ -71,7 +65,6 @@ function theme_taxonomy_overview_vocabul $row = array(); $row[] = drupal_render($vocabulary['name']); - $row[] = drupal_render($vocabulary['types']); if (isset($vocabulary['weight'])) { $vocabulary['weight']['#attributes']['class'] = array('vocabulary-weight'); $row[] = drupal_render($vocabulary['weight']); @@ -87,7 +80,7 @@ function theme_taxonomy_overview_vocabul $rows[] = array(array('data' => t('No vocabularies available. Add vocabulary.', array('@link' => url('admin/structure/taxonomy/add'))), 'colspan' => '5')); } - $header = array(t('Vocabulary name'), t('Content types')); + $header = array(t('Vocabulary name')); if (isset($form['submit'])) { $header[] = t('Weight'); drupal_add_tabledrag('taxonomy', 'order', 'sibling', 'vocabulary-weight'); @@ -110,12 +103,7 @@ function taxonomy_form_vocabulary($form, 'name' => '', 'machine_name' => '', 'description' => '', - 'help' => '', - 'nodes' => array(), 'hierarchy' => 0, - 'tags' => 0, - 'multiple' => 0, - 'required' => 0, 'weight' => 0, ); $form['#vocabulary'] = (object) $edit; @@ -155,47 +143,11 @@ function taxonomy_form_vocabulary($form, 'js' => array(drupal_get_path('module', 'system') . '/system.js', $js_settings), ), ); - $form['help'] = array( - '#type' => 'textfield', - '#title' => t('Help text'), - '#maxlength' => 255, - '#default_value' => $edit['help'], - '#description' => t('Instructions to present to the user when selecting terms, e.g., "Enter a comma separated list of words".'), - ); $form['description'] = array( '#type' => 'textfield', '#title' => t('Description'), '#default_value' => $edit['description'], ); - $form['nodes'] = array( - '#type' => 'checkboxes', - '#title' => t('Apply to content types'), - '#default_value' => $edit['nodes'], - '#options' => array_map('check_plain', node_type_get_names()), - ); - $form['settings'] = array( - '#type' => 'fieldset', - '#title' => t('Settings'), - '#collapsible' => TRUE, - ); - $form['settings']['tags'] = array( - '#type' => 'checkbox', - '#title' => t('Tags'), - '#default_value' => $edit['tags'], - '#description' => t('Terms are created by users when submitting posts by typing a comma separated list.'), - ); - $form['settings']['multiple'] = array( - '#type' => 'checkbox', - '#title' => t('Multiple select'), - '#default_value' => $edit['multiple'], - '#description' => t('Allows posts to have more than one term from this vocabulary (always true for tags).'), - ); - $form['settings']['required'] = array( - '#type' => 'checkbox', - '#title' => t('Required'), - '#default_value' => $edit['required'], - '#description' => t('At least one term in this vocabulary must be selected when submitting a post.'), - ); // Set the hierarchy to "multiple parents" by default. This simplifies the // vocabulary form and standardizes the term form. $form['hierarchy'] = array( @@ -225,6 +177,11 @@ function taxonomy_form_vocabulary_valida if (!preg_match('!^[a-z0-9_]+$!', $form_state['values']['machine_name'])) { form_set_error('machine_name', t('The machine-readable name must contain only lowercase letters, numbers, and underscores.')); } + // Restrict machine names to 21 characters to avoid exceeding the limit + // for field names. + if (drupal_strlen($machine_name) > 21) { + form_set_error('machine_name', t('The machine-readable name must not exceed 21 characters.')); + } // Do not allow duplicate machine names. $vocabularies = taxonomy_get_vocabularies(); @@ -246,8 +203,6 @@ function taxonomy_form_vocabulary_submit $form_state['confirm_delete'] = TRUE; return; } - // Fix up the nodes array to remove unchecked nodes. - $form_state['values']['nodes'] = array_filter($form_state['values']['nodes']); $vocabulary = (object) $form_state['values']; if ($vocabulary->machine_name != $old_vocabulary->machine_name) { field_attach_rename_bundle($old_vocabulary->machine_name, $vocabulary->machine_name); @@ -306,106 +261,71 @@ function taxonomy_overview_terms($form, // An array of the terms to be displayed on this page. $current_page = array(); - // Case for free tagging. - if ($vocabulary->tags) { - // We are not calling taxonomy_get_tree because that might fail with a big - // number of tags in the freetagging vocabulary. - $query = db_select('taxonomy_term_data', 't')->extend('PagerDefault'); - $query->join('taxonomy_term_hierarchy', 'h', 't.tid = h.tid'); - $query->addTag('term_access'); - $query->condition('t.vid', $vocabulary->vid); - - // Store count in total entries and use this as count query. - $count_query = db_select('taxonomy_term_data', 't'); - $count_query->join('taxonomy_term_hierarchy', 'h', 't.tid = h.tid'); - $count_query->addTag('term_access'); - $count_query->condition('t.vid', $vocabulary->vid); - $count_query->addExpression('COUNT(t.tid)'); - $total_entries = $count_query->execute(); - $query->setCountQuery($count_query); - - $result = $query - ->fields('t') - ->fields('h', array('parent')) - ->orderBy('weight') - ->orderBy('name') - ->limit($page_increment) - ->execute(); - - foreach ($result as $term) { - $key = 'tid:' . $term->tid . ':0'; - $current_page[$key] = $term; - $page_entries++; + $term_deltas = array(); + $tree = taxonomy_get_tree($vocabulary->vid); + $term = current($tree); + do { + // In case this tree is completely empty. + if (empty($term)) { + break; } - } - // Case for restricted vocabulary. - else { - $term_deltas = array(); - $tree = taxonomy_get_tree($vocabulary->vid); - $term = current($tree); - do { - // In case this tree is completely empty. - if (empty($term)) { - break; - } - // Count entries before the current page. - if ($page && ($page * $page_increment) > $before_entries && !isset($back_peddle)) { - $before_entries++; - continue; - } - // Count entries after the current page. - elseif ($page_entries > $page_increment && isset($complete_tree)) { - $after_entries++; - continue; - } - - // Do not let a term start the page that is not at the root. - if (isset($term->depth) && ($term->depth > 0) && !isset($back_peddle)) { - $back_peddle = 0; - while ($pterm = prev($tree)) { - $before_entries--; - $back_peddle++; - if ($pterm->depth == 0) { - prev($tree); - continue 2; // Jump back to the start of the root level parent. - } - } - } - $back_peddle = isset($back_peddle) ? $back_peddle : 0; - - // Continue rendering the tree until we reach the a new root item. - if ($page_entries >= $page_increment + $back_peddle + 1 && $term->depth == 0 && $root_entries > 1) { - $complete_tree = TRUE; - // This new item at the root level is the first item on the next page. - $after_entries++; - continue; - } - if ($page_entries >= $page_increment + $back_peddle) { - $forward_peddle++; - } - - // Finally, if we've gotten down this far, we're rendering a term on this page. - $page_entries++; - $term_deltas[$term->tid] = isset($term_deltas[$term->tid]) ? $term_deltas[$term->tid] + 1 : 0; - $key = 'tid:' . $term->tid . ':' . $term_deltas[$term->tid]; - - // Keep track of the first term displayed on this page. - if ($page_entries == 1) { - $form['#first_tid'] = $term->tid; - } - // Keep a variable to make sure at least 2 root elements are displayed. - if ($term->parents[0] == 0) { - $root_entries++; - } - $current_page[$key] = $term; - } while ($term = next($tree)); - - // Because we didn't use a pager query, set the necessary pager variables. - $total_entries = $before_entries + $page_entries + $after_entries; - $pager_total_items[0] = $total_entries; - $pager_page_array[0] = $page; - $pager_total[0] = ceil($total_entries / $page_increment); - } + // Count entries before the current page. + if ($page && ($page * $page_increment) > $before_entries && !isset($back_peddle)) { + $before_entries++; + continue; + } + // Count entries after the current page. + elseif ($page_entries > $page_increment && isset($complete_tree)) { + $after_entries++; + continue; + } + + // Do not let a term start the page that is not at the root. + if (isset($term->depth) && ($term->depth > 0) && !isset($back_peddle)) { + $back_peddle = 0; + while ($pterm = prev($tree)) { + $before_entries--; + $back_peddle++; + if ($pterm->depth == 0) { + prev($tree); + continue 2; // Jump back to the start of the root level parent. + } + } + } + $back_peddle = isset($back_peddle) ? $back_peddle : 0; + + // Continue rendering the tree until we reach the a new root item. + if ($page_entries >= $page_increment + $back_peddle + 1 && $term->depth == 0 && $root_entries > 1) { + $complete_tree = TRUE; + // This new item at the root level is the first item on the next page. + $after_entries++; + continue; + } + if ($page_entries >= $page_increment + $back_peddle) { + $forward_peddle++; + } + + // Finally, if we've gotten down this far, we're rendering a term on this page. + $page_entries++; + $term_deltas[$term->tid] = isset($term_deltas[$term->tid]) ? $term_deltas[$term->tid] + 1 : 0; + $key = 'tid:' . $term->tid . ':' . $term_deltas[$term->tid]; + + // Keep track of the first term displayed on this page. + if ($page_entries == 1) { + $form['#first_tid'] = $term->tid; + } + // Keep a variable to make sure at least 2 root elements are displayed. + if ($term->parents[0] == 0) { + $root_entries++; + } + $current_page[$key] = $term; + } while ($term = next($tree)); + + // Because we didn't use a pager query, set the necessary pager variables. + $total_entries = $before_entries + $page_entries + $after_entries; + $pager_total_items[0] = $total_entries; + $pager_page_array[0] = $page; + $pager_total[0] = ceil($total_entries / $page_increment); // If this form was already submitted once, it's probably hit a validation // error. Ensure the form is rebuilt in the same order as the user submitted. @@ -433,7 +353,7 @@ function taxonomy_overview_terms($form, } $form[$key]['view'] = array('#markup' => l($term->name, "taxonomy/term/$term->tid")); - if (!$vocabulary->tags && $vocabulary->hierarchy < 2 && count($tree) > 1) { + if ($vocabulary->hierarchy < 2 && count($tree) > 1) { $form['#parent_fields'] = TRUE; $form[$key]['tid'] = array( '#type' => 'hidden', @@ -460,7 +380,7 @@ function taxonomy_overview_terms($form, $form['#forward_peddle'] = $forward_peddle; $form['#empty_text'] = t('No terms available. Add term.', array('@link' => url('admin/structure/taxonomy/' . $vocabulary->vid . '/list/add'))); - if (!$vocabulary->tags && $vocabulary->hierarchy < 2 && count($tree) > 1) { + if ($vocabulary->hierarchy < 2 && count($tree) > 1) { $form['submit'] = array( '#type' => 'submit', '#value' => t('Save') @@ -690,7 +610,6 @@ function taxonomy_form_term($form, &$for $form['#term'] = $edit; $form['#term']['parent'] = $parent; $form['#vocabulary'] = $vocabulary; - $form['#vocabulary']->nodes = drupal_map_assoc($vocabulary->nodes); $form['#builder_function'] = 'taxonomy_form_term_submit_builder'; // Check for confirmation forms. @@ -747,7 +666,7 @@ function taxonomy_form_term($form, &$for } $exclude[] = $edit['tid']; - $form['advanced']['parent'] = _taxonomy_term_select(t('Parents'), $parent, $vocabulary->vid, t('Parent terms') . '.', 1, '<' . t('root') . '>', $exclude); + $form['advanced']['parent'] = _taxonomy_term_select(t('Parents'), $parent, $vocabulary->vid, t('Parent terms') . '.', '<' . t('root') . '>', $exclude); } $form['advanced']['synonyms'] = array( '#type' => 'textarea', @@ -814,7 +733,7 @@ function taxonomy_form_term_submit($form return; } // Rebuild the form to confirm enabling multiple parents. - elseif ($form_state['clicked_button']['#value'] == t('Save') && !$form['#vocabulary']->tags && count($form_state['values']['parent']) > 1 && $form['#vocabulary']->hierarchy < 2) { + elseif ($form_state['clicked_button']['#value'] == t('Save') && count($form_state['values']['parent']) > 1 && $form['#vocabulary']->hierarchy < 2) { $form_state['rebuild'] = TRUE; $form_state['confirm_parents'] = TRUE; return; @@ -834,26 +753,24 @@ function taxonomy_form_term_submit($form break; } - if (!$form['#vocabulary']->tags) { - $current_parent_count = count($form_state['values']['parent']); - $previous_parent_count = count($form['#term']['parent']); - // Root doesn't count if it's the only parent. - if ($current_parent_count == 1 && isset($form_state['values']['parent'][0])) { - $current_parent_count = 0; - $form_state['values']['parent'] = array(); - } - - // If the number of parents has been reduced to one or none, do a check on the - // parents of every term in the vocabulary value. - if ($current_parent_count < $previous_parent_count && $current_parent_count < 2) { - taxonomy_check_vocabulary_hierarchy($form['#vocabulary'], $form_state['values']); - } - // If we've increased the number of parents and this is a single or flat - // hierarchy, update the vocabulary immediately. - elseif ($current_parent_count > $previous_parent_count && $form['#vocabulary']->hierarchy < 2) { - $form['#vocabulary']->hierarchy = $current_parent_count == 1 ? 1 : 2; - taxonomy_vocabulary_save($form['#vocabulary']); - } + $current_parent_count = count($form_state['values']['parent']); + $previous_parent_count = count($form['#term']['parent']); + // Root doesn't count if it's the only parent. + if ($current_parent_count == 1 && isset($form_state['values']['parent'][0])) { + $current_parent_count = 0; + $form_state['values']['parent'] = array(); + } + + // If the number of parents has been reduced to one or none, do a check on the + // parents of every term in the vocabulary value. + if ($current_parent_count < $previous_parent_count && $current_parent_count < 2) { + taxonomy_check_vocabulary_hierarchy($form['#vocabulary'], $form_state['values']); + } + // If we've increased the number of parents and this is a single or flat + // hierarchy, update the vocabulary immediately. + elseif ($current_parent_count > $previous_parent_count && $form['#vocabulary']->hierarchy < 2) { + $form['#vocabulary']->hierarchy = $current_parent_count == 1 ? 1 : 2; + taxonomy_vocabulary_save($form['#vocabulary']); } $form_state['tid'] = $term->tid; Index: modules/taxonomy/taxonomy.install =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.install,v retrieving revision 1.23 diff -u -p -r1.23 taxonomy.install --- modules/taxonomy/taxonomy.install 29 Sep 2009 15:13:56 -0000 1.23 +++ modules/taxonomy/taxonomy.install 6 Oct 2009 16:40:39 -0000 @@ -93,42 +93,6 @@ function taxonomy_schema() { 'primary key' => array('tid', 'parent'), ); - $schema['taxonomy_term_node'] = array( - 'description' => 'Stores the relationship of terms to nodes.', - 'fields' => array( - 'nid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Primary Key: The {node}.nid of the node.', - ), - 'vid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Primary Key: The {node}.vid of the node.', - ), - 'tid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => 'Primary Key: The {taxonomy_term_data}.tid of a term assigned to the node.', - ), - ), - 'indexes' => array( - 'vid' => array('vid'), - 'nid' => array('nid'), - ), - 'foreign keys' => array( - 'nid' => array('node' => 'nid'), - 'vid' => array('node' => 'vid'), - 'tid' => array('taxonomy_term_data' => 'tid'), - ), - 'primary key' => array('tid', 'vid'), - ); $schema['taxonomy_term_synonym'] = array( 'description' => 'Stores term synonyms.', 'fields' => array( @@ -191,13 +155,6 @@ function taxonomy_schema() { 'size' => 'big', 'description' => 'Description of the vocabulary.', ), - 'help' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => 'Help text to display for the vocabulary.', - ), 'relations' => array( 'type' => 'int', 'unsigned' => TRUE, @@ -214,30 +171,6 @@ function taxonomy_schema() { 'size' => 'tiny', 'description' => 'The type of hierarchy allowed within the vocabulary. (0 = disabled, 1 = single, 2 = multiple)', ), - 'multiple' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'size' => 'tiny', - 'description' => 'Whether or not multiple terms from this vocabulary may be assigned to a node. (0 = disabled, 1 = enabled)', - ), - 'required' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'size' => 'tiny', - 'description' => 'Whether or not terms are required for nodes using this vocabulary. (0 = disabled, 1 = enabled)', - ), - 'tags' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'size' => 'tiny', - 'description' => 'Whether or not free tagging is enabled for the vocabulary. (0 = disabled, 1 = enabled)', - ), 'module' => array( 'type' => 'varchar', 'length' => 255, @@ -250,7 +183,7 @@ function taxonomy_schema() { 'not null' => TRUE, 'default' => 0, 'size' => 'tiny', - 'description' => 'The weight of the vocabulary in relation to other vocabularies.', + 'description' => 'The weight of this vocabulary in relation to other vocabularies.', ), ), 'primary key' => array('vid'), @@ -259,30 +192,44 @@ function taxonomy_schema() { ), ); - $schema['taxonomy_vocabulary_node_type'] = array( - 'description' => 'Stores which node types vocabularies may be used with.', + $schema['taxonomy_index'] = array( + 'description' => 'Maintains denormalized information about node/term relationships.', 'fields' => array( - 'vid' => array( + 'nid' => array( + 'description' => 'The {node}.nid this record tracks.', 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, - 'description' => 'Primary Key: the {taxonomy_vocabulary}.vid of the vocabulary.', ), - 'type' => array( - 'type' => 'varchar', - 'length' => 32, + 'tid' => array( + 'description' => 'The term ID.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'sticky' => array( + 'description' => 'Boolean indicating whether the node is sticky.', + 'type' => 'int', + 'not null' => FALSE, + 'default' => 0, + 'size' => 'tiny', + ), + 'created' => array( + 'description' => 'The Unix timestamp when the node was created.', + 'type' => 'int', + 'unsigned' => TRUE, 'not null' => TRUE, - 'default' => '', - 'description' => 'The {node}.type of the node type for which the vocabulary may be used.', + 'default'=> 0, ), ), - 'primary key' => array('type', 'vid'), 'indexes' => array( - 'vid' => array('vid'), + 'term_node' => array('tid', 'sticky', 'created'), ), 'foreign keys' => array( - 'vid' => array('taxonomy_vocabulary' => 'vid'), + 'node' => 'nid', + 'taxonomy_term_data' => 'tid', ), ); @@ -322,3 +269,170 @@ function taxonomy_update_7002() { function taxonomy_update_7003() { db_drop_field('taxonomy_vocabulary', 'relations'); } + +/** + * Move taxonomy vocabulary associations for nodes to fields and field instances. + */ +function taxonomy_update_7004() { + $taxonomy_index = array( + 'description' => 'Maintains denormalized information about node/term relationships.', + 'fields' => array( + 'nid' => array( + 'description' => 'The {node}.nid this record tracks.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'tid' => array( + 'description' => 'The term ID.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'sticky' => array( + 'description' => 'Boolean indicating whether the node is sticky.', + 'type' => 'int', + 'not null' => FALSE, + 'default' => 0, + 'size' => 'tiny', + ), + 'created' => array( + 'description' => 'The Unix timestamp when the node was created.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default'=> 0, + ), + ), + 'indexes' => array( + 'term_node' => array('tid', 'sticky', 'created'), + ), + 'foreign keys' => array( + 'node' => 'nid', + 'taxonomy_term_data' => 'tid', + ), + ); + db_create_table('taxonomy_index', $taxonomy_index); + + // Use an inline version of Drupal 6 taxonomy_get_vocabularies() here since + // we can no longer rely on $vocabulary->nodes from the API function. + $result = db_query('SELECT v.*, n.type FROM {taxonomy_vocabulary} v LEFT JOIN {taxonomy_vocabulary_node_type} n ON v.vid = n.vid ORDER BY v.weight, v.name'); + $vocabularies = array(); + foreach ($result as $record) { + // If no node types are associated with a vocabulary, the LEFT JOIN will + // return a NULL value for type. + if (isset($record->type)) { + $node_types[$record->vid][$record->type] = $record->type; + unset($record->type); + $record->nodes = $node_types[$record->vid]; + } + elseif (!isset($record->nodes)) { + $record->nodes = array(); + } + $vocabularies[$record->vid] = $record; + } + + foreach ($vocabularies as $vocabulary) { + $field_name = 'taxonomy_' . $vocabulary->machine_name; + $field = array( + 'field_name' => $field_name, + 'type' => 'taxonomy_term', + 'cardinality' => $vocabulary->multiple || $vocabulary->tags ? FIELD_CARDINALITY_UNLIMITED : 1, + 'settings' => array( + 'required' => $vocabulary->required ? TRUE : FALSE, + 'allowed_values' => array( + array( + 'vid' => $vocabulary->vid, + 'parent' => 0, + ), + ), + ), + ); + field_create_field($field); + + foreach ($vocabulary->nodes as $bundle) { + $instance = array( + 'label' => $vocabulary->name, + 'field_name' => $field_name, + 'bundle' => $bundle, + 'description' => $vocabulary->help, + 'widget' => array( + 'type' => $vocabulary->tags ? 'taxonomy_autocomplete' : 'select', + ), + ); + field_create_instance($instance); + } + } + db_drop_table('taxonomy_vocabulary_node_type'); + $fields = array('help', 'multiple', 'required', 'tags'); + foreach ($fields as $field) { + db_drop_field('taxonomy_vocabulary', $field); + } +} + +/** + * Migrate {taxonomy_term_node} table to field storage. + */ +function taxonomy_update_7005(&$sandbox) { + // Since we are upgrading from Drupal 6, we know that only + // field_sql_storage.module will be enabled. + $field = field_info_field($field['field_name']); + $data_table = _field_sql_storage_tablename($field); + $revision_table = _field_sql_storage_revision_tablename($field); + $etid = _field_sql_storage_etid('node'); + $value_column = $field['field_name'] . '_value'; + $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta', $value_column); + + // This is a multi-pass update. On the first call we need to initialize some + // variables. + if (!isset($sandbox['total'])) { + $sandbox['last'] = 0; + $sandbox['count'] = 0; + + $query = db_select('taxonomy_term_node', 't'); + $sandbox['total'] = $query->countQuery()->execute()->fetchField(); + $found = (bool) $sandbox['total']; + } + else { + // We do each pass in batches of 1000, this should result in a + // maximum of 2000 insert queries each operation. + $batch = 1000 + $sandbox['last']; + + // Query and save data for the current revision. + $result = db_query_range('SELECT td.tid, tn.nid, td.weight, tn.vid, n2.type, n2.created, n2.sticky FROM {taxonomy_term_data} td INNER JOIN {taxonomy_term_node} tn ON td.tid = tn.tid INNER JOIN {node} n2 ON tn.nid = n2.nid INNER JOIN {node} n ON tn.vid = n.vid AND td.vid = :vocabulary_id ORDER BY td.weight ASC', array(':vocabulary_id' => $vocabulary->vid), $sandbox['last'], $batch); + $deltas = array(); + foreach ($result as $record) { + $found = TRUE; + $sandbox['count'] += 1; + // Start deltas from 0, and increment by one for each + // term attached to a node. + $deltas[$record->nid] = isset($deltas[$record->nid]) ? ++$deltas[$record->nid] : 0; + $values = array($etid, $record->nid, $record->vid, $record->type, $deltas[$record->nid], $record->tid); + db_insert($data_table)->fields($columns)->values($values)->execute(); + + // Update the {taxonomy_index} table. + db_insert('taxonomy_index') + ->fields(array('nid', 'tid', 'sticky', 'created',)) + ->values(array($record->nid, $record->tid, $record->sticky, $record->created)) + ->execute(); + } + + // Query and save data for all revisions. + $result = db_query('SELECT td.tid, tn.nid, td.weight, tn.vid, n.type FROM {taxonomy_term_data} td INNER JOIN {taxonomy_term_node} tn ON td.tid = tn.tid AND td.vid = :vocabulary_id INNER JOIN {node} n ON tn.nid = n.nid ORDER BY td.weight ASC', array(':vocabulary_id' => $vocabulary->vid), $sandbox['last'][$batch]); + $deltas = array(); + foreach ($result as $record) { + $found = TRUE; + $sandbox['count'] += 1; + // Start deltas at 0, and increment by one for each term attached to a revision. + $deltas[$record->vid] = isset($deltas[$record->vid]) ? ++$deltas[$record->vid] : 0; + $values = array($etid, $record->nid, $record->vid, $record->type, $deltas[$record->vid], $record->tid); + db_insert($revision_table)->fields($columns)->values($values)->execute(); + } + $sandbox['last'] = $batch; + } + if (!$found) { + db_drop_table('taxonomy_term_node'); + } +} Index: modules/taxonomy/taxonomy.module =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v retrieving revision 1.513 diff -u -p -r1.513 taxonomy.module --- modules/taxonomy/taxonomy.module 26 Sep 2009 15:57:39 -0000 1.513 +++ modules/taxonomy/taxonomy.module 6 Oct 2009 16:40:39 -0000 @@ -63,6 +63,54 @@ function taxonomy_entity_info() { } /** + * Return nodes attached to a term across all field instances. + * + * This function requires taxonomy module to be maintaining it's own tables, + * and will return an empty array if it is not. If using other field storage + * methods alternatives methods for listing terms will need to be used. + * + * @param $term + * The term object. + * @param $pager + * Boolean to indicate whether a pager should be used. + * @order + * An array of fields and directions. + * + * @return + * An array of nids matching the query. + */ +function taxonomy_select_nodes($term, $pager = TRUE, $order = array('t.sticky' => 'DESC', 't.created' => 'DESC')) { + if (!variable_get('taxonomy_maintain_index_table', TRUE)) { + return array(); + } + $query = db_select('taxonomy_index', 't'); + $query->addTag('node_access'); + if ($pager) { + $count_query = clone $query; + $count_query->addExpression('COUNT(t.nid)'); + + $query = $query + ->extend('PagerDefault') + ->limit(variable_get('default_nodes_main', 10)); + $query->setCountQuery($count_query); + } + else { + $query->range(0, variable_get('feed_default_items', 10)); + } + $query->condition('tid', $term->tid ); + $query->addField('t', 'nid'); + $query->addField('t', 'tid'); + foreach ($order as $field => $direction) { + $query->orderBy($field, $direction); + // ORDER BY fields need to be loaded too, assume they are in the form + // table_alias.name + list($table_alias, $name) = explode('.', $field); + $query->addField($table_alias, $name); + } + return $query->execute()->fetchCol(); +} + +/** * Implement hook_field_build_modes(); * * @TODO: build mode for display as a field (when attached to nodes etc.). @@ -98,66 +146,6 @@ function taxonomy_theme() { } /** - * Implement hook_node_view(). - */ -function taxonomy_node_view($node, $build_mode) { - if (empty($node->taxonomy)) { - return; - } - - if ($build_mode == 'rss') { - // Provide category information for RSS feeds. - foreach ($node->taxonomy as $term) { - $node->rss_elements[] = array( - 'key' => 'category', - 'value' => $term->name, - 'attributes' => array('domain' => url(taxonomy_term_path($term), array('absolute' => TRUE))), - ); - } - } - else { - $links = array(); - - // If previewing, the terms must be converted to objects first. - if (!empty($node->in_preview)) { - $node->taxonomy = taxonomy_preview_terms($node); - } - - foreach ($node->taxonomy as $term) { - // During preview the free tagging terms are in an array unlike the - // other terms which are objects. So we have to check if a $term - // is an object or not. - if (is_object($term)) { - $links['taxonomy_term_' . $term->tid] = array( - 'title' => $term->name, - 'href' => taxonomy_term_path($term), - 'attributes' => array('rel' => 'tag', 'title' => strip_tags($term->description)) - ); - } - // Previewing free tagging terms; we don't link them because the - // term-page might not exist yet. - else { - foreach ($term as $free_typed) { - $typed_terms = drupal_explode_tags($free_typed); - foreach ($typed_terms as $typed_term) { - $links['taxonomy_preview_term_' . $typed_term] = array( - 'title' => $typed_term, - ); - } - } - } - } - - $node->content['links']['terms'] = array( - '#theme' => 'links', - '#links' => $links, - '#attributes' => array('class' => array('links', 'inline')), - '#sorted' => TRUE, - ); - } -} - -/** * For vocabularies not maintained by taxonomy.module, give the maintaining * module a chance to provide a path for terms in that vocabulary. * @@ -244,14 +232,6 @@ function taxonomy_menu() { 'type' => MENU_CALLBACK, 'file' => 'taxonomy.pages.inc', ); - // TODO: remove with taxonomy_term_node_* - $items['taxonomy/autocomplete/legacy'] = array( - 'title' => 'Autocomplete taxonomy', - 'page callback' => 'taxonomy_autocomplete_legacy', - 'access arguments' => array('access content'), - 'type' => MENU_CALLBACK, - 'file' => 'taxonomy.pages.inc', - ); $items['admin/structure/taxonomy/%taxonomy_vocabulary'] = array( 'title' => 'Vocabulary', // this is replaced by callback @@ -303,9 +283,6 @@ function taxonomy_admin_vocabulary_title * Save a vocabulary given a vocabulary object. */ function taxonomy_vocabulary_save($vocabulary) { - if (empty($vocabulary->nodes)) { - $vocabulary->nodes = array(); - } if (!empty($vocabulary->name)) { // Prevent leading and trailing spaces in vocabulary names. @@ -318,38 +295,12 @@ function taxonomy_vocabulary_save($vocab if (!empty($vocabulary->vid) && !empty($vocabulary->name)) { $status = drupal_write_record('taxonomy_vocabulary', $vocabulary, 'vid'); - db_delete('taxonomy_vocabulary_node_type') - ->condition('vid', $vocabulary->vid) - ->execute(); - - if (!empty($vocabulary->nodes)) { - $query = db_insert('taxonomy_vocabulary_node_type') - ->fields(array('vid', 'type')); - foreach ($vocabulary->nodes as $type => $selected) { - $query->values(array( - 'vid' => $vocabulary->vid, - 'type' => $type, - )); - } - $query->execute(); - } module_invoke_all('taxonomy_vocabulary_update', $vocabulary); } elseif (empty($vocabulary->vid)) { $status = drupal_write_record('taxonomy_vocabulary', $vocabulary); - - if (!empty($vocabulary->nodes)) { - $query = db_insert('taxonomy_vocabulary_node_type') - ->fields(array('vid', 'type')); - foreach ($vocabulary->nodes as $type => $selected) { - $query->values(array( - 'vid' => $vocabulary->vid, - 'type' => $type, - )); - } - $query->execute(); - } field_attach_create_bundle($vocabulary->machine_name); + taxonomy_vocabulary_create_field($vocabulary); module_invoke_all('taxonomy_vocabulary_insert', $vocabulary); } @@ -373,9 +324,6 @@ function taxonomy_vocabulary_delete($vid db_delete('taxonomy_vocabulary') ->condition('vid', $vid) ->execute(); - db_delete('taxonomy_vocabulary_node_type') - ->condition('vid', $vid) - ->execute(); $result = db_query('SELECT tid FROM {taxonomy_term_data} WHERE vid = :vid', array(':vid' => $vid))->fetchCol(); foreach ($result as $tid) { taxonomy_term_delete($tid); @@ -432,6 +380,31 @@ function taxonomy_check_vocabulary_hiera } /** + * Create a default field when a vocabulary is created. + * + * @param $vocabulary + * A taxonomy vocabulary object. + */ +function taxonomy_vocabulary_create_field($vocabulary) { + $field = array( + 'field_name' => 'taxonomy_' . $vocabulary->machine_name, + 'type' => 'taxonomy_term', + // Set cardinality to unlimited so that select + // and autocomplete widgets behave as normal. + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, + 'settings' => array( + 'allowed_values' => array( + array( + 'vid' => $vocabulary->vid, + 'parent' => 0, + ), + ), + ), + ); + field_create_field($field); +} + +/** * Save a term object to the database. * * @param $term @@ -470,6 +443,9 @@ function taxonomy_term_save($term) { if (!isset($term->parent) || empty($term->parent)) { $term->parent = array(0); } + if (!is_array($term->parent)) { + $term->parent = array($term->parent); + } $query = db_insert('taxonomy_term_hierarchy') ->fields(array('tid', 'parent')); if (is_array($term->parent)) { @@ -490,12 +466,6 @@ function taxonomy_term_save($term) { } } } - else { - $query->values(array( - 'tid' => $term->tid, - 'parent' => $parent - )); - } $query->execute(); db_delete('taxonomy_term_synonym') @@ -556,9 +526,6 @@ function taxonomy_term_delete($tid) { db_delete('taxonomy_term_synonym') ->condition('tid', $tid) ->execute(); - db_delete('taxonomy_term_node') - ->condition('tid', $tid) - ->execute(); field_attach_delete('taxonomy_term', $term); _taxonomy_clean_field_cache($term); @@ -606,26 +573,18 @@ function taxonomy_form($vid, $value = 0, $vocabulary = taxonomy_vocabulary_load($vid); $help = ($help) ? $help : filter_xss_admin($vocabulary->help); - if (!$vocabulary->multiple) { - $blank = ($vocabulary->required) ? t('- Please choose -') : t('- None selected -'); - } - else { - $blank = ($vocabulary->required) ? 0 : t('- None -'); - } + $blank = t('- Please choose -'); - return _taxonomy_term_select(check_plain($vocabulary->name), $value, $vid, $help, intval($vocabulary->multiple), $blank); + return _taxonomy_term_select(check_plain($vocabulary->name), $value, $vid, $help, $blank); } /** * Generate a set of options for selecting a term from all vocabularies. */ -function taxonomy_form_all($free_tags = 0) { +function taxonomy_form_all() { $vocabularies = taxonomy_get_vocabularies(); $options = array(); foreach ($vocabularies as $vid => $vocabulary) { - if ($vocabulary->tags && !$free_tags) { - continue; - } $tree = taxonomy_get_tree($vid); if ($tree && (count($tree) > 0)) { $options[$vocabulary->name] = array(); @@ -643,9 +602,8 @@ function taxonomy_form_all($free_tags = * @param $type * If set, return only those vocabularies associated with this node type. */ -function taxonomy_get_vocabularies($type = NULL) { - $conditions = !empty($type) ? array('type' => $type) : NULL; - return taxonomy_vocabulary_load_multiple(FALSE, $conditions); +function taxonomy_get_vocabularies() { + return taxonomy_vocabulary_load_multiple(FALSE, array()); } /** @@ -660,330 +618,6 @@ function taxonomy_vocabulary_get_names() } /** - * Implement hook_form_alter(). - * Generate a form for selecting terms to associate with a node. - * We check for taxonomy_override_selector before loading the full - * vocabulary, so contrib modules can intercept before hook_form_alter - * and provide scalable alternatives. - */ -function taxonomy_form_alter(&$form, $form_state, $form_id) { - if (!variable_get('taxonomy_override_selector', FALSE) && !empty($form['#node_edit_form'])) { - $node = $form['#node']; - - if (!isset($node->taxonomy)) { - $terms = empty($node->nid) ? array() : taxonomy_node_get_terms($node); - } - else { - // After preview the terms must be converted to objects. - if (isset($form_state['node_preview'])) { - $node->taxonomy = taxonomy_preview_terms($node); - } - $terms = $node->taxonomy; - } - $query = db_select('taxonomy_vocabulary', 'v'); - $query->join('taxonomy_vocabulary_node_type', 'n', 'v.vid = n.vid'); - $query->addTag('term_access'); - - $result = $query - ->fields('v') - ->condition('n.type', $node->type) - ->orderBy('v.weight') - ->orderBy('v.name') - ->execute(); - - foreach ($result as $vocabulary) { - if ($vocabulary->tags) { - if (isset($form_state['node_preview'])) { - // Typed string can be changed by the user before preview, - // so we just insert the tags directly as provided in the form. - $typed_string = $node->taxonomy['tags'][$vocabulary->vid]; - } - else { - $typed_string = taxonomy_implode_tags($terms, $vocabulary->vid) . (array_key_exists('tags', $terms) ? $terms['tags'][$vocabulary->vid] : NULL); - } - if ($vocabulary->help) { - $help = filter_xss_admin($vocabulary->help); - } - else { - $help = t('A comma-separated list of terms describing this content. Example: funny, bungee jumping, "Company, Inc."'); - } - $form['taxonomy']['tags'][$vocabulary->vid] = array('#type' => 'textfield', - '#title' => $vocabulary->name, - '#description' => $help, - '#required' => $vocabulary->required, - '#default_value' => $typed_string, - '#autocomplete_path' => 'taxonomy/autocomplete/legacy/' . $vocabulary->vid, - '#weight' => $vocabulary->weight, - '#maxlength' => 1024, - ); - } - else { - // Extract terms belonging to the vocabulary in question. - $default_terms = array(); - foreach ($terms as $term) { - // Free tagging has no default terms and also no vid after preview. - if (isset($term->vid) && $term->vid == $vocabulary->vid) { - $default_terms[$term->tid] = $term; - } - } - $form['taxonomy'][$vocabulary->vid] = taxonomy_form($vocabulary->vid, array_keys($default_terms), filter_xss_admin($vocabulary->help)); - $form['taxonomy'][$vocabulary->vid]['#weight'] = $vocabulary->weight; - $form['taxonomy'][$vocabulary->vid]['#required'] = $vocabulary->required; - } - } - if (!empty($form['taxonomy']) && is_array($form['taxonomy'])) { - if (count($form['taxonomy']) > 1) { - // Add fieldset only if form has more than 1 element. - $form['taxonomy'] += array( - '#type' => 'fieldset', - '#title' => t('Vocabularies'), - '#collapsible' => TRUE, - '#collapsed' => FALSE, - ); - } - $form['taxonomy']['#weight'] = -3; - $form['taxonomy']['#tree'] = TRUE; - } - } -} - -/** - * Helper function to convert terms after a preview. - * - * After preview the tags are an array instead of proper objects. This function - * converts them back to objects with the exception of 'free tagging' terms, - * because new tags can be added by the user before preview and those do not - * yet exist in the database. We therefore save those tags as a string so - * we can fill the form again after the preview. - */ -function taxonomy_preview_terms($node) { - $taxonomy = array(); - if (isset($node->taxonomy)) { - foreach ($node->taxonomy as $key => $term) { - unset($node->taxonomy[$key]); - // A 'Multiple select' and a 'Free tagging' field returns an array. - if (is_array($term)) { - foreach ($term as $tid) { - if ($key == 'tags') { - // Free tagging; the values will be saved for later as strings - // instead of objects to fill the form again. - $taxonomy['tags'] = $term; - } - else { - $taxonomy[$tid] = taxonomy_term_load($tid); - } - } - } - // A 'Single select' field returns the term id. - elseif ($term) { - $taxonomy[$term] = taxonomy_term_load($term); - } - } - } - return $taxonomy; -} - -/** - * Find all terms associated with the given node, within one vocabulary. - */ -function taxonomy_node_get_terms_by_vocabulary($node, $vid, $key = 'tid') { - $query = db_select('taxonomy_term_data', 't'); - $query->join('taxonomy_term_node', 'r', 'r.tid = t.tid'); - $query->addTag('term_access'); - - $result = $query - ->fields('t') - ->condition('t.vid', $vid) - ->condition('r.vid', $node->vid) - ->orderBy('weight') - ->execute(); - - $terms = array(); - foreach ($result as $term) { - $terms[$term->$key] = $term; - } - return $terms; -} - -/** - * Find all term IDs associated with a set of nodes. - * - * @param $nodes - * An array of node objects. - * - * @return - * An array of term and node IDs ordered by vocabulary and term weight. - */ -function taxonomy_get_tids_from_nodes($nodes) { - $node_vids = array(); - foreach ($nodes as $node) { - $node_vids[] = $node->vid; - } - $query = db_select('taxonomy_term_node', 'r'); - $query->join('taxonomy_term_data', 't', 'r.tid = t.tid'); - $query->join('taxonomy_vocabulary', 'v', 't.vid = v.vid'); - $query->addTag('term_access'); - - return $query - ->fields('r', array('tid', 'nid', 'vid')) - ->condition('r.vid', $node_vids, 'IN') - ->orderBy('v.weight') - ->orderBy('t.weight') - ->orderBy('t.name') - ->execute() - ->fetchAll(); -} - -/** - * Find all terms associated with the given node, ordered by vocabulary and term weight. - */ -function taxonomy_node_get_terms($node, $key = 'tid') { - $terms = &drupal_static(__FUNCTION__); - - if (!isset($terms[$node->vid][$key])) { - $query = db_select('taxonomy_term_node', 'r'); - $query->join('taxonomy_term_data', 't', 'r.tid = t.tid'); - $query->join('taxonomy_vocabulary', 'v', 't.vid = v.vid'); - $query->addTag('term_access'); - - $result = $query - ->fields('r', array('tid', 'nid', 'vid')) - ->condition('r.vid', $node->vid) - ->orderBy('v.weight') - ->orderBy('t.weight') - ->orderBy('t.name') - ->execute(); - $terms[$node->vid][$key] = array(); - foreach ($result as $term) { - $terms[$node->vid][$key][$term->$key] = $term; - } - } - return $terms[$node->vid][$key]; -} - -/** - * Save term associations for a given node. - */ -function taxonomy_node_save($node, $terms) { - taxonomy_node_revision_delete($node); - - // Free tagging vocabularies do not send their tids in the form, - // so we'll detect them here and process them independently. - if (isset($terms['tags'])) { - $typed_input = $terms['tags']; - unset($terms['tags']); - - foreach ($typed_input as $vid => $vid_value) { - $vocabulary = taxonomy_vocabulary_load($vid); - $typed_terms = drupal_explode_tags($vid_value); - - $inserted = array(); - foreach ($typed_terms as $typed_term) { - // See if the term exists in the chosen vocabulary - // and return the tid; otherwise, add a new record. - $possibilities = taxonomy_get_term_by_name($typed_term); - $typed_term_tid = NULL; // tid match, if any. - foreach ($possibilities as $possibility) { - if ($possibility->vid == $vid) { - $typed_term_tid = $possibility->tid; - } - } - - if (!$typed_term_tid) { - $edit = array( - 'vid' => $vid, - 'name' => $typed_term, - 'vocabulary_machine_name' => $vocabulary->machine_name, - ); - $term = (object)$edit; - $status = taxonomy_term_save($term); - $typed_term_tid = $term->tid; - } - - // Defend against duplicate, differently cased tags - if (!isset($inserted[$typed_term_tid])) { - db_insert('taxonomy_term_node') - ->fields(array( - 'nid' => $node->nid, - 'vid' => $node->vid, - 'tid' => $typed_term_tid - )) - ->execute(); - $inserted[$typed_term_tid] = TRUE; - } - } - } - } - - if (is_array($terms) && !empty($terms)) { - $query = db_insert('taxonomy_term_node') - ->fields(array('nid', 'vid', 'tid')); - - foreach ($terms as $term) { - if (is_array($term)) { - foreach ($term as $tid) { - if ($tid) { - $query->values(array( - 'nid' => $node->nid, - 'vid' => $node->vid, - 'tid' => $tid, - )); - } - } - } - elseif (is_object($term)) { - $query->values(array( - 'nid' => $node->nid, - 'vid' => $node->vid, - 'tid' => $term->tid, - )); - } - elseif ($term) { - $query->values(array( - 'nid' => $node->nid, - 'vid' => $node->vid, - 'tid' => $term, - )); - } - } - $query->execute(); - } -} - -/** - * Implement hook_node_type_insert(). - */ -function taxonomy_node_type_insert($info) { - drupal_static_reset('taxonomy_term_count_nodes'); -} - -/** - * Implement hook_node_type_update(). - */ -function taxonomy_node_type_update($info) { - if (!empty($info->old_type) && $info->type != $info->old_type) { - db_update('taxonomy_vocabulary_node_type') - ->fields(array( - 'type' => $info->type, - )) - ->condition('type', $info->old_type) - ->execute(); - } - drupal_static_reset('taxonomy_term_count_nodes'); -} - -/** - * Implement hook_node_type_delete(). - */ -function taxonomy_node_type_delete($info) { - db_delete('taxonomy_vocabulary_node_type') - ->condition('type', $info->type) - ->execute(); - - drupal_static_reset('taxonomy_term_count_nodes'); -} - -/** * Find all parents of a given term ID. */ function taxonomy_get_parents($tid, $key = 'tid') { @@ -1150,51 +784,6 @@ function taxonomy_get_synonym_root($syno } /** - * Count the number of published nodes classified by a term. - * - * @param $tid - * The term ID - * @param $type - * (Optional) The $node->type. If given, taxonomy_term_count_nodes only counts - * nodes of $type that are classified with the term $tid. - * - * @return - * An integer representing a number of nodes. - * Results are statically cached. - */ -function taxonomy_term_count_nodes($tid, $type = NULL) { - $count = &drupal_static(__FUNCTION__, array()); - // Reset the taxonomy tree when first called (or if reset). - if (empty($count)) { - drupal_static_reset('taxonomy_get_tree'); - } - // If $type is NULL, change it to 0 to allow it to be used as an array key - // for the static cache. - $type = empty($type) ? 0 : $type; - - if (!isset($count[$type][$tid])) { - $term = taxonomy_term_load($tid); - $tree = taxonomy_get_tree($term->vid, $tid, NULL); - $tids = array($tid); - foreach ($tree as $descendent) { - $tids[] = $descendent->tid; - } - - $query = db_select('taxonomy_term_node', 't'); - $query->addExpression('COUNT(DISTINCT(n.nid))', 'nid_count'); - $query->join('node', 'n', 't.vid = n.vid'); - $query->condition('t.tid', $tids, 'IN'); - $query->condition('n.status', 1); - if (!is_numeric($type)) { - $query->condition('n.type', $type); - } - $query->addTag('term_access'); - $count[$type][$tid] = $query->execute()->fetchField(); - } - return $count[$type][$tid]; -} - -/** * Try to map a string to an existing term, as for glossary use. * * Provides a case-insensitive and trimmed mapping, to maximize the @@ -1211,17 +800,6 @@ function taxonomy_get_term_by_name($name } /** - * Return array of tids and join operator. - * - * This is a wrapper function for taxonomy_terms_parse_string which is called - * by the menu system when loading a path with taxonomy terms. - */ -function taxonomy_terms_load($str_tids) { - $terms = taxonomy_terms_parse_string($str_tids); - return $terms; -} - -/** * Controller class for taxonomy terms. * * This extends the DrupalDefaultEntityController class. Only alteration is @@ -1251,10 +829,6 @@ class TaxonomyTermController extends Dru // Add the machine name field from the {taxonomy_vocabulary} table. $this->query->innerJoin('taxonomy_vocabulary', 'v', 'base.vid = v.vid'); $this->query->addField('v', 'machine_name', 'vocabulary_machine_name'); - - if (!empty($this->type)) { - $this->query->innerJoin('taxonomy_vocabulary_node_type', 'n', 'base.vid = n.vid AND n.type = :type', array(':type' => $this->type)); - } } protected function cacheGet($ids) { @@ -1278,24 +852,8 @@ class TaxonomyTermController extends Dru * special handling for taxonomy vocabulary objects. */ class TaxonomyVocabularyController extends DrupalDefaultEntityController { - protected $type; - public function load($ids = array(), $conditions = array()) { - if (isset($conditions['type'])) { - $this->type = $conditions['type']; - unset($conditions['type']); - } - return parent::load($ids, $conditions); - } - protected function buildQuery() { parent::buildQuery(); - if (!empty($this->type)) { - $this->query->innerJoin('taxonomy_vocabulary_node_type', 'n', 'base.vid = n.vid AND n.type = :type', array(':type' => $this->type)); - } - else { - $this->query->leftJoin('taxonomy_vocabulary_node_type', 'n', 'base.vid = n.vid'); - } - $this->query->addField('n', 'type'); $this->query->orderBy('base.weight'); $this->query->orderBy('base.name'); } @@ -1304,14 +862,6 @@ class TaxonomyVocabularyController exten foreach ($records as $record) { // If no node types are associated with a vocabulary, the LEFT JOIN will // return a NULL value for type. - if (isset($record->type)) { - $node_types[$record->vid][$record->type] = $record->type; - unset($record->type); - $record->nodes = $node_types[$record->vid]; - } - elseif (!isset($record->nodes)) { - $record->nodes = array(); - } $queried_vocabularies[$record->vid] = $record; } $records = $queried_vocabularies; @@ -1419,7 +969,7 @@ function taxonomy_term_load($tid) { * @see taxonomy_form() * @see taxonomy_form_term() */ -function _taxonomy_term_select($title, $value, $vocabulary_id, $description, $multiple, $blank, $exclude = array()) { +function _taxonomy_term_select($title, $value, $vocabulary_id, $description, $blank, $exclude = array()) { $tree = taxonomy_get_tree($vocabulary_id); $options = array(); @@ -1441,8 +991,6 @@ function _taxonomy_term_select($title, $ '#default_value' => $value, '#options' => $options, '#description' => $description, - '#multiple' => $multiple, - '#size' => $multiple ? min(9, count($options)) : 0, '#weight' => -15, '#theme' => 'taxonomy_term_select', ); @@ -1459,186 +1007,6 @@ function theme_taxonomy_term_select($ele } /** - * Finds all nodes that match selected taxonomy conditions. - * - * @param $tids - * An array of term IDs to match. - * @param $operator - * How to interpret multiple IDs in the array. Can be "or" or "and". - * @param $depth - * How many levels deep to traverse the taxonomy tree. Can be a non-negative - * integer or "all". - * @param $pager - * Whether the nodes are to be used with a pager (the case on most Drupal - * pages) or not (in an XML feed, for example). - * @param $order - * The order clause for the query that retrieve the nodes. - * It is important to specifc the table alias (n). - * - * Example: - * array('table_alias.field_name' = 'DESC'); - * @return - * An array of node IDs. - */ -function taxonomy_select_nodes($tids = array(), $operator = 'or', $depth = 0, $pager = TRUE, $order = array('n.sticky' => 'DESC', 'n.created' => 'DESC')) { - if (count($tids) <= 0) { - return array(); - } - // For each term ID, generate an array of descendant term IDs to the right depth. - $descendant_tids = array(); - if ($depth === 'all') { - $depth = NULL; - } - $terms = taxonomy_term_load_multiple($tids); - foreach ($terms as $term) { - $tree = taxonomy_get_tree($term->vid, $term->tid, $depth); - $descendant_tids[] = array_merge(array($term->tid), array_map('_taxonomy_get_tid_from_term', $tree)); - } - - $query = db_select('node', 'n'); - $query->addTag('node_access'); - $query->condition('n.status', 1); - - if ($operator == 'or') { - $args = call_user_func_array('array_merge', $descendant_tids); - $query->join('taxonomy_term_node', 'tn', 'n.vid = tn.vid'); - $query->condition('tn.tid', $args, 'IN'); - } - else { - foreach ($descendant_tids as $tids) { - $alias = $query->join('taxonomy_term_node', 'tn', 'n.vid = tn.vid'); - $query->condition($alias . '.tid', $tids, 'IN'); - } - } - - if ($pager) { - $count_query = clone $query; - $count_query->addExpression('COUNT(DISTINCT n.nid)'); - - $query = $query - ->extend('PagerDefault') - ->limit(variable_get('default_nodes_main', 10)); - $query->setCountQuery($count_query); - } - else { - $query->range(0, variable_get('feed_default_items', 10)); - } - - $query->distinct(TRUE); - $query->addField('n', 'nid'); - foreach ($order as $field => $direction) { - $query->orderBy($field, $direction); - // ORDER BY fields need to be loaded too, assume they are in the form table_alias.name - list($table_alias, $name) = explode('.', $field); - $query->addField($table_alias, $name); - } - - return $query->execute()->fetchCol(); -} - -/** - * Implement hook_node_load(). - */ -function taxonomy_node_load($nodes) { - // Get an array of tid, vid associations ordered by vocabulary and term - // weight. - $tids = taxonomy_get_tids_from_nodes($nodes); - - // Extract the tids only from this array. - $term_ids = array(); - foreach ($tids as $term) { - $term_ids[$term->tid] = $term->tid; - } - - // Load the full term objects for these tids. - $terms = taxonomy_term_load_multiple($term_ids); - foreach ($tids as $term) { - $nodes[$term->nid]->taxonomy[$term->tid] = $terms[$term->tid]; - } - foreach ($nodes as $node) { - if (!isset($nodes[$node->nid]->taxonomy)) { - $node->taxonomy = array(); - } - } -} - -/** - * Implement hook_node_insert(). - */ -function taxonomy_node_insert($node) { - if (!empty($node->taxonomy)) { - taxonomy_node_save($node, $node->taxonomy); - } -} - -/** - * Implement hook_node_update(). - */ -function taxonomy_node_update($node) { - if (!empty($node->taxonomy)) { - taxonomy_node_save($node, $node->taxonomy); - } -} - -/** - * Implement hook_node_delete(). - * - * Remove associations of a node to its terms. - */ -function taxonomy_node_delete($node) { - db_delete('taxonomy_term_node') - ->condition('nid', $node->nid) - ->execute(); - drupal_static_reset('taxonomy_term_count_nodes'); -} - -/** - * Implement hook_node_revision_delete(). - * - * Remove associations of a node to its terms. - */ -function taxonomy_node_revision_delete($node) { - db_delete('taxonomy_term_node') - ->condition('vid', $node->vid) - ->execute(); - drupal_static_reset('taxonomy_term_count_nodes'); -} - -/** - * Implement hook_node_validate(). - * - * Make sure incoming vids are free tagging enabled. - */ -function taxonomy_node_validate($node, $form) { - if (!empty($node->taxonomy)) { - $terms = $node->taxonomy; - if (!empty($terms['tags'])) { - foreach ($terms['tags'] as $vid => $vid_value) { - $vocabulary = taxonomy_vocabulary_load($vid); - if (empty($vocabulary->tags)) { - // see form_get_error $key = implode('][', $element['#parents']); - // on why this is the key - form_set_error("taxonomy][tags][$vid", t('The %name vocabulary can not be modified in this way.', array('%name' => $vocabulary->name))); - } - } - } - } -} - -/** - * Implement hook_node_update_index(). - */ -function taxonomy_node_update_index($node) { - $output = array(); - foreach ($node->taxonomy as $term) { - $output[] = $term->name; - } - if (count($output)) { - return '(' . implode(', ', $output) . ')'; - } -} - -/** * Implement hook_help(). */ function taxonomy_help($path, $arg) { @@ -1658,9 +1026,6 @@ function taxonomy_help($path, $arg) { return $output; case 'admin/structure/taxonomy/%/list': $vocabulary = taxonomy_vocabulary_load($arg[3]); - if ($vocabulary->tags) { - return '

' . t('%capital_name is a free-tagging vocabulary. To change the name or description of a term, click the edit link next to the term.', array('%capital_name' => drupal_ucfirst($vocabulary->name))) . '

'; - } switch ($vocabulary->hierarchy) { case 0: return '

' . t('%capital_name is a flat vocabulary. You may organize the terms in the %name vocabulary by using the handles on the left side of the table. To change the name or description of a term, click the edit link next to the term.', array('%capital_name' => drupal_ucfirst($vocabulary->name), '%name' => $vocabulary->name)) . '

'; @@ -1823,7 +1188,7 @@ function taxonomy_field_validate($obj_ty * Implement hook_field_is_empty(). */ function taxonomy_field_is_empty($item, $field) { - if (empty($item['value']) && (string) $item['value'] !== '0') { + if (!is_array($item) || (empty($item['value']) && (string) $item['value'] !== '0')) { return TRUE; } return FALSE; @@ -2166,3 +1531,95 @@ function taxonomy_field_settings_form($f return $form; } + +/** + * @defgroup taxonomy indexing Taxonomy functions maintaining {taxonomy_index}. + * + * Taxonomy uses default field storage to store canonical relationships + * between terms and fieldable entities. However its most common use case + * requires listing all content associated with a term or group of terms + * sorted by creation date. To avoid slow queries due to joining across + * multiple node and field tables with various conditions and order by criteria, + * we maintain a denormalized table with all relationships between terms, + * published nodes and common sort criteria such as sticky and created. + * This is used as a lookup table by taxonomy_select_nodes(). When using other + * field storage engines or alternative methods of denormalizing this data + * you should set the variable 'taxonomy_maintain_index_table' to FALSE + * to avoid unnecessary writes in SQL. + * @{ + */ + +/** + * Implement hook_field_insert(). + */ +function taxonomy_field_insert($obj_type, $object, $field, $instance, $langcode, &$items) { + // We maintain a denormalized table of term/node relationships, containing + // only data for current, published nodes. + if (variable_get('taxonomy_maintain_index_table', TRUE) && $field['storage']['type'] == 'field_sql_storage' && $obj_type == 'node' && $object->status) { + $query = db_insert('taxonomy_index')->fields(array('nid', 'tid', 'sticky', 'created', )); + foreach ($items as $item) { + $query->values(array( + 'nid' => $object->nid, + 'tid' => $item['value'], + 'sticky' => $object->sticky, + 'created' => $object->created, + )); + } + $query->execute(); + } +} + +/** + * Implement hook_field_update(). + */ +function taxonomy_field_update($obj_type, $object, $field, $instance, $langcode, &$items) { + if (variable_get('taxonomy_maintain_index_table', TRUE) && $field['storage']['type'] == 'field_sql_storage' && $obj_type = 'node') { + $first_call = &drupal_static(__FUNCTION__, array()); + + // We don't maintain data for old revisions, so clear all previous values + // from the table. Since this hook runs once per field, per object, make + // sure we only wipe values once. + if (!isset($first_call[$object->nid])) { + $first_call[$object->nid] = FALSE; + db_delete('taxonomy_index')->condition('nid', $object->nid)->execute(); + } + // Only save data to the table if the node is published. + if ($object->status) { + $query = db_insert('taxonomy_index')->fields(array('nid', 'tid', 'sticky', 'created')); + foreach ($items as $item) { + $query->values(array( + 'nid' => $object->nid, + 'tid' => $item['value'], + 'sticky' => $object->sticky, + 'created' => $object->created, + )); + } + $query->execute(); + } + } +} + +/** + * Implement hook_node_delete(). + */ +function taxonomy_node_delete($node) { + if (variable_get('taxonomy_maintain_index_table', TRUE)) { + // Clean up the {taxonomy_index} table when nodes are deleted. + db_delete('taxonomy_index')->condition('nid', $node->nid)->execute(); + } +} + +/** + * Implement hook_taxonomy_term_delete(). + */ +function taxonomy_taxonomy_term_delete($term) { + if (variable_get('taxonomy_maintain_index_table', TRUE)) { + // Clean up the {taxonomy_index} table when terms are deleted. + db_delete('taxonomy_index')->condition('tid', $term->tid)->execute(); + } +} + +/** + * @} End of "defgroup taxonomy indexing" + */ + Index: modules/taxonomy/taxonomy.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.pages.inc,v retrieving revision 1.36 diff -u -p -r1.36 taxonomy.pages.inc --- modules/taxonomy/taxonomy.pages.inc 21 Sep 2009 07:56:08 -0000 1.36 +++ modules/taxonomy/taxonomy.pages.inc 6 Oct 2009 16:40:39 -0000 @@ -40,8 +40,7 @@ function taxonomy_term_page($term) { '#suffix' => '', ); } - - if ($nids = taxonomy_select_nodes(array($term->tid), NULL, TRUE)) { + if ($nids = taxonomy_select_nodes($term)) { $nodes = node_load_multiple($nids); $build += node_build_multiple($nodes); $build['pager'] = array( @@ -71,7 +70,7 @@ function taxonomy_term_feed($term) { // Only display the description if we have a single term, to avoid clutter and confusion. // HTML will be removed from feed description, so no need to filter here. $channel['description'] = $term->description; - $nids = taxonomy_select_nodes(array($term->tid, NULL, NULL, FALSE)); + $nids = taxonomy_select_nodes(array($term->tid, FALSE)); node_feed($nids, $channel); } @@ -91,56 +90,6 @@ function taxonomy_term_edit($term) { /** * Helper function for autocompletion */ -function taxonomy_autocomplete_legacy($vid = 0, $tags_typed = '') { - // The user enters a comma-separated list of tags. We only autocomplete the last tag. - $tags_typed = drupal_explode_tags($tags_typed); - $tag_last = drupal_strtolower(array_pop($tags_typed)); - - $matches = array(); - if ($tag_last != '') { - $query = db_select('taxonomy_term_data', 't'); - $query->addTag('term_access'); - $query->leftJoin('taxonomy_term_synonym', 'ts', 't.tid = ts.tid'); - // Don't select already entered terms. - if (count($tags_typed)) { - $query->condition('t.name', $tags_typed, 'NOT IN'); - } - $tags_return = $query - ->fields('t', array('tid', 'name')) - ->condition('t.vid', $vid) - // Select rows that either match by term or synonym name. - ->condition(db_or() - ->where("LOWER(t.name) LIKE :last_string", array(':last_string' => '%' . $tag_last . '%')) - ->where("LOWER(ts.name) LIKE :last_string", array(':last_string' => '%' . $tag_last . '%')) - ) - ->range(0, 10) - ->execute() - ->fetchAllKeyed(); - - $prefix = count($tags_typed) ? implode(', ', $tags_typed) . ', ' : ''; - - // We use two arrays to make sure synonym suggestions appear last. - $term_matches = $synonym_matches = array(); - foreach ($tags_return as $tid => $name) { - $n = $name; - // Commas and quotes in terms are special cases, so encode 'em. - if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) { - $n = '"' . str_replace('"', '""', $name) . '"'; - } - // Inform the user his query matched a synonym rather than a term. - if (strpos(drupal_strtolower($name), $tag_last) === FALSE) { - $name = t('Did you mean %suggestion', array('%suggestion' => $name)); - $synonym_matches[$prefix . $n] = filter_xss($name); - } - } - } - - drupal_json_output(array_merge($term_matches, $synonym_matches)); -} - -/** - * Helper function for autocompletion - */ function taxonomy_autocomplete($field_name, $bundle, $tags_typed = '') { $instance = field_info_instance($field_name, $bundle); $field = field_info_field($field_name); Index: modules/taxonomy/taxonomy.test =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.test,v retrieving revision 1.47 diff -u -p -r1.47 taxonomy.test --- modules/taxonomy/taxonomy.test 20 Sep 2009 17:40:42 -0000 1.47 +++ modules/taxonomy/taxonomy.test 6 Oct 2009 16:40:39 -0000 @@ -73,11 +73,6 @@ class TaxonomyVocabularyFunctionalTest e $edit['name'] = $this->randomName(); $edit['description'] = $this->randomName(); $edit['machine_name'] = $machine_name; - $edit['help'] = $this->randomName(); - $edit['nodes[article]'] = 'article'; - $edit['tags'] = 1; - $edit['multiple'] = 1; - $edit['required'] = 1; $this->drupalPost(NULL, $edit, t('Save')); $this->assertRaw(t('Created new vocabulary %name.', array('%name' => $edit['name'])), t('Vocabulary created successfully')); @@ -154,7 +149,6 @@ class TaxonomyVocabularyFunctionalTest e $edit = array( 'name' => $this->randomName(), 'machine_name' => drupal_strtolower($this->randomName()), - 'nodes[article]' => 'article', ); $this->drupalPost('admin/structure/taxonomy/add', $edit, t('Save')); $this->assertText(t('Created new vocabulary'), t('New vocabulary was created.')); @@ -276,27 +270,23 @@ class TaxonomyVocabularyUnitTest extends // Fetch all of the vocabularies using taxonomy_get_vocabularies(). // Confirm that the vocabularies are ordered by weight. $vocabularies = taxonomy_get_vocabularies(); - $this->assertEqual(array_shift($vocabularies), $vocabulary1, t('Vocabulary was found in the vocabularies array.')); - $this->assertEqual(array_shift($vocabularies), $vocabulary2, t('Vocabulary was found in the vocabularies array.')); - $this->assertEqual(array_shift($vocabularies), $vocabulary3, t('Vocabulary was found in the vocabularies array.')); + $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary1->vid, t('Vocabulary was found in the vocabularies array.')); + $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary2->vid, t('Vocabulary was found in the vocabularies array.')); + $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary3->vid, t('Vocabulary was found in the vocabularies array.')); // Fetch the vocabularies with taxonomy_vocabulary_load_multiple(), specifying IDs. // Ensure they are returned in the same order as the original array. $vocabularies = taxonomy_vocabulary_load_multiple(array($vocabulary3->vid, $vocabulary2->vid, $vocabulary1->vid)); - $this->assertEqual(array_shift($vocabularies), $vocabulary3, t('Vocabulary loaded successfully by ID.')); - $this->assertEqual(array_shift($vocabularies), $vocabulary2, t('Vocabulary loaded successfully by ID.')); - $this->assertEqual(array_shift($vocabularies), $vocabulary1, t('Vocabulary loaded successfully by ID.')); + $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary3->vid, t('Vocabulary loaded successfully by ID.')); + $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary2->vid, t('Vocabulary loaded successfully by ID.')); + $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary1->vid, t('Vocabulary loaded successfully by ID.')); // Fetch vocabulary 1 by name. - $this->assertTrue(current(taxonomy_vocabulary_load_multiple(array(), array('name' => $vocabulary1->name))) == $vocabulary1, t('Vocabulary loaded successfully by name.')); + $vocabulary = current(taxonomy_vocabulary_load_multiple(array(), array('name' => $vocabulary1->name))); + $this->assertTrue($vocabulary->vid == $vocabulary1->vid, t('Vocabulary loaded successfully by name.')); // Fetch vocabulary 1 by name and ID. - $this->assertTrue(current(taxonomy_vocabulary_load_multiple(array($vocabulary1->vid), array('name' => $vocabulary1->name))) == $vocabulary1, t('Vocabulary loaded successfully by name and ID.')); - - // Fetch vocabulary 1 with specified node type. - entity_get_controller('taxonomy_vocabulary')->resetCache(); - $vocabulary_node_type = current(taxonomy_vocabulary_load_multiple(array($vocabulary1->vid), array('type' => 'article'))); - $this->assertEqual($vocabulary_node_type, $vocabulary1, t('Vocabulary with specified node type loaded successfully.')); + $this->assertTrue(current(taxonomy_vocabulary_load_multiple(array($vocabulary1->vid), array('name' => $vocabulary1->name)))->vid == $vocabulary1->vid, t('Vocabulary loaded successfully by name and ID.')); } } @@ -312,65 +302,6 @@ class TaxonomyTermUnitTest extends Taxon 'group' => 'Taxonomy', ); } - - /** - * Tests for taxonomy_term_count_nodes(). - * - * Attach nodes to a hierarchical vocabulary and check they are counted - * correctly. - */ - function testTaxonomyTermCountNodes() { - // Create a vocabulary with three terms. - $vocabulary = $this->createVocabulary(); - $term1 = $this->createTerm($vocabulary); - $term2 = $this->createTerm($vocabulary); - $term3 = $this->createTerm($vocabulary); - - // Attach term1 to a node. - $node1 = $this->drupalCreateNode(array('type' => 'page')); - $node1->taxonomy = array($term1->tid); - node_save($node1); - $this->assertEqual(taxonomy_term_count_nodes($term1->tid), 1, t('Term has one valid node association.')); - - // Attach term2 to a node. - $node2 = $this->drupalCreateNode(array('type' => 'article')); - $node2->taxonomy = array($term2->tid); - node_save($node2); - $this->assertEqual(taxonomy_term_count_nodes($term2->tid), 1, t('Term has one valid node association.')); - - // Confirm that term3 is not associated with any nodes. - $this->assertEqual(taxonomy_term_count_nodes($term3->tid), 0, t('Term is not associated with any nodes')); - - // Set term3 as the parent of term1. - $term1->parent = array($term3->tid); - taxonomy_term_save($term1); - - // Confirm that the term hierarchy is altered correctly. - $children = taxonomy_get_children($term3->tid); - $this->assertTrue(isset($children[$term1->tid]), t('Term 3 saved as parent of term 1')); - - $this->assertEqual(count(taxonomy_get_tree($term3->vid, $term3->tid)), 1, t('Term 3 has one child term')); - - // Confirm that term3's parental relationship with term1 leads to a - // node assocation being counted. - $this->assertEqual(taxonomy_term_count_nodes($term3->tid, NULL), 1, t('Term has one valid node association due to child term.')); - - // Set term3 as the parent of term2. - $term2->parent = array($term3->tid); - taxonomy_term_save($term2); - - // term3 should now have two node associations counted. - $this->assertEqual(taxonomy_term_count_nodes($term3->tid, NULL), 2, t('Term has two valid node associations due to child terms.')); - - // Save node1 with both child taxonomy terms, this should still result - // in term3 having two node associations. - $node1->taxonomy = array($term1->tid, $term2->tid); - node_save($node1); - $this->assertEqual(taxonomy_term_count_nodes($term3->tid, NULL), 2, t('Term has two valid node associations.')); - - // Confirm that the node type argument returns a single node association. - $this->assertEqual(taxonomy_term_count_nodes($term3->tid, 'page'), 1, t("Term is associated with one node of type 'page'.")); - } } /** @@ -391,6 +322,20 @@ class TaxonomyTermTestCase extends Taxon $this->admin_user = $this->drupalCreateUser(array('administer taxonomy', 'bypass node access')); $this->drupalLogin($this->admin_user); $this->vocabulary = $this->createVocabulary(); + + $this->instance = array( + 'field_name' => 'taxonomy_' . $this->vocabulary->machine_name, + 'bundle' => 'article', + 'widget' => array( + 'type' => 'options_select', + ), + 'display' => array( + 'full' => array( + 'type' => 'taxonomy_term_link', + ), + ), + ); + field_create_instance($this->instance); } /** @@ -422,7 +367,7 @@ class TaxonomyTermTestCase extends Taxon // Edit $term2, setting $term1 as parent. $edit = array(); - $edit['parent[]'] = $term1->tid; + $edit['parent'] = $term1->tid; $this->drupalPost('taxonomy/term/' . $term2->tid . '/edit', $edit, t('Save')); // Check the hierarchy. @@ -454,7 +399,7 @@ class TaxonomyTermTestCase extends Taxon $edit['title'] = $this->randomName(); $langcode = FIELD_LANGUAGE_NONE; $edit["body[$langcode][0][value]"] = $this->randomName(); - $edit['taxonomy[' . $this->vocabulary->vid . ']'] = $term1->tid; + $edit[$this->instance['field_name'] . '[' . $langcode .'][value][]'] = $term1->tid; $this->drupalPost('node/add/article', $edit, t('Save')); // Check that the term is displayed when the node is viewed. @@ -463,19 +408,11 @@ class TaxonomyTermTestCase extends Taxon $this->assertText($term1->name, t('Term is displayed when viewing the node.')); // Edit the node with a different term. - $edit['taxonomy[' . $this->vocabulary->vid . ']'] = $term2->tid; + $edit[$this->instance['field_name'] . '[' . $langcode . '][value][]'] = $term2->tid; $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); $this->drupalGet('node/' . $node->nid); $this->assertText($term2->name, t('Term is displayed when viewing the node.')); - - // Delete node through browser. - $this->drupalPost('node/' . $node->nid . '/delete', array(), t('Delete')); - $this->drupalGet('node/' . $node->nid); - $this->assertNoText($term2->name, t('Checking if node exists')); - // Checking database fields. - $result = db_query('SELECT * FROM {taxonomy_term_node} WHERE nid = :nid', array(':nid' => $node->nid))->fetch(); - $this->assertTrue(empty($result), t('Term/node relationships are no longer in the database table.')); } /** @@ -483,22 +420,25 @@ class TaxonomyTermTestCase extends Taxon */ function testNodeTermCreation() { // Enable tags in the vocabulary. - $this->vocabulary->tags = 1; - taxonomy_vocabulary_save($this->vocabulary); + $instance = $this->instance; + $instance['widget'] = array('type' => 'taxonomy_autocomplete'); + $instance['bundle'] = 'page'; + field_create_instance($instance); $terms = array( $this->randomName(), $this->randomName(), $this->randomName(), ); + $edit = array(); $edit['title'] = $this->randomName(); - // Insert the terms in a comma separated list. Vocabulary 1 is a - // free-tagging field created by the default profile. - $edit['taxonomy[tags][' . $this->vocabulary->vid . ']'] = implode(', ', $terms); $langcode = FIELD_LANGUAGE_NONE; $edit["body[$langcode][0][value]"] = $this->randomName(); - $this->drupalPost('node/add/article', $edit, t('Save')); - $this->assertRaw(t('@type %title has been created.', array('@type' => t('Article'), '%title' => $edit['title'])), t('The node was created successfully')); + // Insert the terms in a comma separated list. Vocabulary 1 is a + // free-tagging field created by the default profile. + $edit[$instance['field_name'] . "[$langcode][value]"] = implode(', ', $terms); + $this->drupalPost('node/add/page', $edit, t('Save')); + $this->assertRaw(t('@type %title has been created.', array('@type' => t('Page'), '%title' => $edit['title'])), t('The node was created successfully')); foreach ($terms as $term) { $this->assertText($term, t('The term was saved and appears on the node page')); } @@ -514,7 +454,7 @@ class TaxonomyTermTestCase extends Taxon ); // Explicitly set the parents field to 'root', to ensure that // taxonomy_form_term_submit() handles the invalid term ID correctly. - $edit['parent[]'] = 0; + $edit['parent'] = 0; // Create the term to edit. $this->drupalPost('admin/structure/taxonomy/' . $this->vocabulary->vid . '/list/add', $edit, t('Save')); Index: profiles/default/default.install =================================================================== RCS file: /cvs/drupal/drupal/profiles/default/default.install,v retrieving revision 1.8 diff -u -p -r1.8 default.install --- profiles/default/default.install 1 Oct 2009 13:22:43 -0000 1.8 +++ profiles/default/default.install 6 Oct 2009 16:40:39 -0000 @@ -175,20 +175,26 @@ function default_install() { variable_set('theme_settings', $theme_settings); // Create a default vocabulary named "Tags", enabled for the 'article' content type. - $vocabulary = new stdClass; - $vocabulary->name = 'Tags'; - $vocabulary->description = st('Use tags to group articles on similar topics into categories.'); - $vocabulary->machine_name = 'tags'; - $vocabulary->help = st('Enter a comma-separated list of words to describe your content.'); - $vocabulary->relations = 0; - $vocabulary->hierarchy = 0; - $vocabulary->multiple = 0; - $vocabulary->required = 0; - $vocabulary->tags = 1; - $vocabulary->module = 'taxonomy'; - $vocabulary->weight = 0; - $vocabulary->nodes = array('article' => 'article'); + $description = st('Use tags to group articles on similar topics into categories.'); + $help = st('Enter a comma-separated list of words to describe your content.'); + $vocabulary = (object) array( + 'name' => 'Tags', + 'description' => $description, + 'machine_name' => 'tags', + 'help' => $help, + 'weight' => 0, + ); taxonomy_vocabulary_save($vocabulary); + $instance = array( + 'field_name' => 'taxonomy_' . $vocabulary->machine_name, + 'label' => $vocabulary->name, + 'bundle' => 'article', + 'description' => $vocabulary->help, + 'widget' => array( + 'type' => 'taxonomy_autocomplete', + ), + ); + field_create_instance($instance); // Enable default permissions for system roles. user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access content', 'use text format 1'));