All too often popular sites get flooded with useless comments which would be nice to hide by default from common visitors.
There are several comment moderation methods out there, but after careful examination it becomes apparent that Slashdot comment moderation system is superior to all: http://slashdot.org/moderation.shtml
I propose a module that implements Slashdot comment display/moderation system for Drupal.
Below is preliminary code for the module.
It does the following:
1. Transforms comment control toolbar (adds threshold field, removes date sorting, simplifies display modes)
2. Overrides default comment_render function from core because comment_render is neither a hook or a themeable function.
3. Adds moderation form to every comment. Moderation form is an exact copy of Slashdot moderation form.
What is lacking:
1. AJAX moderation of comments.
2. Displaying collapsed/expanded individual comments depending on the comment's rating and the user's threshold.
Todo:
1. Hook up with Karma.
Delf.
File slashcomments.info
; $Id: $
name = Slashcomments
description = Upgrades default comment system to Slashdot comment system
dependencies[] = comment
dependencies[] = votingapi
core = 6.x
File slashcomments.module
<?php
// $Id$
/*
Module to enable Slashdot comment system
*/
/**
* Define new comments display modes
*/
define('COMMENT_MODE_FLAT', 1);
define('COMMENT_MODE_THREADED', 2);
/**
* Implemenatation of hook_init, overrides core function comment_render
*/
function slashcomments_init() {
if (function_exists('comment_render')) {
drupal_set_message(t('Please comment out function comment_render in comment module: ') . drupal_get_path('module', 'comment') . '/comment.module');
}
else {
function comment_render($node, $cid = 0) {
global $user;
$output = '';
if (user_access('access comments')) {
// Pre-process variables.
$nid = $node->nid;
if (empty($nid)) {
$nid = 0;
}
$mode = _comment_get_display_setting('mode', $node);
$order = _comment_get_display_setting('sort', $node);
$comments_per_page = _comment_get_display_setting('comments_per_page', $node);
if ($cid && is_numeric($cid)) {
// Single comment view.
$query = 'SELECT c.cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, c.homepage, u.uid, u.name AS registered_name, u.signature, u.picture, u.data, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d';
$query_args = array($cid);
if (!user_access('administer comments')) {
$query .= ' AND c.status = %d';
$query_args[] = COMMENT_PUBLISHED;
}
$query = db_rewrite_sql($query, 'c', 'cid');
$result = db_query($query, $query_args);
if ($comment = db_fetch_object($result)) {
$comment->name = $comment->uid ? $comment->registered_name : $comment->name;
$links = module_invoke_all('link', 'comment', $comment, 1);
drupal_alter('link', $links, $node);
$output .= theme('comment_view', $comment, $node, $links);
}
}
else {
// Multiple comment view
$query_count = 'SELECT COUNT(*) FROM {comments} WHERE nid = %d';
$query = 'SELECT c.cid as cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, c.homepage, u.uid, u.name AS registered_name, u.signature, u.picture, u.data, c.thread, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.nid = %d';
$query_args = array($nid);
if (!user_access('administer comments')) {
$query .= ' AND c.status = %d';
$query_count .= ' AND status = %d';
$query_args[] = COMMENT_PUBLISHED;
}
if ($mode == COMMENT_MODE_FLAT) {
$query .= ' ORDER BY c.cid';
}
else {
// See comment above. Analysis reveals that this doesn't cost too
// much. It scales much much better than having the whole comment
// structure.
$query .= ' ORDER BY SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))';
}
$query = db_rewrite_sql($query, 'c', 'cid');
$query_count = db_rewrite_sql($query_count, 'c', 'cid');
// Start a form, for use with comment control.
$result = pager_query($query, $comments_per_page, 0, $query_count, $query_args);
$divs = 0;
$num_rows = FALSE;
$comments = '';
drupal_add_css(drupal_get_path('module', 'comment') .'/comment.css');
while ($comment = db_fetch_object($result)) {
$comment = drupal_unpack($comment);
$comment->name = $comment->uid ? $comment->registered_name : $comment->name;
$comment->depth = count(explode('.', $comment->thread)) - 1;
if ($mode == COMMENT_MODE_THREADED) {
if ($comment->depth > $divs) {
$divs++;
$comments .= '<div class="indented">';
}
else {
while ($comment->depth < $divs) {
$divs--;
$comments .= '</div>';
}
}
}
if ($mode == COMMENT_MODE_FLAT) {
$comments .= theme('comment_flat_expanded', $comment, $node);
}
else if ($mode == COMMENT_MODE_THREADED) {
$comments .= theme('comment_thread_expanded', $comment, $node);
}
$num_rows = TRUE;
}
while ($divs-- > 0) {
$comments .= '</div>';
}
$comment_controls = variable_get('comment_controls_'. $node->type, COMMENT_CONTROLS_HIDDEN);
if ($num_rows && ($comment_controls == COMMENT_CONTROLS_ABOVE || $comment_controls == COMMENT_CONTROLS_ABOVE_BELOW)) {
$output .= drupal_get_form('comment_controls', $mode, $order, $comments_per_page);
}
$output .= $comments;
$output .= theme('pager', NULL, $comments_per_page, 0);
if ($num_rows && ($comment_controls == COMMENT_CONTROLS_BELOW || $comment_controls == COMMENT_CONTROLS_ABOVE_BELOW)) {
$output .= drupal_get_form('comment_controls', $mode, $order, $comments_per_page);
}
}
// If enabled, show new comment form if it's not already being displayed.
$reply = arg(0) == 'comment' && arg(1) == 'reply';
if (user_access('post comments') && node_comment_mode($nid) == COMMENT_NODE_READ_WRITE && (variable_get('comment_form_location_'. $node->type, COMMENT_FORM_SEPARATE_PAGE) == COMMENT_FORM_BELOW) && !$reply) {
$output .= comment_form_box(array('nid' => $nid), t('Post new comment'));
}
$output = theme('comment_wrapper', $output, $node);
}
return $output;
}
}
}
/**
* Implementation of hook_perm()
*/
function slashcomments_perm() {
return array('moderate comments');
}
/**
* Implementation of hook_comment()
*/
function slashcomments_comment($a1, $op) {
switch ($op) {
case 'view':
global $user;
$a1->subject = $a1->subject . ' (' . slashcomments_get_comment_rating($a1->cid) . ')';
if (slashcomments_user_can_vote($user->uid, $a1->cid)) {
$a1->comment = $a1->comment . drupal_get_form('slashcomments_moderation_form', $a1->cid);
}
break;
}
}
/**
* Add threshold to comments control form
*/
function slashcomments_form_alter(&$form, $form_state, $form_id) {
if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
$form['comment']['comment_default_threshold'] = array(
'#type' => 'select',
'#title' => t('Default comments threshold'),
'#default_value' => variable_get('comment_default_threshold_'. $form['#node_type']->type, 3),
'#options' => _slashcomments_thresholds(),
'#description' => t('Default threshold for comments. Comments with lower score will be collapsed.'),
'#weight' => -5,
);
$form['comment']['comment_default_mode'] = array(
'#type' => 'select',
'#title' => t('Default display mode'),
'#default_value' => variable_get('comment_default_mode_'. $form['#node_type']->type, COMMENT_MODE_FLAT),
'#options' => _slashcomments_modes(),
'#description' => t('The default view for comments.'),
'#weight' => -4,
);
$form['comment']['comment_default_per_page']['#weight'] = -3;
//order is always oldest first
unset($form['comment']['comment_default_order']);
}
if ($form_id == 'comment_controls') {
// WARNING: we cannot get node type from form info, so, we have to load from arg(1)
$node = node_load(arg(1));
$form['threshold'] = array(
'#type' => 'select',
'#options' => _slashcomments_thresholds(),
'#default_value' => _slashcomments_get_display_setting('threshold', $node),
'#weight' => 0,
);
$form['mode'] = array(
'#type' => 'select',
'#default_value' => _slashcomments_get_display_setting('mode', $node),
'#options' => _slashcomments_modes(),
'#weight' => 1,
);
//order is always oldest first
$form['order'] = array(
'#type' => 'hidden',
'#value' => COMMENT_ORDER_OLDEST_FIRST,
);
$form['comments_per_page'] = array(
'#type' => 'select',
'#default_value' => _slashcomments_get_display_setting('comments_per_page', $node),
'#options' => _slashcomments_per_page(),
'#weight' => 3,
);
$form['#submit'][] = 'slashcomments_comment_control_submit';
}
}
function slashcomments_comment_control_submit($form, &$form_state) {
global $user;
$threshold = $form_state['values']['threshold'];
$mode = $form_state['values']['mode'];
$order = $form_state['values']['order'];
$comments_per_page = $form_state['values']['comments_per_page'];
if ($user->uid) {
$account = user_save($user, array('threshold' => $threshold, 'mode' => $mode, 'sort' => $order, 'comments_per_page' => $comments_per_page));
// Terminate if an error occured during user_save().
if (!$account) {
drupal_set_message(t("Error saving user account."), 'error');
return;
}
$user = $account;
}
$_SESSION['comment_threshold'] = $threshold;
$_SESSION['comment_mode'] = $mode;
$_SESSION['comment_sort'] = $order;
$_SESSION['comment_comments_per_page'] = $comments_per_page;
}
function _slashcomments_get_display_setting($setting, $node) {
global $user;
if (isset($_GET[$setting])) {
$value = $_GET[$setting];
}
else {
// get the setting's site default
switch ($setting) {
case 'threshold':
$default = variable_get('comment_default_threshold_'. $node->type, 3);
break;
case 'mode':
$default = variable_get('comment_default_mode_'. $node->type, COMMENT_MODE_THREADED_EXPANDED);
break;
case 'sort':
$default = variable_get('comment_default_order_'. $node->type, COMMENT_ORDER_NEWEST_FIRST);
break;
case 'comments_per_page':
$default = variable_get('comment_default_per_page_'. $node->type, 50);
}
if (variable_get('comment_controls_'. $node->type, COMMENT_CONTROLS_HIDDEN) == COMMENT_CONTROLS_HIDDEN) {
// if comment controls are disabled use site default
$value = $default;
}
else {
// otherwise use the user's setting if set
if (isset($user->$setting) && $user->$setting) {
$value = $user->$setting;
}
else if (isset($_SESSION['comment_'. $setting]) && $_SESSION['comment_'. $setting]) {
$value = $_SESSION['comment_'. $setting];
}
else {
$value = $default;
}
}
}
return $value;
}
function slashcomments_user_can_vote($uid, $cid) {
$can_vote = FALSE;
if (user_access('moderate comments')) {
if (!votingapi_select_votes(array('content_type' => 'comment', 'content_id' => $cid, 'uid' => $uid))) {
// if user is not participating in the discussion
$can_vote = !slashcomments_user_participating_in_discussion($uid, $cid);
}
}
return $can_vote;
}
function slashcomments_user_participating_in_discussion($uid, $cid){
return FALSE;
}
function slashcomments_moderation_form($form_state, $cid) {
$form['cid'] = array(
'#type' => 'value',
'#value' => $cid,
);
$form['vote'] = array(
'#title' => '',
'#type' => 'select',
'#options' => _slashcomments_moderation_options(),
'#default_value' => 0,
'#description' => t(''),
'#weight' => 30,
'#prefix' => '<div class="container-inline">'
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Moderate'),
'#weight' => 50,
'#suffix' => '</div>',
);
return $form;
}
function slashcomments_moderation_form_submit($form, &$form_state) {
$vote = array(
'content_type' => 'comment',
'content_id' => $form_state['values']['cid'],
'value_type' => 'option',
'value' => $form_state['values']['vote'],
);
votingapi_add_votes($vote);
votingapi_recalculate_results('comment', $form_state['values']['cid'], TRUE);
}
function slashcomments_get_comment_rating($cid) {
$results = votingapi_select_results(array('content_type'=>'comment', 'content_id'=>$cid));
$max = $results['0'];
$rating = 0;
foreach ($results as $vote) {
if ($vote['value'] > $max['value']) {
$max = $vote;
}
if( ($vote['function'] >= 1 && $vote['function'] <= 4) || $vote['function'] == 9){
$rating -= $vote['value'];
}
if( ($vote['function'] >= 5 && $vote['function'] <= 8) || $vote['function'] == 10){
$rating += $vote['value'];
}
}
$descriptions = _slashcomments_moderation_options();
$rating = t('Score:') . $rating;
if ($descriptions[$max['function']]) {
$rating .= ', ' . $descriptions[$max['function']];
}
return $rating;
}
function _slashcomments_moderation_options() {
return array(
0 => t('Normal'),
1 => t('Offtopic'),
2 => t('Flamebait'),
3 => t('Troll'),
4 => t('Redundant'),
5 => t('Insightful'),
6 => t('Interesting'),
7 => t('Informative'),
8 => t('Funny'),
9 => t('Overrated'),
10 => t('Underrated'),
);
}
function _slashcomments_thresholds() {
$thresholds = array(-1, 0, 1, 2, 3, 4, 5);
$options = array();
foreach ($thresholds as $i) {
$options[$i] = t('Threshold: ' . $i);
}
return $options;
}
function _slashcomments_modes() {
return array(
COMMENT_MODE_FLAT => t('Display: flat'),
COMMENT_MODE_THREADED => t('Display: threaded'),
);
}
function _slashcomments_per_page() {
$per_page = array(10, 30, 50, 70, 90, 150, 200, 250, 300);
$options = array();
foreach ($per_page as $i) {
$options[$i] = t('!a comments per page', array('!a' => $i));
}
return $options;
}
Comments
Thanks for posting this
Thanks for posting this. I actually didn't know you'd posted this until I saw your followup at http://groups.drupal.org/node/12439
Will you be creating a project with downloadable releases? Do you have any plans for a Drupal 5 backport?
Large Robot
http://www.largerobot.com