Ruby
Article

Polling Your Users with Rails

By Ilya Bodrov-Krukowski

bright poll of light bulb

Here are some questions for you:

  • Have you ever participated in polls on a website? Yes/No?
  • Have you created polls yourself? Yes/No?
  • How about building today a web app allowing to create users their own polls and participate in them?! Yes/No?

In this article, I am going to show you how to build a web app that allows authenticated users to create, manage, and participate in polls. While building it, we will discuss the following:

  • Nested attributes in Rails and the Cocoon gem
  • Many-to-many relationships with an intermediate table
  • Authentication via Facebook using an OmniAuth strategy
  • Using the jQuery Validate plugin and a dash of AJAX to improve the user’s experience
  • Model caching, counter caches (with the cache_culture gem), and eager loading for improved performance
  • Visualizing a poll’s statistics with Bootstrap’s progress bars (and a bit of Math)

All that will take only six iterations! Cool? Then let’s get started!

The source code is available at GitHub.

Working demo can be found at http://sitepoint-poller.herokuapp.com.

Some Ground Work

I was not able to think of a cool name for our web service, so to keep things simple we’ll call it Poller. Create a new Rails app without the default testing suite:

$ rails new poller -T

We are going to have many polls created by different users with an unlimited number of voting options. Creating a single table to hold both poll topics and the list of possible options is not prudent because each user can only vote once. Moreover, the option each user chooses will also be recorded to count the total number of votes. As such, a relationship between voting option and user will exist.

Therefore, let’s create two separate tables – polls and vote_options. The first table has only one field (apart from the default id, created_at and updated_at):

  • topic (text) – the topic (duh) of the poll

The vote_options table has:

  • title(string) – the actual text of the voting option
  • poll_id (integer) – the foreign key to establish a one-to-many relation between vote_option and polls

Create and apply the appropriate migrations:

$ rails g model Poll topic:text
$ rails g model VoteOption title:string poll:references
$ rake db:migrate

Modify the model files, adding relationships and some validations:

models/poll.rb

[...]
has_many :vote_options, dependent: :destroy
validates :topic, presence: true
[...]

models/vote_option.rb

[...]
validates :title, presence: true
[...]

Now, time to deal with views, controllers, and routes. Create the PollsController:

polls_controller.rb

class PollsController < ApplicationController
  def index
    @polls = Poll.all
  end

  def new
    @poll = Poll.new
  end

  def create
    @poll = Poll.new(poll_params)
    if @poll.save
      flash[:success] = 'Poll was created!'
      redirect_to polls_path
    else
      render 'new'
    end
  end

  private

  def poll_params
    params.require(:poll).permit(:topic)
  end
end

and the corresponding routes:

config/routes.rb

resources :polls
root to: 'polls#index'

I am a fan of Twitter Bootstrap to help us build a pretty-looking design, so add that to the Gemfile:

Gemfile

[...]
gem 'bootstrap-sass'
[...]

Don’t forget to run bundle install!

Rename your application.css to application.css.scss and replace its contents with:

@import 'bootstrap';
@import 'bootstrap/theme';

Alter the layout like so:

layouts/application.html.erb

[...]
<div class="navbar navbar-inverse">
  <div class="container">
    <div class="navbar-header">
      <%= link_to 'Poller', root_path, class: 'navbar-brand' %>
    </div>
    <ul class="nav navbar-nav">
      <li><%= link_to 'Add poll', new_poll_path %></li>
    </ul>
  </div>
</div>

<div class="container">
  <% flash.each do |key, value| %>
    <div class="alert alert-<%= key %>">
      <%= value %>
    </div>
  <% end %>

  <div class="page-header">
    <h1><%= yield :page_header %></h1>
  </div>

  <%= yield %>
</div>
[...]

We are using yield :page_header to display page headers without the need to copy and paste those div and h1 tags every time.

Create some views:

polls/index.html.erb

