7.x-3.x branch
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.
| Name | Description |
|---|---|
| votingapi_add_results | Save a bundle of vote results to the database. |
| votingapi_add_votes | Save a collection of votes to the database. |
| votingapi_cron | Implementation of hook_cron(). |
| 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_entity_delete | Implements hook_entity_delete(). |
| votingapi_menu | Implementation 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_select_results | Select cached vote results from the database. |
| votingapi_select_single_result_value | Retrieve the value of the first result matching the criteria passed in. |
| votingapi_select_single_vote_value | Retrieve the value of the first vote matching the criteria passed in. |
| votingapi_select_votes | Select individual votes from the database. |
| votingapi_set_votes | Cast a vote on a particular piece of content. |
| votingapi_views_api | Implementation of hook_views_api(). |
| votingapi_votingapi_storage_add_vote | Implementation of hook_votingapi_storage_add_vote(). |
| votingapi_votingapi_storage_delete_votes | Implementation of hook_votingapi_storage_delete_votes(). |
| votingapi_votingapi_storage_select_votes | Implementation of hook_votingapi_storage_select_votes(). |
| votingapi_votingapi_storage_standard_results | Builds the default VotingAPI results for the three supported voting styles. |
| _votingapi_cron_delete_orphaned | Delete votes and cache entries for a number of entities in the queue. |
| _votingapi_delete_cache_by_entity | Helper function to delete all cache entries on given entities. |
| _votingapi_delete_votes_by_entity | Helper function to delete all votes on given entities. |
| _votingapi_prep_vote | Populate the value of any unset vote properties. |
- <?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.
- */
-
- /**
- * Implementation 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'),
- ),
- );
- }
-
- /**
- * Implementation of hook_views_api().
- */
- function votingapi_views_api() {
- return array(
- 'api' => 3,
- 'path' => drupal_get_path('module', 'votingapi') . '/views',
- );
- }
-
- /**
- * Implementation 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);
- }
-
- _votingapi_cron_delete_orphaned();
- }
-
- /**
- * 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_add_votes() and
- * manage the deletion/recalculation tasks manually.
- *
- * @param $votes
- * An array of votes, each with the following structure:
- * $vote['entity_type'] (Optional, defaults to 'node')
- * $vote['entity_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 REQUEST_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 keys are skipped)
- * $criteria['entity_type']
- * $criteria['entity_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[$entity_type][$entity_id][$tag][$value_type][$function]
- *
- * @see votingapi_add_votes()
- * @see votingapi_recalculate_results()
- */
- function votingapi_set_votes(&$votes, $criteria = NULL) {
- $touched = array();
- if (!empty($votes['entity_id'])) {
- $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 = votingapi_current_user_identifier();
- $tmp += $vote;
- if (isset($tmp['value'])) {
- unset($tmp['value']);
- }
- votingapi_delete_votes(votingapi_select_votes($tmp));
- }
- }
- elseif (is_array($criteria)) {
- // The calling function passed in an explicit set of delete filters.
- if (!empty($criteria['entity_id'])) {
- $criteria = array($criteria);
- }
- foreach ($criteria as $c) {
- votingapi_delete_votes(votingapi_select_votes($c));
- }
- }
-
- foreach ($votes as $key => $vote) {
- _votingapi_prep_vote($vote);
- $votes[$key] = $vote; // Is this needed? Check to see how PHP4 handles refs.
- }
-
- // Cast the actual votes, inserting them into the table.
- votingapi_add_votes($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;
- }
-
- /**
- * 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('uid' => $user->uid);
- if (!$user->uid) {
- $criteria['vote_source'] = ip_address();
- }
- return $criteria;
- }
-
- /**
- * Implementation of hook_votingapi_storage_add_vote().
- */
- function votingapi_votingapi_storage_add_vote(&$vote) {
- drupal_write_record('votingapi_vote', $vote);
- }
-
- /**
- * Implementation of hook_votingapi_storage_delete_votes().
- */
- function votingapi_votingapi_storage_delete_votes($votes, $vids) {
- db_delete('votingapi_vote')->condition('vote_id', $vids, 'IN')->execute();
- }
-
- /**
- * Implementation of hook_votingapi_storage_select_votes().
- */
- function votingapi_votingapi_storage_select_votes($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);
- }
- return $query->execute()->fetchAll(PDO::FETCH_ASSOC);
- }
-
- /**
- * 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
- * A vote or array of votes, each with the following structure:
- * $vote['entity_type'] (Optional, defaults to 'node')
- * $vote['entity_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 REQUEST_TIME)
- * @return
- * The same votes, with vote_id keys populated.
- *
- * @see votingapi_set_votes()
- * @see votingapi_recalculate_results()
- */
- function votingapi_add_votes(&$votes) {
- if (!empty($votes['entity_id'])) {
- $votes = array($votes);
- }
- $function = variable_get('votingapi_storage_module', 'votingapi') . '_votingapi_storage_add_vote';
- foreach ($votes as $key => $vote) {
- _votingapi_prep_vote($vote);
- $function($vote);
- $votes[$key] = $vote;
- }
- module_invoke_all('votingapi_insert', $votes);
- return $votes;
- }
-
- /**
- * 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
- * An array of vote results, each with the following properties:
- * $vote_result['entity_type']
- * $vote_result['entity_id']
- * $vote_result['value_type']
- * $vote_result['value']
- * $vote_result['tag']
- * $vote_result['function']
- * $vote_result['timestamp'] (Optional, defaults to REQUEST_TIME)
- */
- function votingapi_add_results($vote_results = array()) {
- if (!empty($vote_results['entity_id'])) {
- $vote_results = array($vote_results);
- }
-
- foreach ($vote_results as $vote_result) {
- $vote_result['timestamp'] = REQUEST_TIME;
- drupal_write_record('votingapi_cache', $vote_result);
- }
- }
-
- /**
- * Delete votes from the database.
- *
- * @param $votes
- * An array of votes to delete. Minimally, each vote must have the 'vote_id'
- * key 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'];
- }
- $function = variable_get('votingapi_storage_module', 'votingapi') . '_votingapi_storage_delete_votes';
- $function($votes, $vids);
- }
- }
-
- /**
- * Delete cached 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.
- */
- function votingapi_delete_results($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();
- }
- }
-
- /**
- * 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 keys are skipped)
- * $criteria['entity_id']
- * $criteria['entity_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.)
- * @param $limit
- * An optional integer specifying the maximum number of votes to return.
- * @return
- * An array of votes matching the criteria.
- */
- function votingapi_select_votes($criteria = array(), $limit = 0) {
- $anon_window = variable_get('votingapi_anonymous_window', 86400);
- if (!empty($criteria['vote_source']) && $anon_window >= 0) {
- $criteria['timestamp'] = REQUEST_TIME - $anon_window;
- }
- $function = variable_get('votingapi_storage_module', 'votingapi') . '_votingapi_storage_select_votes';
- return $function($criteria, $limit);
- }
-
- /**
- * 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 keys are skipped)
- * $criteria['entity_id']
- * $criteria['entity_type']
- * $criteria['value_type']
- * $criteria['tag']
- * $criteria['function']
- * $criteria['timestamp'] (If this is set, records with timestamps
- * GREATER THAN the set value will be selected.)
- * @param $limit
- * An optional integer specifying the maximum number of votes to return.
- * @return
- * An array of vote results matching the criteria.
- */
- function votingapi_select_results($criteria = array(), $limit = 0) {
- $query = db_select('votingapi_cache')->fields('votingapi_cache');
- foreach ($criteria as $key => $value) {
- $query->condition($key, $value, is_array($value) ? 'IN' : '=');
- }
- if (!empty($limit)) {
- $query->range(0, $limit);
- }
- return $query->execute()->fetchAll(PDO::FETCH_ASSOC);
- }
-
- /**
- * 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();
-
- $function = variable_get('votingapi_storage_module', 'votingapi') . '_votingapi_storage_standard_results';
- // Bulk query to pull the majority of the results we care about.
- $cache = $function($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[] = array(
- 'entity_type' => $entity_type,
- 'entity_id' => $entity_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, $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;
- }
-
- /**
- * Builds the default VotingAPI results for the three supported voting styles.
- */
- function votingapi_votingapi_storage_standard_results($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;
- }
-
- /**
- * Retrieve the value of the first vote matching the criteria passed in.
- */
- function votingapi_select_single_vote_value($criteria = array()) {
- if ($results = votingapi_select_votes($criteria, 1)) {
- return $results[0]['value'];
- }
- }
-
- /**
- * Retrieve the value of the first result matching the criteria passed in.
- */
- function votingapi_select_single_result_value($criteria = array()) {
- if ($results = votingapi_select_results($criteria, 1)) {
- return $results[0]['value'];
- }
- }
-
- /**
- * Populate the value of any unset vote properties.
- *
- * @param $vote
- * A single vote.
- * @return
- * A vote object with all required properties filled in with
- * their default values.
- */
- function _votingapi_prep_vote(&$vote) {
- global $user;
- if (empty($vote['prepped'])) {
- $vote += array(
- 'entity_type' => 'node',
- 'value_type' => 'percent',
- 'tag' => 'vote',
- 'uid' => $user->uid,
- 'timestamp' => REQUEST_TIME,
- 'vote_source' => ip_address(),
- 'prepped' => TRUE
- );
- }
- }
-
- /**
- * 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_delete_cache_by_entity($id, $type);
- _votingapi_delete_votes_by_entity($id, $type);
- }
-
- /**
- * Helper function to delete all cache entries on given entities.
- *
- * @param array entity ids
- * @param string entity type
- */
- function _votingapi_delete_cache_by_entity($entity_ids, $type) {
- $result = db_select('votingapi_cache', 'v')
- ->fields('v', array('vote_cache_id'))
- ->condition('entity_type', $type)
- ->condition('entity_id', $entity_ids)
- ->execute();
- $votes = array();
- foreach ($result as $row) {
- $votes[]['vote_cache_id'] = $row->vote_cache_id;
- }
- votingapi_delete_results($votes);
- }
-
- /**
- * Helper function to delete all votes on given entities.
- *
- * @param array entity ids
- * @param string entity type
- */
- function _votingapi_delete_votes_by_entity($entity_ids, $type) {
- $result = db_select('votingapi_vote', 'v')
- ->fields('v', array('vote_id', 'entity_type', 'entity_id'))
- ->condition('entity_type', $type)
- ->condition('entity_id', $entity_ids)
- ->execute();
- $votes = array();
- foreach ($result as $row) {
- $votes[] = (array) $row;
- }
- votingapi_delete_votes($votes);
- }
-
- /**
- * Delete votes and cache entries for a number of entities in the queue.
- */
- function _votingapi_cron_delete_orphaned() {
- $queue = DrupalQueue::get('VotingAPIOrphaned');
- $limit = variable_get('votingapi_cron_orphaned_max', 50);
- $done = 0;
- while (($item = $queue->claimItem()) && $done++ < $limit) {
- _votingapi_delete_cache_by_entity(array($item->data['entity_id']), $item->data['entity_type']);
- _votingapi_delete_votes_by_entity(array($item->data['entity_id']), $item->data['entity_type']);
- $queue->deleteItem($item);
- }
- }
-