You are here

votingapi.module in Voting API 7.3

A generalized voting API for Drupal.

Maintains and provides an interface for a shared bin of vote and rating data. 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.

File

votingapi.module
View source
<?php

/**
 * @file
 * A generalized voting API for Drupal.
 *
 * Maintains and provides an interface for a shared bin of vote and rating
 * data. 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.
 */

/**
 * Model class for votingapi_vote table.
 */
class VotingApi_Vote {
  public $vote_id = NULL;
  public $entity_type = 'node';
  public $entity_id = NULL;
  public $value_type = 'percent';
  public $value = NULL;
  public $tag = 'vote';
  public $uid = NULL;

  // defaults to current user
  public $vote_source = NULL;

  // defaults to current IP
  public $timestamp = REQUEST_TIME;
  public function __construct($data) {
    foreach ($data as $k => $v) {
      $this->{$k} = $v;
    }
    $this->uid = is_null($this->uid) ? $GLOBALS['user']->uid : $this->uid;
    $this->vote_source = is_null($this->vote_source) ? ip_address() : $this->vote_source;
  }

  /**
   * Save a collection of votes to the database.
   *
   * This function does most of the heavy lifting for VotingAPI the main
   * votingapi_set_votes() function, but does NOT automatically triger re-tallying
   * of results. As such, it's useful for modules that must insert their votes in
   * separate batches without triggering unecessary recalculation.
   *
   * Remember that any module calling this function implicitly assumes responsibility
   * for calling votingapi_recalculate_results() when all votes have been inserted.
   *
   * @param $votes array of VotingApi_Vote instances
   * @return
   *   The same votes, with vote_id keys populated.
   *
   * @see votingapi_set_votes()
   * @see votingapi_recalculate_results()
   */
  public static function saveMultiple(&$votes) {
    if (is_object($votes)) {
      $votes = array(
        $votes,
      );
    }
    foreach ($votes as $key => $vote) {
      $vote
        ->save();
    }
    module_invoke_all('votingapi_insert', $votes);
    return $votes;
  }

  /**
   * Delete votes from the database.
   *
   * @param $votes An array of votes to delete. Each vote must have the 'vote_id' key set.
   */
  public static function deleteMultiple($votes = array()) {
    if (!empty($votes)) {
      module_invoke_all('votingapi_delete', $votes);
      $vids = array();
      foreach ($votes as $vote) {
        $vids[] = $vote->vote_id;
      }
      VotingApi_VoteStorage::get()
        ->deleteVotes($votes, $vids);
    }
  }
  public function save() {
    VotingApi_VoteStorage::get()
      ->addVote($this);
  }

}

/**
 * Criteria to select VotingApi_Votes
 *
 * This is basically the same as VotingApi_Vote except that
 * timestamp is interpreted as the vote-rollover.
 */
class VotingApi_Criteria extends VotingApi_Vote {
  public function __construct($data) {
    parent::__construct($data);
    if ($this->uid != 0) {
      $this->vote_source = NULL;
    }
    $this
      ->calculateWindow();
  }

  /**
   * Calculate the correct timestamp for the vote-rollover.
   */
  protected function calculateWindow() {
    $window = -1;
    if ($this->uid == 0) {
      if (!empty($this->vote_source)) {
        $window = variable_get('votingapi_anonymous_window', 86400);
      }
    }
    else {
      $window = variable_get('votingapi_user_window', -1);
    }
    if ($window >= 0) {
      $this->timestamp = REQUEST_TIME - $window;
    }
  }

  /**
   * Select individual votes from the database.
   *
   * @param $limit
   *   An optional integer specifying the maximum number of votes to return.
   * @return
   *   An array of votes matching the criteria.
   */
  public function select($limit = 0) {
    return VotingApi_VoteStorage::get()
      ->selectVotes($this, $limit);
  }

  /**
   * Delete all matching votes.
   */
  public function delete() {
    VotingApi_Vote::deleteMultiple($this
      ->select());
  }
  public static function byEntity($entity_type, $entity_ids) {
    $class = get_called_class();
    return new $class(array(
      'entity_type' => $entity_type,
      'entity_id' => $entity_ids,
    ));
  }

  /**
   * Retrieve the value of the first vote matching the criteria.
   */
  public function singleValue() {
    $results = $this
      ->select();
    return $results[0]->value;
  }

}
class VotingApi_Result {
  public $vote_cache_id = NULL;
  public $entity_type = 'node';
  public $entity_id = NULL;
  public $value_type = 'percent';
  public $value = NULL;
  public $tag = 'vote';
  public $function = NULL;
  public $timestamp = REQUEST_TIME;
  public function __construct($data) {
    foreach ($data as $k => $v) {
      $this->{$k} = $v;
    }
  }