<% content_for(:page_header) {"Participate in our polls right now!"} %>

polls/new.html.erb

<% content_for(:page_header) {"Create a new poll"} %>

<%= render 'form' %>

polls/_form.html.erb

<%= form_for @poll do |f| %>
  <%= render 'shared/errors', object: @poll %>

  <div class="form-group">
    <%= f.label :topic %>
    <%= f.text_area :topic, rows: 3, required: true, class: 'form-control' %>
  </div>

  <%= f.submit 'Create', class: 'btn btn-primary btn-lg' %>
<% end %>

shared/_errors.html.erb

<% if object.errors.any? %>
  <div class="alert alert-warning">
    <h4>The following errors were found:</h4>
    <ul>
      <% object.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
<% end %>

The ground work is done and we are ready to move on to the meat of the article.

Creating Vote Options

Stop for a second and take a look at what’ve done so far. We have some controller methods and a couple of views to list and create new polls. However, we have no page to create vote options for a specific poll. Should a separate controller and a view be created for that purpose? Suppose you need ten vote options, would you want to submit the form ten times?

Creating vote options along with creation of a poll is much better. This can be achieved by employing Rails’ nested attributes to allow saving attributes of associated records through the parent.

First of all, we have to enable nested attributes in the poll.rb (because Poll is the parent for the VoteOption):

models/poll.rb

[...]
accepts_nested_attributes_for :vote_options, :reject_if => :all_blank, :allow_destroy => true
[...]

:reject_if => :all_blank raises an error and rejects saving the poll if it has no vote options.

:allow_destroy => true allows users to delete vote options through nested attributes (when opening
an Edit poll page, which we will create shortly).

To allow creating an unlimited number of vote options on the same page, you could write some helper functions and a bit of JavaScript. However, for this demo we’ll use the Cocoon gem created by Nathan Van der Auwera to help us quickly achieve the desired result. This gem helps in building dynamic nested forms and works with basic Rails forms, Formtastic, and Simple_form.

Add these gems to your Gemfile:

Gemfile

[...]
gem 'jquery-turbolinks'
gem "cocoon"
[...]

and run bundle install.

jquery-turbolinks should be added only if you are using Turbolinks. It brings the default jQuery document.ready event back to the page load.

Include the corresponding JavaScript files:

application.js

[...]
//= require jquery.turbolinks
//= require cocoon
[...]

The last thing to do before starting to build the nested form is altering the controller’s method a bit to permit some new attributes:

polls_controller.rb

[...]
def poll_params
  params.require(:poll).permit(:topic, vote_options_attributes: [:id, :title, :_destroy])
end
[...]

Permitting :_destroy is required only if you wish to allow destroying vote options via nested attributes.

Let’s proceed to the actual form:

polls/_form.html.erb

<%= form_for @poll do |f| %>
  <%= render 'shared/errors', object: @poll %>

  <div class="form-group">
    <%= f.label :topic %>
    <%= f.text_area :topic, rows: 3, required: true, class: 'form-control' %>
  </div>

  <div class="panel panel-default">
    <div class="panel-heading">Options</div>
    <div class="panel-body">
      <%= f.fields_for :vote_options do |options_form| %>
        <%= render 'vote_option_fields', f: options_form %>
      <% end %>

      <div class="links">
        <%= link_to_add_association 'add option', f, :vote_options %>
      </div>
    </div>
  </div>

  <%= f.submit 'Create', class: 'btn btn-primary btn-lg' %>
<% end %>

fields_for helper method is used to build a nested form and vote_option_fields is the partial that we’re going to create shortly.

link_to_add_association is the helper method introduced by Cocoon that renders a link to dynamically add new nested fields (to add a new vote option in our case). This method accepts the name of the link to show on the page, the form builder object, and the plural name of the association. It is required to wrap this helper with div class="links" because nested fields will be added just before this wrapper. Also, this helper expects to find the partial “singular_association_name_fields” inside the same directory from where it was called. If you want it to use another partial, use the partial option like this:

link_to_add_association 'add something', f, :somethings,
    :partial => 'shared/something_fields'

There are plenty of other options that can be passed to this helper.

Create a new partial:

polls/vote_option_fields.html.erb

<div class="nested-fields">
  <div class="form-group">
    <%= f.label :title %>
    <%= f.text_field :title, class: 'form-control', required: true %>
  </div>
  <%= link_to_remove_association "remove option", f %>
</div>

A wrapper with the nested-fields class is required here. link_to_remove_association is yet another Cocoon helper to render a link that removes the nested form. When the parent form is submitted, the corresponding record is also being deleted (provided that you’ve specified :allow_destroy => true for the accepts_nested_attributes_for method earlier and permitted _destroy attribute in the controller).

Boot up your server and try to create a poll and some related options. Seems pretty easy, eh?

Listing and Managing Polls

We have to tie up some loose ends at this point. Specifically, the index page does not display the list of created polls and there is no way to edit or destroy polls. This can be easily fixed.

First, add some more methods to the controller:

polls_controller.rb

[...]
def edit
  @poll = Poll.find_by_id(params[:id])
end

def update
  @poll = Poll.find_by_id(params[:id])
  if @poll.update_attributes(poll_params)
    flash[:success] = 'Poll was updated!'
    redirect_to polls_path
  else
    render 'edit'
  end
end

def destroy
  @poll = Poll.find_by_id(params[:id])
  if @poll.destroy
    flash[:success] = 'Poll was destroyed!'
  else
    flash[:warning] = 'Error destroying poll...'
  end
  redirect_to polls_path
end
[...]

Next, add code to display all available polls:

polls/index.html.erb

[...]
<% @polls.each do |poll| %>
  <div class="well">
    <h2><%= poll.topic %></h2>

    <div class="btn-group">
      <%= link_to 'Edit', edit_poll_path(poll), class: 'btn btn-default' %>
      <%= link_to 'Delete', poll_path(poll),
                  method: :delete,
                  class: 'btn btn-danger', data: {confirm: 'Are you sure?'} %>
    </div>
  </div>
<% end %>

Also, add a simple style to remove top margin for the headers:

application.css.scss

.well {
  h2 {
    margin-top: 0;
  }
}

Lastly, create an edit view:

polls/edit.html.erb

<% content_for(:page_header) {"Edit poll"} %>

<%= render 'form' %>

At this point, the polls are done. It’s time to implement the core functionality: authentication and the actual voting.

Authentication

Let’s allow users to authenticate via Facebook. To do that, we will need a Facebook strategy for OmniAuth, which I’ve described in one of my previous posts.

Gemfile

[...]
gem 'omniauth-facebook'
[...]

Then run bundle install.

OK, now set up your new Facebook provider by creating an omniauth.rb file inside the config/initializers directory with the following content:

config/initializers/omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider  :facebook, 
            ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'],
            scope: 'public_profile', display: 'page', image_size: 'square'
end

Obtain the key-secret pair by creating a new app on the Facebook Developers page, open the “Settings” tab and add the following information:

  • Add a new “Platform” (“Website”)
  • Fill in “Site URL” with the address of your site
  • Fill in “App Domains” (should be derived from the Site URL)
  • Fill in Contact E-mail.

Next navigate to the “Status and Review” tab and make your application active (this makes it available for everyone, FYI). Return to the “Dashboard” and notice the “App ID” and “App Secret” – those are the keys you are looking for. Add the to the OmniAuth initializer.

Create a table to store user’s information. We will need only some basic data:

  • uid (string) – unique user’s identifier on Facebook
  • name (string) – name and surname
  • image_url (string) – URL to fetch user’s avatar

Create the appropriate migration:

$ rails g model User name:string image_url:string uid:string:index

The uid will be used frequently to find users, so it is a good idea to make this field indexed.

