Polling Your Users with Rails

Share this article

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!

Frequently Asked Questions (FAQs) about Polling Users in Rails

How can I create a progress bar in Rails?

Creating a progress bar in Rails involves several steps. First, you need to install the ‘ruby-progressbar’ gem. Add gem 'ruby-progressbar' to your Gemfile and run bundle install. Then, you can create a new progress bar with ProgressBar.create. You can customize the progress bar by passing options like :total, :format, and :progress_mark. To increment the progress, use the increment method. Remember to update the progress bar in your controller action where the long-running task is performed.

How can I integrate a poll feature in a Discord chat using Rails?

To integrate a poll feature in a Discord chat using Rails, you would need to use a Discord bot. You can create a bot on the Discord developer portal, add it to your server, and then use a library like ‘discordrb’ to interact with the Discord API from your Rails application. You can then create commands for creating polls, voting, and displaying poll results.

How can I use the ‘ruby-progressbar’ gem?

The ‘ruby-progressbar’ gem is a flexible progress bar implementation for Ruby. You can install it by adding gem 'ruby-progressbar' to your Gemfile and running bundle install. Then, you can create a new progress bar with ProgressBar.create. You can customize the progress bar by passing options like :total, :format, and :progress_mark. To increment the progress, use the increment method.

How can I make a poll on Discord using Rails?

Making a poll on Discord using Rails involves creating a Discord bot and using it to interact with the Discord API from your Rails application. You can create commands for creating polls, voting, and displaying poll results. You would need to use a library like ‘discordrb’ to interact with the Discord API.

How can I build an async progress bar with Rails?

Building an async progress bar with Rails involves using JavaScript along with Rails. You would need to create a controller action that performs the long-running task and updates the progress. Then, you can use JavaScript to periodically fetch the progress and update the progress bar on the client side. You can use the ‘ruby-progressbar’ gem to create and manage the progress bar on the server side.

How can I customize the format of the progress bar in Rails?

You can customize the format of the progress bar in Rails by passing the :format option when creating a new progress bar with ProgressBar.create. The format string can include placeholders like %a for elapsed time, %e for estimated total time, %p for percentage, and %b for the progress bar.

How can I update the progress bar in Rails?

You can update the progress bar in Rails by calling the increment method on the progress bar object. You should call this method in your controller action where the long-running task is performed. Each call to increment will increase the progress by one. You can also pass a number to increment to increase the progress by that amount.

How can I display the progress bar in Rails?

Displaying the progress bar in Rails involves updating the progress bar on the server side and then using JavaScript to fetch the progress and update the progress bar on the client side. You can use AJAX to periodically fetch the progress from the server and update the progress bar.

How can I handle errors when creating a progress bar in Rails?

When creating a progress bar in Rails, you should handle errors by using exception handling. If an error occurs when creating or updating the progress bar, you can catch the exception, log the error, and display an error message to the user.

How can I test the progress bar in Rails?

Testing the progress bar in Rails can be done by writing unit tests for the controller action that updates the progress bar. You can use a testing framework like RSpec or MiniTest. You should test that the progress bar is correctly updated when the long-running task is performed. You can also write integration tests to test the JavaScript code that updates the progress bar on the client side.

Ilya Bodrov-KrukowskiIlya Bodrov-Krukowski
View Author

Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.

