You are here

votingapi.module in Voting API 6

File

votingapi.module
View source
<?php

/**
 * VotingAPI 2.0
 *
 * A generalized voting API for Drupal. Modules can cast votes with
 * arbitrary properties and VotingAPI will total them automatically.
 * Support for basic anonymous voting by IP address, multi-criteria
 * voting, arbitrary aggregation functions, etc.
 */

/**
 * Implementation of hook_menu. Adds the url path for the votingapi
 * settings page.
 */
function votingapi_menu() {
  $items = array();
  $items['admin/settings/votingapi'] = array(
    'title' => 'Voting API',
    'description' => 'Global settings for the Voting API.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'votingapi_settings_form',
    ),
    'access callback' => 'user_access',
    'access arguments' => array(
      'administer voting api',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  return $items;
}

/**
 * Implementation of hook_init. Loads the Views integration code.
 */
function votingapi_init() {
  if (module_exists('views')) {
    require_once drupal_get_path('module', 'votingapi') . '/votingapi_views.inc';
  }
}

/**
 * Implementation of hook_perm
 */
function votingapi_perm() {
  return array(
    'administer voting api',
  );
}

/**
 * Implementation of hook_cron. Allows db-intensive recalculations
 * to put off until cron-time.
 */
function votingapi_cron() {
  if (variable_get('votingapi_calculation_schedule', 'immediate') == 'cron') {
    $time = time();
    $last_cron = variable_get('votingapi_last_cron', 0);
    $result = db_query('SELECT DISTINCT content_type, content_id FROM {votingapi_vote} WHERE timestamp > %d', $last_cron);
    while ($content = db_fetch_object($result)) {
      votingapi_recalculate_results($content->content_type, $content->content_id, TRUE);
    }
    variable_set('votingapi_last_cron', $time);
  }
}
function votingapi_settings_form($form_state) {
  $period = array(
    0 => t('Never'),
  );
  $period += drupal_map_assoc(array(
    600,
    3600,
    10800,
    21600,
    32400,
    43200,
    86400,
  ), 'format_interval');
  $form['votingapi_anonymous_window'] = array(
    '#type' => 'select',
    '#title' => t('Anonymous vote rollover'),
    '#description' => t('The amount of time that must pass before two anonymous votes from the same computer are considered unique. Setting this to \'never\' will eliminate most double-voting, but will make it impossible for multiple anonymous on the same computer (like internet cafe customers) from casting votes.'),
    '#default_value' => variable_get('votingapi_anonymous_window', 3600),
    '#options' => $period,
  );
  $form['votingapi_calculation_schedule'] = array(
    '#type' => 'radios',
    '#title' => t('Vote tallying'),
    '#description' => t('On high-traffic sites, administrators can use this setting to postpone the calculation of vote results.'),
    '#default_value' => variable_get('votingapi_calculation_schedule', 'immediate'),
    '#options' => array(
      'immediate' => t('Tally results whenever a vote is cast'),
      'cron' => t('Tally results at cron-time'),
      'manual' => t('Do not tally results automatically: I am using a module that manages its own vote results.'),
    ),
  );
  return system_settings_form($form);
}

/**
 * Cast a vote on a particular piece of content. If a vote already
 * exists, its value is changed. In most cases, this is the function
 * that should be used by external modules.
 *
 * @param $votes
 *   A single vote object, or an array of vote objects, with the following
 *   properties:
 *   $vote->content_type  (Optional, defaults to 'node')
 *   $vote->content_id    (Required)
 *   $vote->value_type    (Optional, defaults to 'percent')
 *   $vote->value         (Required)
 *   $vote->tag           (Optional, defaults to 'vote')
 *   $vote->uid           (Optional, defaults to current user)
 *   $vote->vote_source   (Optional, defaults to current IP)
 *   $vote->timestamp     (Optional, defaults to time())
 * @param $criteria
 *   A keyed array used to determine what votes will be deleted
 *   when the current vote is cast. If no value is specified, all
 *   votes for the current content by the current user will be reset.
 *   If an empty array is passed in, no votes will be reset and all
 *   incoming votes will be saved IN ADDITION to existing ones.
 *   $criteria['vote_id']     (If this is set, all other keyes are skipped)
 *   $criteria['content_type']
 *   $criteria['content_type']
 *   $criteria['value_type']
 *   $criteria['tag']
 *   $criteria['uid']
 *   $criteria['vote_source']
 *   $criteria['timestamp']   (If this is set, records with timestamps
 *      GREATER THAN the set value will be selected.)
 * @return
 *   An array of vote result records affected by the vote. The values
 *   are contained in a nested array keyed thusly:
 *   $value = $results[$content_type][$content_id][$tag][$value_type][$function]
 *   If $silent is TRUE, this array will be empty.
 */
function votingapi_set_vote($votes = array(), $criteria = NULL) {
  if (is_object($votes)) {
    $votes = array(
      $votes,
    );
  }
  $touched = array();
  foreach ($votes as $key => $vote) {

    // First nuke existing votes by this user.
    $vote = _votingapi_prep_vote($vote);
    if (!isset($criteria)) {
      $criteria = votingapi_current_user_identifier();
      $criteria += (array) $vote;
    }
    if (!empty($criteria)) {
      votingapi_delete_votes(votingapi_select_votes($criteria));
    }
    $votes[$key] = votingapi_add_votes($vote);
    $touched[$vote->content_type][$vote->content_id] = TRUE;
  }
  if (variable_get('votingapi_calculation_schedule', 'immediate') != 'cron') {
    foreach ($touched as $type => $ids) {
      foreach ($ids as $id => $bool) {
        $touched[$type][$id] = votingapi_recalculate_results($type, $id);
      }
    }
  }
  return $touched;
}

/**
 * Generate a proper identifier for the current user: if they have
 * an account, return their UID. Otherwise, return their IP address.
 */
function votingapi_current_user_identifier() {
  global $user;
  $criteria = array();
  if ($user->uid) {
    $criteria['uid'] = $user->uid;
  }
  else {
    $criteria['vote_source'] = ip_address();
  }
  return $criteria;
}

/**
 * Save a single vote to the database.
 *
 * @param $vobj
 *   An array of vote objects with the following properties:
 *   $vote->content_type  (Optional, defaults to 'node')
 *   $vote->content_id    (Required)
 *   $vote->value_type    (Optional, defaults to 'percent')
 *   $vote->value         (Required)
 *   $vote->tag           (Optional, defaults to 'vote')
 *   $vote->uid           (Optional, defaults to current user)
 *   $vote->vote_source   (Optional, defaults to current IP)
 *   $vote->timestamp     (Optional, defaults to time())
 * @return
 *   The same vote objects, with vote_ids property populated.
 */
function votingapi_add_votes($votes = array()) {
  if (is_object($votes)) {
    $votes = array(
      $votes,
    );
  }
  foreach ($votes as $vobj) {
    $vobj = _votingapi_prep_vote($vobj);
    db_query("INSERT INTO {votingapi_vote} (content_type, content_id, value, value_type, tag, uid, timestamp, vote_source) VALUES ('%s', %d, %f, '%s', '%s', %d, %d, '%s')", $vobj->content_type, $vobj->content_id, $vobj->value, $vobj->value_type, $vobj->tag, $vobj->uid, $vobj->timestamp, $vobj->vote_source);
    $vobj->vote_id = db_last_insert_id('votingapi_vote', 'vote_id');
  }
  module_invoke_all('votingapi_insert', $votes);
  return $votes;
}

/**
 * Save a single vote result to the database.
 *
 * @param robj
 *   An array of vote result objects with the following properties:
 *   $robj->content_type
 *   $robj->content_id
 *   $robj->value_type
 *   $robj->value
 *   $robj->tag
 *   $robj->function
 *   $robj->timestamp   (Optional, defaults to time())
 */
function votingapi_add_results($vote_results = array()) {
  if (is_object($vote_results)) {
    $vote_results = array(
      $vote_results,
    );
  }
  foreach ($vote_results as $robj) {
    $robj->timestamp = time();
    db_query("INSERT INTO {votingapi_cache} (content_type, content_id, value, value_type, tag, function, timestamp) VALUES ('%s', %d, %f, '%s', '%s', '%s', %d)", $robj->content_type, $robj->content_id, $robj->value, $robj->value_type, $robj->tag, $robj->function, $robj->timestamp);
  }
}

/**
 * Delete votes from the database.
 *
 * @param $votes
 *   An array of vote objects to delete. Minimally, each object must
 *   have the vote_id property set.
 */
function votingapi_delete_votes($votes = array()) {
  if (!empty($votes)) {
    module_invoke_all('votingapi_delete', $votes);
    $vids = array();
    foreach ($votes as $vote) {
      $vids[] = $vote->vote_id;
    }
    db_query("DELETE FROM {votingapi_vote} WHERE vote_id IN (" . implode(',', array_fill(0, count($vids), '%d')) . ")", $vids);
  }
}

/**
 * Delete cached vote results from the database.
 *
 * @param $vote_results
 *   An array of vote result objects to delete. Minimally, each object
 *   must have the vote_cache_id property set.
 */
function votingapi_delete_results($vote_results = array()) {
  if (!empty($vote_results)) {
    $vids = array();
    foreach ($vote_results as $vote) {
      $vids[] = $vote->vote_cache_id;
    }
    db_query("DELETE FROM {votingapi_cache} WHERE vote_cache_id IN (" . implode(',', array_fill(0, count($vids), '%d')) . ")", $vids);
  }
}

/**
 * Select individual votes from the database.
 *
 * @param $criteria
 *   A keyed array used to build the select query. Keys can contain
 *   a single value or an array of values to be matched.
 *   $criteria['vote_id']       (If this is set, all other keyes are skipped)
 *   $criteria['content_id']
 *   $criteria['content_type']
 *   $criteria['value_type']
 *   $criteria['tag']
 *   $criteria['uid']
 *   $criteria['vote_source']
 *   $criteria['timestamp']   (If this is set, records with timestamps
 *      GREATER THAN the set value will be selected.)
 * @return
 *   An array of vote objects matching the criteria.
 */
function votingapi_select_votes($criteria = array()) {
  if (!empty($criteria['vote_source'])) {
    $criteria['timestamp'] = time() - variable_get('votingapi_anonymous_window', 3600);
  }
  $votes = array();
  $result = _votingapi_query('vote', $criteria);
  while ($vote = db_fetch_object($result)) {
    $votes[] = $vote;
  }
  return $votes;
}

/**
 * Select cached vote results from the database.
 *
 * @param $criteria
 *   A keyed array used to build the select query. Keys can contain
 *   a single value or an array of values to be matched.
 *   $criteria['vote_cache_id']     (If this is set, all other keyes are skipped)
 *   $criteria['content_type']
 *   $criteria['content_type']
 *   $criteria['value_type']
 *   $criteria['tag']
 *   $criteria['uid']
 *   $criteria['vote_source']
 *   $criteria['timestamp']   (If this is set, records with timestamps
 *      GREATER THAN the set value will be selected.)
 * @return
 *   An array of vote result objects matching the criteria.
 */
function votingapi_select_results($criteria = array()) {
  $cached = array();
  $result = _votingapi_query('cache', $criteria);
  while ($cache = db_fetch_object($result)) {
    $cached[] = $cache;
  }
  return $cached;
}

/**
 * Loads all votes for a given piece of content, then calculates and
 * caches the aggregate vote results. This is only intended for modules
 * that have assumed responsibility for the full voting cycle: the
 * votingapi_set_vote() function recalculates automatically.
 *
 * @param $content_type
 *   A string identifying the type of content being rated. Node, comment,
 *   aggregator item, etc.
 * @param $content_id
 *   The key ID of the content being rated.
 * @return
 *   An array of the resulting votingapi_cache records, structured thusly:
 *   $value = $results[$ag][$value_type][$function]
 */
function votingapi_recalculate_results($content_type, $content_id, $force_calculation = FALSE) {

  // if we're operating in cron mode, and the 'force recalculation' flag is NOT set,
  // bail out. The cron run will pick up the results.
  if (variable_get('votingapi_calculation_schedule', 'immediate') != 'cron' || $force_calculation == TRUE) {

    // blow away the existing cache records.
    $criteria = array(
      'content_type' => $content_type,
      'content_id' => $content_id,
    );
    $old_cache = votingapi_select_results($criteria);
    votingapi_delete_results($old_cache);
    $votes = votingapi_select_votes($criteria);
    $cache = array();

    // Loop through, calculate per-type and per-tag totals, etc.
    foreach ($votes as $vote) {
      switch ($vote->value_type) {
        case 'percent':
          $cache[$vote->tag][$vote->value_type]['count'] += 1;
          $cache[$vote->tag][$vote->value_type]['sum'] += $vote->value;
          break;
        case 'points':
          $cache[$vote->tag][$vote->value_type]['count'] += 1;
          $cache[$vote->tag][$vote->value_type]['sum'] += $vote->value;
          break;
        case 'option':
          $cache[$vote->tag][$vote->value_type][$vote->value] += 1;
          break;
      }
    }

    // Do a quick loop through to calculate averages.
    // This is also a good example of how external modules can do their own processing.
    foreach ($cache as $tag => $types) {
      foreach ($types as $type => $functions) {
        if ($type == 'percent' || $type == 'points') {
          $cache[$tag][$type]['average'] = $functions['sum'] / $functions['count'];
        }
        if ($type == 'percent') {

          // we don't actually need the sum for this. discard it to avoid cluttering the db.
          unset($cache[$tag][$type]['sum']);
        }
      }
    }

    // Give other modules a chance to alter the collection of votes.
    drupal_alter('votingapi_results', $cache, $votes, $content_type, $content_id);

    // Now, do the caching. Woo.
    foreach ($cache as $tag => $types) {
      foreach ($types as $type => $functions) {
        foreach ($functions as $function => $value) {
          $cached[] = (object) array(
            'content_type' => $content_type,
            'content_id' => $content_id,
            'value_type' => $type,
            'value' => $value,
            'tag' => $tag,
            'function' => $function,
          );
        }
      }
    }
    votingapi_add_results($cached);

    // Give other modules a chance to act on the results of the vote totaling.
    module_invoke_all('votingapi_results', $cached, $votes, $content_type, $content_id);
    return $cached;
  }
}

/**
 * Simple wrapper function for votingapi_select_votes. Returns the
 * value of the first vote matching the criteria passed in.
 */
function votingapi_select_single_vote_value($criteria = array()) {
  if ($votes = votingapi_select_votes($criteria) && !empty($votes)) {
    return $votes[0]->value;
  }
}

/**
 * Simple wrapper function for votingapi_select_results. Returns the
 * value of the first result matching the criteria passed in.
 */
function votingapi_select_single_result_value($criteria = array()) {
  return db_result(_votingapi_query('cache', $criteria, 1));
}

/**
 * Populate the value of any unset vote properties.
 *
 * @param $vobj
 *   A single vote object.
 * @return
 *   A vote object with all required properties filled in with
 *   their default values.
 */
function _votingapi_prep_vote($vobj) {
  global $user;
  if (empty($vobj->prepped)) {
    $varray = (array) $vobj;
    $vote = array();
    $vote['content_type'] = 'node';
    $vote['value_type'] = 'percent';
    $vote['tag'] = 'vote';
    $vote['uid'] = $user->uid;
    $vote['timestamp'] = time();
    $vote['vote_source'] = ip_address();
    $vote['prepped'] = TRUE;
    $varray += $vote;
    return (object) $varray;
  }
  return $vobj;
}
function _votingapi_query($table = 'vote', $criteria = array(), $limit = 0) {
  $criteria += array(
    'vote_id' => NULL,
    'vote_cache_id' => NULL,
    'content_id' => NULL,
    'content_type' => NULL,
    'value_type' => NULL,
    'value' => NULL,
    'tag' => NULL,
    'uid' => NULL,
    'timestamp' => NULL,
    'vote_source' => NULL,
    'function' => NULL,
  );
  $query = "SELECT * FROM {votingapi_" . $table . "} v WHERE 1 = 1";
  $args = array();
  if (!empty($criteria['vote_id'])) {
    _votingapi_query_builder('vote_id', $criteria['vote_id'], $query, $args);
  }
  elseif (!empty($criteria['vote_cache_id'])) {
    _votingapi_query_builder('vote_cache_id', $criteria['vote_cache_id'], $query, $args);
  }
  else {
    _votingapi_query_builder('content_type', $criteria['content_type'], $query, $args, TRUE);
    _votingapi_query_builder('content_id', $criteria['content_id'], $query, $args);
    _votingapi_query_builder('value_type', $criteria['value_type'], $query, $args, TRUE);
    _votingapi_query_builder('tag', $criteria['tag'], $query, $args, TRUE);
    _votingapi_query_builder('function', $criteria['function'], $query, $args);
    _votingapi_query_builder('uid', $criteria['uid'], $query, $args);
    _votingapi_query_builder('vote_source', $criteria['vote_source'], $query, $args, TRUE);
    _votingapi_query_builder('timestamp', $criteria['timestamp'], $query, $args);
  }
  return $limit ? db_query_range($query, $args, 0, $limit) : db_query($query, $args);
}
function _votingapi_query_builder($name, $value, &$query, &$args, $col_is_string = FALSE) {
  if (!isset($value)) {

    // Do nothing
  }
  elseif ($name === 'timestamp') {
    $query .= " AND v.timestamp >= %d";
    $args[] = $value;
  }
  else {
    if (is_array($value)) {
      if ($col_is_string) {
        $query .= " AND v." . $name . " IN ('" . array_fill(1, count($value), "'%s'") . "')";
        $args += $value;
      }
      else {
        $query .= " AND v." . $name . " IN (" . array_fill(1, count($value), "%d") . ")";
        $args += $value;
      }
    }
    else {
      if ($col_is_string) {
        $query .= " AND v." . $name . " = '%s'";
        $args[] = $value;
      }
      else {
        $query .= " AND v." . $name . " = %d";
        $args[] = $value;
      }
    }
  }
}

Functions

Namesort descending Description
votingapi_add_results Save a single vote result to the database.
votingapi_add_votes Save a single vote to the database.
votingapi_cron Implementation of hook_cron. Allows db-intensive recalculations to put off until cron-time.
votingapi_current_user_identifier Generate a proper identifier for the current user: if they have an account, return their UID. Otherwise, return their IP address.
votingapi_delete_results Delete cached vote results from the database.
votingapi_delete_votes Delete votes from the database.
votingapi_init Implementation of hook_init. Loads the Views integration code.
votingapi_menu Implementation of hook_menu. Adds the url path for the votingapi settings page.
votingapi_perm Implementation of hook_perm
votingapi_recalculate_results Loads all votes for a given piece of content, then calculates and caches the aggregate vote results. This is only intended for modules that have assumed responsibility for the full voting cycle: the votingapi_set_vote() function recalculates…
votingapi_select_results Select cached vote results from the database.
votingapi_select_single_result_value Simple wrapper function for votingapi_select_results. Returns the value of the first result matching the criteria passed in.
votingapi_select_single_vote_value Simple wrapper function for votingapi_select_votes. Returns the value of the first vote matching the criteria passed in.
votingapi_select_votes Select individual votes from the database.
votingapi_settings_form
votingapi_set_vote Cast a vote on a particular piece of content. If a vote already exists, its value is changed. In most cases, this is the function that should be used by external modules.
_votingapi_prep_vote Populate the value of any unset vote properties.
_votingapi_query
_votingapi_query_builder