Overriding The Poll Module In Drupal 6

The Poll module is a useful little module that comes with Drupal and allows the addition of simple polls to pages or blocks. However, there was one major issue that I wanted to correct on a certain site, but I didn't want to directly edit the core functionality of the module. The default behaviour of the module is to record one vote per IP address or user, which is fine for normal uses but in some situations it does tend to fall over. A Drupal site with one administrator that allow anonymous users to vote on your polls seems fine, but lets say that this site wants votes to come from people in the business world. The problem here is that multiple people might work for the same business, be situtated behind a firewall and therefore would have the same IP address. What this means is that if one person in that company votes on your poll it will block all other people from voting for that poll.

To override the default behaviour of the Poll module I created a new module called Poll Override and intercepted the Poll module in key areas to allow a session based vote logging approach. The second thing I wanted to achive from this new module was to change the location of the "you have voted" message so that it appeared within the poll area, rather than at the top of the screen. Doing these two things is a useful way of understanding how Drupal works.

The first task is to create a .info file called poll_override.info.

; $Id$

name = Poll Override
description = "Override some of the functionality of the poll module"
package = "#! code"
dependencies[] = poll
core = 6.x<

Next, we create a .module file (called poll_override.module) and enter a description.

<?php
/**
 * @file
 * Overrides some of the funcionality of the Poll module.
 *
 * Changes the way in which anonymous users vote and how voting 
 * confirmation messages are displayed.
 *
 */

Put both of these files in a directory called poll_override and put it in the sites/default/modules directory of your Drupal install.

The first thing to do is to create a function called poll_override_form_poll_view_voting_alter(), which is an implementation of hook_form_alter(). The only purpose of this function is to rewrite where the submit of the vote form of the Poll module goes to. In this case we are directing it to a function called poll_override_form_submit().

/**
  * Implementation of hook_form_alter().
  *
  */
function poll_override_form_poll_view_voting_alter(&$form, &$form_state){
  // Override the normal form submit for a submit function in this module.
  $form['vote']['#submit'] = array('poll_override_form_submit');   
}

Next we need to create the poll_override_form_submit() to handle the action of someone voting on a poll. When the poll module is installed it creates three tables, these are as follows:

  • poll - This stores details of the polls avaialable on the system.
  • poll_choices - This stores the questions for each poll.
  • poll_votes - This stores the votes made against each poll.

The table we are interested in is poll_votes. The normal behaviour of the Poll module is to store the vote along with the IP address of the user who submitted the vote and use this to varify the user the next time around. Rather than override this completely we will allow users without cookies enabled to vote using the old IP address method, and include an if statement that stores the session key for everyone else. Finally, we add an item called poll_override_message to the $_SESSION array so that we can print out a message when we refresh the page after the vote has been recorded.

/**
 * Submit function for Poll Override module.
 *
 */
function poll_override_form_submit($form, &$form_state) {
  $node = $form['#node'];
  $choice = $form_state['values']['choice'];
 
  global $user;
  if ($user->uid) {
    db_query('INSERT INTO {poll_votes} (nid, chorder, uid) VALUES (%d, %d, %d)', $node->nid, $choice, $user->uid);
  }
  else {
    if ( isset($_COOKIE[session_name()]) ) {
      // If a cookie has been set for this user then use the session id to record the vote.
      db_query("INSERT INTO {poll_votes} (nid, chorder, hostname) VALUES (%d, %d, '%s')", $node->nid, $choice, $_COOKIE[session_name()]);
    } else {
      // Otherwise just use the IP address (this is the normal functionality).
      db_query("INSERT INTO {poll_votes} (nid, chorder, hostname) VALUES (%d, %d, '%s')", $node->nid, $choice, ip_address());
    }
  }
 
  // Add one to the votes.
  db_query("UPDATE {poll_choices} SET chvotes = chvotes + 1 WHERE nid = %d AND chorder = %d", $node->nid, $choice);
 
  // Instead of using drupal_set_message() to set the message we will just use a session variable.
  // This will be deleted after the page has loaded.
  $_SESSION['poll_override_message'] = t('Your vote was recorded.');
  // Return the user to whatever page they voted from.
}