Now apply your migration:

$ rake db:migrate

Add some new routes:

routes.rb

get '/auth/:provider/callback', to: 'sessions#create'
get '/auth/failure', to: 'sessions#auth_fail'
get '/sign_out', to: 'sessions#destroy', as: :sign_out

The /auth/:provider/callback route is where the user will be redirected after a successful authentication (along with the authentication hash containing user’s data).

On to the controller:

sessions_controller.rb

class SessionsController < ApplicationController
  def create
    user = User.from_omniauth(request.env['omniauth.auth'])
    cookies[:user_id] = user.id
    flash[:success] = "Welcome, #{user.name}!"
    redirect_to root_url
  end

  def destroy
    cookies.delete(:user_id)
    flash[:success] = "Goodbye!"
    redirect_to root_url
  end

  def auth_fail
    render text: "You've tried to authenticate via #{params[:strategy]}, but the following error
occurred: #{params[:message]}", status: 500
  end
end

request.env['omniauth.auth'] contains the user’s data sent by Facebook. The from_omniauth method fetches and stores the user data. As you can see, nothing fancy is going on in this controller.

models/user.rb

class User < ActiveRecord::Base
  class << self
    def from_omniauth(auth)
      uid = auth.uid
      info = auth.info.symbolize_keys!
      user = User.find_or_initialize_by(uid: uid)
      user.name = info.name
      user.image_url = info.image
      user.save!
      user
    end
  end
end

We are going to need a method to check if the user is authenticated. Traditionally it is called current_user:

application_controller.rb

[...]
def current_user
  @current_user ||= User.find_by(id: cookies[:user_id]) if cookies[:user_id]
end

helper_method :current_user
[...]

helper_method ensures that current_user can be used in views as well.

Great. The last thing to do is to allow the user to authenticate or display the user info along with a “Logout” link if already logged in:

layouts/application.html.erb

[...]
<ul class="nav navbar-nav">
  <li><%= link_to 'Add poll', new_poll_path %></li>
</ul>
<ul class="nav navbar-nav navbar-right">
  <% if current_user %>
    <li><%= image_tag current_user.image_url, alt: current_user.name %></li>
    <li><%= link_to 'Logout', sign_out_path %></li>
  <% else %>
    <li><%= link_to 'Sign in', '/auth/facebook' %></li>
  <% end %>
</ul>
[...]

Before moving on to the next iteration let’s also add a “Participate” button next to each poll if the user is authenticated:

polls/index.html.erb

<% @polls.each do |poll| %>
  <div class="well">
    <h2><%= poll.topic %></h2>

    <p>
      <% if current_user %>
        <%= link_to 'Participate!', poll_path(poll), class: 'btn btn-primary btn-lg block' %>
      <% else %>
        Please sign in via <%= link_to 'Facebook', '/auth/facebook' %> to participate in this poll.
      <% end %>
    </p>

    <div class="btn-group">
      <%= link_to 'Edit', edit_poll_path(poll), class: 'btn btn-default' %>
      <%= link_to 'Delete', poll_path(poll),
                  method: :delete,
                  class: 'btn btn-danger', data: {confirm: 'Are you sure?'} %>
    </div>
  </div>
<% end %>

A new controller method will be needed:

polls_controller.rb

[...]
def show
  @poll = Poll.find_by_id(params[:id])
end
[...]

The authentication system is ready and it is high time to start crafting voting functionality.

Voting

As we discussed earlier, a relationship between user and vote options must be set to track which user chose which option. Every user may vote for many options (however he cannot vote for multiple options that belong to the same poll) and each option may be chosen by many users. Therefore, we need a many-to-many relationship with an intermediate table (direct many-to-many relation could be used as well, but it is not as flexible).

Let’s call this new intermediate table votes and create the corresponding migration:

$ rails g model Vote user:references vote_option:references

Then, modify migration’s file a bit:

