Showing posts with label Ruby. Show all posts
Showing posts with label Ruby. Show all posts

Friday, 26 December 2025

Web Tutorial: Ruby On Rails Xmas Poll (Part 4/4)

We'll, of course, want to find a way to view the poll results. To do that, let's first go back to Oracle APEX and create an endpoint for this. Remember the template we created in the first part of this web tutorial? It was called /poll/:id/results/.

Then we create a GET handler for this.


And this is the Pl/SQL code. Do bear in mind that here, averages and medians only make sense because the answers in the poll are numerical. In any case, tables are joined and columns are grouped to get these averages and medians. Before using the AVG() and MEDIAN() functions, we must first run TO_NUMBER() on the data, to convert them to numerical values.
SELECT
pq.TITLE,
AVG(TO_NUMBER(pr.RESULT)) AS AVG_RESULT,
MEDIAN(TO_NUMBER(pr.RESULT)) AS MEDIAN_RESULT
FROM POLL_QUESTIONS pq LEFT JOIN POLL_RESULTS pr
ON pr.QUESTION_SERIAL_NO = pq.SERIAL_NO AND pr.POLL_ID = :id AND pq.POLL_ID = :id
GROUP BY pq.TITLE
ORDER BY AVG_RESULT DESC, MEDIAN_RESULT DESC


The results should be something like this.
{
 "items": [
  {
   "title": "IT CAME UPON THE MIDNIGHT CLEAR",
   "avg_result": 4,
   "median_result": 5
  },
  {
   "title": "JINGLE BELL ROCK",
   "avg_result": 4,
   "median_result": 4.5
  },
  {
   "title": "SILENT NIGHT",
   "avg_result": 3.5,
   "median_result": 3
  },
  {
   "title": "THE FIRST NOEL",
   "avg_result": 3.16666666666666666666666666666666666667,
   "median_result": 3
  },
  {
   "title": "JINGLE BELLS",
   "avg_result": 3,
   "median_result": 3
  },
  {
   "title": "HARK THE HERALD ANGELS SING",
   "avg_result": 2.83333333333333333333333333333333333333,
   "median_result": 3
  },
  {
   "title": "DECK THE HALLS",
   "avg_result": 2.66666666666666666666666666666666666667,
   "median_result": 3
  },
  {
   "title": "JOY TO THE WORLD",
   "avg_result": 2.54545454545454545454545454545454545455,
   "median_result": 3
  },
  {
   "title": "RUDOLPH THE RED-NOSED REINDEER",
   "avg_result": 2.5,
   "median_result": 2.5
  },
  {
   "title": "I SAW MOMMY KISSING SANTA CLAUS",
   "avg_result": 2.27272727272727272727272727272727272727,
   "median_result": 2
  }
 ],
 "hasMore": false,
 "limit": 25,
 "offset": 0,
 "count": 10,
 "links": [
  {
   "rel": "self",
   "href": "https://oracleapex.com/ords/teochewthunder/polls/poll/1/results/"
  },
  {
   "rel": "describedby",
   "href": "https://oracleapex.com/ords/teochewthunder/metadata-catalog/polls/poll/1/results/"
  },
  {
   "rel": "first",
   "href": "https://oracleapex.com/ords/teochewthunder/polls/poll/1/results/"
  }
 ]
}


Back to the Rails server! In the CLI, run this command to create the controller result.
rails generate controller result

Once we've done that, it's time to modify the generated file. Again, we need httparty. We'll reuse the environment variable ORDS_API_URL but append "/results" to it.

app/controllers/result_controller.rb
require "httparty"

class ResultController < ApplicationController
    ORDS_API_URL = ENV["ORDS_API_URL"] + "/results"

    def index

    end

end

Of course, the next step is to mirror what we did for the index action of the root page, and use HTTParty's get() method to call the URL endpoint, then have an If block to handle the result.

app/controllers/result_controller.rb
require "httparty"

class ResultController < ApplicationController
    ORDS_API_URL = ENV["ORDS_API_URL"] + "/results"

    def index
        response = HTTParty.get(
            ORDS_API_URL
        )

        if response.code == 200


        else

            flash.now[:alert] = "Error fetching data."
            @api_data = {}
        end
    end