  /**
   * Save this result to the database.
   */
  public function save() {
    drupal_write_record('votingapi_cache', $vote_result);
  }

  /**
   * Save a bundle of vote results to the database.
   *
   * This function is called by votingapi_recalculate_results() after tallying
   * the values of all cast votes on a piece of content. This function will be of
   * little use for most third-party modules, unless they manually insert their
   * own result data.
   *
   * @param vote_results array of VotingApi_Result objects
   */
  public static function saveMultiple($vote_results = array()) {
    if (is_object($vote_results)) {
      $vote_results = array(
        $vote_results,
      );
    }
    foreach ($vote_results as $vote_result) {
      $vote_result
        ->save();
    }
  }

  /**
   * Delete vote results from the database.
   *
   * @param $vote_results
   *   An array of vote results to delete. Minimally, each vote result must have
   *   the 'vote_cache_id' key set.
   */
  public static function deleteMultiple($vote_results = array()) {
    if (!empty($vote_results)) {
      $vids = array();
      foreach ($vote_results as $vote) {
        $vids[] = $vote->vote_cache_id;
      }
      db_delete('votingapi_cache')
        ->condition('vote_cache_id', $vids, 'IN')
        ->execute();
    }
  }

}

/**
 * Criteria to select VotingApi_Results
 *
 * This is basically the same as VotingApi_Result except that
 * timestamp is interpreted as the vote-rollover.
 */
class VotingApi_ResultCriteria extends VotingApi_Result {

  /**
   * Select cached vote results from the database.
   *
   * @param $limit
   *   An optional integer specifying the maximum number of votes to return.
   * @return
   *   An array of vote results matching the criteria.
   */
  function select($limit = 0) {
    $query = db_select('votingapi_cache')
      ->fields('votingapi_cache');
    $this
      ->addConditions($query);
    if (!empty($limit)) {
      $query
        ->range(0, $limit);
    }
    $result = $query
      ->execute();
    $result->fetchOptions['class_name'] = 'VotingApi_Result';
    return $result
      ->fetchAll(PDO::FETCH_CLASS);
  }
  public static function byEntity($entity_type, $entity_ids) {
    $class = get_called_class();
    return new $class(array(
      'entity_type' => $entity_type,
      'entity_id' => $entity_ids,
    ));
  }
  protected function addConditions(&$query) {
    foreach ($this as $key => $value) {
      if (!isset($value)) {
        continue;
      }
      $query
        ->condition($key, $value, is_array($value) ? 'IN' : '=');
    }
  }
  public function delete() {
    $query = db_delete('votingapi_cache');
    $this
      ->addConditions($query);
    $query
      ->execute();
  }

  /**
   * Retrieve the value of the first result matching the criteria.
   */
  public function singleValue() {
    $results = $this
      ->select();
    return $results[0]->value;
  }

}
class VotingApi_VoteStorage {
  public static $instance = NULL;
  public static function get() {
    if (!self::$instance) {
      $class = variable_get('votingapi_vote_storage', 'VotingApi_VoteStorage');
      self::$instance = new $class();
    }
    return self::$instance;
  }
  public function addVote(&$vote) {
    drupal_write_record('votingapi_vote', $vote);
  }
  public function deleteVotes($votes, $vids) {
    db_delete('votingapi_vote')
      ->condition('vote_id', $vids, 'IN')
      ->execute();
  }
  public function selectVotes($criteria, $limit) {
    $query = db_select('votingapi_vote')
      ->fields('votingapi_vote');
    foreach ($criteria as $key => $value) {
      if ($key == 'timestamp') {
        $query
          ->condition($key, $value, '>');
      }
      else {
        $query
          ->condition($key, $value, is_array($value) ? 'IN' : '=');
      }
    }
    if (!empty($limit)) {
      $query
        ->range(0, $limit);
    }
    $result = $query
      ->execute();
    $result->fetchOptions['class_name'] = 'VotingApi_Vote';
    return $result
      ->fetchAll(PDO::FETCH_CLASS);
  }