migrations/xxx_create_votes.rb

[...]
add_index :votes, [:vote_option_id, :user_id], unique: true
[...]

This will create a clustered index that enforces uniqueness for the combination of vote_option_id and user_id. Obviously, there can’t be multiple votes by the same user for the same option.

Next apply this migration:

$ rake db:migrate

Your model file should look like:

models/vote.rb

class Vote < ActiveRecord::Base
  belongs_to :user
  belongs_to :vote_option
end

Add those associations to User and VoteOption models:

models/user.rb

[...]
has_many :votes, dependent: :destroy
has_many :vote_options, through: :votes
[...]

models/vote_option.rb

[...]
has_many :votes, dependent: :destroy
has_many :users, through: :votes
[...]

Create a view for the show action:

polls/show.html.erb

<% content_for(:page_header) {"Share your opinion"} %>

<h2><%= @poll.topic %></h2>

<%= render 'voting_form' %>

The actual voting form is taken to a separate partial – this will come in handy soon.

polls/_voting_form.html.erb

<%= form_tag votes_path, method: :post, remote: true, id: 'voting_form' do %>
  <%= hidden_field_tag 'poll[id]', @poll.id %>

  <%= render partial: 'polls/vote_option', collection: @poll.vote_options, as: :option %>

  <% if current_user.voted_for?(@poll) %>
    <p>You have already voted!</p>
  <% else %>
    <%= submit_tag 'Vote', class: 'btn btn-lg btn-primary' %>
  <% end %>
<% end %>

Here we use the nonexistent votes_path route to create a new vote. This form will be sent asynchronously. The voted_for? method checks whether a user has already participated in the specified poll – it will be created shortly.

As you can see, we are using @poll.vote_options to fetch a poll’s options, so it is a good practice to add eager loading:

polls_controller.rb

[...]
def show
  @poll = Poll.includes(:vote_options).find_by_id(params[:id])
end
[...]

Add a new route:

routes.rb

[...]
resources :votes, only: [:create]
[...]

and create the partial:

polls/_vote_option.html.erb

<div class="form-group">
  <%= content_tag(:label) do %>
    <% unless current_user.voted_for?(@poll) %>
      <%= radio_button_tag 'vote_option[id]', option.id %>
    <% end %>
    <%= option.title %>
  <% end %>
</div>

content_tag wraps the radio button and option’s title with the label tag. The radio button is not shown if the user has already participated in the poll.

It is time to implement the voted_for? method:

models/user.rb

[...]
def voted_for?(poll)
  vote_options.any? {|v| v.poll == poll }
end
[...]

Here we check if the user has any vote options that belong to the specified poll. Later, you may use model caching to improve performance of the app like this:

def voted_for?(poll)
  Rails.cache.fetch('user_' + id.to_s + '_voted_for_' + poll.id.to_s) { vote_options.any? {|v| v.poll == poll } }
end

and flush this cache each time the user participates in a poll.

Lastly, we need a controller and the create method:

votes_controller.rb

class VotesController < ApplicationController
  def create
    if current_user && params[:poll] && params[:poll][:id] && params[:vote_option] && params[:vote_option][:id]
      @poll = Poll.find_by_id(params[:poll][:id])
      @option = @poll.vote_options.find_by_id(params[:vote_option][:id])
      if @option && @poll && !current_user.voted_for?(@poll)
        @option.votes.create({user_id: current_user.id})
      else
        render js: 'alert(\'Your vote cannot be saved. Have you already voted?\');'
      end
    else
      render js: 'alert(\'Your vote cannot be saved.\');'
    end
  end
end

Here, check that all the the required parameters were sent, that the user has authenticated, and the user has not participated in the poll yet. If all of those conditions are true, create a new vote. Otherwise, show an alert with an error message. On to the view:

votes/create.js.erb

$('#voting_form').replaceWith('<%= j render 'polls/voting_form' %>');