end

If successful, we return the result to the view by binding it to api_data.

app/controllers/result_controller.rb
require "httparty"

class ResultController < ApplicationController
    ORDS_API_URL = ENV["ORDS_API_URL"] + "/results"

    def index
        response = HTTParty.get(
            ORDS_API_URL
        )

        if response.code == 200
            @api_data = response.parsed_response
        else
            flash.now[:alert] = "Error fetching data."
            @api_data = {}
        end
    end
end

This is the view. We have a header, a div styled using the CSS class pollresult, and a table.

app/views/result/index.html.erb
<h1>Poll Results</h1>
<div class="pollresult">
  <table>
    <tr>
      <td width="300px"><b>Carol</b></td><td width="150px" style="text-align: right;"><b>Average Rating</b></td>
      <td width="150px" style="text-align: right;"><b>Median Rating</b></td>
    </tr>
  </table>
</div>

Refer to the sample JSON result I showed you earlier. We'll use the title, avg_result and median_result properties here.

app/views/result/index.html.erb
<h1>Poll Results</h1>
<div class="pollresult">
  <table>
    <tr>
      <td width="300px"><b>Carol</b></td><td width="150px" style="text-align: right;"><b>Average Rating</b></td>
      <td width="150px" style="text-align: right;"><b>Median Rating</b></td>
    </tr>

    <% @api_data["items"].each do |result| %>
      <tr>
        <td><%= "#{result['title']}" %></td>
        <td style="text-align: right;"><%= "#{result['avg_result']}" %></td>
        <td style="text-align: right;"><%= "#{result['median_result']}" %></td>
      </tr>
    <% end %>

  </table>
</div>


For the CSS, if you're a lazy bastard like me, you might just want to piggyback off this existing class, or create a new one with a custom design.

app/assets/stylesheets/application.css
.pollform, .pollresult
{
  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;
}


Do you see the problem? The average is way too long and needs to be truncated.

We use sprintf(), passing in "%.1f" to truncate the text to one decimal place.

app/views/result/index.html.erb
<h1>Poll Results</h1>
<div class="pollresult">
  <table>
    <tr>
      <td width="300px"><b>Carol</b></td><td width="150px" style="text-align: right;"><b>Average Rating</b></td>
      <td width="150px" style="text-align: right;"><b>Median Rating</b></td>
    </tr>

    <% @api_data["items"].each do |result| %>
      <tr>
        <td><%= "#{result['title']}" %></td>
        <td style="text-align: right;"><%= "#{sprintf('%.1f', result['avg_result'])}" %></td>
        <td style="text-align: right;"><%= "#{sprintf('%.1f', result['median_result'])}" %></td>
      </tr>
    <% end %>
  </table>
</div>


There you go!


For a final touch, add a nav link to each of these pages.

app/views/poll/index.html.erb
<h1><%= @api_data["items"][0]["name"] %></h1>
<h2><%= link_to "View Results", result_page_path %></h2>
<div class="pollform">


app/views/result/index.html.erb
<h1>Poll Results</h1>
<h2><%= link_to "View Poll", root_path %></h2>
<div class="pollresult">


Then style the h2 tag.

app/assets/stylesheets/application.css
h1
{
  text-align: center;
  color: rgba(255, 255, 255, 0.5);
}


h2
{
  text-align: center;
  font-size: 0.8em;
}


.pollform, .pollresult
{
  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;
}






Unit Tests

It's time to write a few unit tests! Bear in mind that these are just samples. There are probably better tests one can write. This is the default test that is generated when we create a controller.

test/controllers/poll_controller_test.rb
require "test_helper"

class PollControllerTest < ActionDispatch::IntegrationTest
    test "should get index" do
        get poll_page_url
        assert_response :success
        assert_not_nil assigns(:api_data)
    end
end


And more. Here, we test submitting the form by sending a sample payload, and checking a couple of expected behaviors. We check if there's a redirect to the form page, and that there's a flash notice.

test/controllers/poll_controller_test.rb
require "test_helper"

