Index: modules/tracker/tracker.install
===================================================================
RCS file: modules/tracker/tracker.install
diff -N modules/tracker/tracker.install
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/tracker/tracker.install	18 Nov 2008 20:05:11 -0000
@@ -0,0 +1,106 @@
+<?php
+// $Id$
+
+/**
+ * Implementation of hook_install().
+ */
+function tracker_install() {
+  // Create tables.
+  drupal_install_schema('tracker');
+}
+
+/**
+ * Implementation of hook_uninstall().
+ */
+function tracker_uninstall() {
+  // Remove tables
+  drupal_uninstall_schema('tracker');
+
+  variable_del('tracker_index_nid');
+  variable_del('tracker_batch_size');
+}
+
+/**
+ * Implementation of hook_enable().
+ */
+function tracker_enable() {
+  $max_nid = db_result(db_query('SELECT MAX(nid) FROM {node}'));
+  variable_set('tracker_index_nid', $max_nid);
+  drupal_set_message(t('Tracker will index from node %nid downward.', array('%nid' => $max_nid)));
+}
+
+/**
+ * Implementation of hook_schema().
+ */
+function tracker_schema() {
+  $schema['tracker_node'] = array(
+    'description' => 'Stores information about node.',
+    'fields' => array(
+      'nid' => array(
+        'description' => 'The nid of the node.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'published' => array(
+        'description' => 'Boolean indicating whether the node is published.',
+        'type' => 'int',
+        'not null' => FALSE,
+        'default' => 0,
+        'size' => 'tiny',
+      ),
+      'changed' => array(
+        'description' => 'The Unix timestamp when the node was most recently saved.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'indexes' => array(
+      'tracker' => array('published', 'changed'),
+    ),
+    'primary key' => array('nid'),
+  );
+
+  $schema['tracker_user'] = array(
+    'description' => 'Stores information about node per user.',
+    'fields' => array(
+      'nid' => array(
+        'description' => 'The id of the node.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'uid' => array(
+        'description' => 'The id of the user.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'published' => array(
+        'description' => 'Boolean indicating whether the node is published.',
+        'type' => 'int',
+        'not null' => FALSE,
+        'default' => 0,
+        'size' => 'tiny',
+      ),
+      'changed' => array(
+        'description' => 'The Unix timestamp when the node was most recently saved.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'indexes' => array(
+      'tracker' => array('uid', 'published', 'changed'),
+    ),
+    'primary key' => array('nid', 'uid'),
+  );
+
+  return $schema;
+}
Index: modules/tracker/tracker.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/tracker/tracker.module,v
retrieving revision 1.157
diff -u -p -r1.157 tracker.module
--- modules/tracker/tracker.module	6 May 2008 12:18:51 -0000	1.157
+++ modules/tracker/tracker.module	18 Nov 2008 20:05:24 -0000
@@ -43,7 +43,7 @@ function tracker_menu() {
 
   $items['user/%user/track'] = array(
     'title' => 'Track',
-    'page callback' => 'tracker_page',
+    'page callback' => 'tracker_track_user',
     'page arguments' => array(1, TRUE),
     'access callback' => '_tracker_user_access',
     'access arguments' => array(1),
@@ -53,10 +53,79 @@ function tracker_menu() {
     'title' => 'Track posts',
     'type' => MENU_DEFAULT_LOCAL_TASK,
   );
+
+  $items['admin/settings/tracker'] = array(
+    'title' => 'Tracker',
+    'description' => 'Settings for the tracker module',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('tracker_admin_settings'),
+    'access arguments' => array('administer tracker')
+  );
+
   return $items;
 }
 
 /**
+ * Implementation of hook_perm().
+ */
+function tracker_perm() {
+  return array(
+    'administer tracker' => array(
+      'title' => t('Administer tracker'),
+      'description' => t('Manage tracker administration settings.'),
+    ),
+  );
+}
+
+/**
+ * Implementation of hook_cron().
+ */
+function tracker_cron() {
+  $max_nid = variable_get('tracker_index_nid', 0);
+  $batch_size = variable_get('tracker_batch_size', 1000);
+  if ($max_nid > 0) {
+    $last_nid = FALSE;
+    $res = db_query_range('SELECT nid, uid, status FROM {node} WHERE nid <= %d ORDER BY nid DESC', $max_nid, 0, $batch_size);
+
+    $count = 0;
+
+    while ($row = db_fetch_object($res)) {
+      // Calculate the changed timestamp for this node.
+      $changed = _tracker_calculate_changed($row->nid);
+
+      // Remove existing data for this node.
+      db_query('DELETE FROM {tracker_node} WHERE nid = %d', $row->nid);
+      db_query('DELETE FROM {tracker_user} WHERE nid = %d', $row->nid);
+
+      // Insert the node-level data.
+      db_query('INSERT INTO {tracker_node} (nid, published, changed) VALUES (%d, %d, %d)', $row->nid, $row->status, $changed);
+
+      // Insert the user-level data for the node's author.
+      db_query('INSERT INTO {tracker_user} (nid, published, uid, changed) VALUES (%d, %d, %d, %d)', $row->nid, $row->status, $row->uid, $changed);
+
+      // Insert the user-level data for the commenters (except if a commenter is the node's author).
+      db_query('INSERT INTO {tracker_user} (nid, published, uid, changed) SELECT DISTINCT %d AS nid, %d AS published, uid, %d AS changed FROM {comments} WHERE nid = %d AND uid <> %d AND status = %d', $row->nid, $row->status, $changed, $row->nid, $row->uid, COMMENT_PUBLISHED);
+
+      // Note that we have indexed at least one node.
+      $last_nid = $row->nid;
+
+      ++$count;
+    }
+
+    if ($last_nid !== FALSE) {
+      // Prepare a starting point for the next run
+      variable_set('tracker_index_nid', $last_nid - 1);
+
+      watchdog('tracker', t('Indexed %count nodes for tracking.', array('%count' => $count)));
+    }
+    else {
+      // If all nodes have been indexed, set to zero to skip future cron runs
+      variable_set('tracker_index_nid', 0);
+    }
+  }
+}
+
+/**
  * Access callback for tracker/%user_uid_optional
  */
 function _tracker_myrecent_access($account) {
@@ -71,3 +140,178 @@ function _tracker_user_access($account) 
   return user_view_access($account) && user_access('access content');
 }
 
+/**
+ * Menu callback argument. Prints a listing of active nodes on the site.
+ */
+function tracker_admin_settings() {
+  $form = array();
+
+  $max_nid = variable_get('tracker_index_nid', 0);
+
+  if ($max_nid) {
+    $form['max_nid'] = array(
+      '#markup' => t('Max node ID for indexing on the next cron run: @max', array('@max' => $max_nid)),
+    );
+  }
+  else {
+    $form['max_nid'] = array(
+      '#markup' => t('Existing nodes have finished tracker indexing.'),
+    );
+  }
+
+  $form['tracker_batch_size'] = array(
+    '#title' => t('Batch size'),
+    '#description' => t('Number of nodes to index during each cron run.'),
+    '#type' => 'textfield',
+    '#size' => 6,
+    '#maxlength' => 7,
+    '#default_value' => variable_get('tracker_batch_size', 1000),
+  );
+
+  return system_settings_form($form);
+}
+
+/**
+ * Implementation of hook_form_alter().
+ */
+function tracker_form_alter(&$form, $form_state, $form_id) {
+  if ($form_id == 'node_admin_nodes') {
+    $form['#submit']['tracker2_batch_node_alter'] = array();
+  }
+}
+
+function tracker_batch_node_alter($form, &$form_state) {
+  $op = $form_state['values']['operation'];
+  foreach($form_state['values']['nodes'] as $nid => $selected) {
+    if ($selected) {
+      if ($op == 'publish') {
+        db_query('UPDATE {tracker_node} SET published = 1 WHERE nid = %d', $nid);
+        db_query('UPDATE {tracker_user} SET published = 1 WHERE nid = %d', $nid);
+      }
+      else if ($op == 'unpublish') {
+        db_query('UPDATE {tracker_node} SET published = 0 WHERE nid = %d', $nid);
+        db_query('UPDATE {tracker_user} SET published = 0 WHERE nid = %d', $nid);
+      }
+      else if ($op == 'delete') {
+        _tracker_remove($nid);
+      }
+    }
+  }
+}
+
+/**
+ * Implementation of hook_nodeapi_insert().
+ */
+function tracker_nodeapi_insert(&$node, $arg = 0) {
+  _tracker_add($node->nid, $node->uid, $node->changed);
+}
+
+/**
+ * Implementation of hook_nodeapi_update().
+ */
+function tracker_nodeapi_update(&$node, $arg = 0) {
+  _tracker_add($node->nid, $node->uid, $node->changed);
+}
+
+/**
+ * Implementation of hook_nodeapi_delete().
+ */
+function tracker_nodeapi_delete(&$node, $arg = 0) {
+  _tracker_remove($node->nid, $node->uid, $node->changed);
+}
+
+/**
+ * Implementation of hook_comment().
+ */
+function tracker_comment(&$a1, $op) {
+  $comment = (array) $a1;
+  if ($op == 'insert' || $op == 'update' || $op == 'publish') {
+    if ($comment['status'] == COMMENT_PUBLISHED) {
+      _tracker_add($comment['nid'], $comment['uid'], $comment['timestamp']);
+    }
+    else {
+      _tracker_remove($comment['nid'], $comment['uid'], $comment['timestamp']);
+    }
+  }
+  else if ($op == 'delete' || $op == 'unpublish') {
+    _tracker_remove($comment['nid'], $comment['uid'], $comment['timestamp']);
+  }
+}
+
+function _tracker_add($nid, $uid, $changed) {
+  $node = db_fetch_object(db_query('SELECT nid, status, uid, changed FROM {node} WHERE nid = %d', $nid));
+
+  // Adding a comment can only increase the changed timestamp, so our calculation here is easy.
+  $changed = max($node->changed, $changed);
+
+  // Update the node-level data
+  $exists = db_result(db_query('SELECT COUNT(*) FROM {tracker_node} WHERE nid = %d', $nid));
+  if ($exists) {
+    db_query('UPDATE {tracker_node} SET changed = %d, published = %d WHERE nid = %d', $changed, $node->status, $nid);
+  }
+  else {
+    db_query('INSERT INTO {tracker_node} (changed, published, nid) VALUES (%d, %d, %d)', $changed, $node->status, $nid);
+  }
+
+  // Create or update the user-level data
+  db_query('UPDATE {tracker_user} SET changed = %d, published = %d WHERE nid = %d', $changed, $node->status, $nid);
+  $exists = db_result(db_query('SELECT COUNT(*) FROM {tracker_user} WHERE nid = %d AND uid = %d', $nid, $uid));
+  if (!$exists) {
+    db_query('INSERT INTO {tracker_user} (changed, published, nid, uid) VALUES (%d, %d, %d, %d)', $changed, $node->status, $nid, $uid);
+  }
+}
+
+function _tracker_calculate_changed($nid) {
+  $changed = db_result(db_query('SELECT changed FROM {node} WHERE nid = %d', $nid));
+  $latest_comment = db_fetch_object(db_query_range('SELECT cid, timestamp FROM {comments} WHERE nid = %d AND status = %d ORDER BY timestamp DESC', $nid, COMMENT_PUBLISHED, 0, 1));
+  if ($latest_comment && $latest_comment->timestamp > $changed) {
+    $changed = $latest_comment->timestamp;
+  }
+  return $changed;
+}
+
+function _tracker_remove($nid, $uid = NULL, $changed = NULL) {
+  $node = db_fetch_object(db_query('SELECT nid, status, uid, changed FROM {node} WHERE nid = %d', $nid));
+
+  // The user only keeps his or her subscription if both of the following are true:
+  // (1) The node exists.
+  // (2) The user is either the node author or has commented on the node.
+  $keep_subscription = FALSE;
+
+  if ($node) {
+    // Self-authorship is one reason to keep the user's subscription.
+    $keep_subscription = ($node->uid == $uid);
+
+    // Comments are a second reason to keep the user's subscription.
+    if (!$keep_subscription) {
+      // Check if the user has commented at least once on the given nid
+      $keep_subscription = db_result(db_query_range('SELECT COUNT(*) FROM {comments} WHERE nid = %d AND uid = %d AND status = 0', $nid, $uid, 0, 1));
+    }
+
+    // If we haven't found a reason to keep the user's subscription, delete it.
+    if (!$keep_subscription) {
+      db_query('DELETE FROM {tracker_user} WHERE nid = %d AND uid = %d', $nid, $uid);
+    }
+
+    // Now we need to update the (possibly) changed timestamps for other users and the node itself.
+
+    // We only need to do this if the removed item has a timestamp that equals
+    // or exceeds the listed changed timestamp for the node
+    $tracker_node = db_fetch_object(db_query('SELECT nid, changed FROM {tracker_node} WHERE nid = %d', $nid));
+    if ($tracker_node && $changed >= $tracker_node->changed) {
+      // If we're here, the item being removed is *possibly* the item that established the node's changed timestamp.
+
+      // We just have to recalculate things from scratch.
+      $changed = _tracker_calculate_changed($nid);
+
+      // And then we push the out the new changed timestamp to our denormalized tables.
+      db_query('UPDATE {tracker_node} SET changed = %d, published = %d WHERE nid = %d', $changed, $node->status, $nid);
+      db_query('UPDATE {tracker_user} SET changed = %d, published = %d WHERE nid = %d', $changed, $node->status, $nid);
+    }
+  }
+  else {
+    // If the node doesn't exist, remove everything
+    db_query('DELETE FROM {tracker_node} WHERE nid = %d', $nid);
+    db_query('DELETE FROM {tracker_user} WHERE nid = %d', $nid);
+  }
+}
Index: modules/tracker/tracker.pages.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/tracker/tracker.pages.inc,v
retrieving revision 1.12
diff -u -p -r1.12 tracker.pages.inc
--- modules/tracker/tracker.pages.inc	26 Oct 2008 18:06:39 -0000	1.12
+++ modules/tracker/tracker.pages.inc	18 Nov 2008 20:05:24 -0000
@@ -3,62 +3,70 @@
 
 /**
  * @file
- * User page callbacks for the tracker module.
+ * Page callbacks for the tracker module.
  */
 
-
 /**
  * Menu callback. Prints a listing of active nodes on the site.
  */
-function tracker_page($account = NULL, $set_title = FALSE) {
-  // Add CSS
-  drupal_add_css(drupal_get_path('module', 'tracker') . '/tracker.css', array('preprocess' => FALSE));
-
-  if ($account) {
-    if ($set_title) {
-      // When viewed from user/%user/track, display the name of the user
-      // as page title -- the tab title remains Track so this needs to be done
-      // here and not in the menu definiton.
-      drupal_set_title($account->name);
-    }
-  // TODO: These queries are very expensive, see http://drupal.org/node/105639
-    $sql = 'SELECT DISTINCT(n.nid), n.title, n.type, n.changed, n.uid, u.name, GREATEST(n.changed, l.last_comment_timestamp) AS last_updated, l.comment_count FROM {node} n INNER JOIN {node_comment_statistics} l ON n.nid = l.nid INNER JOIN {users} u ON n.uid = u.uid LEFT JOIN {comments} c ON n.nid = c.nid AND (c.status = %d OR c.status IS NULL) WHERE n.status = 1 AND (n.uid = %d OR c.uid = %d) ORDER BY last_updated DESC';
-    $sql = db_rewrite_sql($sql);
-    $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n LEFT JOIN {comments} c ON n.nid = c.nid AND (c.status = %d OR c.status IS NULL) WHERE n.status = 1 AND (n.uid = %d OR c.uid = %d)';
-    $sql_count = db_rewrite_sql($sql_count);
-    $result = pager_query($sql, 25, 0, $sql_count, COMMENT_PUBLISHED, $account->uid, $account->uid);
+function tracker_page($uid = 0) {
+  drupal_add_css(drupal_get_path('module', 'tracker') .'/tracker.css',  array('type' => 'module', 'preprocess' => FALSE));
+
+  if ($uid) {
+    $sql = 'SELECT tu.nid, tu.changed AS last_activity FROM {tracker_user} tu WHERE tu.published = 1 AND tu.uid = %d ORDER BY tu.changed DESC';
+    $sql = db_rewrite_sql($sql, 'tu');
+    $sql_count = 'SELECT COUNT(tu.nid) FROM {tracker_user} tu WHERE tu.published = 1 AND tu.uid = %d';
+    $sql_count = db_rewrite_sql($sql_count, 'tu');
+    $result = pager_query($sql, 25, 0, $sql_count, $uid);
   }
   else {
-    $sql = 'SELECT DISTINCT(n.nid), n.title, n.type, n.changed, n.uid, u.name, GREATEST(n.changed, l.last_comment_timestamp) AS last_updated, l.comment_count FROM {node} n INNER JOIN {users} u ON n.uid = u.uid INNER JOIN {node_comment_statistics} l ON n.nid = l.nid WHERE n.status = 1 ORDER BY last_updated DESC';
-    $sql = db_rewrite_sql($sql);
+    $sql = 'SELECT tn.nid, tn.changed AS last_activity FROM {tracker_node} tn WHERE tn.published = 1 ORDER BY tn.changed DESC';
+    $sql = db_rewrite_sql($sql, 'tn');
     $sql_count = 'SELECT COUNT(n.nid) FROM {node} n WHERE n.status = 1';
     $sql_count = db_rewrite_sql($sql_count);
     $result = pager_query($sql, 25, 0, $sql_count);
   }
 
-  $rows = array();
+  // This array acts as a placeholder for the data selected later
+  // while keeping the correct order.
+  $nodes = array();
   while ($node = db_fetch_object($result)) {
-    // Determine the number of comments:
-    $comments = 0;
-    if ($node->comment_count) {
-      $comments = $node->comment_count;
-
-      if ($new = comment_num_new($node->nid)) {
-        $comments .= '<br />';
-        $comments .= l(format_plural($new, '1 new', '@count new'), "node/$node->nid", array('query' => comment_new_page_count($node->comment_count, $new, $node), 'fragment' => 'new'));
+    $nodes[$node->nid] = $node;
+  }
+
+  if (!empty($nodes)) {
+    // Now, get the data and put into the placeholder array
+    $placeholders = implode(',', array_fill(0, count($nodes), '%d'));
+    $result = db_query("SELECT n.nid, n.title, n.type, n.changed, n.uid, u.name, l.comment_count FROM {node} n INNER JOIN {node_comment_statistics} l ON n.nid = l.nid INNER JOIN {users} u ON n.uid = u.uid WHERE n.nid IN ($placeholders)", array_keys($nodes));
+    while ($node = db_fetch_object($result)) {
+      $node->last_activity = $nodes[$node->nid]->last_activity;
+      $nodes[$node->nid] = $node;
+    }
+
+    // Finally display the data
+    $rows = array();
+    foreach ($nodes as $node) {
+      // Determine the number of comments:
+      $comments = 0;
+      if ($node->comment_count) {
+        $comments = $node->comment_count;
+
+        if ($new = comment_num_new($node->nid)) {
+          $comments .= '<br />';
+          $comments .= l(format_plural($new, '1 new', '@count new'), 'node/'. $node->nid, NULL, NULL, 'new');
+        }
       }
+
+      $rows[] = array(
+        check_plain(node_get_types('name', $node->type)),
+        l($node->title, "node/$node->nid") .' '. theme('mark', node_mark($node->nid, $node->changed)),
+        theme('username', $node),
+        array('class' => 'replies', 'data' => $comments),
+        t('!time ago', array('!time' => format_interval(time() - $node->last_activity)))
+      );
     }
-
-    $rows[] = array(
-      check_plain(node_get_types('name', $node->type)),
-      l($node->title, "node/$node->nid") . ' ' . theme('mark', node_mark($node->nid, $node->changed)),
-      theme('username', $node),
-      array('class' => 'replies', 'data' => $comments),
-      t('!time ago', array('!time' => format_interval(REQUEST_TIME - $node->last_updated)))
-    );
   }
-
-  if (!$rows) {
+  else {
     $rows[] = array(array('data' => t('No posts available.'), 'colspan' => '5'));
   }
 
@@ -71,3 +79,22 @@ function tracker_page($account = NULL, $
 
   return $output;
 }
+
+
+/**
+ * Menu callback. Prints a listing of active nodes on the site.
+ */
+function tracker_track_user() {
+  if ($account = user_load(array('uid' => arg(1)))) {
+    if ($account->status || user_access('administer users')) {
+      drupal_set_title(check_plain($account->name));
+      return tracker_page($account->uid);
+    }
+    else {
+      drupal_access_denied();
+    }
+  }
+  else {
+    drupal_not_found();
+  }
+}
