A few months ago I hosted a vote for the best-dressed Tweak Me theme. I needed to allow awesome readers like you to browse through the entries and super easily vote for their favourites.
Here was my goal:
- Allow readers to vote.
- People can only vote once so we need a certain level of security here to combat cheating.
- Allow me to view the results.
- Voting would close after a certain time period.
- I wanted to be able to deactivate the plugin when I was done (not keep it installed FOREVER).
- I wanted the voting page to maintain all its formatting even after uninstalling the plugin.
Those last two points were the most important. I didn’t want to deactivate the plugin and be left with a horrendous shortcode that didn’t work or something like that.
So, I coded my own super simple voting plugin. It did the job perfectly!
Let’s code a plugin!
Overview of how it will work.
Before we dive into the code, let’s look at a quick overview of how the plugin will work from a coding and UX standpoint.
-
We’ll add all the voting choices into a WordPress post using specific markup. The entries will be wrapped in
<div id="ng-vote">
and when people click on buttons with the classbtn
that will trigger the voting effect.Example markup:
<div id="ng-vote"> <!-- Entry #1 --> <a href="http://mostlyyabookobsessed.com/" target="_blank" rel="noopener noreferrer"><img src="https://www.nosegraze.com/wp-content/uploads/2015/08/mostly-ya-book-obsessed.jpg" alt="Mostly YA Book Obsessed" width="350" height="300" class="aligncenter size-full wp-image-24344" /></a> <p class="text-center"><a href="http://mostlyyabookobsessed.com/" class="btn btn-primary btn-block">Vote</a></p> <!-- Entry #2 --> <a href="http://bookshelfery.com/" target="_blank" rel="noopener noreferrer"><img src="https://www.nosegraze.com/wp-content/uploads/2015/08/bookshelfery.jpg" alt="BookShelfery" width="350" height="300" class="aligncenter size-full wp-image-24350"></a> <p class="text-center"><a href="http://bookshelfery.com/" class="btn btn-primary btn-block">Vote</a></p> </div>
- The click on the button will be captured via JavaScript. Since, in my case, people are voting on websites, we’re going to grab the
href
value as the thing they’re voting for (the link URL). Then we’ll pass this to PHP using ajax. - The PHP is what will handle all the vote casting. Using a combination of cookies and IP address, we’ll check to see if the person has already voted. If so, we’ll send them an error message. If not, we’ll increment the vote number for that URL. Then we’ll mark that person as having voted.
- All the votes are stored in the wp_options table. The tally can be checked by simply using
get_option()
or the WP-CLI (usingwp option get {option name}
).
Let’s set things up.
Open up your text editor of choice, my friend. I use PhpStorm because it’s AMAZEBALLS. But I also use and recommend Notepad++ (Windows) or Textwrangler (Mac).
Create a new folder called ng-vote or whatever slug you want to use for your plugin. Then create a file in there called ng-vote.php or awesome-voting-plugin.php — whatever makes you feel cool.
Tell WordPress it’s a plugin.
Now let’s enter our plugin header stuff:
<?php /* * Plugin Name: NG Vote * Plugin URI: https://www.nosegraze.com * Description: A super simple voting plugin for techy minimalists. * Version: 1.0.0 * Author: Nose Graze * Author URI: https://www.nosegraze.com * License: GPL2 */
For you newbies out there, this is a comment block that tells WordPress, “Hey, biatch! I’m a plugin! Here’s all my info.” You can change whatever values you want, like the plugin name, description, whatevs.
Bring in our soon-to-be-created file.
Next, add in this:
require_once plugin_dir_path( __FILE__ ) . 'includes/class-ng-vote.php';
This is us including a file that we haven’t created yet, but we’ll be creating that in just a few minutes, so sit tight.
And let’s get this party started!
And right below that, plop in this:
/** * Returns an instance of the voting plugin object. * Basically gets the party started. * * @return NG_Vote */ function NG_Vote() { $instance = NG_Vote::instance( __FILE__, '1.0.0' ); return $instance; } NG_Vote();
This is where we get the party started. We basically just get all our plugin actions running, which we’ll go over next because we haven’t technically coded them yet. Oops.
So onto that!
Let’s build out our NG_Vote class.
That NG_Vote thing from before? Yeah, let’s get that bad boy made.
Inside your ng-vote folder, create ANOTHER folder called includes and inside that new folder, create a file called class-ng-vote.php. We’re going to work on that file next.
Let’s start it off with this:
<?php /** * The class that powers the whole NG Vote plugin. * Adds all actions and stuff to WordPress. * * @package ng-vote * @copyright Copyright (c) 2015, Ashley Evans * @license GPL2+ */ class NG_Vote { // Everything we do next will go in here. }
Here I’ve just got some comments describing the file, then I’ve declared my class, and everything that comes next is going to go inside those curly braces.
Set up some useful variables.
I like to include some useful environment variables to use throughout my plugins. So stick these inside the class (in the curly braces) at the top:
/** * The single instance of NG_Vote * * @var NG_Vote * @access private * @since 1.0.0 */ private static $_instance = null; /** * The token. * * @var string * @access public * @since 1.0.0 */ public $_token; /** * The main plugin file. * * @var string * @access public * @since 1.0.0 */ public $file; /** * The main plugin directory. * * @var string * @access public * @since 1.0.0 */ public $dir; /** * The plugin assets directory. * * @var string * @access public * @since 1.0.0 */ public $assets_dir; /** * The plugin assets URL. * * @var string * @access public * @since 1.0.0 */ public $assets_url;
Each one is nicely commented if you want to know what they’re for. We’ll be using these later.
Build out the constructor.
Next we’re going to build out the constructor. Remember that thing from before in ng-vote.php where we got the party started? Well everything in this constructor method gets run when that party gets started.
/** * Constructor function. * * @param string $file * @param string $version The plugin version number * * @access public * @since 1.0.0 * @return void */ public function __construct( $file = '', $version = '1.0.0' ) { $this->_version = $version; $this->_token = 'ng-vote'; // Load plugin environment variables. $this->file = $file; $this->dir = dirname( $this->file ); $this->assets_dir = trailingslashit( $this->dir ) . 'assets'; $this->assets_url = esc_url( trailingslashit( plugins_url( '/assets/', $this->file ) ) ); // Add JavaScript to the front-end of the site. add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) ); // All ajax. $this->ajax(); }
So stick that below your environment variables.
Here’s what’s happening:
- We’re assigning values to our variables. We set the version number, the token (slug), the file path, directory, etc. Just populating variables.
- We hook into a WordPress action called
wp_enqueue_scripts
. We’ll create ourenqueue_assets
method in a bit, but that method will basically be adding some JavaScript to the front-end of our site. - Then we call a
ajax()
method. This method contains our actions for registering our function with WordPress.
We need our instance method!
In our “get the party” started code from the beginning, we had this line:
$instance = NG_Vote::instance( __FILE__, '1.0.0' );
That uses a static method called instance
. But that doesn’t exist yet, so we need to create it!
Inside our NG_Vote
class (in the curly braces, at the end), add in this:
/** * Main NG_Vote Instance * * Ensures only one instance of NG_Vote is loaded or can be loaded. * * @since 1.0.0 * @static * @see NG_Vote() * @return NG_Vote instance */ public static function instance( $file = '', $version = '1.0.0' ) { if ( is_null( self::$_instance ) ) { self::$_instance = new self( $file, $version ); } return self::$_instance; }
The first environment variable we created was $_instance
. What this method does is load the class object into the $_instance
variable and return that.
But if you’re not super tech savvy, you can just throw it in there and know it helps the whole plugin run.
Add our JavaScript file to the front-end.
Remember how we had this in our constructor?
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
That’s basically saying, hook our enqueue_assets
method into the wp_enqueue_scripts
action. wp_enqueue_scripts
is where WordPress adds CSS and JavaScript to the site, so we need to build out functionality for adding that in our enqueue_assets
method. So let’s add that next.
/** * Adds JavaScript to the front-end of the site. * * @param string $hook * * @access public * @since 1.0.0 * @return void */ public function enqueue_assets() { // JavaScript wp_enqueue_script( $this->_token, $this->assets_url . 'js/ng-vote.js', array( 'jquery' ), $this->_version, true ); $data = array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'cast_tweak_me_vote' ) ); wp_localize_script( $this->_token, 'NG_VOTE', $data ); }
In here we use a WordPress function called wp_enqueue_script
. You can read more up on that if you want. But basically it adds a JavaScript file. We just need to pass in a few parameters, the second one being the URL of the file.
For that, I used one of our environment variables, $assets_url
. That’s the URL to the assets directory, which we set to be assets/
in our plugin folder. Then I appended js/ng-vote.js
.
Long story short, it means our JavaScript file will need to be called ng-vote.js, go in a new folder called js, which is inside a folder called assets.
Our final file structure will look like this:
-
ng-vote/
-
assets/
-
js/
- ng-vote.js
-
js/
-
includes/
- class-ng-vote.php
- ng-vote.php
-
assets/
Now see those last few lines? These:
$data = array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'cast_ng_vote' ) ); wp_localize_script( $this->_token, 'NG_VOTE', $data );
These are our little ajax helpers. They create a few variables we can use in the JavaScript.
- The first variable (“ajaxurl”) gives us the URL to the ajax file. This is how we send stuff to PHP.
- The second variable (“nonce”) is for security purposes.
Then we can end up using these in the JavaScript like NG_VOTE.ajaxurl
.
And speaking of JavaScript… let’s add that file in!
Create your JavaScript file! This is how votes get captured.
Create your folder and files for JavaScript as we specified before. That’s in assets/js/ng-vote.js.
I’m just going to paste the entire contents of my JavaScript file. It’s pretty well documented if you want to read through it.
jQuery.noConflict(); jQuery(document).ready(function ($) { $('#ng-vote .btn').click(function (e) { // Prevent them from actually visiting the URL when clicking. e.preventDefault(); // Get some useful variables rollin'. var isDisabled = $(this).attr('disabled'); var blogURL = $(this).attr('href'); if (typeof isDisabled !== typeof undefined && isDisabled !== false) { return false; } // Add a little confirm box saying, "Are you sure you want to vote?" // If they select 'cancel' then this whole thing bails and nothing after this gets executed/ if (!confirm('Are you sure you wish to vote for ' + blogURL + '? You only get one vote and this cannot be undone!')) { return false; } $(this).attr('disabled', true); // Add a little 'waiting' thingie to the cursor. $(document.body).css({'cursor': 'wait'}); // Start ajaxin'! $.ajax({ type: 'POST', url: NG_VOTE.ajaxurl, data: { action: 'ng_cast_vote', // Ajax hooked into WordPress blog_url: blogURL, // The blog URL nonce: NG_VOTE.nonce // Our security nonce }, dataType: 'json', success: function (response) { // Add an alert with our success message. alert(response.data); // Change the cursor back to normal. $(document.body).css({'cursor': 'default'}); } }).fail(function (response) { // This stuff only happens if things fail miserably. $(document.body).css({'cursor': 'default'}); if (window.console && window.console.log) { console.log(response); } }); }); });
Here’s a quick run down of what happens:
- Everything gets triggered when someone clicks on an element with the class ‘btn’ inside of #ng-vote.
- We put the URL of the link into a variable, because in my case, that’s how I was identifying each entry. So if they were voting for Nose Graze then
blogURL
would equal https://www.nosegraze.com/ - We create a little alert pop up saying, “Are you sure you want to cast your vote?” They click ‘Ok’ to proceed or ‘Cancel’ to cancel.
- If they click ‘cancel’ everything stops!
- We change the cursor to the ‘wait’ style while we communicate with the back-end.
-
We send three pieces of data to the back-end:
- The name of the action we hooked into WordPress. For us this is
ng_cast_vote
- The blog URL variable. This is the value of
href
on the link they clicked. - Our nonce that we sent in via
wp_localize_script
- The name of the action we hooked into WordPress. For us this is
- We wait for PHP to reply back and then add another alert pop up thingie with any response (like “Success!” or “Epic fail” or “Trying to vote twice, you bloody cheat”).
Register the ajax action with WordPress.
Remember in the JavaScript where we send some data to the back-end? And specifically we have this line:
action: 'ng_cast_vote', // Ajax hooked into WordPress
Now we need to actually code that function.
Back in our constructor, we had this:
// All ajax. $this->ajax();
Let’s create that ajax()
method. This is where we register our ajax functions with WordPress:
/** * Holds all ajax actions. * * @access public * @since 1.0.0 * @return void */ public function ajax() { add_action( 'wp_ajax_nopriv_ng_cast_vote', array( $this, 'cast_vote' ) ); add_action( 'wp_ajax_ng_cast_vote', array( $this, 'cast_vote' ) ); }
We’re hooking into two WordPress actions:
wp_ajax_nopriv_{action name} wp_ajax_{action name}
That {action name} needs to be what we specified in the JavaScript. Here that is again:
action: 'ng_cast_vote', // Ajax hooked into WordPress
The first action is for people who are not logged in. If you don’t include that, then your ajax won’t work for people who aren’t logged in.
So we’ve registered our action with WordPress and told it to refer to the cast_vote()
method. Let’s create that now.
Create the function that actually casts a vote!
Here’s our game plan:
- Do a security check on that nonce we passed through the JavaScript.
- Check to see if the person has already voted. If so, send them a mean error message.
- If they haven’t voted already, add the person’s IP address to the array of all IPs that have voted. Also set a cookie on their browser.
- Add their vote to the results and update the database.
- Send a success message.
Here’s how the whole method looks:
/** * Cast Vote * * Ajax callback for casting a vote. * * @access public * @since 1.0.0 * @return void */ public function cast_vote() { // Security check. If this doesn't validate, the script will automatically die. check_ajax_referer( 'cast_tweak_me_vote', 'nonce' ); // If they've already voted, they can't vote again. if ( $this->has_voted() ) { wp_send_json_error( __( 'Error: You have already voted! I hope you\'re not trying to cheat because that\'s just lame.', $this->_token ) ); exit; } // Get the blog URL they're voting for. $blog_url = strip_tags( $_POST['blog_url'] ); // Get their IP address. $ip = $this->get_ip(); // Add their IP to the array of voted IPs. $voted_ips = get_option( 'ng_voted_ips', array() ); $voted_ips[] = $ip; update_option( 'ng_voted_ips', $voted_ips ); // Set a cookie. Change "August 15, 2016" to the date you want the cookie to expire. setcookie( 'ng_vote_tweak_me', $blog_url, strtotime( 'August 15, 2016' ), COOKIEPATH, COOKIE_DOMAIN, false, false ); // Update their vote. $votes = get_option( 'ng_votes_tweak_me', array() ); $number_votes = array_key_exists( $blog_url, $votes ) ? $votes[ $blog_url ] + 1 : 1; $votes[ $blog_url ] = $number_votes; // Sort the array from most votes to least votes. arsort( $votes ); // Update the votes in the database. update_option( 'ng_votes_tweak_me', $votes ); wp_send_json_success( sprintf( __( 'Your vote has been cast successfully. Thanks! P.S. there are %s votes for this design. Wish it luck! (Yes, you can wish blog designs good luck.)', $this->_token ), $number_votes ) ); }
It’s pretty well commented if you want to follow along. But notice we use two methods that don’t exist yet:
has_voted()
and
get_ip()
Here’s the code for those:
Check to see if the person has already voted.
/** * Whether or not the current user has cast a vote. * * @access public * @since 1.0.0 * @return bool True if they've voted. */ public function has_voted() { $ip = $this->get_ip(); $voted_ips = get_option( 'ng_voted_ips', array() ); // If their IP is in the array of voted IPs, they've voted. if ( in_array( $ip, $voted_ips ) ) { return true; } // If the cookie is set, they've voted. if ( isset( $_COOKIE['ng_vote_tweak_me'] ) ) { return true; } return false; }
Get the user’s IP
/** * Gets the current user's IP address. * * @access public * @since 1.0.0 * @return string */ public function get_ip() { if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) { //check ip from share internet $ip = $_SERVER['HTTP_CLIENT_IP']; } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { //to check ip is pass from proxy $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; } else { $ip = $_SERVER['REMOTE_ADDR']; } return $ip; }
We’re done!
That’s all there is to it!
So you might be wondering: how do you check the results? I personally used WP-CLI. Something like:
wp option get ng_votes_tweak_me
Or, you can code in a menu page that displays the results.
How to add a results page to the admin panel.
Go back inside the constructor and add in this:
// Add admin menu page. add_action( 'admin_menu', array( $this, 'admin_menu' ) );
Now we need to create that admin_menu()
method:
/** * Admin Menu * * Adds a new submenu page under "Tools" that displays the voting results. * * @access public * @since 1.0.0 * @return void */ public function admin_menu() { add_submenu_page( 'tools.php', __( 'NG Vote Results', $this->_token ), __( 'Voting Results', $this->_token ), 'manage_options', 'ng-vote-results', array( $this, 'admin_menu_callback' ) ); }
This is the code used to add a new submenu page to the admin area. In this case, I’m adding the new menu under “Tools”.
That last parameter I’m passing in is a callback. This callback is the function used to display the contents of the menu page. Here’s the code for that:
/** * Admin Menu Callback * * Renders the admin menu page. * * @access public * @since 1.0.0 * @return void */ public function admin_menu_callback() { $votes = get_option( 'ng_votes_tweak_me', array() ); arsort( $votes ); ?> <div class="wrap"> <h1><?php esc_html_e( 'NG Vote Results', $this->_token ); ?></h1> <table> <thead> <tr> <th><?php esc_html_e( 'Site URL', $this->_token ); ?></th> <th><?php esc_html_e( 'Number of Votes', $this->_token ); ?></th> </tr> </thead> <tbody> <?php foreach ( $votes as $site_url => $number ) : ?> <tr> <td> <a href="<?php echo esc_url( $site_url ); ?>" target="_blank"><?php echo esc_url( $site_url ); ?></a> </td> <td> <?php echo intval( $number ); ?> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> <?php }
- We’re fetching the votes from the database:
$votes = get_option( 'ng_votes_tweak_me', array() );
- Then creating a new table with two columns: Site URL and Number of Votes.
- Then we loop through each entry and display the two pieces of data.
Delete the options when you’re done.
When your contest is over, you can safely delete the two options created by the plugin: the one that stores the votes, and the one that stores the IP addresses.
Here’s the WP-CLI command I used:
wp option delete ng_voted_ips wp option delete ng_votes_tweak_me
Or if you prefer to do it in PHP:
delete_option( 'ng_voted_ips' ); delete_option( 'ng_votes_tweak_me' );
Download the full plugin
Keep in mind that even in its completed state, this plugin is meant to be edited and modified by you to make it relevant to your vote. You should still read the post to learn how it works and what you need to do to get things set up.
This is not a plugin for beginners who need a admin interface for managing their vote.
Download “NG Vote”ng-vote.zip – 4.38 KB
Awesome sauce!! This is such a great post. Thanks for sharing. I’ll be trying out this tut, I’ve always wanted a nifty voting mechanism on my site.
I love how you SHOW the code and explain what it is! Javascript scares me but this makes it a little less dauntless 🙂
Thanks, sensi-san!
When I downloaded the zip file onto my Mac, it would not unzip it.
Hey Ashley, thanks for sharing this. I was able to build my own function based on bits and pieces from your code. Loved the explanation and inline code comments.
You’re very welcome. 🙂
A fantastic post Ashley, thank you. Is there an issue with the zip file? It doesn’t open on my machine (Windows 10) for some reason. Its no biggie because you’ve done such a great job of explaining each step – I got it up and running without the zip file. Thank you again, you’ve saved me hours of frustration by sharing this.
Hi Ashley, great post! I was reading on different solutions for voting on posts and some articles mentioned problems with race conditions when traffic goes up. Does this affect your solution to?
This is great, I recently made my own plugin (Style the Toolbar in the WP Directory) but it’s very basic. I’m trying to make a more advanced one but I’m having a bit of trouble with it. Will definitely have a look at the code in more detail to see how you’ve done this. Thanks!