class PollControllerTest < ActionDispatch::IntegrationTest
    test "should get index" do
        get poll_page_url
        assert_response :success
        assert_not_nil assigns(:api_data)
    end

    test "should submit results" do
        post submit_poll_form_url, params: { answers: { "1" => "3", "2" => "5", "3" => "2"} }
        assert_response :redirect
        assert_redirected_to root_path
        assert_not_nil flash[:notice]
    end
end

The result controller test should look like this as well.

test/controllers/result_controller_test.rb
require "test_helper"

class ResultControllerTest < ActionDispatch::IntegrationTest
    test "should get index" do
        get result_page_url
        assert_response :success
        assert_not_nil assigns(:api_data)
    end
end


When you run these tests, that's what you should get.




Conclusion

Looks like it's the day after Christmas.

This concludes my first Ruby On Rails web tutorial in years. I like to think it's better than my last couple efforts. That's because despite being an old fart, I'm still growing and adapting, and so should you. Enjoy your holidays!

Poll-ite season's greetings!
T___T

Tuesday, 23 December 2025

Web Tutorial: Ruby On Rails Xmas Poll (Part 3/4)

We have a form, and now it's a matter of submitting it. In the Submit action of the Poll controller, we want to collect this data and send it to Oracle APEX.

We'll have to first massage this data into a payload to send. For that, we declare answers as the the collection of all the HTML elements with answers as the name. Then we declare payload as an object with one property, answers. The value of that, will be answers.

app/controllers/poll_controller.rb
def submit
    answers = params[:answers]
    payload = { answers: answers }

end


We then use HTTParty to POST, just like we used it to send a GET request earlier. The base URL is the same - we'll use ORDS_API_URL. For the body, we use payload after running the to_json() method on it, to convert it to a JSON object. And because of this, we should specify that it's JSON in the headers object. The result is returned in the variable response.

app/controllers/poll_controller.rb
def submit
    answers = params[:answers]
    payload = { answers: answers }

    response = HTTParty.post(
        ORDS_API_URL,

        body: payload.to_json,
        headers: {
            "Content-Type" => "application/json"
        }
    )
end


Now, if it's successful, the code property of response will be 200. In that case flash a green success message. If not, flash a red error message.

app/controllers/poll_controller.rb
def submit
    answers = params[:answers]
    payload = { answers: answers }

    response = HTTParty.post(
        ORDS_API_URL,
        body: payload.to_json,
        headers: {
            "Content-Type" => "application/json"
        }
    )

    if response.code == 200
        flash[:notice] = "Submission successful!"

    else
        flash[:alert] = "API error."

    end
end


When all's said and done, use the redirect_to statement to return to root_path, which is the poll form.
app/controllers/poll_controller.rb
def submit
    answers = params[:answers]
    payload = { answers: answers }

    response = HTTParty.post(
        ORDS_API_URL,
        body: payload.to_json,
        headers: {
            "Content-Type" => "application/json"
        }
    )

    if response.code == 200
        flash[:notice] = "Submission successful!"
    else
        flash[:alert] = "API error."
    end

    redirect_to root_path
end


Next, let's go back to Oracle APEX. Remember we created the GET handler for the API endpoint "poll/:id"? Well, now create a POST handler.


Make sure the id variable is defined, and we tell Oracle APEX that it's to be found in the URL.



Before we examine the PL/SQL code for the handler, this is the shape of the data that will be sent, as an example.
{
  "Answers": {
    "1": "4",
    "2": "5",
    "3": "1",
    "4": "2",
    "5": "1",
    "6": "4",
    "7": "4",
    "8": "3",
    "9": "3",
    "10": "5"
  }
}