Now we have sorted out how we are storing the votes we need to display the poll. This means that we either display the voting form or a set of results. What we need to do here is to intercept the poll node before it is displayed and detect whether it is still eligable for voting or not. We do this with a call to the poll_override_nodeapi() function, which is a implentation of hook_nopeapi(). This function might look complicated but we are essentially running the following steps.

  • Check if we are loading the node and if the node is a poll.
  • Load the poll and the associated choices for that poll.
  • Set a variable of the node called allowvotes to false. This is used to display either the results (if false) or a voting form (if true).
  • Detect if the user is able to vote on polls and if the poll is active. If not then we skip the rest of this function and the form is not displayed.
  • If the user object has a uid property then this user is logged in so we load the vote using this information.
  • If the user is not logged in we can either use the session cookie or the IP address depending on the information we have available.
  • Finally, if a vote has been detected in the last 2 steps we pass this onto the node, otherwise we pass nothing and set allowvotes to true so that the user can vote.

Here is the code in full.

/**
 * Implementation of hook_nodeapi().
 *
 */
function poll_override_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL ) {
  if ( $op == 'load' && $node->type == 'poll' ) {    
      global $user;
      
      $poll = db_fetch_object(db_query("SELECT runtime, active FROM {poll} WHERE nid = %d", $node->nid));
 
      // Load the appropriate choices into the $poll object.
      $result = db_query("SELECT chtext, chvotes, chorder FROM {poll_choices} WHERE nid = %d ORDER BY chorder", $node->nid);
      while ( $choice = db_fetch_array($result) ) {
        $node->choice[$choice['chorder']] = $choice;
      }
 
      // Determine whether or not this user is allowed to vote.
      $node->allowvotes = FALSE;
      if ( user_access('vote on polls') && $poll->active ) {
        if ( $user->uid ) {
          $result = db_fetch_object(db_query('SELECT chorder FROM {poll_votes} WHERE nid = %d AND uid = %d', $node->nid, $user->uid));
        } else {
          if ( isset($_COOKIE[session_name()]) ) {        
            // If a cookie has been set for this user then use the session id to retreive the vote.
            $result = db_fetch_object(db_query("SELECT chorder FROM {poll_votes} WHERE nid = %d AND hostname = '%s'", $node->nid, $_COOKIE[session_name()]));
          } else {
            // Otherwise just use the IP address (this is the normal functionality).
            $result = db_fetch_object(db_query("SELECT chorder FROM {poll_votes} WHERE nid = %d AND hostname = '%s'", $node->nid, ip_address()));
          }
        }
        if ( isset($result->chorder) ) {
          $node->vote = $result->chorder;
        } else {
          $node->vote = -1;
          $node->allowvotes = TRUE;
        }
      }     
  }
}

The Poll module includes a function called theme_preprocess_poll_results(). This is essentially a low level hook that we can override by using a more specific theme function. So by calling a fucntion poll_override_preprocess_poll_results() we can override the theme_preprocess_poll_results() function within the Poll module and change the output of the results block. This code will be run when printing out the results of a poll and does two things. It is possible to cancel the vote on a poll so the first thing we do is to include the form that allows users to do this. The second thing we do it detect if the poll_override_message has been set, and if so assign this value to the $variables array (which will be passed to the template) and unset the item in the $_SESSION array.

/**
 * Implementation of theme_preprocess_poll_results()
 *
 */
function poll_override_preprocess_poll_results(&$variables) {
  $variables['links'] = theme('links', $variables['raw_links']);
  if ( isset($variables['vote']) && $variables['vote'] > -1 && user_access('cancel own vote') ) {
    $variables['cancel_form'] = drupal_get_form('poll_cancel_form', $variables['nid']);
  }
  $variables['title'] = check_plain($variables['raw_title']);
 
  // If this is a block, allow a different tpl.php to be used.
  if ( $variables['block'] ) {
    $variables['template_files'][] = 'poll-results-block';
    if ( isset($_SESSION['poll_override_message']) ) {
      $variables['message'] = $_SESSION['poll_override_message'];
      unset($_SESSION['poll_override_message']);
    }
  }
}

The very final step is to create a file called poll-results-block.tpl.php and include the following code.

<div class="poll">
  <div class="title"><?php print $title ?></div>
  <?php print $results ?>
  <div class="total">
    <?php print t('Total votes: @votes', array('@votes' => $votes)); ?>
  </div>
  <?php
  if ( isset($message) ) {
    print '<p class="voted">'.$message.'</p>';
  }
  ?>  
</div>
<div class="links"><?php print $links; ?></div>

