Building the Module
Open up the empty plus1. module in a text editor and add the standard Drupal header documentation:
* A simple +1 voting widget.
Next you'll start knocking off the Drupal hooks you're going to use. An easy one is the use of hook_perm (), which lets you add the "rate content" permission to Drupal's role-based access control page. You'll use this permission to prevent anonymous users from voting without first creating an account or logging in.
* Implementation of hook_perm().
function plus1_perm() { return array('rate content');
Now you'll begin to implement some Ajax functionality. One of the great features of jQuery is its ability to submit its own HTTP GET or POST requests, which is how you'll submit the vote to Drupal without refreshing the entire page. jQuery will intercept the clicking on a Vote link and will send a request to Drupal to save the vote and return the score. jQuery will use the new score value to update the score on the page. Figure 17-4 shows a "big picture" overview of where we're going.
You voted
Figure 17-4. Overview of the vote updating process
You voted
Figure 17-4. Overview of the vote updating process
Once jQuery intercepts the clicking of the Vote link, it needs to be able to hand the URL over to Drupal for submission. We'll use hook_menu() to map the vote URL submitted by jQuery to a Drupal PHP function. The PHP function saves the vote to the database and returns the new score to jQuery in JavaScript Object Notation (JSON).
* Implementation of hook_menu().
function plus1_menu($may_cache) { $items = array(); if ($may_cache) { $items[] = array(
return $items;
In the preceding function, whenever a request for the path plus1/vote comes in, the function plus1_vote() handles it when the user requesting the path has the "rate content" permission. The path plus1/vote/3 translates into the PHP function call plus1_vote(3) (see Chapter 4, about Drupal's menu/callback system, for more details).
* Called by jQuery.
* This submits the vote request and returns JSON to be parsed by jQuery. */
function plus1_vote($nid) { global $user;
// Authors may not vote on their own posts.
$is_author = db_result(db_query('SELECT uid FROM {node} WHERE nid = %d AND uid = %d', $nid, $user->uid));
// Before processing the vote we check that the user is logged in, // we have a node ID, and the user is not the author of the node. if ($user->uid && ($nid > 0) && !$is_author) { $vote = plus1_get_vote($nid, $user->uid); if (!$vote) { $values = array( 'uid' => $user->uid, 'nid' => $nid, 'vote' => 1, );
plus1_vote_save($values);
watchdog('plus1', t('Vote by @user accepted', array('@user' => $user->name))); $score = plus1_get_score($nid);
// This print statement will return results to jQuery's request. print drupal_to_js(array( 'score' => $score, 'voted' => t('You voted') )
The preceding plus1_vote() function saves the current vote and returns information to jQuery in the form of an associative array containing the new score and the string You voted, which replaces the "Vote" text underneath the voting widget. We pass in the t('You voted') string rather than creating it in jQuery so it remains translatable to other languages. This array is passed into drupal_to_js(), which converts PHP variables into their JavaScript equivalents, in this case converting a PHP associative array to a JavaScript associative array. Drupal serializes the JavaScript into the JSON format (for more on JSON, see http://en.wikipedia.org/
wiki/JSON). Now, we called a couple basic functions in the preceding code, so let's create those:
* Return the number of votes for a given node ID/user ID pair.
* @return Integer
* Number of votes the user has cast on this node.
function plus1_get_vote($nid, $uid) { return (int) db_result(db_query('SELECT vote FROM {plus1_vote} WHERE nid = %d AND uid = %d', $nid, $uid));
* Return the total score of a node.
* @return Integer
function plus1_get_score($nid) { return (int) db_result(db_query('SELECT SUM(vote) FROM {plus1_vote} WHERE nid = %d', $nid));
* An array of the values to save to the database.
function plus1_vote_save($values) { db_query('DELETE FROM {plus1_vote} WHERE uid = %d AND nid = %d', $values['uid'], $values['nid']);
db_query('INSERT INTO {plus1_vote} (uid, nid, vote, created) VALUES (%d, %d, %d, %d)', $values['uid'], $values['nid'], $values['vote'], time());
Now that the basic getter and setter functions are in place, let's focus on getting the voting widget to display alongside the posts:
* Create voting widget to display on the webpage.
function plus1_jquery_widget($nid) { // Load the JavaScript and CSS files.
drupal_add_js(drupal_get_path('module', 'plus1') .'/jquery.plus1.js'); drupal_add_css(drupal_get_path('module', 'plus1') .'/plus1.css');
global $user;
$is_author = db_result(db_query('SELECT uid FROM {node} WHERE nid = %d
AND uid = %d', $nid, $user->uid)); $voted = plus1_get_vote($nid, $user->uid);
return theme('plus1_widget', $nid, $score, $is_author, $voted);
* Theme for the voting widget.
function theme_plus1_widget($nid, $score, $is_author, $voted) { $output = '<div class="plus1-widget">'; $output .= '<div class="score">'; $output .= $score; $output .= '</div>';
if ($is_author) { // User is author; not allowed to vote. $output .= t('Votes');
elseif ($voted) { // User already voted. $output .= t('You voted');
else { // User is eligible to vote. // The class plusl-link is what we will search for in our jQuery later. $output .= l(t('Vote'), "plus1/vote/$nid", array('class' => 'plusl-link'));
$output .= '</div>'; $output .= '</div>';
return $output;
In plus1_jquery_widget() in the preceding code, we make sure the corresponding CSS and JavaScript files are loaded, and then hand off the theming of the widget to a custom theme function we created called theme_plus1_widget(). Keep in mind that theme( 'plus1_widget') actually calls theme_plus1_widget () (see Chapter 8 for how that works). Creating a separate theme function rather than building the HTML inside the plus1_jquery_widget() function allows designers to override this function if they want to change the markup. Our theme function, theme_plus1_widget(), makes sure to create CSS class selectors for the key HTML components to make targeting within jQuery really easy. Also, take a look at the URL of the link. It's pointing to plus1/vote/$nid, where $nid is the current node ID of the post. When the user clicks on the link, it will be intercepted and processed by jQuery instead of Drupal. This happens because we'll wire jQuery up to watch for the onClick event for that link. See how we defined the plus1-link CSS selector when building the link? Look for that selector to appear in our JavaScript later on as a. plus1- link. That is, an anchor (<a>) HTML element with the CSS class plus1-link.
The plus1_jquery_widget() function is what generates the voting widget to be sent to the browser. You want this widget to appear in node views so that users can use it to vote on the node they're looking at. Can you guess which Drupal hook would be a good one to use? It's our old friend hook_nodeapi(), which allows us to modify any node as it's being built.
* Implementation of hook_nodeapi(). */
function plus1_nodeapi(&$node, $op, $teaser, $page) { switch ($op) { case 'view':
// Show the widget, but only if the full node is being displayed. if (!$teaser) { $node->content['plus1_widget'] = array(
'#value' => plus1_jquery_widget($node->nid), '#weight' => 100,
break; case 'delete':
db_query('DELETE FROM {plus1_vote} WHERE nid = %d', $node->nid); break;
We set the weight element to a large (or "heavy") number so that it shows at the bottom rather than the top of the post. We sneak a delete case in to remove voting records for a node when that node is deleted.
That's it for the content of plus1.module. All that's left until our module is complete is filling out jquery.plus1.js, which is a meager 15 lines of code!
// Global killswitch: only run if we are in a supported browser. if (Drupal.jsEnabled) { $(document).ready(function(){ $('a.plus1-link').click(function(){ var voteSaved = function (data) { var result = Drupal.parseJson(data); $('div.score').fadeIn('slow').html(result['score']); $('div.vote').html(result['voted']);
$.get(this.href, null, voteSaved); return false; }); });
You should wrap all your jQuery code in a Drupal.jsEnabled test. This test makes sure certain DOM methods are supported within the current browser (if they're not, there's no point in our JavaScript being run).
This JavaScript adds an event listener to a.plus1-link (remember we defined plus1-link as a CSS selector?) so that when users click the link it fires off an HTTP GET request to the URL it's pointing to. After that request is completed, the return value (sent over from Drupal) is passed as the data parameter into the anonymous function that's assigned to the variable voteSaved. The return value is a JavaScript array serialized in JSON format, so you unserialize it with Drupal.parseJson(). The array is referenced by the associative array keys that were initially built in the plus1_vote() function inside Drupal. Finally, the JavaScript updates the score and changes the "Vote" text to "You voted."
To prevent the entire page from reloading (because this is an Ajax request), use a return value of false from the JavaScript jQuery function.
■Tip If you're following along at home and the widget doesn't seem to be functioning, double-check that you aren't logged in as the user that created the content (because users can't vote on their own content) and that your voting user has the "rate content" permission. A great add-on to debug Ajax requests is the Firefox extension called Firebug, which you can download at http://getfirebug.com/.
Average user rating: 3 stars out of 2 votes
Post a comment