We have a DECLARE, BEGIN and END statements. After DECLARE, we declare l_request_body_clob as a CLOB object. A CLOB is a Character Large Object, which pretty much describes the data that will be sent to Oracle APEX via the form. Then we declare l_keys as an array used to store strings. (If you're curious, the "l" prefix is used to say "local". Seems superfluous, but it's Oracle's convention, so...)
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN

END;


body_text refers to the data sent in the form, via the API endpoint. This value is assigned to the variable l_request_body_clob.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;
END;


Then we use the parse() method of the APEX_JSON object, passing in l_request_body_clob as the p_source parameter's value. This, in effect, parses the form body data.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);
END;


Now for l_keys. We want to get the keys from the answers object. So we use the get_members() method of the APEX_JSON object (which already parsed the form data) and specify that the name of the object is "answers" by setting that as the parameter value of p_path. This in effect produces an array of all the keys in the form data, and binds that value to the array l_keys.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');
END;


To be safe, we have an IF block to check that l_keys is a valid non-empty array. Then we iterate through l_keys.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');

    IF l_keys IS NOT NULL AND l_keys.COUNT > 0 THEN
        FOR i IN 1..l_keys.COUNT LOOP

            DECLARE

            BEGIN


            END;

        END LOOP;
    END IF;
END;


We'll declare the serial number and answer here, in the variables l_serial_no and l_answer_value respectively. We know that those are just numbers and they won't go above 10, so "VARCHAR2(2)" is safe enough.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');

    IF l_keys IS NOT NULL AND l_keys.COUNT > 0 THEN
        FOR i IN 1..l_keys.COUNT LOOP
            DECLARE
                l_serial_no VARCHAR2(2);
                l_answer_value VARCHAR2(2);
            BEGIN

            END;
        END LOOP;
    END IF;
END;


Then we assign the value of the current element of l_keys, to l_serial_no. And we use the get_varchar2() method of APEX_JSON, again using "answers." and the current value of l_serial_number ("||" is actually concatenation in PL/SQL, so we're trying to read the value of answers.1, answers.2, and so on.) as the value of p_path, and assign the value to l_answer_value. Phew! That was a mouthful. But you get the idea... I hope.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');

    IF l_keys IS NOT NULL AND l_keys.COUNT > 0 THEN
        FOR i IN 1..l_keys.COUNT LOOP
            DECLARE
                l_serial_no VARCHAR2(2);
                l_answer_value VARCHAR2(2);
            BEGIN
                l_serial_no := l_keys(i);
                l_answer_value := APEX_JSON.get_varchar2(p_path => 'answers.' || l_serial_no);
            END;
        END LOOP;
    END IF;
END;


And we write an INSERT statement that adds a row with the values of l_serial_no and l_answer_value. Because QUESTION_SERIAL_NO is an integer, we need to use the TO_NUMBER() function on l_serial_no. POLL_ID will be the variable id in the POST handler.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');

    IF l_keys IS NOT NULL AND l_keys.COUNT > 0 THEN
        FOR i IN 1..l_keys.COUNT LOOP
            DECLARE
                l_serial_no VARCHAR2(2);
                l_answer_value VARCHAR2(2);
            BEGIN
                l_serial_no := l_keys(i);
                l_answer_value := APEX_JSON.get_varchar2(p_path => 'answers.' || l_serial_no);

                INSERT INTO POLL_RESULTS (POLL_ID, QUESTION_SERIAL_NO, RESULT)
                VALUES (:id, TO_NUMBER(l_serial_no), l_answer_value);
            END;
        END LOOP;
    END IF;
END;


The we use the open_object(), write() and close_object() of APEX_JSON to set status and message as a response.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');

    IF l_keys IS NOT NULL AND l_keys.COUNT > 0 THEN
        FOR i IN 1..l_keys.COUNT LOOP
            DECLARE
                l_serial_no VARCHAR2(255);
                l_answer_value VARCHAR2(4000);
            BEGIN
                l_serial_no := l_keys(i);
                l_answer_value := APEX_JSON.get_varchar2(p_path => 'answers.' || l_serial_no);

                INSERT INTO POLL_RESULTS (POLL_ID, QUESTION_SERIAL_NO, RESULT)
                VALUES (:id, TO_NUMBER(l_serial_no), l_answer_value);
            END;
        END LOOP;
    END IF;

    APEX_JSON.open_object;
    APEX_JSON.write('status', 'success');
    APEX_JSON.write('message', 'Answers processed successfully');
    APEX_JSON.close_object;

END;


Then we have a provision for if anything goes wrong.
DECLARE
    l_request_body_clob CLOB;
    l_keys APEX_T_VARCHAR2 := APEX_T_VARCHAR2();
BEGIN
    l_request_body_clob := :body_text;

    APEX_JSON.parse(p_source => l_request_body_clob);

    l_keys := APEX_JSON.get_members(p_path => 'answers');

    IF l_keys IS NOT NULL AND l_keys.COUNT > 0 THEN
        FOR i IN 1..l_keys.COUNT LOOP
            DECLARE
                l_serial_no VARCHAR2(255);
                l_answer_value VARCHAR2(4000);
            BEGIN
                l_serial_no := l_keys(i);
                l_answer_value := APEX_JSON.get_varchar2(p_path => 'answers.' || l_serial_no);

                INSERT INTO POLL_RESULTS (POLL_ID, QUESTION_SERIAL_NO, RESULT)
                VALUES (:id, TO_NUMBER(l_serial_no), l_answer_value);
            END;
        END LOOP;
    END IF;

    APEX_JSON.open_object;
    APEX_JSON.write('status', 'success');
    APEX_JSON.write('message', 'Answers processed successfully');
    APEX_JSON.close_object;

    EXCEPTION
        WHEN OTHERS THEN

            APEX_JSON.open_object;
            APEX_JSON.write("status", "error");
            APEX_JSON.write("message", "PL/SQL Error: " || SQLERRM);

            APEX_JSON.close_object;
END;


Time to test this! Fill in the poll and click SEND.


You should see this!


And the results in Oracle APEX's database, in the POLL_RESULTS table!


One more thing...

An anti-CSFR token is very easy to apply in Ruby On Rails. Just go to this file. When you rerun your form, you should see a hidden field if you view the source. Everything else is taken care of, including the validation.

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
    allow_browser versions: :modern
    protect_from_forgery with: :exception
end


Next

Viewing the results, and testing.

Friday, 19 December 2025

Web Tutorial: Ruby On Rails Xmas Poll (Part 2/4)

For this part of the web tutorial, you'll need to have Rails installed. Here, you'll start a new project in the CLI using the rails new command followed by the name of your project. We'll add "--skip-active-record" because we won't be using the built-in database functionnality.
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]


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


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


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>


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>


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);
}


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>


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


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


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


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"
    }
  ]
}


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