Here, replace the old voting form with a new one using the previously created partial.

Let’s add some visualization for our polls.

Showing the Voting Statistics

Currently the show page is lacking an important piece: It does not show how many users voted for each option.
Indeed, this needs to be fixed! However, presenting only votes’ count is boring so let’s visualize the statistics by taking advantage of Bootstrap’s styles:

polls/_vote_option.html.erb

<div class="form-group">
  <%= content_tag(:label) do %>
    <% unless current_user.voted_for?(@poll) %>
      <%= radio_button_tag 'vote_option[id]', option.id %>
    <% end %>
    <%= option.title %>
  <% end %>
  <%= visualize_votes_for option %>
</div>

Here I’ve added only one line calling visualize_votes_for helper:

helpers/polls_helper.rb

module PollsHelper
  def visualize_votes_for(option)
    content_tag :div, class: 'progress' do
      content_tag :div, class: 'progress-bar',
                  style: "width: #{option.poll.normalized_votes_for(option)}%" do
        "#{option.votes.count}"
      end
    end
  end
end

Wrap the div that has a class of progress-bar with yet another div.progress. Those classes are provided
by Bootstrap and were originally intended to display progress bars, but I think we can employ them in this case as well.

div.progress-bar has a style attribute to set its width. The width is specified in percent and obviously should be no more than 100%. To ensure this, I am using the normalized_votes_for method:

models/poll.rb

[...]
def normalized_votes_for(option)
  votes_summary == 0 ? 0 : (option.votes.count.to_f / votes_summary) * 100
end
[...]

First of all, check that the summary votes count is not zero (votes_summary is yet another method that will be introduced shortly) – without this check we may get a division by zero error. So, if there are no votes for any option of the poll, just return zero. Otherwise, check how many votes were given for the specified option by using ActiveRecord’s count method. This result is divided by the total number of votes and then multiplied by 100 to convert it to percent.

Please note that option.votes.count should be also converted to float using to_f. Otherwise, the result of the division will always be an integer.

This method is simple. For example, if a poll has a total of 10 votes, option A has 3 votes and option B has 7 votes then:

  • Option A: (3 / 10) * 100 = 30 (%)
  • Option B: (7 / 10) * 100 = 70 (%)

Great! Lastly, we need the votes_summary method:

models/poll.rb

[...]
def votes_summary
  vote_options.inject(0) {|summary, option| summary + option.votes.count}
end
[...]

Here, inject is used to accumulate the values (0 here means that the initial value is zero; summary is the accumulator). Once again, you could use model caching to improve performance.

The last thing to do is modify the progress bar’s background to make it look a bit nicer:

application.css.scss

