'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('Never tally votes: I am using a custom module to control vote results') ), ); $form['votingapi_actions_mode'] = array( '#type' => 'radios', '#title' => t('Actions'), '#description' => t('Voting actions can perform tasks depending on the results of a vote. Requires actions.module.'), '#default_value' => variable_get('votingapi_actions_mode', 'all'), '#options' => array( 'all' => t('Allow modules to register their own voting actions'), 'user' => t('Only perform voting actions I configure (votingapi_actions.module must be enabled)'), 'none' => t('Disable voting actions ') ), ); if (!module_exist('actions')) { $form['votingapi_actions_mode']['#attributes'] = array('disabled' => 'true'); } return $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 $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. * @param $vote * This is slightly ugly. $vote can be one of three possible data types: * 1: $vote is a number, and is inserted as a vote with default value_type and tag. * 2: $vote is an object, with $vote->value, $vote->value_type, and $vote->tag properties. * 3: $vote is an array of $vote objects as used in #2, and is iterated through. * See docs for votingapi_add_vote() for details on vote_types and tags. * @param $uid * The uid of the user casting the vote. If none is specified, the currently logged in user's uid will be inserted. * @return * An array of the votingapi_cache records affected by the vote. */ function votingapi_set_vote($content_type, $content_id, $vote, $uid = NULL, $recursion = FALSE) { if ($uid == NULL) { global $user; $uid = $user->uid; } if (is_array($vote)) { foreach($vote as $vobj) { $user_votes[] = votingapi_set_vote($content_type, $content_id, $vobj, $uid, TRUE); } } else if (is_numeric($vote)) { $vobj->value = $vote; $vobj->value_type = VOTINGAPI_VALUE_DEFAULT_TYPE; $vobj->tag = VOTINGAPI_VALUE_DEFAULT_TAG; votingapi_set_vote($content_type, $content_id, $vobj, $uid, TRUE); } else if (is_object($vote)) { if (!isset($vote->value_type)) { $vote->value_type = VOTINGAPI_VALUE_DEFAULT_TYPE; } if (!isset($vote->tag)) { $vote->tag = VOTINGAPI_VALUE_DEFAULT_TAG; } $result = db_query("SELECT * FROM {votingapi_vote} WHERE content_type='%s' AND content_id=%d AND tag='%s' AND value_type='%s' AND uid=%d", $content_type, $content_id, $vote->tag, $vote->value_type, $uid); while ($vobj = db_fetch_object($result)) { votingapi_change_vote($vobj, $vote->value); $exists = TRUE; } if (!$exists) { votingapi_add_vote($content_type, $content_id, $vote->value, $vote->value_type, $vote->tag, $uid); } } if ($recursion) { return $vobj; } else { if (variable_get('votingapi_calculation_schedule', 'immediate') != 'cron') { return votingapi_recalculate_results($content_type, $content_id); } } } /** * Deletes all votes cast on a particular content-object by a user. * In most cases, this is the function that should be used by external modules. * * @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. * @param $vote * @param $uid * The uid of the user casting the vote. If none is specified, the currently logged in user's uid will be inserted. */ function votingapi_unset_vote($content_type, $content_id, $uid = NULL) { if ($uid == NULL) { global $user; $uid = $user->uid; } $votes = votingapi_get_user_votes($content_type, $content_id, $uid); foreach ($votes as $vobj) { votingapi_delete_vote($vobj); } if (variable_get('votingapi_calculation_schedule', 'immediate') != 'cron') { return votingapi_recalculate_results($content_type, $content_id); } } /** * Add a new vote for a given piece of content. If the user has already voted, this casts an additional vote. * In most cases, this should not be called directly by external modules. * * @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. * @param $value * An int representing the value of the vote being cast. * @param $value_type * An optional int representing the meaning of the value param. Three standard types are handled by votingapi: * 'percent' -- 'Value' is a number from 0-100. The API will cache an average for all votes. * 'points' -- 'Value' is a positive or negative int. The API will cache the sum of all votes. * 'option' -- 'Value' is an int representing a specific option. The API will cache a vote-count for each option. * Other value_types can be passed in, but no default actions will be taken with them by the API. * If no value is passed in, 'percent' is the default. * @param $tag * A string to separate multiple voting criteria. For example, a voting system that rates software for 'stability' * and 'features' would cast two votes, each with a different tag. If none is specified, the default 'vote' tag is used. * @param $uid * The uid of the user casting the vote. If none is specified, the currently logged in user's uid will be inserted. * @return * The $vote object cast. */ function votingapi_add_vote($content_type, $content_id, $value, $value_type = VOTINGAPI_VALUE_DEFAULT_TYPE, $tag = VOTINGAPI_VALUE_DEFAULT_TAG, $uid = NULL) { if ($uid == NULL) { global $user; $uid = $user->uid; } $vobj->vote_id = db_next_id('{votingapi_vote}'); $vobj->content_type = $content_type; $vobj->content_id = $content_id; $vobj->value = $value; $vobj->value_type = $value_type; $vobj->tag = $tag; $vobj->uid = $uid; $vobj->timestamp = time(); if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $vobj->hostname = $_SERVER['HTTP_X_FORWARDED_FOR']; } else { $vobj->hostname = $_SERVER['REMOTE_ADDR']; } db_query("INSERT INTO {votingapi_vote} (vote_id, content_type, content_id, value, value_type, tag, uid, timestamp, hostname) VALUES (%d, '%s', %d, %f, '%s', '%s', %d, %d, '%s')", $vobj->vote_id, $vobj->content_type, $vobj->content_id, $vobj->value, $vobj->value_type, $vobj->tag, $vobj->uid, $vobj->timestamp, $vobj->hostname); // Give other modules a chance to act on the insert operation. votingapi_invoke('insert', $vobj); return $vobj; } /** * Alters a user's existing vote, if one exists. * In most cases, this should not be called directly by external modules. * * @param $vobj * A discrete $vote object, or minimally any object with a valid $vobj->vote_id * @param $value * The new value for the vote. * @return * The $new version of the vote object. */ function votingapi_change_vote($vobj, $value) { db_query("UPDATE {votingapi_vote} SET value=%f WHERE vote_id=%d", $value, $vobj->vote_id); // Give other modules a chance to respond to the change. // Both the existing vote object and the new vote value are handed off to interested // modules. votingapi_invoke('update', $vobj, $value); // update the existing $vobj and return it. $vobj->value = $value; return $vobj; } /** * Deletes a user's existing vote, if one exists. * * @param $vobj * A discrete $vote object, or minimally any object with a valid $vobj->vote_id */ function votingapi_delete_vote($vobj) { if (!(isset($vobj->content_type) && isset($vobj->content_id))) { $vobj = votingapi_get_vote_by_id($vobj->vote_id); } votingapi_invoke('delete', $vobj); db_query("DELETE FROM {votingapi_vote} WHERE vote_id=%d", $vobj->vote_id); } /** * Deletes a collection of vote objects. * * @param $votes * An array of vote objects */ function votingapi_delete_votes($votes = array()) { if (is_array($votes)) { foreach ($votes as $vobj) { votingapi_delete_vote($vobj); } } } /** * An internal utility function used to pull raw votes for processing. Undocumented at the moment.. */ function _votingapi_get_raw_votes($content_type, $content_id, $value_type = NULL, $tag_list = NULL, $uid = NULL) { if ($tag_list) { $filter_string .= " AND v.tag IN ('" . implode("','",$tag_list) . "')"; } // Still not sure all this checking is necessary, but a quick cast can't hurt. if (is_numeric($uid)) { $filter_string .= ' AND v.uid = '. (int)$uid; } elseif (is_array($uid)) { $uid_list = array(); foreach ($uid as $discrete_uid) { if (is_numeric($discrete_uid)) { $uid_list[] = $discrete_uid; } } if (count($uid_list)) { $filter_string .= " AND v.uid IN ('" . implode("','",$uid_list) . "')"; } } if (isset($value_type)) { $filter_string .= " AND v.value_type = '" . $value_type . "'"; } $votes = array(); $result = db_query("SELECT * FROM {votingapi_vote} v WHERE content_type='%s' AND content_id=%d $filter_string", $content_type, $content_id); while ($vobj = db_fetch_object($result)) { // Give other modules a chance to alter the vote object, add additional data, etc. votingapi_invoke('load', $vobj); $votes[] = $vobj; } return $votes; } /** * A simple helper function that returns all votes cast by a given user for a piece of content. * * @param $content_type * A string identifying the type of content whose votes are being retrieved. Node, comment, aggregator item, etc. * @param $content_id * The key ID of the content whose votes are being retrieved. * @param $uid * The integer uid of the user whose votes should be retrieved. * @return * An array of matching votingapi_vote records. */ function votingapi_get_user_votes($content_type, $content_id, $uid = NULL) { if ($uid == NULL) { global $user; $uid = $user->uid; } return _votingapi_get_raw_votes($content_type, $content_id, NULL, NULL, $uid); } /** * A helper function that returns an array of votes for one piece of content, keyed by the user who cast them. * * @param $content_type * A string identifying the type of content whose votes are being retrieved. Node, comment, aggregator item, etc. * @param $content_id * The key ID of the content whose votes are being retrieved. * @return * An array of matching votingapi_vote records. */ function votingapi_get_content_votes($content_type, $content_id) { $votes = _votingapi_get_raw_votes($content_type, $content_id); $uid_keyed = array(); foreach ($votes as $vote) { $uid_keyed[$vote->uid][] = $vote; } return $uid_keyed; } /** * A simple helper function that returns a single vote record. * * @param $content_type * A string identifying the type of content whose votes are being retrieved. Node, comment, aggregator item, etc. * @param $content_id * The key ID of the content whose votes are being retrieved. * @param $value_type * An optional int representing the type of value saved in the vote. Three standard types are defined by votingapi: * 'percent' -- 'Value' is a number from 0-100. The API will cache an average for all votes. * 'points' -- 'Value' is a positive or negative int. The API will cache the sum of all votes. * 'option' -- 'Value' is an int representing a specific option. The API will cache a vote-count for each option. * @param $tag * A string to separate multiple voting criteria. For example, a voting system that rates software for 'stability' * and 'features' would cast two votes, each with a different tag. If none is specified, the default 'vote' tag is used. * @param $uid * A string to indicate the aggregate function being retrieved (average, count, max, etc) * @return * A single vote object. */ function votingapi_get_vote($content_type, $content_id, $value_type, $tag, $uid) { $result = db_query("SELECT * FROM {votingapi_vote} v WHERE content_type='%s' AND content_id=%d AND value_type='%s' AND tag='%s' AND uid=%d", $content_type, $content_id, $value_type, $tag, $uid); $vote = db_fetch_object($result); return $vote; } function votingapi_get_vote_by_id($vote_id) { $result = db_query("SELECT * FROM {votingapi_vote} v WHERE vote_id = %d", $vote_id); $vote = db_fetch_object($result); return $vote; } /** * A simple helper function that returns the cached voting results for a given piece of content. * * @param $content_type * A string identifying the type of content whose votes are being retrieved. Node, comment, aggregator item, etc. * @param $content_id * The key ID of the content whose votes are being retrieved. * @return * An array of matching votingapi_cache records. */ function votingapi_get_voting_results($content_type, $content_id) { $voting_results = array(); $result = db_query("SELECT * FROM {votingapi_cache} v WHERE content_type='%s' AND content_id=%d", $content_type, $content_id); while ($cached = db_fetch_object($result)) { $voting_results[] = $cached; } return $voting_results; } /** * A simple helper function that returns a single cached voting result. * * @param $content_type * A string identifying the type of content whose votes are being retrieved. Node, comment, aggregator item, etc. * @param $content_id * The key ID of the content whose votes are being retrieved. * @param $value_type * An optional int representing the type of value saved in the vote. Three standard types are defined by votingapi: * 'percent' -- 'Value' is a number from 0-100. The API will cache an average for all votes. * 'points' -- 'Value' is a positive or negative int. The API will cache the sum of all votes. * 'option' -- 'Value' is an int representing a specific option. The API will cache a vote-count for each option. * @param $tag * A string to separate multiple voting criteria. For example, a voting system that rates software for 'stability' * and 'features' would cast two votes, each with a different tag. If none is specified, the default 'vote' tag is used. * @param $function * A string to indicate the aggregate function being retrieved (average, count, max, etc) * @return * A single voting result object. */ function votingapi_get_voting_result($content_type, $content_id, $value_type, $tag, $function) { $result = db_query("SELECT * FROM {votingapi_cache} v WHERE content_type='%s' AND content_id=%d AND value_type='%s' AND tag='%s' AND function='%s'", $content_type, $content_id, $value_type, $tag, $function); while ($cached = db_fetch_object($result)) { $voting_results = $cached; } return $voting_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 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: * array($tag => array($value_type => array($calculation_function => $value))); */ 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. db_query("DELETE FROM {votingapi_cache} WHERE content_type = '%s' AND content_id = %d", $content_type, $content_id); $cache = array(); $votes = _votingapi_get_raw_votes($content_type, $content_id); // 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. votingapi_invoke('calculate', $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[] = _votingapi_insert_cache_result($content_type, $content_id, $value, $type, $tag, $function); } } } // Give other modules a chance to act on the results of the vote totaling. votingapi_invoke('results', $cached, $votes, $content_type, $content_id); // If the actions module is enabled, hand off the voting results // for possible node promotion, user banning, or what not. if (module_exist('actions') && variable_get('votingapi_actions_mode', 'all') != 'none') { _votingapi_process_actions($content_id, $content_type, $votes, $cached); } return $cached; } } /** * The following three functions can be used by external modules, themes, * etc. to display formatted results for a vote object or a vote result * object. It's up to a VotingAPI module to catch the 'format' $op, and * return something that makes sense. The first module that responds 'wins', * so try to filter your responses sensibly. If it's not a voting object * your module would normally create, just return;. The next module can * take a stab. */ function votingapi_format_tag($vobj) { $vals = votingapi_invoke('format', $vobj, 'tag'); if (isset($vals[0])) { return $vals[0]; } else { return ucfirst($vobj->tag); } } function votingapi_format_value($vobj) { $vals = votingapi_invoke('format', $vobj, 'value'); if (isset($vals[0])) { return $vals[0]; } else { return $vobj->value; } } function votingapi_format_result($vobj) { $vals = votingapi_invoke('format', $vobj, 'result'); if (isset($vals[0])) { return $vals[0]; } else { return $vobj->value; } } /** * 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); } } /** * Insert a cached aggregate vote for a given piece of content. * * @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. * @param $value * An int representing the aggregate value of the votes cast. * @param $value_type * An int representing the value_type shared by the aggregated votes. * @param $tag * A string to separate multiple voting criteria. For example, a voting system that rates software for 'stability' * and 'features' would cast two votes, each with a different tag. If none is specified, the default 'vote' tag is used. * @param $function * The summary function being used to aggregate the votes. 'sum', 'count', and 'average' are used by votingapi. * If the value_type is 'option', this field contains the key. Slightly ugly, but oh well. * @return * The resulting cached object. */ function _votingapi_insert_cache_result($content_type, $content_id, $value, $value_type, $tag, $function) { $vobj->vote_cache_id = db_next_id('{votingapi_cache}'); $vobj->content_type = $content_type; $vobj->content_id = $content_id; $vobj->value = $value; $vobj->value_type = $value_type; $vobj->tag = $tag; $vobj->function = $function; $vobj->timestamp = time(); db_query("INSERT INTO {votingapi_cache} (vote_cache_id, content_type, content_id, value, value_type, tag, function, timestamp) VALUES (%d, '%s', %d, %f, '%s', '%s', '%s', %d)", $vobj->vote_cache_id, $vobj->content_type, $vobj->content_id, $vobj->value, $vobj->value_type, $vobj->tag, $vobj->function, $vobj->timestamp); return $vobj; } /** * Invoke a hook_votingapi_*() operation. */ function votingapi_invoke($hook, &$a1, $a2 = NULL, $a3 = NULL, $a4 = NULL, $a5 = NULL, $a6 = NULL) { $hook = 'votingapi_' . $hook; foreach (module_implements($hook) as $module) { $function = $module . '_' . $hook; $result = ($function($a1, $a2, $a3, $a4, $a5, $a6)); if (is_array($result)) { $return = array_merge($return, $result); } else if (isset($result)) { $return[] = $result; } } return $return; } /** * A helper function that loads content from type/id pairs. * * @param $content_type * The content type to be loaded. node, comment, aggregator-item, and user are * supported. * @param $content_id * The key id of the content to be loaded. * @return * The loaded content type. */ function _votingapi_load_content($content_id = 0, $content_type = 'node') { switch ($content_type) { case 'node': $content = node_load($content_id); break; case 'comment': $content = _comment_load($content_id); break; case 'aggregator-item': $result = db_query('SELECT * FROM {aggregator_item} WHERE iid = %d', $content_id); $content = db_fetch_object($result); break; case 'user': $content = user_load(array('uid' => $content_id)); break; case 'term': $content = taxonomy_get_term($content_id); break; } return $content; } // A series of helper functions mostly useful for making selectable lists of // vote and cache properties. function votingapi_cache_functions() { static $votingapi_cache_functions; if (!is_array($votingapi_cache_functions)) { $votingapi_cache_functions = array( 'average' => t('Average vote value'), 'count' => t('Number of votes'), 'sum' => t('Sum of vote values'), ); $result = db_query('SELECT DISTINCT function FROM {votingapi_cache}'); while ($fnc = db_fetch_object($result)) { if (!isset($votingapi_cache_functions[$fnc->function])) { $votingapi_cache_functions[$fnc->function] = $fnc->function; } } } return $votingapi_cache_functions; } function votingapi_cache_tags() { static $votingapi_cache_tags; if (!is_array($votingapi_cache_tags)) { $votingapi_cache_tags[0] = t('-- Any tag --'); $result = db_query('SELECT DISTINCT tag FROM {votingapi_cache}'); while ($fnc = db_fetch_object($result)) { if (!isset($votingapi_cache_tags[$fnc->tag])) { $votingapi_cache_tags[$fnc->tag] = $fnc->tag; } } } return $votingapi_cache_tags; } function votingapi_cache_value_types() { static $votingapi_cache_value_types; if (!is_array($votingapi_cache_value_types)) { $votingapi_cache_value_types[0] = t('-- Any value type --'); $result = db_query('SELECT DISTINCT value_type FROM {votingapi_cache}'); while ($fnc = db_fetch_object($result)) { if (!isset($votingapi_cache_value_types[$fnc->value_type])) { $votingapi_cache_value_types[$fnc->value_type] = $fnc->value_type; } } } return $votingapi_cache_value_types; } function votingapi_vote_tags() { static $votingapi_vote_tags; if (!is_array($votingapi_vote_tags)) { $votingapi_vote_tags[0] = t('-- Any tag --'); $result = db_query('SELECT DISTINCT tag FROM {votingapi_vote}'); while ($fnc = db_fetch_object($result)) { if (!isset($votingapi_vote_tags[$fnc->tag])) { $votingapi_vote_tags[$fnc->tag] = $fnc->tag; } } } return $votingapi_vote_tags; } function votingapi_vote_value_types() { static $votingapi_vote_value_types; if (!is_array($votingapi_vote_value_types)) { $votingapi_vote_value_types[0] = t('-- Any value type --'); $result = db_query('SELECT DISTINCT value_type FROM {votingapi_vote}'); while ($fnc = db_fetch_object($result)) { if (!isset($votingapi_vote_value_types[$fnc->value_type])) { $votingapi_vote_value_types[$fnc->value_type] = $fnc->value_type; } } } return $votingapi_vote_value_types; }