[matt.mc]

Matt.tech_ramblings

Part 2: Real-Time Voting With PubNub

In part 1, we created a really simple and utterly useless voting application. If you’re following along, at this point you should have an application that displays a round of songs that you can vote on and never see them update unless you refresh the page. Now comes the fun part of tying everything together with PubNub.

If you don’t already have a PubNub account, you’re going to need to go to their site and create one. After you’ve signed up, make note of the Publish and Subscribe keys you see on your Account page, as you’ll be using those shortly.

Now that we’ve got a PubNub account ready, let’s start prepping the application for it. First we’ll need to add the PubNub gem to our Gemfile. Note: At this point we still need to use an old version of the gem due to a bug causing some problems with Thin, but I’ll update here once it’s resolved.

gem 'pubnub', '~> 0.1'

The version isn’t necessarily required, but I like to include them just in case. If you were curious, the version constraint ~> is identical to >= 3.3.0 and < 3.4. We can assume they won’t change anything that would break our code in versions 3.3.1 through 3.3.9, so we can allow it to update to those versions when we update our bundle.

Once you’ve added the gem and run bundle install, we can set up the necessary PubNub connections in our app. First let’s set up the publishing in the app. We need to instantiate a new PubNub object using at least the publish key from our PubNub account page. I like to do it above all the routes, so we’ll place it right above our root route. Make sure you switch out the publish and subscribe placeholders for your own!

app.rb
1
2
3
4
5
6
7
8
9
10
PUBNUB = Pubnub.new(
  'PUBLISH-KEY',
  'SUBSCRIBE-KEY',
  '', # Secret Key (optional)
  '', # Cipher Key (optional)
  false # Use SSL? (optional)
)

get '\' do

Now we just need to add the actual publish command to the vote route. We want to happen after a song and round have successfully been saved.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

if @song.save and @round.save
  @songs = Round.current.first.songs
  logger.info "Total votes: #{@round.total_votes}"

  publish = PUBNUB.publish({
    'channel' => 'votr-vote',
    'message' => { 'total_votes' => @round.total_votes, 'songs' => @songs },
    'callback' => lambda do |message|
      logger.info message
    end
  })

  render :success => true, :attributes => { 'round' => @round, 'songs' => @songs }

See how easy that was? Fire up the application using padrino s and vote on a few songs. If you want to be able to see the messages you’re sending, go to your PubNub console, make sure you connect to the correct channel (“votr-vote” if you used what I did) and then send some more votes. You should see something like this in your console.

PubNub Console

At this point, we’re able to accept votes and publish a message to our PubNub channel with the updated round stats. All that’s left is to write a little JavaScript to subscribe to that channel and update the round whenever a new message comes in.