[...]
.progress {
  background-image: linear-gradient(to bottom, #bbb 0%, #ccc 100%)
}
[...]

That votes_summary method can also be employed in the views like this:

polls/_voting_form.html.erb

<%= form_tag votes_path, method: :post, remote: true, id: 'voting_form' do %>
  <%= hidden_field_tag 'poll[id]', @poll.id %>

  <%= render partial: 'polls/vote_option', collection: @poll.vote_options, as: :option %>

  <p><b>Total votes: <%= @poll.votes_summary %></b></p>
[...]

polls/index.html.erb

[...]
<% @polls.each do |poll| %>
  <div class="well">
    <h2><%= poll.topic %> <small>(voted: <%= poll.votes_summary %>)</small></h2>
[...]

Before moving on, I should warn you about a small catch. If you start the server and try to vote in a poll, the votes count is updated but the Vote button remains. Refresh the page and the Vote button will be replaced with the “You have already voted” text. This happens because Rails cached the association in the voted_for? method. When the _voting_form partial is rendered again, this method provides the previous result (false), even if a new vote was actually created. There are at least three possible solutions to this problem.

The first one is simply clearing the association cache after creating a new vote by using reset method:

votes_controller.rb

[...]
def create
  [...]
  @option.votes.create({user_id: current_user.id})  + current_user.votes.create({vote_option_id: @option.id})
  current_user.vote_options.reset
[...]

The second one is rewriting the voted_for? method a bit:

user.rb

[...]
def voted_for?(poll)
  votes.any? {|v| v.vote_option.poll == poll}
end
[...]

This way we are directly specifying the intermediate model and Rails will instantly know that there was a new vote created.

The third solution is to set force_reload to true when calling the association:

user.rb

[...]
def voted_for?(poll)
  vote_options(true).any? {|v| v.poll == poll }
end
[...]

If you know other solutions to this problem, please share them in the comments.

A Bit of Caching

You may have noticed that option.votes.count is used in a couple of methods. How about adding a counter cache to improve this query?

One of the possible ways to solve this is by using the counter_culture gem created Magnus von Koeller.

Drop the gem into your Gemfile:

Gemfile

[...]
gem 'counter_culture', '~> 0.1.23'
[...]

and run bundle install.

Next, run the generator and apply the created migration:

$ rails g counter_culture VoteOption votes_count
$ rake db:migrate

Also add the following line of code to the *vote.rb*:

counter_culture :vote_option

Now, the counter cache for votes count will be created and managed automatically. Cool, isn’t it?

The counter_culture gem can be used in more complex scenarios and has a bunch of options, so check out its documentation.

A Bit of Client-Side Validation

To improve the user experience a bit, add some client-side validation to check if a user has chosen one of the options before voting. If not – show an error instead of submitting the form. There is a great jQuery plugin called jQuery Validate which, as the name suggests, helps creating various validation rules. Just download the plugin’s files, place them in vendor/assets/javascripts and include jquery.validate.js and additional-methods.js into your project:

application.js

[...]
//= require jquery.validate
[...]

In the simplest case, all you need to do is to use the validate() method on the form to use Validate’s magic. However, we have a bit more complex scenario, so we have to provide some options:

polls/show.html.erb

[...]
<script data-turbolinks-track="true">
  $(document).ready(function() {
    var voting_form = $('#voting_form');
    voting_form.validate({
      focusInvalid: false,
      errorClass: 'alert alert-warning',
      errorElement: "div",
      errorPlacement: function(error, element) { voting_form.before(error); },
      rules: {
        'vote_option[id]': {
          required: true
        }
      },
      messages: {
        'vote_option[id]': {
          required: "Please select one of the options."
        }
      }
    });
  });
</script>

focusInvalid means that the first invalid field should not be focused (because we have many radio boxes and only one of them should be selected). errorClass specifies the CSS class to assign to the error message. errorElement sets the wrapper for the error message box and errorPlacement provides a function to call before placing the error message. I want it to be placed before the form, so I use jQuery’s before method is here (error contains the actual error message element).

rules takes an object with validation rules. As long as we want to ensure that only one of the radio boxes is checked, use vote_option[id] (this is the name of radio boxes). messages, in turn, is used to set custom error messages for the rules provided.

At this point you may check how validation is working by simply trying to submit a form with no radio boxes checked. Awesome!

Showing User’s Profile

We are done with the core functionality. Users may participate in polls and check statistics. The last thing to do is create the user’s profile page to show the polls in which they participated. This can be done in three simple steps: create the controller, create the view, and add a new route.

users_controller.rb

class UsersController < ApplicationController
  def show
    @user = User.find_by_id(params[:id])
  end
end

users/show.html.erb

<% content_for(:page_header) do %>
  <%= image_tag @user.image_url, alt: @user.name %>
  <%= "#{@user.name}'s profile" %>
<% end %>

<h2>Participation in polls</h2>

<% @user.vote_options.each do |option| %>
  <div class="panel panel-default">
    <div class="panel-heading"><%= link_to option.poll.topic, poll_path(option.poll) %></div>
    <div class="panel-body">
      <%= option.title %>
    </div>
  </div>
<% end %>

Take each user’s vote option and display it along with the poll’s topic. Use eager loading here to improve performance:

users_controller.rb

[...]
def show
  @user = User.includes(:vote_options).find_by_id(params[:id])
end
[...]

Don’t forget to set up the route:

routes.rb

[...]
resources :users, only: [:show]
[...]

The last thing to do is provide the link to the user’s profile:

layouts/application.html.erb

[...]
<ul class="nav navbar-nav navbar-right">
  <% if current_user %>
    <li><%= image_tag current_user.image_url, alt: current_user.name %></li>
    <li><%= link_to 'Profile', user_path(current_user) %></li>
    <li><%= link_to 'Logout', sign_out_path %></li>
  <% else %>
    <li><%= link_to 'Sign in', '/auth/facebook' %></li>
  <% end %>
</ul>
[...]

Boot up your server and check it out!

Conclusion

While building this Poller app, we discussed nested_attributes, the Cocoon gem, had a look at normalizing votes count, and discussed some caching issues, like model caching and using the counter_culture gem. Also, we looked at the jQuery Validate plugin and some fancy Bootstrap’s styles. I guess this is enough for today!

Of course, this app may be improved by adding more caching, an ability to re-vote or cancel a vote, styling it a bit more, etc. Feel free to clone my repo with the demo code and experiment with it!

Hope you found this article useful and interesting (should I create a poll to find that out?). Thank you for staying with me till the end and see you soon!

  • http://w3guy.com Agbonghama Collins

    Everything points to Ilya Bodrov (http://www.sitepoint.com/author/ibodrov/) as the author of the source. Is he the author of this post or the Ruby Editor Glenn.

    Maybe an admin who published the post forgot to attribute the post to the right author.

    • Ilya Bodrov

      Well I think Glenn provided his name by mistake :) That happens

      • http://w3guy.com Agbonghama Collins

        Thought as much. Hope it get fixed soon.

        • ggsp

          I fixed it. My bad…

  • Николай

    Thanks.
    A little tip: .find_by_id(params[:id]) could be written shorter with .find(params[:id])

    • Ilya Bodrov

      Yes, that is true! The difference is that `find` will return an exception (ActiveRecord::RecordNotFound) if the record is not found, `find_by_id` will just return `nil` so in many cases I prefer the latter so we can use condition like `if @collection`

  • hihihaha

    how could i sort options by total votes?

    • Ilya Bodrov

      As long as we are using counter_culture that is done fairly easy. Each VoteOption has the votes_count column available and automatically managed, so use it for ordering. For example:

      default_scope { order(‘votes_count DESC’) }

      put that order method call somewhere in your code if you don’t need like the default_scope which should be used carefully.

      The only thing that I’ve forgot to mention is that the vote.rb should have the following line of code as well for counter_culture to operate correctly:

      counter_culture :vote_option

      Will add it to the article as well.

      Having this in place, ordering should work fine.

  • Ariff

    Hey @ilyabodrov:disqus, I’m using the counter culture gem for a silimar implementation but facing an issue with the .votes_count. I’m creating and destroying votes by ajax on the show page. The vote count on this page will update accordingly with I use post.vote.count but lags when I use post.vote_count. Any idea how I can fix this?

  • http://borowskiy.ru Dmitriy Borowskiy

    Thanks for nice article. I think better use ActiveRecords for searching in voted_for? – Vote.where(“user_id=? AND vote_option_id IN (?)”,

    self.id, poll.vote_options.all.map(&:id))

    • Ilya Bodrov

      Seems like a nice idea!

  • Jonathan Bias

    How could I only allow users who have actually made the poll access the edit and delete functions.

  • David W.

    Hi, I keep getting a NoMethodError in Polls#show, which is coming from my polls/_vote_option.html.erb for an “undefined method ‘voted_for?’ for nil:NilClass”.

    why might that be the case?

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.