Newer
Older
Dries Buytaert
committed
* Tracks recent content posted by a user or users.
Angie Byron
committed
use Drupal\Core\Entity\EntityInterface;
Dries Buytaert
committed
* Implements hook_help().
Gábor Hojtsy
committed
function tracker_help($path, $arg) {
switch ($path) {
Angie Byron
committed
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Tracker module displays the most recently added and updated content on your site, and allows you to follow new content created by each user. This module has no configuration options. For more information, see the online handbook entry for <a href="@tracker">Tracker module</a>.', array('@tracker' => 'http://drupal.org/documentation/modules/tracker')) . '</p>';
Angie Byron
committed
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
Dries Buytaert
committed
$output .= '<dt>' . t('Tracker navigation') . '</dt>';
$output .= '<dd>' . t('The Tracker module adds a new menu item to the Tools menu, called <em>Recent content</em>. You can configure menu items via the <a href="@menus">Menus administration page</a>.', array('@menus' => url('admin/structure/menu'))) . '</dd>';
Angie Byron
committed
$output .= '<dt>' . t('Tracking new and updated site content') . '</dt>';
$output .= '<dd>' . t("The <a href='@recent'>Recent content</a> page shows new and updated content in reverse chronological order, listing the content type, title, author's name, number of comments, and time of last update. Content is considered updated when changes occur in the text, or when new comments are added. The <em>My recent content</em> tab limits the list to the currently logged-in user.", array('@recent' => url('tracker'))) . '</dd>';
$output .= '<dt>' . t('Tracking user-specific content') . '</dt>';
$output .= '<dd>' . t("To follow a specific user's new and updated content, select the <em>Track</em> tab from the user's profile page.") . '</dd>';
$output .= '</dl>';
Dries Buytaert
committed
return $output;
Dries Buytaert
committed
* Implements hook_menu().
Dries Buytaert
committed
function tracker_menu() {
$items['tracker'] = array(
'title' => 'Recent content',
Dries Buytaert
committed
'page callback' => 'tracker_page',
'access arguments' => array('access content'),
'weight' => 1,
'file' => 'tracker.pages.inc',
Dries Buytaert
committed
);
$items['tracker/all'] = array(
'title' => 'All recent content',
Dries Buytaert
committed
'type' => MENU_DEFAULT_LOCAL_TASK,
);
Dries Buytaert
committed
$items['tracker/%user_uid_optional'] = array(
'title' => 'My recent content',
Dries Buytaert
committed
'page callback' => 'tracker_page',
Dries Buytaert
committed
'access callback' => '_tracker_myrecent_access',
'access arguments' => array(1),
Gábor Hojtsy
committed
'page arguments' => array(1),
'type' => MENU_LOCAL_TASK,
Dries Buytaert
committed
'file' => 'tracker.pages.inc',
Dries Buytaert
committed
);
$items['user/%user/track'] = array(
'title' => 'Track',
Gábor Hojtsy
committed
'page callback' => 'tracker_page',
'page arguments' => array(1, TRUE),
Dries Buytaert
committed
'access callback' => '_tracker_user_access',
'access arguments' => array(1),
Dries Buytaert
committed
'type' => MENU_LOCAL_TASK,
'file' => 'tracker.pages.inc',
Dries Buytaert
committed
);
$items['user/%user/track/content'] = array(
'title' => 'Track content',
Dries Buytaert
committed
'type' => MENU_DEFAULT_LOCAL_TASK,
);
Dries Buytaert
committed
Dries Buytaert
committed
/**
Dries Buytaert
committed
* Implements hook_cron().
Dries Buytaert
committed
*
Angie Byron
committed
* Updates tracking information for any items still to be tracked. The state
* 'tracker.index_nid' is set to ((the last node ID that was indexed) - 1) and
Dries Buytaert
committed
* used to select the nodes to be processed. If there are no remaining nodes to
Angie Byron
committed
* process, 'tracker.index_nid' will be 0.
Dries Buytaert
committed
*/
function tracker_cron() {
$state = Drupal::state();
Angie Byron
committed
$max_nid = $state->get('tracker.index_nid') ?: 0;
Dries Buytaert
committed
if ($max_nid > 0) {
Angie Byron
committed
$batch_size = config('tracker.settings')->get('cron_index_limit');
Dries Buytaert
committed
$last_nid = FALSE;
// @todo This should be actually filtering on the desired language and just
// fall back to the default language.
$result = db_query_range('SELECT nid, uid, status FROM {node_field_data} WHERE nid <= :max_nid AND default_langcode = 1 ORDER BY nid DESC', 0, $batch_size, array(':max_nid' => $max_nid), array('target' => 'slave'));
Dries Buytaert
committed
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
$count = 0;
foreach ($result as $row) {
// Calculate the changed timestamp for this node.
$changed = _tracker_calculate_changed($row->nid);
// Remove existing data for this node.
db_delete('tracker_node')
->condition('nid', $row->nid)
->execute();
db_delete('tracker_user')
->condition('nid', $row->nid)
->execute();
// Insert the node-level data.
db_insert('tracker_node')
->fields(array(
'nid' => $row->nid,
'published' => $row->status,
'changed' => $changed,
))
->execute();
// Insert the user-level data for the node's author.
db_insert('tracker_user')
->fields(array(
'nid' => $row->nid,
'published' => $row->status,
'changed' => $changed,
'uid' => $row->uid,
))
->execute();
$query = db_select('comment', 'c', array('target' => 'slave'));
Dries Buytaert
committed
// Force PostgreSQL to do an implicit cast by adding 0.
$query->addExpression('0 + :changed', 'changed', array(':changed' => $changed));
$query->addField('c', 'status', 'published');
$query
->distinct()
->fields('c', array('uid', 'nid'))
->condition('c.nid', $row->nid)
->condition('c.uid', $row->uid, '<>')
->condition('c.status', COMMENT_PUBLISHED);
// Insert the user-level data for the commenters (except if a commenter
// is the node's author).
db_insert('tracker_user')
->from($query)
->execute();
// 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.
Angie Byron
committed
$state->set('tracker.index_nid', $last_nid - 1);
Dries Buytaert
committed
Dries Buytaert
committed
watchdog('tracker', 'Indexed %count content items for tracking.', array('%count' => $count));
Dries Buytaert
committed
}
else {
// If all nodes have been indexed, set to zero to skip future cron runs.
Angie Byron
committed
$state->set('tracker.index_nid', 0);
Dries Buytaert
committed
}
}
}
/**
Dries Buytaert
committed
* Access callback: Determines access permission for a user's own account.
*
* @param int $account
* The account ID to check.
*
* @return boolean
* TRUE if a user is accessing tracking info for their own account and
* has permission to access the content.
*
* @see tracker_menu()
Dries Buytaert
committed
*/
function _tracker_myrecent_access($account) {
// This path is only allowed for authenticated users looking at their own content.
Dries Buytaert
committed
return $account->id() && ($GLOBALS['user']->id() == $account->id()) && user_access('access content');
Dries Buytaert
committed
}
/**
Dries Buytaert
committed
* Access callback: Determines access permission for an account.
*
* @param int $account
* The user account ID to track.
*
* @return boolean
* TRUE if a user has permission to access the account for $account and
* has permission to access the content.
*
* @see tracker_menu()
Dries Buytaert
committed
*/
function _tracker_user_access($account) {
return $account->access('view') && user_access('access content');
Dries Buytaert
committed
}
Dries Buytaert
committed
/**
* Implements hook_node_insert().
Dries Buytaert
committed
*
* Adds new tracking information for this node since it's new.
Dries Buytaert
committed
*/
Angie Byron
committed
function tracker_node_insert(EntityInterface $node, $arg = 0) {
_tracker_add($node->id(), $node->uid, $node->changed);
Dries Buytaert
committed
}
/**
* Implements hook_node_update().
Dries Buytaert
committed
*
* Adds tracking information for this node since it's been updated.
Dries Buytaert
committed
*/
Angie Byron
committed
function tracker_node_update(EntityInterface $node, $arg = 0) {
_tracker_add($node->id(), $node->uid, $node->changed);
Dries Buytaert
committed
}
/**
catch
committed
* Implements hook_node_predelete().
Dries Buytaert
committed
*
* Deletes tracking information for a node.
Dries Buytaert
committed
*/
Angie Byron
committed
function tracker_node_predelete(EntityInterface $node, $arg = 0) {
Angie Byron
committed
db_delete('tracker_node')
->condition('nid', $node->id())
Angie Byron
committed
->execute();
db_delete('tracker_user')
->condition('nid', $node->id())
Angie Byron
committed
->execute();
Dries Buytaert
committed
}
/**
Dries Buytaert
committed
* Implements hook_comment_update().
Dries Buytaert
committed
*
* Comment module doesn't call hook_comment_unpublish() when saving individual
* comments so we need to check for those here.
*/
function tracker_comment_update($comment) {
// $comment->save() calls hook_comment_publish() for all published comments
Dries Buytaert
committed
// so we need to handle all other values here.
if ($comment->status->value != COMMENT_PUBLISHED) {
Dries Buytaert
committed
_tracker_remove($comment->nid->target_id, $comment->uid->target_id, $comment->changed->value);
Dries Buytaert
committed
}
}
/**
Dries Buytaert
committed
* Implements hook_comment_publish().
Dries Buytaert
committed
*
* This actually handles the insert and update of published nodes since
* $comment->save() calls hook_comment_publish() for all published comments.
Dries Buytaert
committed
*/
function tracker_comment_publish($comment) {
Dries Buytaert
committed
_tracker_add($comment->nid->target_id, $comment->uid->target_id, $comment->changed->value);
Dries Buytaert
committed
}
/**
Dries Buytaert
committed
* Implements hook_comment_unpublish().
Dries Buytaert
committed
*/
function tracker_comment_unpublish($comment) {
Dries Buytaert
committed
_tracker_remove($comment->nid->target_id, $comment->uid->target_id, $comment->changed->value);
Dries Buytaert
committed
}
/**
Dries Buytaert
committed
* Implements hook_comment_delete().
Dries Buytaert
committed
*/
function tracker_comment_delete($comment) {
Dries Buytaert
committed
_tracker_remove($comment->nid->target_id, $comment->uid->target_id, $comment->changed->value);
Dries Buytaert
committed
}
/**
Dries Buytaert
committed
* Updates indexing tables when a node is added, updated, or commented on.
Dries Buytaert
committed
*
* @param $nid
* A node ID.
* @param $uid
* The node or comment author.
* @param $changed
* The node updated timestamp or comment timestamp.
*/
function _tracker_add($nid, $uid, $changed) {
// @todo This should be actually filtering on the desired language and just
// fall back to the default language.
$node = db_query('SELECT nid, status, uid, changed FROM {node_field_data} WHERE nid = :nid AND default_langcode = 1 ORDER BY changed DESC, status DESC', array(':nid' => $nid))->fetchObject();
Dries Buytaert
committed
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
// Adding a comment can only increase the changed timestamp, so our
// calculation here is simple.
$changed = max($node->changed, $changed);
// Update the node-level data.
db_merge('tracker_node')
->key(array('nid' => $nid))
->fields(array(
'changed' => $changed,
'published' => $node->status,
))
->execute();
// Create or update the user-level data.
db_merge('tracker_user')
->key(array(
'nid' => $nid,
'uid' => $uid,
))
->fields(array(
'changed' => $changed,
'published' => $node->status,
))
->execute();
}
/**
Dries Buytaert
committed
* Determines the max timestamp between $node->changed and the last comment.
Dries Buytaert
committed
*
* @param $nid
* A node ID.
*
* @return
* The $node->changed timestamp, or most recent comment timestamp, whichever
* is the greatest.
*/
function _tracker_calculate_changed($nid) {
// @todo This should be actually filtering on the desired language and just
// fall back to the default language.
$changed = db_query('SELECT changed FROM {node_field_data} WHERE nid = :nid AND default_langcode = 1 ORDER BY changed DESC', array(':nid' => $nid), array('target' => 'slave'))->fetchField();
Dries Buytaert
committed
$latest_comment = db_query_range('SELECT cid, changed FROM {comment} WHERE nid = :nid AND status = :status ORDER BY changed DESC', 0, 1, array(
Dries Buytaert
committed
':nid' => $nid,
':status' => COMMENT_PUBLISHED,
), array('target' => 'slave'))->fetchObject();
Dries Buytaert
committed
if ($latest_comment && $latest_comment->changed > $changed) {
$changed = $latest_comment->changed;
Dries Buytaert
committed
}
return $changed;
}
/**
Dries Buytaert
committed
* Cleans up indexed data when nodes or comments are removed.
Dries Buytaert
committed
*
* @param $nid
* The node ID.
* @param $uid
* The author of the node or comment.
* @param $changed
* The last changed timestamp of the node.
*/
function _tracker_remove($nid, $uid = NULL, $changed = NULL) {
// @todo This should be actually filtering on the desired language and just
// fall back to the default language.
$node = db_query('SELECT nid, status, uid, changed FROM {node_field_data} WHERE nid = :nid AND default_langcode = 1 ORDER BY changed DESC, status DESC', array(':nid' => $nid))->fetchObject();
Dries Buytaert
committed
// The user only keeps his or her subscription if both of the following are true:
Dries Buytaert
committed
// (1) The node exists.
// (2) The user is either the node author or has commented on the node.
Dries Buytaert
committed
$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) {
Dries Buytaert
committed
// Check if the user has commented at least once on the given nid.
$keep_subscription = db_query_range('SELECT COUNT(*) FROM {comment} WHERE nid = :nid AND uid = :uid AND status = :status', 0, 1, array(
Dries Buytaert
committed
':nid' => $nid,
':uid' => $uid,
':status' => COMMENT_PUBLISHED,
Angie Byron
committed
))->fetchField();
Dries Buytaert
committed
}
// If we haven't found a reason to keep the user's subscription, delete it.
if (!$keep_subscription) {
db_delete('tracker_user')
->condition('nid', $nid)
->condition('uid', $uid)
->execute();
}
// 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
Dries Buytaert
committed
// or exceeds the listed changed timestamp for the node.
Dries Buytaert
committed
$tracker_node = db_query('SELECT nid, changed FROM {tracker_node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject();
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_update('tracker_node')
->fields(array(
'changed' => $changed,
'published' => $node->status,
))
->condition('nid', $nid)
->execute();
db_update('tracker_node')
->fields(array(
'changed' => $changed,
'published' => $node->status,
))
->condition('nid', $nid)
->execute();
Dries Buytaert
committed
}
Dries Buytaert
committed
}
else {
// If the node doesn't exist, remove everything.
db_delete('tracker_node')
->condition('nid', $nid)
->execute();
db_delete('tracker_user')
->condition('nid', $nid)
->execute();
}
}