views/layouts/application.haml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<div pub-key="PUBLISH-KEY" sub-key="SUBSCRIBE-KEY" ssl="off" origin="pubsub.pubnub.com" id="pubnub"></div>
%script{ :src => "http://cdn.pubnub.com/pubnub-3.1.min.js" }
- unless @round.blank?
 :javascript

   function disableVoting() {
     
   }

   function update_votes(message) {
     console.log("Message Recieved");
     total_votes = message.total_votes;
     $.each(message.songs, function(index, value) {
       $("#song_" + this.id + " .votes").text(this.votes);
     });
     update_percentages(message);
   }

   function update_percentages(message) {
     total_votes = message.total_votes;
     $.each(message.songs, function(index, value) {
       percentage = this.votes / total_votes;
       $("#song_" + this.id + " .progress .bar").width(percentage * 100 + "%");
     });
   }

   $(function () {

     $('#clock').countdown({until: #{@timer.to_i}, format: 'HMS', compact: true, onExpiry: disableVoting});

     update_votes(#{@msg});

     $('.vote').click(function(event) {
       
     });

     // LISTEN FOR MESSAGES
     PUBNUB.subscribe({
       channel    : "votr-vote",
       restore    : false,
       callback   : update_votes,
       disconnect : function() {        // LOST CONNECTION.
         console.log("Disconnected from PubNub.");
       },
       reconnect  : function() {        // CONNECTION RESTORED.
         console.log("Reconnected to PubNub.");
       },
       connect    : function() {        // CONNECTION ESTABLISHED.
         console.log("Connected to PubNub.");
       }
     })
   });

Let’s follow what’s happening step by step. Right when the page loads, update_votes(#{@msg}) is called. @msg is essentially the same message that gets sent via PubNub, so all we’re doing is sending the round as-is to the user right when the page loads. update_votes() then sets each song’s vote count and then calls update_percentages(), which is what actually sets the width of the progress bars. update_votes() then gets called every time a new message is received from PubNub.

Since we haven’t put any restrictions on the number of times you can vote, open up a few browser windows side by side and start clicking away. You should see all of your open windows updating simultaneously.

So there you have it! You’ll notice in the repository there’s an admin directory. This is a cool feature of Padrino that allows you to essentially drop in an admin interface by simply running padrino g admin. The admin interface in the repository has been modified to look like the rest of the application, but otherwise it’s largely similar to the generated version. For more info on the Padrino Admin generator see the Padrino guides.

Votr Admin Interface

Vote away and enjoy! If you have any questions or suggestions, don’t hesitate to drop me a line on Twitter @matt_mcclure.

Part 1: Real-Time Voting With PubNub

Delivering real-time updates to users has never been easier thanks to some open-source projects and great startups that have popped up. I started using PubNub for TuneWolf, and so far I couldn’t be happier.

When I first started using PubNub, they didn’t have the enormous client-library you see now and the Ruby examples were a little scarce, so I decided to build something fun in order to get acquainted with the service. I’d also been hearing great things about Padrino, so I decided to try out the two together.

The result was Song-Votr, a simple voting application that puts one round of songs up at a time. Users can vote on the songs they like and also see everyone else’s votes happening in real-time. Before we look at any code, let’s lay out the broad overview of how everything interacts.

Song-Votr serves up the current round of songs. When a user clicks on a song, the client places the vote via an AJAX post request. Song-Votr increments the selected song’s vote count along with the total number of votes for the round, then publishes a message to our PubNub channel with the new stats for the round. The user that placed the vote (along with all other connected users) receives that updated round information and the client updates the view.

image

In a nutshell, all clients are only subscribed to PubNub, and the server only publishes messages. This allows Song-Votr to be the gatekeeper of the PubNub channel, making sure that users are only getting votes that were successfully processed.

Setting Up

Since we’re using Padrino let’s go ahead and generate the project. I’m a sucker for Haml and Sass, so we’ll use those along with ActiveRecord for our ORM and, of course, jQuery. The -b flag simply runs bundle install after generation.

$ padrino g project votr -e haml -c sass -s jquery -d activerecord -b

I used ActiveRecord because I come from Rails and I’m familiar with it, but you could always switch that out for whatever adapter you prefer. Assuming you are using AR, the next step is to generate our models. Our model is dead simple, consisting of just two models: a round that has many songs.

$ padrino g model round start_time:datetime end_time:datetime total_votes:integer -a app
$ padrino g model song title:string artist:string votes:integer percentage:float round_id:integer -a app

This will also generate the necessary migrations. Before you run them, let’s go in and add a default value of 0 to total_votes in round and votes in songs. When you’re done, those migrations should look similar to what’s below.

001_create_rounds.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CreateRounds < ActiveRecord::Migration
  def self.up
    create_table :rounds do |t|
      t.datetime :start_time
      t.datetime :end_time
      t.integer :total_votes, :default => 0
      t.timestamps
    end
  end

  def self.down
    drop_table :rounds
  end
end
002_create_songs.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CreateSongs < ActiveRecord::Migration
  def self.up
    create_table :songs do |t|
      t.string :title
      t.string :artist
      t.integer :votes, :default => 0
      t.float :percentage
      t.integer :round_id
      t.timestamps
    end
  end

  def self.down
    drop_table :songs
  end
end

Once you’re done, make sure you run the migrations with the command padrino rake ar:migrate.

With our database now ready, we need to go into the models and define the relationships. While we’re in there, let’s go ahead and write a scope to get the current round since that’s something we’ll definitely need soon.

round.rb
1
2
3
4
5
6
7
class Round < ActiveRecord::Base
  has_many :songs

  scope :current, lambda {
    where("start_time <= ? and end_time > ?", DateTime.now, DateTime.now)
  }
end
song.rb
1
2
3
class Song < ActiveRecord::Base
  belongs_to :round
end

A neat feature about Padrino is its drop-in admin interface, but let’s not worry about that yet. For now, we’ll insert a round and some songs into the database from the console.

$ padrino c
> round = Round.new
> round.start_time = DateTime.now
> round.end_time = DateTime.now + 3.days
> round.save!

> song = round.songs.new
> song.title = "My Heart Will Go On"
> song.artist = "Celine Dion"
> song.save!

Repeat the song process for a few more songs so you can have things to vote on for the next 3 days. Once you’re done, your database should be prepped and ready!

Front page and layout

Because there are only two routes, we can get away with just using the base app.rb file as our only controller. Let’s go ahead and make our root route and decide what we need to pass to our view. We’ll want a timer to show how much time is left in the round, along with the round itself (and associated songs). Notice we’re using the current scope we set up in our model. Padrino is based on Sinatra, so if you’ve ever used Sinatra this syntax should be very familiar.

app.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
get '/' do

  @round = Round.current.first

  unless @round.blank? || @round.end_time.past?

    difference = @round.end_time - DateTime.now.to_i
    @timer = difference
    @songs = @round.songs.order("artist")
    @msg = { :songs => @songs, :round => @round, :total_votes => @round.total_votes }.to_json

  end

  render 'index'
end

Now it’s time to actually make an interface for people to see. I was planning on using this for a demo, so I took the time to style it up. If you’d like to go ahead and simply copy my stylesheet from the repository, go ahead and do that now.

Our layout is going to be simple for now. I almost always start my projects with Nicolas Gallagher’s Normalize, and Keith Wood’s jquery.countdown is what’s going to make our clock actually count down.

views/layouts/application.haml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
!!! 5
%html
  %head
    %title Votr
    %meta{ :content => "", :name => "description" }
    %meta{ :content => "", :name => "author" }
    %meta{ :content => "3 days", :name => "revisit-after" }
    %link{ :href => "http://creativecommons.org/licenses/by/3.0/", :rel => "license", :title => "Creative Commons Attribution 3.0 Unported License" }
    %script{ :src => "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" }
    = stylesheet_link_tag 'normalize', 'application'
    = javascript_include_tag 'application', 'jquery.countdown'
  %body
    = yield

    %footer
      %p
        #{link_to("SongVotr", "http://github.com/sh1ps/padrino-pubnub-votr")}. By #{link_to("@matt_mcclure", "http://www.twitter.com/matt_mcclure")}.

  - unless @round.blank?
    :javascript
      $(function(){
            $('#clock').countdown({until: #{@timer.to_i}, format: 'HMS', compact: true, onExpiry: disableVoting});
          });

The index view itself is pretty self explanatory. We want to display the clock at the top, followed by a list of the songs in the current round, their current votes, and the percentage of the total vote they’ve received. As you can see, we went ahead and made each song title a link. We’ll be making that /vote route next.

views/index.haml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- unless @timer.blank?

  %header
    %h1 What do <span class="blue">you</span> want to hear next?
    %p Click the song titles to vote.
    %h1#clock.blue

  - unless @songs.blank? 
    %ul.selections
      - @songs.each do |s|
        %li[s]
          %h3
            = link_to "#{s.artist} - #{s.title}", "/vote?id=#{s.id}", :class => "vote"
            %small (<span class="votes">#{s.votes}</span> votes)
          .progress
            .outline
            .bar
  - else
    %p No songs to vote on!

- else

  %header
    %h1 Nothing to currently vote on. Come back later.

Placing Votes

The /vote route is where almost all of the real functionality of the app lies. Because we’re only allowing for one active round at a time, we can just get the current round. We can cut down on the number of SQL queries by going ahead and eager loading songs along with the round. This can be done via the includes method you see on Round in the second line.

Other than that, all that’s happening is incrementing the total votes for the round and the number of votes for the song. We then recalculate that song’s percentage (just for archival purposes) and return success or failure based on whether or not the song / round was persisted.

Notice that we’re querying the database again for all of the songs. This is because when we plug PubNub in, we need to be publishing all of the songs in order to be able to recalculate all of their percentages and votes on the client side.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
post '/vote', :provides => :json do
  @round = Round.includes(:songs).current.first

  unless @round.end_time.past?
    @songs = @round.songs.order("artist")
    @song = @songs.find(params[:id])
    @song.votes = @song.votes + 1
    @round.total_votes = @round.total_votes + 1

    @song.percentage = @song.votes.to_f / @round.total_votes.to_f

    if @song.save and @round.save
      @songs = Round.current.first.songs
      logger.info "Total votes: #{@round.total_votes}"

      render :success => true, :attributes => { 'round' => @round, 'songs' => @songs }

    else
      render :success => false, :attributes => {:message => "Unable to save your vote..."}
    end

  else
    render :success => false, :attributes => {:message => "The voting is already over."}
  end
end

Now that the /vote route works, we need to add a little Javascript so the vote is sent via ajax. Change the javascript block in the layout to look something like this:

views/layouts/application.haml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
:javascript
  // Reload the window whenever the clock gets to zero so we can get the next round.
  function disableVoting() {
    alert("This round's over! New round starting...");
    window.location.reload();
  }

  $(function(){
    $('#clock').countdown({until: #{@timer.to_i}, format: 'HMS', compact: true, onExpiry: disableVoting});
      
    $('.vote').click(function(event) {
    event.preventDefault();
    var $link = $(this);
  
      $.ajax({
        type: 'POST',
        url: $link.attr('href'),
        success: function (response) {
          console.log("Vote placed.");
        },
        error: function (response) {
          console.log("Error: vote not placed.");
          alert("Vote could not be placed at this time.");
        }
      });
    });
  });

Making it Live

Now that we have our rounds and we’re able to post votes to them, we need to make it so everyone can see everyone’s votes as their happening. Because this post is starting to get incredibly lengthy, let’s take a breather and tackle the PubNub integration in the next post.

If you’re interested…

If this interested you, Twilio posted a tutorial on building a similar project using their service along with Node.js and CouchDB. Happy hacking!

- Matt

Obligatory First Post

Every blog must have its fist post, and this is it. I’ve been meaning to do this for a long time, but always got hung up choosing the software to make it with or even with styling that software.

I ended up going with Octopress and making changes primarily to the CSS. The default theme uses SASS and it’s organized well, so making it look a little less like the 1000 other Octopress blogs out there was pretty easy.

The first post will be a writeup on Song-Votr, since I saw a tutorial on how to build something pretty similar (down to the name “Votr”). Hopefully that will be up by the end of the weekend!

- Matt