Build a polling app with Ruby on Rails and PostgreSQL
You will need Ruby, Rails and PostgreSQL installed on your machine. A basic understanding of Ruby and CoffeeScript will be helpful.
Introduction
One of the effective means of interpreting and representing data for users of web applications, is by using data visualization tools such as charts, plots and graphs. This among other things aids simplification of data and presents it in a comprehensible and interactive form.
With the increase in popularity of the web and its technologies, representing numerical data with graphs in realtime has become inevitable.
Together in this post, we’ll build a basic poll application where users will be able to provide an answer to a question by selecting one of the options given, then we will proceed to represent these numbers on a pie chart. For a more user-friendly experience, Pusher will be use to update this chart in realtime without the need to refresh the page. This is what the finished application will look like:
The application depicted by the image above will be build with Ruby on Rails and the database powered by PostgreSQL. In addition we will make use of a library called Chartkick; a tool used for creating beautiful JavaScript charts.
Prerequisites
A basic understanding of Ruby, CoffeeScript and PostgresSQL will help you with this tutorial. Ensure that you have PostgreSQL installed on your machine. Kindly check the documentation of PostgreSQL, Ruby and Rails for further installations procedures.
Preparing the application
It is important that you have both Ruby and Rails installed. Run the following code in your terminal to confirm the successful installation of both Ruby and Rails:
$ ruby -v // 2.1 or above
$ rails -v // 4.2 or above
Next, we will run a command that will create a new Rails application in a folder called pusher-rails-chart
. Run the command below in your terminal:
$ rails new pusher-rails-chart -T --database=postgresql
The -T
flag was added to prevent generation of the default testing suite as we won’t be writing any tests.
Go ahead and change directory into the newly created folder:
$ cd pusher-rails-chart
In the root directory of this application, open the Gemfile
and add the following gems:
# Gemfile
gem 'bootstrap', '~> 4.1.0'
gem 'jquery-rails'
gem 'pusher'
gem 'figaro'
gem 'chartkick'
and install them:
$ bundle install
Database setup
To set up the database for our application, create a database called pusher-rails-chart_development
. Check out this article on how to create a Postgres database and an associated user and password.
In your database.yml
file, under the development
key, add the following code:
# config/database.yml
...
development:
<<: *default
database: pusher-rails-chart_development // add this line if it isn't already there
username: database_user // add this line
password: user_password // add this line
...
The username and password in the code above should have access to the pusher-rails-chart_development
database. Next, run the following code to setup the database:
# setup database
$ rails db:setup
Starting the development server
Start the development server by running rails s
from the terminal and visit http://localhost:3000 in your browser. Your page should look like this:
Sign up for a free Pusher account
Head over to Pusher and sign up for a free account.
Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:
Configure an app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate Pusher with, to be provided with boilerplate code:
You can retrieve your keys from the App Keys tab:
Now that you have your Pusher API keys, head over to config/initializers
directory. Create a pusher.rb
file and add the following code:
# config/initializers/pusher.rb
require 'pusher'
Pusher.app_id = ENV["PUSHER_APP_ID"]
Pusher.key = ENV["PUSHER_KEY"]
Pusher.secret = ENV["PUSHER_SECRET"]
Pusher.cluster = ENV["PUSHER_CLUSTER"]
Pusher.logger = Rails.logger
Pusher.encrypted = true
In your terminal, run:
$ figaro install
The command above will generate an application.yml
file. Locate the newly generate file in config/application.yml
and add the Pusher credentials obtained from your dashboard:
# config/application.yml
PUSHER_APP_ID: 'YOUR_APP_ID'
PUSHER_KEY: 'YOUR_APP_KEY'
PUSHER_SECRET: 'YOUR_APP_SECRET'
PUSHER_CLUSTER: 'CLUSTER'
Setting up the model and controller
Next, we will go ahead and generate a model and controller for our application. In your terminal, while in the project’s directory, run the following code:
// generate poll model and controller
$ rails g scaffold poll name:string laptop:binary
// run database migrations
$ rails db:migrate
Setting up the homepage
At the moment, if you restart the development server and reload the browser page, the welcome page will still be displayed. This is because we have not instructed Rails to show the homepage whenever we navigate to the root URL
of our site.
To tell Rails where the actual homepage is, update the route file with the following code:
# config/routes.rb
...
root 'polls#index'
...
In your application.js
file, add the following code just before the last line:
// app/assets/javascripts/application.js
...
//= require jquery3 # add this line
//= require popper # add this line
//= require bootstrap # add this line
//= require Chart.bundle # add this line
//= require chartkick # add this line
//= require_tree .
...
Styling
To set up a basic styling for our application, go ahead and rename your application.css
file to application.scss
, import Bootstrap and add some custom styles:
// app/assets/stylesheets/application.scss
@import "bootstrap";
@import url('https://fonts.googleapis.com/css?family=Josefin+Sans');
body {
font-family: 'Josefin Sans', sans-serif;
}
.navbar-brand {
color: #FFF !important;
&:hover {
background-color: unset;
}
}
.form-control {
outline: none !important;
&:focus {
border-color: transparent !important;
border: 1px !important;
}
}
At the moment, if we restart the development server and reload the application, we would notice changes in the fonts of our page, this is because we have successfully included Bootstrap.
If you encounter any errors relating to application.html.erb
, in config/boot.rb
, change the ExecJS runtime from Duktape to Node.
# config/boot.rb
ENV['EXECJS_RUNTIME'] = 'Node'
Setting up the polls table
To set up the view, replace the code in your index.html.erb
file with the following:
<%# app/views/polls/index.html.erb %>
<div class="container-fluid">
<div class="row">
<div class="col-5 offset-1">
<p id="notice"><%= notice %></p>
<h1>Polls</h1>
<table id='poll-table' class="table table-hover">
<thead class="thead-light">
<tr>
<th>Name</th>
<th>Laptop</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @polls.each do |poll| %>
<tr>
<td><%= poll.name %></td>
<td><%= poll.laptop %></td>
<td><%= link_to 'Show', poll %></td>
<td><%= link_to 'Edit', edit_poll_path(poll) %></td>
<td><%= link_to 'Destroy', poll, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div class="col-6">
<%= pie_chart [["Yes", Poll.where(laptop: "yes").count], ["No", Poll.where(laptop: "no").count]], id:'polls'%>
</div>
</div>
<div class="row">
<div class='col-11 offset-1 col-sm-4'>
<h3>New poll</h3>
<%= render 'form', poll: @poll %>
</div>
</div>
</div>
Here, we have used Bootstrap to style our table and within the second column of the first row we included a line to render the pie chart for this application on the homepage. Chartkick helps simplify the use of chart. You can check here for more information.
Next, update the index method of the polls controller, to include the following:
# app/controllers/polls_controller.rb
class PollsController < ApplicationController
...
def index
@polls = Poll.all
@poll = Poll.new # add this line
end
...
end
The code above adds Bootstrap styling to the table on our landing page. It also instantiates a new poll record in the index
method of the polls controller. If you reload the landing page, you should see the form.
Submitting a new poll
Currently adding a new poll record redirects us to a different page. We would like to handle that with AJAX instead. To achieve this, add remote: true
and format: :json
to your form. Replace the code in _form.html.erb
with the following:
// app/views/polls/_form.html.erb
<%= form_with(model: poll, remote: true, format: :json, id: 'add_poll') do |form| %>
<% if poll.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(poll.errors.count, "error") %> prohibited this poll from being saved:</h2>
<ul>
<% poll.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name, id: :poll_name, class: "form-control" %>
</div>
<div class="field">
<%= form.label :laptop, "Do you own a laptop? " %><br />
<%= form.label :laptop, "Yes", :value => "true" %>
<%= form.radio_button :laptop, 'yes', :checked => true %>
<%= form.label :laptop, "No", :value => "false" %>
<%= form.radio_button :laptop, 'no'%>
</div>
<div class="actions">
<%= form.submit 'submit', class: "btn btn-success"%>
</div>
<% end %>
Now that our form uses AJAX, we’ll handle updating the page with CoffeeScript. Open app/assets/javascripts/polls.coffee
and paste in the code below:
# app/assets/javascripts/polls.coffee
submitPoll = (data) ->
$('#poll-table tbody').append """
<tr>
<td>#{data.name}</td>
<td>#{data.laptop}</td>
<td><a href="/polls/#{data.id}">Show</a></td>
<td><a href="/polls/#{data.id}/edit">Edit</a></td>
<td><a data-confirm="Are you sure?" rel="nofollow" data-method="delete" href="/polls/#{data.id}">Destroy</a></td>
</tr>
"""
return
$ ->
$('#add_poll').on 'ajax:success', (data) ->
$('#add_poll')[0].reset()
submitPoll data.detail[0]
return
return
In the above code, we added an ajax:success
event listener to our form courtesy of jQuery-ujs. Whenever we add a new response, we get our newly added answer and update our page with it.
Let’s reload our page and add a few responses, they should pop up on the page as soon as we hit submit.
If the laptop
column shows ‘undefined’ or returns a null value, this has to do with mass assignment protection from the controller, you can read more about this here. To fix this, navigate to app/controllers/polls_controller.rb
and within the poll_param
method, add :laptop
to the list of params.
Realtime service with Pusher
To update our table realtime, we will publish the new poll record to a channel and subscribe to it on the frontend of our app. In the poll model, we’ll add an after_commit
callback which will call a method that will publish the new poll. We’ll use after_commit
so data is published whenever we add or update a poll.
Add the following code to the poll model:
# app/models/poll.rb
class Poll < ApplicationRecord
after_commit :notify_pusher, on: [:create, :update]
def notify_pusher
Pusher.trigger('poll', 'new', self.as_json)
end
end
Here, our initialized Pusher client triggers an event called new
through a channel named poll
.
Updating the frontend of the application
The client side of our application needs to listen for those changes that are being pushed by the server and update the view with it. To do this, we’ll rename our polls.coffee
file to polls.coffee.erb
so we can add some Ruby code in it.
Update the file with the following code:
# app/assets/javascripts/polls.coffee.erb
submitPoll = (data) ->
$('#poll-table tbody').append """
<tr>
<td>#{data.name}</td>
<td>#{data.laptop}</td>
<td><a href="/polls/#{data.id}">Show</a></td>
<td><a href="/polls/#{data.id}/edit">Edit</a></td>
<td><a data-confirm="Are you sure?" rel="nofollow" data-method="delete" href="/polls/#{data.id}">Delete</a></td>
</tr>
"""
return
$ ->
updateChart = (data) ->
chart = Chartkick.charts['polls'].getChartObject()
if data.laptop == 'yes'
chart.data.datasets[0].data[0]++
chart.update()
else
chart.data.datasets[0].data[1]++
chart.update()
$ ->
$('#add_poll').on 'ajax:success', (data) ->
$('#add_poll')[0].reset()
return
pusher = new Pusher('<%= ENV["PUSHER_KEY"] %>',
cluster: '<%= ENV["PUSHER_CLUSTER"] %>'
encrypted: true)
channel = pusher.subscribe('poll')
channel.bind 'new', (data) ->
submitPoll data
updateChart data
return
return
In the code above, we created two methods:
-
submitPoll()
: updates the poll table with new records upon submission of the poll’s form. -
updateChart()
: updates the datasets of our pie chart without the need to refresh the page.
Lastly, add the Pusher library inside the head tag in the application.html.erb
file just before the javascript_include_tag
:
<%# . app/views/layouts/application.html.erb %>
<head>
<title>Poll</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<script src="https://js.pusher.com/4.1/pusher.min.js"></script>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
Testing the application
Restart the development server if it is currently running. Check your page on http://localhost:3000:
Conclusion
Realtime and visual analysis in web applications has come to stay and can only get better. As shown by this post, you can now go ahead and give your users the best of visual realtime update in Ruby on Rails applications.
I hope you found this tutorial helpful. Feel free to download the complete source can be found on GitHub.
28 May 2018
by Christian Nwamba