The $message variable contains the message that might have been set in the poll_override_nodeapi() function, if it is set here then we print it out. You can include this file in the poll_module directory or within your template directory, this is up to you.

Comments

Nice piece of work, and one of the best descriptions of how you code up a module that I've read. Is there a way to modify this process so an authorized user is allowed to vote once a day?
Permalink
Thanks for the feedback! It should be fairly straightforward. There isn't any way of storing a timestamp in the poll_votes table, so you would need to add that field and use the time() function to store a timestamp when the user voted. There are 86400 seconds in a day so when you retrieve the vote you would need to check for anything less than (time()-86400).
Name
Philip Norton
Permalink
Great code! This should be in the Drupal handbook since lots of people are struggling with this. Two things I'm pondering about: * Cookies are great, but not very safe: it's easy enough to delete them and manipulate the poll. Now you've given us a great insight in how to modify the poll, it's pretty easy to devise alternative ways to enhance it. * I'm no fan of the cache_clear_all() near the end of your submit handler. Your essentially flushing the entire cache of all expireable data. This function should be used with care since it can be a performance monster. I also don't really why you'd put it there since it doesn't really do anything related to the functionality you've written. @Sean: I wouldn't add an extra column to an existing Drupal core database table. It's a bit messing with the preset database schema and it could bit you back when you want to upgrade and there have been changes to it. You could try to serialize the timestamp with the cookie/ip data. Or just create a totally new table all together.
Permalink
...And some code for the 'cancel your vote' functionality...
/**
 * Implementation of hook_form_alter().
 *
 */ 
function across_poll_override_form_poll_cancel_form_alter(&$form, &$form_state) {
  // Override the normal form submit for a submit function in this module.
  $form['submit']['#submit'] = array('across_poll_override_cancel_submit');
}
 
