'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 > %d', $last_cron); while ($content = db_fetch_object($result)) { 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_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; } /** * 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); } foreach ($votes as $key => $vote) { _votingapi_prep_vote($vote); drupal_write_record('votingapi_vote', $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']; } db_delete('votingapi_vote')->condition('vote_id', $vids, 'IN')->execute(); } } /** * 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', 3600); if (!empty($criteria['vote_source']) && $anon_window >= 0) { $criteria['timestamp'] = REQUEST_TIME - $anon_window; } $query = db_select('votingapi_vote')->fields('votingapi_vote'); 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); } /** * 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(); // Bulk query to pull the majority of the results we care about. $cache = _votingapi_get_standard_results($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_get_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 ); } }