'fivestar/vote',
'callback' => 'fivestar_vote',
'type' => MENU_CALLBACK,
'access' => user_access('rate content'),
);
}
return $items;
}
/**
* Implementation of hook_init().
* Not that this will cause Drupal to post a warning on the admin screen
* when agressive caching is activated. Like CCK, Fivestar's use of hook_init
* IS compatible with agressive caching, we just need a way to annouce that.
*/
function fivestar_init() {
// ensure we are not serving a cached page
if (function_exists('drupal_set_content')) {
if (module_exists('content')) {
include_once(drupal_get_path('module', 'fivestar') . '/fivestar_field.inc');
}
}
}
/**
* Implementation of hook_perm (permissions).
* Exposes permissions for rating content, viewing aggregate ratings, and using PHP
* snippets when configuring fivestar CCK fields.
*/
function fivestar_perm() {
return array('rate content', 'view ratings', 'use PHP for fivestar target');
}
/**
* Implementation of hook_form_alter
* Adds fivestar enaable and position to the node-type configuration form.
*
*/
function fivestar_form_alter($form_id, &$form) {
if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
$form['workflow']['fivestar'] = array(
'#type' => 'fieldset',
'#title' => t('Five Star ratings'),
'#collapsible' => TRUE,
);
$form['workflow']['fivestar']['fivestar'] = array(
'#type' => 'checkbox',
'#title' => t('Enable Five Star rating'),
'#default_value' => variable_get('fivestar_'. $form['#node_type']->type, 0),
'#return_value' => 1,
);
$form['workflow']['fivestar']['fivestar_unvote'] = array(
'#type' => 'checkbox',
'#title' => t('Allow users to undo their votes'),
'#default_value' => variable_get('fivestar_unvote_'. $form['#node_type']->type, 0),
'#return_value' => 1,
);
$form['workflow']['fivestar']['fivestar_stars'] = array(
'#type' => 'select',
'#title' => t('Number of stars'),
'#options' => drupal_map_assoc(array(1,2,3,4,5,6,7,8,9,10)),
'#default_value' => variable_get('fivestar_stars_'. $form['#node_type']->type, 5),
);
$form['workflow']['fivestar']['fivestar_style'] = array(
'#type' => 'select',
'#title' => t('Five Star display style'),
'#default_value' => variable_get('fivestar_style_'. $form['#node_type']->type, 'default'),
'#options' => array(
'compact' => t('Nothing but the stars'),
'default' => t('Stars and average'),
'dual' => t('Two sets of stars'),
),
);
$form['workflow']['fivestar']['fivestar_position_teaser'] = array(
'#type' => 'select',
'#title' => t('Widget location (teaser)'),
'#default_value' => variable_get('fivestar_position_teaser_'. $form['#node_type']->type, 'hidden'),
'#options' => array(
'above' => t('Above the teaser body'),
'below' => t('Below the teaser body'),
'hidden' => t('Hidden'),
),
);
$form['workflow']['fivestar']['fivestar_position'] = array(
'#type' => 'select',
'#title' => t('Widget location (full node)'),
'#default_value' => variable_get('fivestar_position_'. $form['#node_type']->type, 'below'),
'#options' => array(
'above' => t('Above the node body'),
'below' => t('Below the node body'),
'hidden' => t('Hidden'),
),
);
}
}
/**
* Callback function for fivestar/vote path
* @param type
* A content-type to log the vote to. 'node' is the most common.
* @param cid
* A content id to log the vote to. This would be a node ID, a comment ID, etc.
* @param value
* A value from 1-100, representing the vote cast for the content.
* @return
* An XML chunk containing the results of the vote, for use by the client-side
* javascript code.
*/
function fivestar_vote($type, $cid, $value) {
$result = _fivestar_cast_vote($type, $cid, $value);
if ($type == 'node') {
$node = node_load($cid);
}
$stars = variable_get('fivestar_stars_'. (!isset($node) ? 'default' : $node->type), 5);
$output = '';
$output .= '';
$output .= '';
if (count($result)) {
foreach ($result as $data) {
$output .= '<'. $data->function .'>' . $data->value . ''. $data->function .'>';
$summary[$data->function] = $data->value;
}
}
$output .= ''. theme('fivestar_summary', $summary['average'], $summary['count'], $stars) .'';
$output .= '';
$output .= '';
$output .= ''. $value .'';
$output .= ''. $type .'';
$output .= ''. $cid .'';
$output .= '';
drupal_set_header("Content-Type: text/xml");
exit($output);
}
/**
* Internal function to handle vote casting, flood control, XSS, IP based
* voting, etc...
*/
function _fivestar_cast_vote($type, $cid, $value) {
global $user;
// Bail out if the user's trying to vote on an invalid object.
if (!_fivestar_validate_target($type, $cid)) {
return array();
}
// Prep variables for anonymous vs. registered voting
if ($user->uid) {
$uid = $user->uid;
}
else {
$uid = 0;
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$hostname = $_SERVER['HTTP_X_FORWARDED_FOR'];
}
else {
$hostname = $_SERVER['REMOTE_ADDR'];
}
}
// sanity-check the incoming values.
if (is_numeric($cid) && is_numeric($value)) {
if ($value > 100) {
$value = 100;
}
// If the vote value is 0, blindly nuke any votes for this content by the user.
if ($value == 0) {
if ($uid) {
// If the user is logged in, we'll look for votes from that uid.
$sql = "DELETE FROM {votingapi_vote} WHERE content_type='%s' AND content_id=%d AND value_type='percent' AND uid=%d";
db_query($sql, $type, $cid, $uid);
return votingapi_recalculate_results($type, $cid);
}
else {
// Otherwise, we'll look for votes from the same IP address in the past day.
$sql = "DELETE FROM {votingapi_vote} WHERE content_type='%s' AND content_id=%d AND value_type='percent' AND uid=%d AND hostname='%s' AND timestamp > %d";
db_query($sql, $type, $cid, $uid, $hostname, time() - (60 * 60 * 24));
return votingapi_recalculate_results($type, $cid);
}
}
// If the vote ISN'T zero, check for existing votes in the database.
else {
if ($uid) {
// If the user is logged in, we'll look for votes from that uid.
$sql = "SELECT vote_id FROM {votingapi_vote} WHERE content_type='%s' AND content_id=%d AND value_type='percent' AND uid=%d";
$result = db_query($sql, $type, $cid, $uid);
}
else {
// Otherwise, we'll look for votes from the same IP address in the past day.
$sql = "SELECT vote_id FROM {votingapi_vote} WHERE content_type='%s' AND content_id=%d AND value_type='percent' AND uid=%d AND hostname='%s' AND timestamp > %d";
$result = db_query($sql, $type, $cid, $uid, $hostname, time() - (60 * 60 * 24));
}
}
// If the old vote exists, either delete it (if the new one is zero)
// or change it. If it doesn't exist and the vote is non-zero, cast
// it and recalculate.
if ($old_vote = db_fetch_object($result)) {
votingapi_change_vote($old_vote, $value);
}
elseif ($value != 0) {
votingapi_add_vote($type, $cid, $value, 'percent', 'vote', $uid);
}
return votingapi_recalculate_results($type, $cid);
}
else {
return array();
}
}
/**
* Internal function to check that node-type accepts votes
*
* @param text $type type of target (currently only node is supported)
* @param text $id identifier within the type (in this case node-type)
*
* @return boolean
*/
function _fivestar_validate_target($type, $id) {
switch ($type) {
case 'node':
if ($node = node_load($id)) {
if (variable_get('fivestar_' . $node->type, 0)) {
return TRUE;
}
else {
return FALSE;
}
}
else {
return FALSE;
}
default:
return FALSE;
}
}
/**
* Implementation of hook_nodeapi()
*
* Adds the fievestar widget to the node view.
*/
function fivestar_nodeapi(&$node, $op, $teaser, $page) {
switch ($op) {
case 'view':
if ($node->in_preview == FALSE && variable_get('fivestar_' . $node->type, 0)) {
if ($teaser) {
$position = variable_get('fivestar_position_teaser_'. $node->type, 'above');
}
else {
$position = variable_get('fivestar_position_'. $node->type, 'above');
}
switch ($position) {
case 'above':
$node->content['fivestar_widget'] = array(
'#value' => fivestar_widget_form($node),
'#weight' => -10,
);
break;
case 'below':
$node->content['fivestar_widget'] = array(
'#value' => fivestar_widget_form($node),
'#weight' => 50,
);
break;
default:
// We'll do nothing.
break;
}
}
break;
}
}
function fivestar_widget_form($node) {
return drupal_get_form('fivestar_form_node_' . $node->nid, 'node', $node->nid, variable_get('fivestar_style_'. $node->type, 'default'));
}
/**
* Implementation of hook_forms. This is necessary when multiple fivestar
* forms appear on the same page, each requiring a separate form_id, but all
* using the same underlying callbacks.
*/
function fivestar_forms() {
$args = func_get_args();
if (strpos($args[0][0], 'fivestar_form') !== FALSE) {
if ($args[0][0] == 'fivestar_form_' . $args[0][1] . '_' . $args[0][2]) {
$forms[$args[0][0]] = array('callback' => 'fivestar_form');
return $forms;
}
}
}
/**
* Create the fivestar form for the current item
* Note that this is not an implementation of hook_form(). We should probably
* change the function to reflext that.
*/
function fivestar_form($content_type, $content_id, $style = 'default') {
global $user;
$current_avg = votingapi_get_voting_result($content_type, $content_id, 'percent', 'vote', 'average');
$current_count = votingapi_get_voting_result($content_type, $content_id, 'percent', 'vote', 'count');
if ($user->uid) {
$current_vote = votingapi_get_vote($content_type, $content_id, 'percent', 'vote', $user->uid);
}
else {
// If the user is anonymous, we never both loading their existing votes.
// Not only would it be hit-or-miss, it would break page caching. Safer to always
// show the 'fresh' version to anon users.
$current_vote->value = 0;
}
if ($content_type == 'node') {
$node = node_load($content_id);
}
$stars = variable_get('fivestar_stars_'. (!isset($node) ? 'default' : $node->type), 5);
$form = array();
$form['content_type'] = array(
'#type' => 'hidden',
'#value' => $content_type,
);
$form['content_id'] = array(
'#type' => 'hidden',
'#value' => $content_id,
);
$form['vote'] = array(
'#type' => 'fivestar',
'#stars' => $stars,
'#vote_count' => $current_count->value,
'#vote_average' => $current_avg->value,
'#default_value' => $current_vote->value,
'#auto_submit' => TRUE,
'#auto_submit_path' => 'fivestar/vote/' . $content_type . '/' . $content_id,
'#allow_clear' => variable_get('fivestar_unvote_'. (!isset($node) ? 'default' : $node->type), FALSE),
'#content_id' => $content_id,
);
switch ($style) {
case 'compact':
// We actually don't need anything more here.
break;
case 'default':
$form['vote']['#title'] = t('Your vote');
$form['vote']['#description'] = theme('fivestar_summary', $current_avg->value, $current_count->value, $stars);
break;
case 'dual':
$form['vote']['#title'] = t('Your vote');
$form['average'] = array(
'#type' => 'item',
'#title' => t('Current rating'),
'#value' => theme('fivestar_static', $current_avg->value, $stars)
);
break;
}
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Submit rating'),
'#attributes' => array('class' => 'fivestar-submit'),
);
$form['#attributes']['class'] = 'fivestar-widget';
$form['#base'] = 'fivestar_form';
$form['#redirect'] = FALSE;
return $form;
}
/**
* Submit handler for the above form
*/
function fivestar_form_submit($form_id, $form_values) {
if ($form_id == 'fivestar_form_' . $form_values['content_type'] . '_' . $form_values['content_id']) {
_fivestar_cast_vote($form_values['content_type'], $form_values['content_id'], $form_values['vote']);
}
}
/**
* Implementation of hook_elements
*
* Defines 'fivestar' form element type
*/
function fivestar_elements() {
$type['fivestar'] = array(
'#input' => TRUE,
'#stars' => 5,
'#allow_clear' => FALSE,
'#auto_submit' => FALSE,
'#auto_submit_path' => '',
'#process' => array('fivestar_expand' => array()),
);
return $type;
}
/**
* Format a five star voting element.
*
* @param $element
* An associative array containing the properties of the element.
* Properties used: title, value, options, description, extra, multiple, required
* @return
* A themed HTML string representing the form element.
*
* It is possible to group options together; to do this, change the format of
* $options to an associative array in which the keys are group labels, and the
* values are associative arrays in the normal $options format.
*/
function theme_fivestar($element) {
drupal_add_css(drupal_get_path('module', 'fivestar') .'/theme/fivestar.css');
$element['#children'] = '
'. $element['#children'] .'
';
if ($element['#title'] || $element['#description']) {
if (isset($element['#description'])) {
$element['#children'] .= '';
$element['#children'] .= $element['#description'] .'
';
}
unset($element['#description']);
unset($element['#id']);
return theme('form_element', $element, $element['#children']);
}
else {
return $element['#children'];
}
}
/**
* Display a plain HTML VIEW ONLY version of the widget
* with the specified rating
*
* @param $rating
* The desired rating to display out of 100 (i.e. 80 is 4 out of 5 stars)
* @param $stars
* The total number of stars this rating is out of
* @return
* A themed HTML string representing the star widget
*
*/
function theme_fivestar_static($rating, $stars = 5) {
drupal_add_css(drupal_get_path('module', 'fivestar') .'/theme/fivestar.css');
$output = '';
$output .= '';
return $output;
}
function theme_fivestar_summary($rating, $votes = 0, $stars = 5) {
if ($votes == 0) {
return t('No votes yet');
}
$stars = round(($rating * $stars) / 100, 1);
$vote_text = format_plural($votes, t('vote'), t('votes'));
return t('Average: !stars (!votes !vote_text)', array('!stars' => $stars, '!votes' => $votes, '!vote_text' => $vote_text));
}
/**
* Process callback for fivestar_element -- see fivestar_element()
*/
function fivestar_expand($element) {
// Add necessary javascript. The current javascript is checked to prevent the
// inline javascript from being duplicated.
if (strpos(drupal_get_js(), 'jquery.rating.js') === FALSE) {
drupal_add_js(drupal_get_path('module', 'fivestar') . '/jquery.rating.js');
drupal_add_js("jQuery(function(){jQuery('div.fivestar-widget').rating();});", 'inline');
}
if (strpos(drupal_get_js(), "jQuery('input.fivestar-submit').hide()") === FALSE) {
drupal_add_js("jQuery(function(){jQuery('input.fivestar-submit').hide();});", 'inline');
}
if (isset($element['#vote_count'])) {
$element['vote_count'] = array(
'#type' => 'hidden',
'#value' => $element['#vote_count'],
);
}
if (isset($element['#vote_average'])) {
$element['vote_average'] = array(
'#type' => 'hidden',
'#value' => $element['#vote_average'],
);
}
if ($element['#auto_submit'] && !empty($element['#auto_submit_path'])) {
$element['auto_submit_path'] = array(
'#type' => 'hidden',
'#value' => url($element['#auto_submit_path']),
'#attributes' => array('class' => 'fivestar-path'),
);
}
for ($i = 0; $i <= $element['#stars']; $i++) {
$this_value = ceil($i * 100/$element['#stars']);
$next_value = ceil(($i+1) * 100/$element['#stars']);
//Display clear button only if allowed
if (($element['#allow_clear'] == TRUE) || ($i > 0)){
$element['vote'][$i]['#type'] = 'radio';
$element['vote'][$i]['#return_value'] = $this_value;
$element['vote'][$i]['#attributes'] = $element['#attributes'];
$element['vote'][$i]['#parents'] = $element['#parents'];
$element['vote'][$i]['#spawned'] = TRUE;
}
// If a default value is not exactly on a radio value, round up to the next one
if ($element['#default_value'] > $this_value && $element['#default_value'] <= $next_value) {
$element['vote'][$i+1]['#default_value'] = $next_value;
}
}
return $element;
}
function fivestar_votingapi_views_formatters($details = array()) {
if ($details['value_type'] == 'percent') {
return array('fivestar_views_value_display_handler' => t('Fivestar rating'));
}
}
function fivestar_views_value_display_handler($op, $filter, $value, &$query) {
if ($value === NULL) {
return $value;
}
else {
return theme('fivestar_static', $value);
}
}