'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_perm. Exposes a permission to edit the module's * settings. */ function votingapi_perm() { return array('administer voting api'); } /** * 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 = 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); } } /** * Administrative settings for VotingAPI. */ function votingapi_settings_form($form_state) { $period = array(0 => t('Immediately')) + drupal_map_assoc(array(300, 900, 1800, 3600, 10800, 21600, 32400, 43200, 86400, 172800, 345600, 604800), 'format_interval') + array(-1 => t('Never')); $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', 86400), '#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 * An array of votes, each with the following structure: * $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] */ function votingapi_set_votes(&$votes, $criteria = NULL) { $touched = array(); if (!empty($votes['content_type'])) { $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; 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['content_type'])) { $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['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('uid' => $user->uid); if (!$user->uid) { $criteria['vote_source'] = ip_address(); } return $criteria; } /** * Save a collection of votes to the database. * * @param $votes * A vote or array of votes, each with the following structure: * $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 votes, with vote_id keys populated. */ function votingapi_add_votes(&$votes) { if (!empty($votes['content_type'])) { $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 vote results to the database. * * @param vote_results * An array of vote results, each with the following properties: * $vote_result['content_type'] * $vote_result['content_id'] * $vote_result['value_type'] * $vote_result['value'] * $vote_result['tag'] * $vote_result['function'] * $vote_result['timestamp'] (Optional, defaults to time()) */ function votingapi_add_results($vote_results = array()) { if (!empty($vote_results['content_type'])) { $vote_results = array($vote_results); } foreach ($vote_results as $vote_result) { $vote_result['timestamp'] = 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_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 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_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 votes 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_select('vote', $criteria); while ($vote = db_fetch_array($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 results matching the criteria. */ function votingapi_select_results($criteria = array()) { $cached = array(); $result = _votingapi_select('cache', $criteria); while ($cache = db_fetch_array($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) { $criteria = array('content_type' => $content_type, 'content_id' => $content_id); _votingapi_delete('cache', $criteria); // Bulk query to pull the majority of the results we care about. $cache = _votingapi_get_standard_results($content_type, $content_id); // Give other modules a chance to alter the collection of votes. drupal_alter('votingapi_results', $cache, $content_type, $content_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( '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, $content_type, $content_id); return $cached; } } /** * Internal function - builds the default set of VotingAPI results * for the three supported voting styles. */ function _votingapi_get_standard_results($content_type, $content_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.content_type = '%s' AND v.content_id = %d AND v.value_type IN ('points', 'percent') "; $sql .= "GROUP BY v.value_type, v.tag"; $results = db_query($sql, $content_type, $content_id); while ($result = db_fetch_array($results)) { $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, COUNT(1) AS score "; $sql .= "FROM {votingapi_vote} v "; $sql .= "WHERE v.content_type = '%s' AND v.content_id = %d AND v.value_type = 'option' "; $sql .= "GROUP BY v.value, v.tag"; $results = db_query($sql, $content_type, $content_id); while ($result = db_fetch_array($results)) { $cache[$result['tag']][$result['value_type']]['option-'. $result['value']] = $result['score']; } return $cache; } /** * 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 $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( 'content_type' => 'node', 'value_type' => 'percent', 'tag' => 'vote', 'uid' => $user->uid, 'timestamp' => time(), 'vote_source' => ip_address(), 'prepped' => TRUE ); } } /** * Internal helper function constructs SELECT queries. Don't use unless you're me. */ function _votingapi_select($table = 'vote', $criteria = array(), $limit = 0) { $query = "SELECT * FROM {votingapi_". $table ."} v WHERE 1 = 1"; $details = _votingapi_query('vote', $criteria); $query .= $details['query']; return $limit ? db_query_range($query, $details['args'], 0, $limit) : db_query($query, $details['args']); } /** * Internal helper function constructs DELETE queries. Don't use unless you're me. */ function _votingapi_delete($table = 'vote', $criteria = array(), $limit = 0) { $query = "DELETE FROM {votingapi_". $table ."} WHERE 1 = 1"; $details = _votingapi_query('vote', $criteria, ''); $query .= $details['query']; db_query($query, $details['args']); } /** * Internal helper function constructs WHERE clauses. Don't use unless you're me. */ function _votingapi_query($table = 'vote', $criteria = array(), $alias = 'v.') { $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 = ''; $args = array(); if (!empty($criteria['vote_id'])) { _votingapi_query_builder($alias . 'vote_id', $criteria['vote_id'], $query, $args); } elseif (!empty($criteria['vote_cache_id'])) { _votingapi_query_builder($alias . 'vote_cache_id', $criteria['vote_cache_id'], $query, $args); } else { _votingapi_query_builder($alias . 'content_type', $criteria['content_type'], $query, $args, TRUE); _votingapi_query_builder($alias . 'content_id', $criteria['content_id'], $query, $args); _votingapi_query_builder($alias . 'value_type', $criteria['value_type'], $query, $args, TRUE); _votingapi_query_builder($alias . 'tag', $criteria['tag'], $query, $args, TRUE); _votingapi_query_builder($alias . 'function', $criteria['function'], $query, $args); _votingapi_query_builder($alias . 'uid', $criteria['uid'], $query, $args); _votingapi_query_builder($alias . 'vote_source', $criteria['vote_source'], $query, $args, TRUE); _votingapi_query_builder($alias . 'timestamp', $criteria['timestamp'], $query, $args); } return array('query' => $query, 'args' => $args); } /** * Internal helper function constructs individual elements of WHERE clauses. * Don't use unless you're me. */ function _votingapi_query_builder($name, $value, &$query, &$args, $col_is_string = FALSE) { if (!isset($value)) { // Do nothing } elseif ($name === 'timestamp') { $query .= " AND timestamp >= %d"; $args[] = $value; } elseif ($name === 'v.timestamp') { $query .= " AND v.timestamp >= %d"; $args[] = $value; } else { if (is_array($value)) { if ($col_is_string) { $query .= " AND $name IN ('". array_fill(1, count($value), "'%s'") ."')"; $args += $value; } else { $query .= " AND $name IN (". array_fill(1, count($value), "%d") .")"; $args += $value; } } else { if ($col_is_string) { $query .= " AND $name = '%s'"; $args[] = $value; } else { $query .= " AND $name = %d"; $args[] = $value; } } } }