function across_poll_override_cancel_submit($form, &$form_state) {
  $node = node_load($form['#nid']);
  global $user;
 
  if ($user->uid) {
    db_query('DELETE FROM {poll_votes} WHERE nid = %d and uid = %d', $node->nid, $user->uid);
  } else {
    if ( isset($_COOKIE[session_name()]) ) { 
      db_query("DELETE FROM {poll_votes} WHERE nid = %d and hostname = '%s'", $node->nid, $_COOKIE[session_name()]);
    } else {
      db_query("DELETE FROM {poll_votes} WHERE nid = %d and hostname = '%s'", $node->nid, ip_address());
    }
  }
 
  // Subtract from the votes.
  db_query("UPDATE {poll_choices} SET chvotes = chvotes - 1 WHERE nid = %d AND chorder = %d", $node->nid, $node->vote);
}
}
Permalink
@Matthias - Thanks for pointing that out about the cache_clear_all(). I have no idea why I put that in there... But you are right about it being pointless, so I have removed it! Cookies are easily deleted, but in this case I created this module to allow any user to vote on a poll. I took the decision early on that I didn't really care about people voting and then clearing cookies. Of course you could even vote in multiple browsers, but it's only a simple poll. Interesting point about editing the table. I have been working with Drupal for about 3 months now and there are some key methodologies I still need to get my head around. So thanks for the tip. :)
Name
Philip Norton
Permalink
@Matthias - You posted that cancel function before I could post my first reply. Thank you very much for the contribution!
Name
Philip Norton
Permalink
I'm getting this in the module section: "Override some of the functionality of the poll module Afhænger af: Array (mangler)" Last part would be translated to something like "depends on: Array (missing)" If I change "dependencies[] = poll" to "dependencies = poll" then I can active the module, but then every submit returns a blank page (and if I then place the cursor in theaddress bar and hit enter, then the page is shown) I'm using Drupal5, that's probably why. Do you have a solution for DP5? Kind regards Kim
Permalink
It might be due to something in Drupal 5, but I have honestly never used anything other than version 6 so I can't really help here. Sorry! If any of readers wishes to submit a similar article for Drupal 5 then I would me more than happy to publish it. :)
Name
Philip Norton
Permalink
Thanks for the article. I wanted to leave a couple of tips from my experience playing around with your module - 1. I found out the use of "cache_clear_all" the hard way. If I am redirecting the form after submission to the *same* page, the new vote cast by the user is not updated. Doing a "cache_clear_all" solves this. 2. Also, I tried removing the initial 2 DB calls in the custom poll_nodeapi since the original poll_load was populating "node" as well. However, in some cases poll_nodeapi gets called without poll_load being called. I dont know what use case causes this. But be aware that this optimization may not work. My $.02. -Prathaban
Permalink
Very helpful. Thanks for the posting.
Permalink
As I can notice this will duplicate records in poll_votes for anonymous users. There is nothing to prevent it. Am I right?
Permalink
Hello Philip, Its really nice article you have written. I tried using this but i was stuck. Its still not allowing anonymous users to vote multiple times. voted as anonymous user(without login) and cleared the browser cache and refreshed the page, but still it didnt allow me to vote again. When I checked by printing module, i found that if (isset($_COOKIE[session_name()]) ) is never true. and $_COOKIE[session_name()] is always empty. Is there any other module required? thanks in advance for help
Permalink
There shouldn't be any other module required to run this code. $_COOKIE is a superglobal array and session_name() is a PHP function so it should all run fine. One thing you might want to check for is that the session cookie is actually being created.
Name
Philip Norton
Permalink
... I'm looking to make my poll have once a day functionality, is the code to do this around anywhere? ...
Permalink
i have installed the poll overriding module its working fine, but found propblem that every time i need to click twise to update the poll its create difficulty. could you advise that how i can update the poll on one click.
Permalink
Just what I needed, thanks man
Permalink
@philipnorton42 Great post but there is a glitch with the naming convention, you named the nodeapi hook wrongly, it should of course be poll_override_nodeapi() The code only works because there is no poll_nodeapi in the poll module ( and doesn't crash due to duplicate php functions). The module will limp along oblivious to this error but if you add and more cck fields to the poll content type they will NOT show up in node object in the hook. It drove me nuts till i spotted it
Permalink
@ndmaque Good spot! I have updated the post with the correct hook name. Thanks for the information! :)
Name
Philip Norton
Permalink
oops another minor tweak, i reckon load in $op == load should be a string ie; 'load' function poll_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL ) { if ( $op == load && $node->type == 'poll' ) { // should be this function poll_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL ) { if ( $op == 'load' && $node->type == 'poll' ) {
Permalink
On the ball today @ndmaque! Clearly unlike I was when I added this post :S Corrected :)
Name
Philip Norton
Permalink

I want to do something similar to what you did.  I'd like to simply override the default Poll module with a new poll_view function in my poll_override module.  However, I setup the .module and .info files as you specified and enabled the new module. But it does not execute my overriden function.  Any ideas as to what to look for?  Thanks!

Permalink

Your poll_view function should have the same name as your module in order for it to hook in properly. A good test is to add something like die('pollview') to the overridden function to make sure that the function is actually being called. If it is then you can move on to figure out what is going on down the line.

Name
Philip Norton
Permalink

I've made a couple of modifications for my own use case, which is to allow unlimited voting by all users:

<code>function poll_override_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL ) {
  if ( $op == 'load' && $node->type == 'poll' ) {   
      global $user;
     
      $poll = db_fetch_object(db_query("SELECT runtime, active FROM {poll} WHERE nid = %d", $node->nid));
 
      // Load the appropriate choices into the $poll object.
      $result = db_query("SELECT chtext, chvotes, chorder FROM {poll_choices} WHERE nid = %d ORDER BY chorder", $node->nid);
      while ( $choice = db_fetch_array($result) ) {
        $node->choice[$choice['chorder']] = $choice;
      }
 
      // Determine whether or not this user is allowed to vote.
      $node->allowvotes = FALSE;
      if ( user_access('vote on polls') && $poll->active ) {
        $node->vote = -1;
        $node->allowvotes = TRUE;
      }  
  }
}</code>

The problem I encountered was duplicate rows in the poll_votes table if someone tried to vote multiple times (from the same session, e.g.). To solve this, I simply replaced the hostname field with a random number as follows:

<code>function poll_override_form_submit($form, &$form_state) {
  $node = $form['#node'];
  $choice = $form_state['values']['choice'];
 
  // just use a random number in place of IP address to avoid duplicate rows in the poll_votes table
  db_query("INSERT INTO {poll_votes} (nid, chorder, hostname) VALUES (%d, %d, '%s')", $node->nid, $choice, mt_rand());

 
  // Add one to the votes.
  db_query("UPDATE {poll_choices} SET chvotes = chvotes + 1 WHERE nid = %d AND chorder = %d", $node->nid, $choice);
 
  drupal_set_message(t('Your vote was recorded.'));
  // Return the user to whatever page they voted from.
}</code>

Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
2 + 4 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.