'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 . '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 .= '
'; for ($n=1; $n <= $stars; $n++) { $star_value = ceil((100/$stars) * $n); $prev_star_value = ceil((100/$stars) * ($n-1)); if ($rating < $star_value && $rating > $prev_star_value) { $percent = (($rating - $prev_star_value) / ($star_value - $prev_star_value)) * 100; $output .= '
' . $n . '
'; } elseif ($rating >= $star_value) { $output .= '
' . $n . '
'; } else { $output .= '
' . $n . '
'; } } $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); } }