  /**
   * Builds the default VotingAPI results for the three supported voting styles.
   */
  function standardResults($entity_type, $entity_id) {
    $cache = array();
    $sql = "SELECT v.value_type, v.tag, ";
    $sql .= "COUNT(v.value) as value_count, SUM(v.value) as value_sum  ";
    $sql .= "FROM {votingapi_vote} v ";
    $sql .= "WHERE v.entity_type = :type AND v.entity_id = :id AND v.value_type IN ('points', 'percent') ";
    $sql .= "GROUP BY v.value_type, v.tag";
    $results = db_query($sql, array(
      ':type' => $entity_type,
      ':id' => $entity_id,
    ));
    foreach ($results as $result) {
      $cache[$result->tag][$result->value_type]['count'] = $result->value_count;
      $cache[$result->tag][$result->value_type]['average'] = $result->value_sum / $result->value_count;
      if ($result->value_type == 'points') {
        $cache[$result->tag][$result->value_type]['sum'] = $result->value_sum;
      }
    }
    $sql = "SELECT v.tag, v.value, v.value_type, COUNT(1) AS score ";
    $sql .= "FROM {votingapi_vote} v ";
    $sql .= "WHERE v.entity_type = :type AND v.entity_id = :id AND v.value_type = 'option' ";
    $sql .= "GROUP BY v.value, v.tag, v.value_type";
    $results = db_query($sql, array(
      ':type' => $entity_type,
      ':id' => $entity_id,
    ));
    foreach ($results as $result) {
      $cache[$result->tag][$result->value_type]['option-' . $result->value] = $result->score;
    }
    return $cache;
  }

}

/**
 * Implements of hook_menu().
 */
function votingapi_menu() {
  $items = array();
  $items['admin/config/search/votingapi'] = array(
    'title' => 'Voting API',
    'description' => 'Configure sitewide settings for user-generated ratings and votes.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'votingapi_settings_form',
    ),
    'access callback' => 'user_access',
    'access arguments' => array(
      'administer voting api',
    ),
    'file' => 'votingapi.admin.inc',
    'type' => MENU_NORMAL_ITEM,
  );
  if (module_exists('devel_generate')) {
    $items['admin/config/development/generate/votingapi'] = array(
      'title' => 'Generate votes',
      'description' => 'Generate a given number of votes on site content. Optionally delete existing votes.',
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'votingapi_generate_votes_form',
      ),
      'access arguments' => array(
        'administer voting api',
      ),
      'file' => 'votingapi.admin.inc',
    );
  }
  return $items;
}

/**
 * Implements hook_permission().
 */
function votingapi_permission() {
  return array(
    'administer voting api' => array(
      'title' => t('Administer Voting API'),
    ),
  );
}

/**
 * Implements of hook_views_api().
 */
function votingapi_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'votingapi') . '/views',
  );
}

/**
 * Implements of hook_cron().
 *
 * Allows db-intensive recalculations to be deferred until cron-time.
 */
function votingapi_cron() {
  if (variable_get('votingapi_calculation_schedule', 'immediate') == 'cron') {
    $time = REQUEST_TIME;
    $last_cron = variable_get('votingapi_last_cron', 0);
    $result = db_query('SELECT DISTINCT entity_type, entity_id FROM {votingapi_vote} WHERE timestamp > :timestamp', array(
      ':timestamp' => $last_cron,
    ));
    foreach ($result as $content) {
      votingapi_recalculate_results($content->entity_type, $content->entity_id, TRUE);
    }
    variable_set('votingapi_last_cron', $time);
  }
}

/**
 * Cast a vote on a particular piece of content.
 *
 * This function does most of the heavy lifting needed by third-party modules
 * based on VotingAPI. Handles clearing out old votes for a given piece of
 * content, saving the incoming votes, and re-tallying the results given the
 * new data.
 *
 * Modules that need more explicit control can call VotingApi_Vote::saveMultiple() and
 * manage the deletion/recalculation tasks manually.
 *
 * @param $votes An array of (or a single) VotingApi_Vote objects
 * @param $criteria An array of (or a single) VotingApi_Criteria objects 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 object is
 *   passed in, no votes will be reset and all incoming votes will be saved IN ADDITION to
 *   existing ones.
 * @return
 *   An array of vote result records affected by the vote. The values are
 *   contained in a nested array keyed thusly:
 *   $value = $results[$entity_type][$entity_id][$tag][$value_type][$function]
 *
 * @see VotingApi_Vote::saveMultiple()
 * @see votingapi_recalculate_results()
 */