GlennG
Share this article
Read Next
From Idea to Prototype in Minutes: Claude Sonnet 3.5
From Idea to Prototype in Minutes: Claude Sonnet 3.5
Zain
Essential Plugins for WordPress Developers: Top Picks for 2024
Essential Plugins for WordPress Developers: Top Picks for 2024
SitePoint Sponsors
WebAssembly vs JavaScript: A Comparison
WebAssembly vs JavaScript: A Comparison
Kaan Güner
The Functional Depth of Docker and Docker Compose
The Functional Depth of Docker and Docker Compose
Vultr
How Top HR Agencies Build Trust Through Logo Designs
How Top HR Agencies Build Trust Through Logo Designs
Evan Brown
Leveraging Progressive Web Apps (PWAs) for Enhanced Mobile User Engagement
Leveraging Progressive Web Apps (PWAs) for Enhanced Mobile User Engagement
SitePoint Sponsors
10 Artificial Intelligence APIs for Developers
10 Artificial Intelligence APIs for Developers
SitePoint Sponsors
The Ultimate Guide to Navigating SQL Server With SQLCMD
The Ultimate Guide to Navigating SQL Server With SQLCMD
Nisarg Upadhyay
Retrieval-augmented Generation: Revolution or Overpromise?
Retrieval-augmented Generation: Revolution or Overpromise?
Kateryna ReshetiloOlexandr Moklyak
How to Deploy Apache Airflow on Vultr Using Anaconda
How to Deploy Apache Airflow on Vultr Using Anaconda
Vultr
Cloud Native: How Ampere Is Improving Nightly Arm64 Builds
Cloud Native: How Ampere Is Improving Nightly Arm64 Builds
Dave NearyAaron Williams
How to Create Content in WordPress with AI
How to Create Content in WordPress with AI
Çağdaş Dağ
A Beginner’s Guide to Setting Up a Project in Laravel
A Beginner’s Guide to Setting Up a Project in Laravel
Claudio Ribeiro
Enhancing DevSecOps Workflows with Generative AI: A Comprehensive Guide
Enhancing DevSecOps Workflows with Generative AI: A Comprehensive Guide
Gitlab
Creating Fluid Typography with the CSS clamp() Function
Creating Fluid Typography with the CSS clamp() Function
Daine Mawer
Comparing Full Stack and Headless CMS Platforms
Comparing Full Stack and Headless CMS Platforms
Vultr
7 Easy Ways to Make a Magento 2 Website Faster
7 Easy Ways to Make a Magento 2 Website Faster
Konstantin Gerasimov
Powerful React Form Builders to Consider in 2024
Powerful React Form Builders to Consider in 2024
Femi Akinyemi
Quick Tip: How to Animate Text Gradients and Patterns in CSS
Quick Tip: How to Animate Text Gradients and Patterns in CSS
Ralph Mason
Sending Email Using Node.js
Sending Email Using Node.js
Craig Buckler
Creating a Navbar in React
Creating a Navbar in React
Vidura Senevirathne
A Complete Guide to CSS Logical Properties, with Cheat Sheet
A Complete Guide to CSS Logical Properties, with Cheat Sheet
Ralph Mason
Using JSON Web Tokens with Node.js
Using JSON Web Tokens with Node.js
Lakindu Hewawasam
How to Build a Simple Web Server with Node.js
How to Build a Simple Web Server with Node.js
Chameera Dulanga
Building a Digital Fortress: How to Strengthen DNS Against DDoS Attacks?
Building a Digital Fortress: How to Strengthen DNS Against DDoS Attacks?
Beloslava Petrova
Crafting Interactive Scatter Plots with Plotly
Crafting Interactive Scatter Plots with Plotly
Binara Prabhanga
GenAI: How to Reduce Cost with Prompt Compression Techniques
GenAI: How to Reduce Cost with Prompt Compression Techniques
Suvoraj Biswas
How to Use jQuery’s ajax() Function for Asynchronous HTTP Requests
How to Use jQuery’s ajax() Function for Asynchronous HTTP Requests
Aurelio De RosaMaria Antonietta Perna
Quick Tip: How to Align Column Rows with CSS Subgrid
Quick Tip: How to Align Column Rows with CSS Subgrid
Ralph Mason
15 Top Web Design Tools & Resources To Try in 2024
15 Top Web Design Tools & Resources To Try in 2024
SitePoint Sponsors
7 Simple Rules for Better Data Visualization
7 Simple Rules for Better Data Visualization
Mariia Merkulova
Cloudways Autonomous: Fully-Managed Scalable WordPress Hosting
Cloudways Autonomous: Fully-Managed Scalable WordPress Hosting
SitePoint Team
Best Programming Language for AI
Best Programming Language for AI
Lucero del Alba
Quick Tip: How to Add Gradient Effects and Patterns to Text
Quick Tip: How to Add Gradient Effects and Patterns to Text
Ralph Mason
Logging Made Easy: A Beginner’s Guide to Winston in Node.js
Logging Made Easy: A Beginner’s Guide to Winston in Node.js
Vultr
How to Optimize Website Content for Featured Snippets
How to Optimize Website Content for Featured Snippets
Dipen Visavadiya
Psychology and UX: Decoding the Science Behind User Clicks
Psychology and UX: Decoding the Science Behind User Clicks
Tanya Kumari
Build a Full-stack App with Node.js and htmx
Build a Full-stack App with Node.js and htmx
James Hibbard
Digital Transformation with AI: The Benefits and Challenges
Digital Transformation with AI: The Benefits and Challenges
Priyanka Prajapat
Quick Tip: Creating a Date Picker in React
Quick Tip: Creating a Date Picker in React
Dianne Pena
How to Create Interactive Animations Using React Spring
How to Create Interactive Animations Using React Spring
Yemi Ojedapo
10 Reasons to Love Google Docs
10 Reasons to Love Google Docs
Joshua KrausZain Zaidi
How to Use Magento 2 for International Ecommerce Success
How to Use Magento 2 for International Ecommerce Success
Mitul Patel
5 Exciting New JavaScript Features in 2024
5 Exciting New JavaScript Features in 2024
Olivia GibsonDarren Jones
Tools and Strategies for Efficient Web Project Management
Tools and Strategies for Efficient Web Project Management
Juliet Ofoegbu
Choosing the Best WordPress CRM Plugin for Your Business
Choosing the Best WordPress CRM Plugin for Your Business
Neve Wilkinson
ChatGPT Plugins for Marketing Success
ChatGPT Plugins for Marketing Success
Neil Jordan
Managing Static Files in Django: A Comprehensive Guide
Managing Static Files in Django: A Comprehensive Guide
Kabaki Antony
The Ultimate Guide to Choosing the Best React Website Builder
The Ultimate Guide to Choosing the Best React Website Builder
Dianne Pena
Exploring the Creative Power of CSS Filters and Blending
Exploring the Creative Power of CSS Filters and Blending
Joan Ayebola
How to Use WebSockets in Node.js to Create Real-time Apps
How to Use WebSockets in Node.js to Create Real-time Apps
Craig Buckler
Best Node.js Framework Choices for Modern App Development
Best Node.js Framework Choices for Modern App Development
Dianne Pena
SaaS Boilerplates: What They Are, And 10 of the Best
SaaS Boilerplates: What They Are, And 10 of the Best
Zain Zaidi
Understanding Cookies and Sessions in React
Understanding Cookies and Sessions in React
Blessing Ene Anyebe
Enhanced Internationalization (i18n) in Next.js 14
Enhanced Internationalization (i18n) in Next.js 14
Emmanuel Onyeyaforo
Essential React Native Performance Tips and Tricks
Essential React Native Performance Tips and Tricks
Shaik Mukthahar
How to Use Server-sent Events in Node.js
How to Use Server-sent Events in Node.js
Craig Buckler
Five Simple Ways to Boost a WooCommerce Site’s Performance
Five Simple Ways to Boost a WooCommerce Site’s Performance
Palash Ghosh
Elevate Your Online Store with Top WooCommerce Plugins
Elevate Your Online Store with Top WooCommerce Plugins
Dianne Pena
Unleash Your Website’s Potential: Top 5 SEO Tools of 2024
Unleash Your Website’s Potential: Top 5 SEO Tools of 2024
Dianne Pena
How to Build a Chat Interface using Gradio & Vultr Cloud GPU
How to Build a Chat Interface using Gradio & Vultr Cloud GPU
Vultr
Enhance Your React Apps with ShadCn Utilities and Components
Enhance Your React Apps with ShadCn Utilities and Components
David Jaja
10 Best Create React App Alternatives for Different Use Cases
10 Best Create React App Alternatives for Different Use Cases
Zain Zaidi
Control Lazy Load, Infinite Scroll and Animations in React
Control Lazy Load, Infinite Scroll and Animations in React
Blessing Ene Anyebe
Building a Research Assistant Tool with AI and JavaScript
Building a Research Assistant Tool with AI and JavaScript
Mahmud Adeleye
Understanding React useEffect
Understanding React useEffect
Dianne Pena
Web Design Trends to Watch in 2024
Web Design Trends to Watch in 2024
Juliet Ofoegbu
Building a 3D Card Flip Animation with CSS Houdini
Building a 3D Card Flip Animation with CSS Houdini
Fred Zugs
How to Use ChatGPT in an Unavailable Country
How to Use ChatGPT in an Unavailable Country
Dianne Pena
An Introduction to Node.js Multithreading
An Introduction to Node.js Multithreading
Craig Buckler
How to Boost WordPress Security and Protect Your SEO Ranking
How to Boost WordPress Security and Protect Your SEO Ranking
Jaya Iyer
Understanding How ChatGPT Maintains Context
Understanding How ChatGPT Maintains Context
Dianne Pena
Building Interactive Data Visualizations with D3.js and React
Building Interactive Data Visualizations with D3.js and React
Oluwabusayo Jacobs
JavaScript vs Python: Which One Should You Learn First?
JavaScript vs Python: Which One Should You Learn First?
Olivia GibsonDarren Jones
13 Best Books, Courses and Communities for Learning React
13 Best Books, Courses and Communities for Learning React
Zain Zaidi
5 jQuery.each() Function Examples
5 jQuery.each() Function Examples
Florian RapplJames Hibbard
Implementing User Authentication in React Apps with Appwrite
Implementing User Authentication in React Apps with Appwrite
Yemi Ojedapo
AI-Powered Search Engine With Milvus Vector Database on Vultr
AI-Powered Search Engine With Milvus Vector Database on Vultr
Vultr
Understanding Signals in Django
Understanding Signals in Django
Kabaki Antony
Why React Icons May Be the Only Icon Library You Need
Why React Icons May Be the Only Icon Library You Need
Zain Zaidi
View Transitions in Astro
View Transitions in Astro
Tamas Piros
Getting Started with Content Collections in Astro
Getting Started with Content Collections in Astro
Tamas Piros
What Does the Java Virtual Machine Do All Day?
What Does the Java Virtual Machine Do All Day?
Peter Kessler
Become a Freelance Web Developer on Fiverr: Ultimate Guide
Become a Freelance Web Developer on Fiverr: Ultimate Guide
Mayank Singh
Layouts in Astro
Layouts in Astro
Tamas Piros
.NET 8: Blazor Render Modes Explained
.NET 8: Blazor Render Modes Explained
Peter De Tender
Mastering Node CSV
Mastering Node CSV
Dianne Pena
A Beginner’s Guide to SvelteKit
A Beginner’s Guide to SvelteKit
Erik KückelheimSimon Holthausen
Brighten Up Your Astro Site with KwesForms and Rive
Brighten Up Your Astro Site with KwesForms and Rive
Paul Scanlon
Which Programming Language Should I Learn First in 2024?
Which Programming Language Should I Learn First in 2024?
Joel Falconer
Managing PHP Versions with Laravel Herd
Managing PHP Versions with Laravel Herd
Dianne Pena
Accelerating the Cloud: The Final Steps
Accelerating the Cloud: The Final Steps
Dave Neary
An Alphebetized List of MIME Types
An Alphebetized List of MIME Types
Dianne Pena
The Best PHP Frameworks for 2024
The Best PHP Frameworks for 2024
Claudio Ribeiro
11 Best WordPress Themes for Developers & Designers in 2024
11 Best WordPress Themes for Developers & Designers in 2024
SitePoint Sponsors
Top 10 Best WordPress AI Plugins of 2024
Top 10 Best WordPress AI Plugins of 2024
Dianne Pena
20+ Tools for Node.js Development in 2024
20+ Tools for Node.js Development in 2024
Dianne Pena
The Best Figma Plugins to Enhance Your Design Workflow in 2024
The Best Figma Plugins to Enhance Your Design Workflow in 2024
Dianne Pena
Harnessing the Power of Zenserp for Advanced Search Engine Parsing
Harnessing the Power of Zenserp for Advanced Search Engine Parsing
Christopher Collins
Build Your Own AI Tools in Python Using the OpenAI API
Build Your Own AI Tools in Python Using the OpenAI API
Zain Zaidi
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form