We can actually test this. Deliberately sabotage the endpoint like this.

app/controllers/poll_controller.rb
response = HTTParty.get(
    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);
}


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


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>


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;
}


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>


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>


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>


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>


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>


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>


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>


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!

Next

Handling form submission.

Wednesday, 17 December 2025

Web Tutorial: Ruby On Rails Xmas Poll (Part 1/4)

Merry Christmas season to all!

It's been years since I got my hands dirty with Ruby On Rails, and seeing as I got time, why not now? Ruby, despite me never having had an opportunity to use it professionally, is probably one of my favorite programming languages that I ever picked up for no goddamn practical reason. And, well, this Christmas season, I wanna go back to my guilty pleasure.

Today, we will be creating a form in Ruby On Rails, to submit answers to a poll. This will be a simple Christmas Carol Poll, where the user rates all presented Chsristmas Carols on a scale of 1 to 5.

Normally, a Ruby On Rails project would have its own built-in server capabilities, but I wanted to see how Oracle APEX worked with Rails. Pretty darn well, actually. Thus, what I did was first log onto my Oracle APEX workspace and create some new tables.

The first, obviously, would be a POLLS table. This is the PL/SQL statement for the table, though you can use the UI to create it. It basically is just an auto-increment integer column, ID, and a string, NAME.
CREATE TABLE "POLLS" (
    "ID" NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 1 START WITH 1 CACHE 20 NOORDER NOCYCLE NOKEEP NOSCALE NOT NULL ENABLE,
    "NAME" VARCHAR2(100 CHAR) NOT NULL ENABLE,
    CONSTRAINT "POLLS_PK" PRIMARY KEY ("ID") USING INDEX ENABLE
);


Far easier to just copy this to clipboard, go to SQL > SQL Command and paste into the box, then run it.


There, you have the POLLS table. Ignore the other tables; they're remnants of my last Oracle APEX project.


I'm just going to enter the name of the poll here, manually.


