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.
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.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
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.
1 2 3 4 5 6 7
1 2 3
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
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
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
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…