rails new xmas-poll-2025 --skip-active-record
Go to the file named gemfile and add these two lines. We are going to install httparty and dotenv-rails. httparty allows us to call endpoints, which is going to be absolutely necessary. dotenv-rails helps us get variables from the .env file. This is not so necessary, but it's a good practice and we're gonna do it.
Gemfile
gem "httparty"
gem "dotenv-rails", groups: [:development, :test]
gem "dotenv-rails", groups: [:development, :test]
Then navigate to the xmas-poll-2025 directory and run this command in the CLI.
bundle install
Still in the CLI, run this command to create the controller poll.
rails generate controller poll
This will create the file, poll_controller.rb, in the controllers directory of the app directory. Some other files will be created, but we can examine them later. Let's first make sure that we have an index action defined, and a submit action.
app/controllers/poll_controller.rb
class PollController < ApplicationController
def index
end
def submit
end
end
def index
end
def submit
end
end
Go to routes.rb in the config directory. Make sure your main route is defined. Add a submit route for the form we're about to create. Note that we have the alias "poll_page" for the main route, and the alias "submit_poll_form" for the form submission route. These will be useful as references. We'll also define the index action of the poll controller as the root.
config/routes.rb
Rails.application.routes.draw do
get "poll/", to: "poll#index", as: "poll_page"
root "poll#index"
post "poll/submit", to: "poll#submit", as: "submit_poll_form"
end
get "poll/", to: "poll#index", as: "poll_page"
root "poll#index"
post "poll/submit", to: "poll#submit", as: "submit_poll_form"
end
In the views directory, navigate to layouts. You should see application.html.erb. Change the title and add placeholders for alerts and notices.
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title><%= content_for(:title) || "Xmas Poll 2025" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= yield :head %>
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
<link rel="icon" href="/icon.png" type="image/png">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.png">
<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<% if flash[:notice] %>
<div class="notice"><%= flash[:notice] %></div>
<% end %>
<% if flash[:alert] %>
<div class="alert"><%= flash[:alert] %></div>
<% end %>
<br />
<%= yield %>
</body>
</html>
<html>
<head>
<title><%= content_for(:title) || "Xmas Poll 2025" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= yield :head %>
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
<link rel="icon" href="/icon.png" type="image/png">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.png">
<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<% if flash[:notice] %>
<div class="notice"><%= flash[:notice] %></div>
<% end %>
<% if flash[:alert] %>
<div class="alert"><%= flash[:alert] %></div>
<% end %>
<br />
<%= yield %>
</body>
</html>
We'll create a view for poll. In the views directory, create the poll directory and in it, create index.html.erb.
In here, we'll want to use the form helper. We can manually write the HTML form, but this really just defeats the purpose of using Ruby On Rails. The form will use the route submit_poll_form, which we defined earlier. Add a button too.
app/views/poll/index.html.erb
<div>
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<button>Send</button>
<% end %>
</div>
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<button>Send</button>
<% end %>
</div>
Run this command in the CLI.
rails server
When you go to http://localhost:3000, you should see this!
Let's just spruce this up a bit. Go to this CSS file. Here, set the font. Background color we'll keep to a Christmassy deep red. Create pollform. I want to give it a nice cheerful bright green background, with round corners. The button will be a big red one. Obviously, all this is just visual and makes no difference to functionality.
app/assets/stylesheets/application.css
body
{
font-family: Verdana;
font-size: 14px;
background-color: rgb(100, 0, 0);
}
.pollform
{
width: 600px;
padding: 10px;
border-radius: 10px;
border: 3px solid rgb(200, 0, 0);
background-color: rgb(0, 200, 0);
margin: 5% auto 0 auto;
}
button
{
width: 10em;
height: 2em;
margin-top: 10%;
float: right;
font-size: 1.5em;
border-radius: 20px;
border: 3px solid rgb(255, 2525, 255);
background-color: rgb(255, 0, 0);
color: rgb(255, 255, 255);
}
button:hover
{
background-color: rgb(155, 0, 0);
}
{
font-family: Verdana;
font-size: 14px;
background-color: rgb(100, 0, 0);
}
.pollform
{
width: 600px;
padding: 10px;
border-radius: 10px;
border: 3px solid rgb(200, 0, 0);
background-color: rgb(0, 200, 0);
margin: 5% auto 0 auto;
}
button
{
width: 10em;
height: 2em;
margin-top: 10%;
float: right;
font-size: 1.5em;
border-radius: 20px;
border: 3px solid rgb(255, 2525, 255);
background-color: rgb(255, 0, 0);
color: rgb(255, 255, 255);
}
button:hover
{
background-color: rgb(155, 0, 0);
}
In here, set the div class to pollform.
app/views/poll/index.html.erb
<div class="pollform">
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<button>Send</button>
<% end %>
</div>
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<button>Send</button>
<% end %>
</div>
There you go. Nothing in the form yet, but you can see that the form details will appear in the bright green area.
Time to work on the controller. Remember installing httparty? We'll use it here with an import.
app/controllers/poll_controller.rb
require "httparty"
class PollController < ApplicationController
def index
end
def submit
end
end
class PollController < ApplicationController
def index
end
def submit
end
end
Before we continue, we need to add this line to the .env file. This is the URL of the API endpoint we defined in the previous part of the web tutorial.
.env
ORDS_API_URL=https://oracleapex.com/ords/teochewthunder/polls/poll/1
Then use the variable ORDS_API_URL from .env, in this way.
app/controllers/poll_controller.rb
require "httparty"
class PollController < ApplicationController
ORDS_API_URL = ENV["ORDS_API_URL"]
def index
end
def submit
end
end
class PollController < ApplicationController
ORDS_API_URL = ENV["ORDS_API_URL"]
def index
end
def submit
end
end
For index, we want to fetch the questions. Use the get() method of HTTParty, passing in ORDS_API_URL as an argument. This makes a call to that specific URL using a GET request. The result should be assigned to the variable response.
app/controllers/poll_controller.rb
class PollController < ApplicationController
ORDS_API_URL = ENV["ORDS_API_URL"]
def index
response = HTTParty.get(
ORDS_API_URL
)
end
def submit
end
end
ORDS_API_URL = ENV["ORDS_API_URL"]
def index
response = HTTParty.get(
ORDS_API_URL
)
end
def submit
end
end
The fetched results should look like this.
{
"items": [
{
"name": "Xmas Poll 2025",
"serial_no": 1,
"title": "JOY TO THE WORLD"
},
{
"name": "Xmas Poll 2025",
"serial_no": 2,
"title": "IT CAME UPON THE MIDNIGHT CLEAR"
},
{
"name": "Xmas Poll 2025",
"serial_no": 3,
"title": "I SAW MOMMY KISSING SANTA CLAUS"
},
{
"name": "Xmas Poll 2025",
"serial_no": 4,
"title": "HARK THE HERALD ANGELS SING"
},
{
"name": "Xmas Poll 2025",
"serial_no": 5,
"title": "DECK THE HALLS"
},
{
"name": "Xmas Poll 2025",
"serial_no": 6,
"title": "THE FIRST NOEL"
},
{
"name": "Xmas Poll 2025",
"serial_no": 7,
"title": "RUDOLPH THE RED-NOSED REINDEER"
},
{
"name": "Xmas Poll 2025",
"serial_no": 8,
"title": "SILENT NIGHT"
},
{
"name": "Xmas Poll 2025",
"serial_no": 9,
"title": "JINGLE BELLS"
},
{
"name": "Xmas Poll 2025",
"serial_no": 10,
"title": "JINGLE BELL ROCK"
}
],
"hasMore": false,
"limit": 25,
"offset": 0,
"count": 10,
"links": [
{
"rel": "self",
"href": "https://oracleapex.com/ords/teochewthunder/polls/poll/1"
},
{
"rel": "edit",
"href": "https://oracleapex.com/ords/teochewthunder/polls/poll/1"
},
{
"rel": "describedby",
"href": "https://oracleapex.com/ords/teochewthunder/metadata-catalog/polls/poll/item"
},
{
"rel": "first",
"href": "https://oracleapex.com/ords/teochewthunder/polls/poll/1"
}
]
}
"items": [
{
"name": "Xmas Poll 2025",
"serial_no": 1,
"title": "JOY TO THE WORLD"
},
{
"name": "Xmas Poll 2025",
"serial_no": 2,
"title": "IT CAME UPON THE MIDNIGHT CLEAR"
},
{
"name": "Xmas Poll 2025",
"serial_no": 3,
"title": "I SAW MOMMY KISSING SANTA CLAUS"
},
{
"name": "Xmas Poll 2025",
"serial_no": 4,
"title": "HARK THE HERALD ANGELS SING"
},
{
"name": "Xmas Poll 2025",
"serial_no": 5,
"title": "DECK THE HALLS"
},
{
"name": "Xmas Poll 2025",
"serial_no": 6,
"title": "THE FIRST NOEL"
},
{
"name": "Xmas Poll 2025",
"serial_no": 7,
"title": "RUDOLPH THE RED-NOSED REINDEER"
},
{
"name": "Xmas Poll 2025",
"serial_no": 8,
"title": "SILENT NIGHT"
},
{
"name": "Xmas Poll 2025",
"serial_no": 9,
"title": "JINGLE BELLS"
},
{
"name": "Xmas Poll 2025",
"serial_no": 10,
"title": "JINGLE BELL ROCK"
}
],
"hasMore": false,
"limit": 25,
"offset": 0,
"count": 10,
"links": [
{
"rel": "self",
"href": "https://oracleapex.com/ords/teochewthunder/polls/poll/1"
},
{
"rel": "edit",
"href": "https://oracleapex.com/ords/teochewthunder/polls/poll/1"
},
{
"rel": "describedby",
"href": "https://oracleapex.com/ords/teochewthunder/metadata-catalog/polls/poll/item"
},
{
"rel": "first",
"href": "https://oracleapex.com/ords/teochewthunder/polls/poll/1"
}
]
}
With that in mind, we'll want to send that data to the view if called successfully. If unsuccessful, we'll want to flash a notice.
app/controllers/poll_controller.rb
def index
response = HTTParty.get(
ORDS_API_URL
)
if response.code == 200
else
flash.now[:alert] = "Error fetching data:"
@api_data = {}
end
end
response = HTTParty.get(
ORDS_API_URL
)
if response.code == 200
else
flash.now[:alert] = "Error fetching data:"
@api_data = {}
end
end
We can actually test this. Deliberately sabotage the endpoint like this.
app/controllers/poll_controller.rb
response = HTTParty.get(
ORDS_API_URL + "/test"
)
ORDS_API_URL + "/test"
)
Here you go, a negative result!
Let's just style the notices. We want them to be a slim bar at the top, thus we set the position property to absolute. Let's have a nice green color scheme for a general success message and a red color scheme for errors.
app/assets/stylesheets/application.css
button:hover
{
background-color: rgb(155, 0, 0);
}
.notice, .alert
{
width: 100%;
position: absolute;
font-size: 0.85em;
font-weight: bold;
padding: 0.5em;
text-align: center;
left: 0;
top: 0;
}
.notice
{
background-color: rgba(100, 255, 100, 0.2);
color: rgb(0, 255, 0);
}
.alert
{
background-color: rgba(255, 100, 100, 0.2);
color: rgb(255, 0, 0);
}
{
background-color: rgb(155, 0, 0);
}
.notice, .alert
{
width: 100%;
position: absolute;
font-size: 0.85em;
font-weight: bold;
padding: 0.5em;
text-align: center;
left: 0;
top: 0;
}
.notice
{
background-color: rgba(100, 255, 100, 0.2);
color: rgb(0, 255, 0);
}
.alert
{
background-color: rgba(255, 100, 100, 0.2);
color: rgb(255, 0, 0);
}
There it is!
Now un-sabotage the API endpoint. We want it to be correct. Make sure that the parsed_response property of response is assigned to api_data. Note that the "@" denotes api_data as an instance variable that can be used in the corresponding view.
app/controllers/poll_controller.rb
def index
response = HTTParty.get(
ORDS_API_URL
)
if response.code == 200
@api_data = response.parsed_response
else
flash.now[:alert] = "Error fetching data: #{response.body}"
@api_data = {}
end
end
response = HTTParty.get(
ORDS_API_URL
)
if response.code == 200
@api_data = response.parsed_response
else
flash.now[:alert] = "Error fetching data: #{response.body}"
@api_data = {}
end
end
Then we'll work on the view. Remember api_data and what the returned JSON looked like? Well, if we want the Poll Title, we just need to use the first element of items, and get the name property.
app/views/poll/index.html.erb
<h1><%= @api_data["items"][0]["name"] %></h1>
<div class="pollform">
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<% end %>
</div>
<div class="pollform">
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<% end %>
</div>
In the CSS, add a styling for h1 tags.
app/assets/stylesheets/application.css
body
{
font-family: Verdana;
font-size: 14px;
background-color: rgb(100, 0, 0);
}
h1
{
text-align: center;
color: rgba(255, 255, 255, 0.5);
}
.pollform
{
width: 600px;
padding: 10px;
border-radius: 10px;
border: 3px solid rgb(200, 0, 0);
background-color: rgb(0, 200, 0);
margin: 5% auto 0 auto;
}
{
font-family: Verdana;
font-size: 14px;
background-color: rgb(100, 0, 0);
}
h1
{
text-align: center;
color: rgba(255, 255, 255, 0.5);
}
.pollform
{
width: 600px;
padding: 10px;
border-radius: 10px;
border: 3px solid rgb(200, 0, 0);
background-color: rgb(0, 200, 0);
margin: 5% auto 0 auto;
}
And here is the header!
Now we'll want to display the rest of api_data. Let's have a table in there, with these headers.
app/views/poll/index.html.erb
<h1><%= @api_data["items"][0]["name"] %></h1>
<div class="pollform">
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<table>
<tr>
<td width="300px"><b>Carol</b></td>
<td width="600px"><b>Rating (1 = lowest, 5 = highest)</b></td>
</tr>
</table>
<button>Send</button>
<% end %>
</div>
<div class="pollform">
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<table>
<tr>
<td width="300px"><b>Carol</b></td>
<td width="600px"><b>Rating (1 = lowest, 5 = highest)</b></td>
</tr>
</table>
<button>Send</button>
<% end %>
</div>
So far so good...
Then we use the each keyword on items, to iterate through it. Each instance of items will be known as question within this loop. And for each instance, we will have a HTML table row and two columns.
app/views/poll/index.html.erb
<h1><%= @api_data["items"][0]["name"] %></h1>
<div class="pollform">
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<table>
<tr>
<td width="300px"><b>Carol</b></td>
<td width="600px"><b>Rating (1 = lowest, 5 = highest)</b></td>
</tr>
<% @api_data["items"].each do |question| %>
<tr>
<td></td>
<td></td>
</tr>
<% end %>
</table>
<button>Send</button>
<% end %>
</div>
<div class="pollform">
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<table>
<tr>
<td width="300px"><b>Carol</b></td>
<td width="600px"><b>Rating (1 = lowest, 5 = highest)</b></td>
</tr>
<% @api_data["items"].each do |question| %>
<tr>
<td></td>
<td></td>
</tr>
<% end %>
</table>
<button>Send</button>
<% end %>
</div>
Here, we'll reflect the question property value for title.
app/views/poll/index.html.erb
<h1><%= @api_data["items"][0]["name"] %></h1>
<div class="pollform">
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<table>
<tr>
<td width="300px"><b>Carol</b></td>
<td width="600px"><b>Rating (1 = lowest, 5 = highest)</b></td>
</tr>
<% @api_data["items"].each do |question| %>
<tr>
<td><%= form.label nil, "#{question['title']}" %></td>
<td></td>
</tr>
<% end %>
</table>
<button>Send</button>
<% end %>
</div>
<div class="pollform">
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<table>
<tr>
<td width="300px"><b>Carol</b></td>
<td width="600px"><b>Rating (1 = lowest, 5 = highest)</b></td>
</tr>
<% @api_data["items"].each do |question| %>
<tr>
<td><%= form.label nil, "#{question['title']}" %></td>
<td></td>
</tr>
<% end %>
</table>
<button>Send</button>
<% end %>
</div>
And here all the questions from the poll, the names of the carols, are displayed!
Next we want to display a series of radio buttons. The values will be from 1 to 5. To that end, we should define rating_options as a collection containing values 1, 2, 3 4 and 5. Then we use the to_a() method to convert it to an array.
app/views/poll/index.html.erb
<h1><%= @api_data["items"][0]["name"] %></h1>
<div class="pollform">
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<% rating_options = (1..5).to_a %>
<table>
<tr>
<td width="300px"><b>Carol</b></td>
<td width="600px"><b>Rating (1 = lowest, 5 = highest)</b></td>
</tr>
<% @api_data["items"].each do |question| %>
<tr>
<td><%= form.label nil, "#{question['title']}" %></td>
<td></td>
</tr>
<% end %>
</table>
<button>Send</button>
<% end %>
</div>
<div class="pollform">
<%= form_with url: submit_poll_form_path, local: true do |form| %>
<% rating_options = (1..5).to_a %>
<table>
<tr>
<td width="300px"><b>Carol</b></td>
<td width="600px"><b>Rating (1 = lowest, 5 = highest)</b></td>
</tr>
<% @api_data["items"].each do |question| %>
<tr>
<td><%= form.label nil, "#{question['title']}" %></td>
<td></td>
</tr>
<% end %>
</table>
<button>Send</button>
<% end %>
</div>
In here, we want to iterate through rating_options using each. Each element will be referred to as option.
app/views/poll/index.html.erb
<tr>
<td><%= form.label nil, "#{question['title']}" %></td>
<td>
<% rating_options.each do |option| %>
<% end %>
</td>
</tr>
<td><%= form.label nil, "#{question['title']}" %></td>
<td>
<% rating_options.each do |option| %>
<% end %>
</td>
</tr>
In here, we use the form helper object radio_button. We pass in option as its value. Since it's a form element, it would be proper to give it name and id properties as well, which we will base on the value of the property serial_no, which is unique in each poll. The default value checked is always 3. You may notice ":none", passed in as the first argument. You can't omit this; I've tried and the server complains. This is actually meant to be the name of the argument, but I've overwritten it later as you can see, and now ":none" is just a placeholder value.
app/views/poll/index.html.erb
<tr>
<td><%= form.label nil, "#{question['title']}" %></td>
<td>
<% rating_options.each do |option| %>
<%= form.radio_button :none,
option,
name: "answers[#{question['serial_no']}]",
id: "answer_#{question['serial_no']}_#{option}",
checked: option == 3
%>
<% end %>
</td>
</tr>
<td><%= form.label nil, "#{question['title']}" %></td>
<td>
<% rating_options.each do |option| %>
<%= form.radio_button :none,
option,
name: "answers[#{question['serial_no']}]",
id: "answer_#{question['serial_no']}_#{option}",
checked: option == 3
%>
<% end %>
</td>
</tr>
And now we have a label which displays option as the text, and preferences the element with the id specified, e.g, answer_2_1.
app/views/poll/index.html.erb
<tr>
<td><%= form.label nil, "#{question['title']}" %></td>
<td>
<% rating_options.each do |option| %>
<%= form.radio_button :none,
option,
name: "answers[#{question['serial_no']}]",
id: "answer_#{question['serial_no']}_#{option}",
checked: option == 3
%>
<%= form.label "answer_#{question['serial_no']}_#{option}", option %>
<% end %>
</td>
</tr>
<td><%= form.label nil, "#{question['title']}" %></td>
<td>
<% rating_options.each do |option| %>
<%= form.radio_button :none,
option,
name: "answers[#{question['serial_no']}]",
id: "answer_#{question['serial_no']}_#{option}",
checked: option == 3
%>
<%= form.label "answer_#{question['serial_no']}_#{option}", option %>
<% end %>
</td>
</tr>
So there, you see we have a bunch of radio buttons and labels!
Believe it or not, this is actually the easy part. Brace yourselves!


