The next table is POLL_QUESTIONS. Here's the SQL. This one is just a bit more complicated. We don't have an ID field here; instead uniqueness is determined by the combination of POLL_ID (which is a foreign key to the POLLS table) and NO. NO is an integer that denotes the serial number of that question within a poll. TITLE is a string which displays the question, and ANSWERS is a comma-separated string of possible values.
CREATE TABLE "POLL_QUESTIONS" (
    "NO" NUMBER(2,0) NOT NULL ENABLE,
    "TITLE" VARCHAR2(100 CHAR),
    "POLL_ID" NUMBER NOT NULL ENABLE,
    "ANSWERS" VARCHAR2(200),
    CONSTRAINT "POLL_QUESTIONS_PK" PRIMARY KEY ("NO", "POLL_ID") USING INDEX ENABLE
);


The easy thing to do would be to run SQL Command with this query.


Again, I'm going to do some manual data entry here. Notice that POLL_ID is always 1, because we only have the one entry in POLLS. For the column ANSWERS, it is a JSON-encoded array of the values 1 to 5.


Finally, we have POLL_RESULTS. ID is a primary key just like in POLLS. Each row will have two foreign keys - POLL_ID and QUESTION_NO which reference POLL_QUESTIONS. RESULT will store the answer for the specific question referenced by the last two fields. POLL_ID and QUESTION_NO.
CREATE TABLE "POLL_RESULTS" (
    "ID" NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 1 START WITH 1 CACHE 20 NOORDER NOCYCLE NOKEEP NOSCALE NOT NULL ENABLE,
    "POLL_ID" NUMBER NOT NULL ENABLE,
    "QUESTION_NO" NUMBER NOT NULL ENABLE,
    "RESULT" VARCHAR2(100 CHAR) NOT NULL ENABLE,
    CONSTRAINT "POLL_RESULTS_PK" PRIMARY KEY ("ID") USING INDEX ENABLE
);


We can create the table the easy way here. We will not be populating this table manually. No, the entire point is to populate this table by means of submission through the Ruby On Rails form.


Time to determine API endpoints!

We've populated the POLLS and POLL_QUESTIONS table because we want to use this data to populate the form. In order for that to happen, we'll need to provide a means of retrieving that data. And that will be REST API endpoints. For now, we will do just a GET endpoint.

Go to SQL Workshop, then go to RESTful Services. Create a new Module and let's call it "polls". The endpoint here will be "/polls/".


Congrats! You have a Module! Now you need to add Templates to this Module. Click on that button, Create Template, near the bottom right side of the screen.

Create two Templates. The first one is poll/:id, and the second is poll/:id/results/.



Go to the first Template you created, poll/:id. You'll need to add a GET handler to it. To do that, click on the Create Handler button somewhere near the bottom right area of the screen. We haven't forgotten about the second Template, but we'll get to it later.


This is the SQL code that will be in that box. It's a simple join between POLLS and POLL_QUESTIONS.
SELECT p.NAME, pq.SERIAL_NO, pq.TITLE FROM POLLS p
LEFT JOIN POLL_QUESTIONS pq ON pq.POLL_ID = p.ID
WHERE p.ID = :id
ORDER BY pq.SERIAL_NO


This is a good place to stop. We've set up some stuff on Oracle APEX that will be used in the next part of this web tutorial.

Next

The Ruby On Rails setup.

Tuesday, 25 November 2025

Tech skillsets: Go deep or go wide? (Part 1/2)

"Jack of all trades, master of none" is a term I've heard all too often when fellow techies describe my stint as a web developer. Back then, I was a generalist dipping my sticky fingers into every new and shiny tech I encountered. In a sense, I'm still that web developer. Just with more money.

Go deep or go wide?

I want to explore the pros and cons of generalizing as opposed to specializing, especially in the context of today's tech landscape. The industry needs both specialists and generalists. That's a simple fact that shouldn't need some nobody tech blogger pointing out, but sometimes people get overly emotional about defending their stance. Want to specialize? Cool, do that. Want to generalize? Also cool, go crazy. But we need to be cognizant of the tradeoffs.

The case for specializing over generalizing