function votingapi_set_votes(&$votes, $criteria = NULL) {
  $touched = array();
  if (is_object($votes)) {
    $votes = array(
      $votes,
    );
  }

  // Handle clearing out old votes if they exist.
  if (!isset($criteria)) {

    // If the calling function didn't explicitly set criteria for vote deletion,
    // build up the delete queries here.
    foreach ($votes as $vote) {
      $tmp = new VotingApi_Criteria($vote);
      if (isset($tmp->value)) {
        $tmp->value = NULL;
      }
      $tmp
        ->delete();
    }
  }
  else {
    if (is_object($criteria)) {
      $criteria = array(
        $criteria,
      );
    }
    foreach ($criteria as $c) {
      $c
        ->delete();
    }
  }

  // Cast the actual votes, inserting them into the table.
  VotingApi_Vote::saveMultiple($votes);
  foreach ($votes as $vote) {
    $touched[$vote->entity_type][$vote->entity_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;
}

/**
 * Recalculates the aggregate results of all votes for a piece of content.
 *
 * 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 $entity_type
 *   A string identifying the type of content being rated. Node, comment,
 *   aggregator item, etc.
 * @param $entity_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]
 *
 * @see votingapi_set_votes()
 */
function votingapi_recalculate_results($entity_type, $entity_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) {
    $query = db_delete('votingapi_cache')
      ->condition('entity_type', $entity_type)
      ->condition('entity_id', $entity_id)
      ->execute();

    // Bulk query to pull the majority of the results we care about.
    $cache = VotingApi_VoteStorage::get()
      ->standardResults($entity_type, $entity_id);

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

    // Now, do the caching. Woo.
    $cached = array();
    foreach ($cache as $tag => $types) {
      foreach ($types as $type => $functions) {
        foreach ($functions as $function => $value) {
          $cached[] = new VotingApi_Result(array(
            'entity_type' => $entity_type,
            'entity_id' => $entity_id,
            'value_type' => $type,
            'value' => $value,
            'tag' => $tag,
            'function' => $function,
          ));
        }
      }
    }
    VotingApi_Result::saveMultiple($cached);

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

/**
 * Returns metadata about tags, value_types, and results defined by vote modules.
 *
 * If your module needs to determine what existing tags, value_types, etc., are
 * being supplied by other modules, call this function. Querying the votingapi
 * tables for this information directly is discouraged, as values may not appear
 * consistently. (For example, 'average' does not appear in the cache table until
 * votes have actually been cast in the cache table.)
 *
 * Three major bins of data are stored: tags, value_types, and functions. Each
 * entry in these bins is keyed by the value stored in the actual VotingAPI
 * tables, and contains an array with (minimally) 'name' and 'description' keys.
 * Modules can add extra keys to their entries if desired.
 *
 * This metadata can be modified or expanded using hook_votingapi_metadata_alter().
 *
 * @return
 *   An array of metadata defined by VotingAPI and altered by vote modules.
 *
 * @see hook_votingapi_metadata_alter()
 */
function votingapi_metadata($reset = FALSE) {
  static $data;
  if ($reset || !isset($data)) {
    $data = array(
      'tags' => array(
        'vote' => array(
          'name' => t('Normal vote'),
          'description' => t('The default tag for votes on content. If multiple votes with different tags are being cast on a piece of content, consider casting a "summary" vote with this tag as well.'),
        ),
      ),
      'value_types' => array(
        'percent' => array(
          'name' => t('Percent'),
          'description' => t('Votes in a specific range. Values are stored in a 1-100 range, but can be represented as any scale when shown to the user.'),
        ),
        'points' => array(
          'name' => t('Points'),
          'description' => t('Votes that contribute points/tokens/karma towards a total. May be positive or negative.'),
        ),
      ),
      'functions' => array(
        'count' => array(
          'name' => t('Number of votes'),
          'description' => t('The number of votes cast for a given piece of content.'),
        ),
        'average' => array(
          'name' => t('Average vote'),
          'description' => t('The average vote cast on a given piece of content.'),
        ),
        'sum' => array(
          'name' => t('Total score'),
          'description' => t('The sum of all votes for a given piece of content.'),
          'value_types' => array(
            'points',
          ),
        ),
      ),
    );
    drupal_alter('votingapi_metadata', $data);
  }
  return $data;
}

/**
 * Implements hook_entity_delete().
 *
 * Delete all votes and cache entries for the deleted entities
 */
function votingapi_entity_delete($entity, $type) {
  $ids = entity_extract_ids($type, $entity);
  $id = array(
    $ids[0],
  );
  VotingApi_ResultCriteria::byEntity($type, $ids)
    ->delete();
  VotingApi_Criteria::byEntity($type, $ids)
    ->delete();
}

Functions

Namesort descending Description
votingapi_cron Implements of hook_cron().
votingapi_entity_delete Implements hook_entity_delete().
votingapi_menu Implements of hook_menu().
votingapi_metadata Returns metadata about tags, value_types, and results defined by vote modules.
votingapi_permission Implements hook_permission().
votingapi_recalculate_results Recalculates the aggregate results of all votes for a piece of content.
votingapi_set_votes Cast a vote on a particular piece of content.
votingapi_views_api Implements of hook_views_api().

Classes

Namesort descending Description
VotingApi_Criteria Criteria to select VotingApi_Votes
VotingApi_Result
VotingApi_ResultCriteria Criteria to select VotingApi_Results
VotingApi_Vote Model class for votingapi_vote table.
VotingApi_VoteStorage