Before every tech and his dog started calling themselves "full-stack developers", specialists seemed to earn a whole lot of money. That was back when I was reading job ads asking for "deep expertise" and extensive work experience in more narrow scopes such as back-end programming and database administration. Some areas, such as COBOL programming, pay a lot for the simple reason that COBOL is still largely in use but COBOL programmers appear to be a dying breed. I've touched a lot of programming languages, but just enough to qualify as a hobbyist in each one. I have a decent frame for comparison between them - a Python lecturer of mine was actually quite entertained at my comparisons of the language to PHP and Ruby - but that often isn't very useful in the professional sense.

Specialists earn big.

The companies whose budgets are large enough to afford to pay for specialists, also tend to use these specialists for projects that are grand in scale. Projects that actually require deep expertise. Projects that add incalculable value to one's CV.

Deep knowledge is also useful for judging the extent of one's resolve and commitment. After all, it takes plenty of both to spend the effort. I personally know someone who specializes in HTML and CSS. It's a very narrow niche and perhaps not very profitable, but undeniably impressive.

Generalists - quite unfairly, I might add - seem to have developed a reputation for being easily distracted. Lacking the necessary discipline and focus required to specialize. I would actually say it takes an extraordinary amount of discipline to avoid going too deeply down one rabbit hole and shutting out everything else, but that's not the general sentiment. Therefore, in some circles, generalists are seen as those who were unable to specialize as opposed to specialists being unable to generalize.

The conventional advice when it comes to tech specialization, is to specialize in one area while simultaneously being decent in a few related areas. For instance, if you're a Data Analytics guru, you may want to specialize in statistical analysis and mathematics, but at the same time get reasonably good with Data Visualization tools and learn a bit of Python. If you're a primarily a Front-end Developer, learning some simple database concepts would be a useful addition to HTML, CSS and JavaScript. Maybe pick up a few frameworks such as ReactJS or VueJS, just to round things out.

I won't argue against that advice, but these days it feels like nothing is ever enough.

Next

We explore the opposite case!

Saturday, 1 November 2025

The year I finally achieved that JLPT certification

Some say "better late than never". I got a huge dose of that this year as I obtained my Japanese Language Proficiency Test (JLPT) N5 Certificate.

Why was this overdue? Well, you see, because I actually began on this journey at the tender age of 15. I was a Polytechnic student at the time, and this was a supplementary module. At first, things went well. Having received education in Chinese, learning the basic sounds of Japanese was a breeze. Learning the Hiragana and Katakana writing systems wasn't much of a stretch as well; in fact it was significantly easier than reading and writing Chinese characters. Until we got to Kanji, which essentially was Chinese characters.

Ugh, more Chinese.

At which point I lost all interest.

Also, at this time, I had begun an obsession with writing code, and building stuff. Japanese was a pretty language, but it just didn't fit into my world the same way C++, SQL or even QBasic, did.

And the rest, as they say, is history. Even after graduation, not only did I neglect whatever little Japanese I had learned, the next few decades were a disastrous collection of bad career choices, even worse lifestyle choices, and sobering life lessons.

COVID-19 and Clubhouse

Fast-forward to 2021. COVID-19 had the world in its loathsome grip and I, like many others, sought human interaction online. That was when I discovered the Clubhouse app. Through it, I encountered numerous communities, not all of which spoke English, Mandarin, or even Cantonese.

A few of these were Japanese speakers. And some were even offering Japanese lessons. I dove right in, relearning all the basics I had forgotten. Many times, I would simply listen in, try to make sense of the conversation, and Google words that came up. Then I started engaging, hesitantly, in the conversations. I even participated in Mandarin-Japanese exchanges!

Little cultural exchanges.

It was slow going. I wrote an app to facilitate relearning Hiragana and Katakana. I practiced writing every day, religiously. But there was this feeling that I could do more.

That was when someone suggested that I take the JLPT. And from that day forth, I took things up a notch.

Training in earnest

My next step was to install Duolingo on my phone. For the next few years, I faithfully partook of the exercises daily. At the same time, I went onto YouTube to search for JLPT Listening exercises. I didn't really have a sense of how far I had progressed; I just knew I needed to keep going. I've never been brilliant; but what I am good at is being consistent.

One year later, I downloaded the Migii app, and paid the subscription fees for accessing sample JLPT exam questions. With Duolingo, the returns were diminishing. I needed to train a different set of linguistic muscles - the kind used for passing the exam. Duolingo had brought me to a certain point where I could read the Japanese exam questions without too much difficulty... now I had to practice answering them.

The next milestone was hit another year later when, while watching a Japanese TV show, with English subtitles, I noticed something odd. Almost constantly, the subtitles would read "It's OK". Even though "It's OK" was a perfectly reasonable thing to say in the context of the current point of the story, it was a pretty poor translation.

Phrase Literal Meaning English Translation
daijobu It's OK. "It's OK."
dou itashimashite You're welcome. "It's OK."
shinpai suna Don't worry. "It's OK."
anshin shite Relax. "It's OK."
ki ni shinai de Pay it no mind. "It's OK."


At this point, I found myself thinking - wow, these translators are so goddamn lazy! They just use "It's OK" for everything!

And my next thought was - hey, if I know this much, shouldn't I be taking the JLPT already?!

The JLPT Test Voucher

That was when I registered for the JLPT on the Japanese Cultural Society of Singapore website. and paid a hundred Singapore dollars for the privilege. A month later, the JLPT test voucher arrived in the mail. The train was in motion; there was no going back.

Meanwhile, I continued training religiously. With Listening practice, especially. Online chatter had it that the recordings would be played only once, so I trained myself similarly, by not rewinding and replaying.

The JLPT N5 Exam

It was July when the day arrived. I head for the Singapore Management Institute where the exam was held, and stood in line with what looked like mostly Burmese folks. Interesting.

The invigilator who took charge of the exam room I had been assigned to, seemed to be Japanese going by the way she spoke English.

Shading answers with a pencil.

The proceedings were charmingly old-school. We were given question and answer sheets. It was all multiple-choice, and the correct answer had to be shaded with a pencil for feeding into a scanning machine. Despite my numerous certificates, I hadn't done this since... 2015? This was because my last couple certifications were earned from doing the coursework and presentations, rather than standardized tests.

The toughest moment in the exam came from the Choukai, which was the Listening portion. Despite my best efforts, my concentration slipped at various points. It was with considerable relief that I handed in my question and answer sheets, and headed off.

In August, I logged on to the JLPT portal to check my results. I had passed, and my results were more than decent. I actually scored higher on Listening than other sections!

The real value of all this

Last September, I got my actual physical certification through the mail. It was a foregone conclusion by that time, but I still felt that little thrill of pride. Job well done, bro, I told my reflection. Not such a big deal in the larger picture, but we've got to celebrate our wins even if they're small. Especially if said win took thirty-three friggin' years to achieve.

My results.

The JLPT N5 Certification isn't going to change my life. As far as professional cred goes, it's barely a blip. Achieving the JLPT N5 Certification probably puts me on par with the average Japanese toddler where the language is concerned. As for the value of understanding multiple languages? That's not much of a flex. This is Southeast Asia; just about everyone and their dog is multilingual.

No; at my age, the act of learning is arguably more important than what's being learned. It helps stave off dementia.

The real value is knowing and affirming, that with sufficient motivation and putting in the time and effort, I can learn pretty much whatever I choose to learn. And that is powerful stuff. In an age where things are constantly and rapidly evolving, the ability to learn shit has become vital - not just in the tech industry, but for life itself.

In hindsight, I should have realized this. How did I learn ASP? PHP? Ruby? VueJS? D3? All by picking up a book, watching videos, visiting websites, and constant practice. Most of the time, it really is that simple. The methods vary, but at the end of the day, it's about the willingness to put in the hours.

The linguistic journey continues!

I do want to see if I can achieve the next rung, the JLPT N4 Certification. Again, I'm not really sure why. Just for the hell of it, I guess. It's not like I plan to visit Japan. It's not like I realistically have anything to prove to anyone.

On the other hand, Korean does seem pretty interesting.

Decisions, decisions...

Ganbarimashoo!
T___T