Monday, 5 January 2026

Five Image File Formats For The Web

There are a variety of image file formats used on the internet. Some of them have been around for a very long time; others not so much. They come in various levels of utility and relevance to the web landscape. Understanding image file formats is arguably more of a design discipline than it is a tech discipline; nevertheless, it's web development know-how and useful for web developers worth their salt, to at least understand the basics.

Since we're only talking about images used for the web, we won't be discussing TIFFs (huge files used by photographers) or Bitmaps.

1. JPEG (.jpg or .jpeg)


The acronym stands for Joint Photographic Experts Group. This is a file format that has been around for almost as long as the internet has been alive. It is still extremely relevant today, though its place is very much under threat from WebP.

PNG (left), JPEG (right); for
the JPEG, logo is blurred
around the edges.

JPEGs are useful because they use a lossy compression format. This means that part of the color information will be lost when compressed, but that's OK because the parts that are lost are generally not the parts perceptible by human eyes. Thus the compression would have to be significantly lower than the original before one can perceive any degradation in quality. And that, in turn, means that file sizes can get significantly smaller.

Thus, especially for small to medium photographic images, JPEGs were (and in many ways, still are) the numero uno choice in the pre-broadband era of internet. Ever tried to access an online photo gallery on a 54kbps modem? It would be an absolute nightmare if low quality JPEG thumbnails weren't used. The economical file sizes were economical for those times, though this may be less of an issue today, now that internet speeds are streaming quality.

Its one weakness is noticeable image degradation in images with flat colors and sharp distinct edges.

Use it for: Photographic images where quality is negotiable.
Don't use it for: Images with flat colors such as simple illustrations and logos.

2. GIF (.gif)

This can be pronounced with a hard or soft "g". I personally think it should be pronounced with a hard sound because the "g" stands for "Graphic", as in Graphical Interchange Format.

This is a lossless compression format, though you would be sadly mistaken if you thought this meant there would be little to no image degradation. The image degradation occurs due to the conversion from a full color palatte of a photographic image to the 256 colors in a GIF palette, and not due to the compression... though the distinction is probably lost on the average layperson. Simply put, there are a limited number of colors in a GIF palatte, and if one was to convert a photographic image to GIF, unpleasant visual effects such as "banding" or "dithering" would appear.

JPEG (left), GIF (right); for
the GIF, image is dithered.

However, for flat colors and crisp edges such as logos or illustrations, GIFs are perfectly adequate.

GIFs were insanely popular back in the day where quality didn't matter so much if you had animation and transparency. And GIF supports both.

Use it for: Low-quality logos or illustrations, especially if transparency and animation are required.
Don't use it for: Photographic images, ever.

3. PNG (.png)


Portable Network Graphics files handle crisp edges and flat colors just as well as GIFs, with the added bonus of transparency and animation (though this requires the APNG extension) just like GIFs. Visually, they're an upgrade over GIFs since they can also handle photographic images withput the dithering or banding that often occurs with GIFs.

Where transparency is concerned, it's worth noting that GIFs offer full or no transparency, while PNGs offer full to no transparency - another major upgrade.

Partial transparency is
possible with PNGs.

Compared to JPEGs, they suffer significantly when comparing compression file sizes, since, like GIFs, they use a lossless compression method.

Still, with transparency (partial or full) and animation options, and the ability to handle both flat or photographic images, PNGs offers increased flexibility.

Use it for: Logos or illustrations, especially if transparency and animation are required.
Don't use it for: Unless transparency and animation are required, JPEGs should be preferred for photographic images... though in that case WebPs are still the superior choice.

4. SVG (.svg)

Take note of the name. Scalable Vector Graphics are just that - scalable. This means that unlike all the other image file formats, they can be scaled up or down with no loss in quality. This is because they are basically XML files, with instructions on how to render them, encoded as text. The browser then renders the images based on these instructions. Since they handle gradients as well, and aren't limited to 256 colors, these graphics aren't limited to flat colors like GIFs.

With SVGs, images can be
scaled up or down with no
loss in quality.

SVGs can suffer in comparison to PNGs at higher levels of complexity, since, as mentioned, they are basically XML files. Thus, a very complex SVG might be a huge file, while the equivalent PNG would still be a flat file and relatively small.

Use it for: Simple logos or illustrations, especially if transparency and animation are required.
Don't use it for: Anything else. Photographic images absolutely can be imported into SVGs, but the results are sub-optimal at best.

5. WebP (.webp)

WebP is Google's offering, and it is the currently apex predator of the image file format jungle. Not only does it handle photographic images at comparable compression sizes like a JPEG, it also handles transparency and animation like a PNG.

The WebP logo.

At just over a decade and change since its inception, It's still the new kid on the block, but already, it's settled in well. It doesn't hurt, of course, that Google the web browser generally uses it to generate image thumbnails in the search engine.

WebP hasn't yet become as ubiquitous and widely-supported as the other file formats, but it's probably a matter of time.

Use it for: For most things image-related, webP handles it all like a champ.
Don't use it for: For scalable logos and illustrations, SVG's still the superior choice.

Conclusion

Things have changed since the beginning of the internet and the online space is a whole new world now. Even though slow internet speeds and severely limited bandwidths are largely a thing of the past, it's still good practice to use the correct image file format for the correct occasion.

Get the picture yet?
T___T

Thursday, 1 January 2026

Finding Your Purpose Professionally

For years now, I've been reading that young people (which, to me, means Gen Z) are unemployable because they want to find purpose in work. By that, they mean that the company they eventually join, should have values that align with their own. They want to impact the world in meaningful ways.

This, of course, may not be compatible with the values that employers traditionally value - hard work and discipline.

Now, I've often stated my opinion that the value of hard work is often overrated. And people who fixate on that at the exclusion of all else, are doomed to fail. That doesn't mean I think that fixating on purpose in work is necessarily an improvement.

Heal the world; make it a
better place...

On the face of it, these are noble goals. Who doesn't want to make the world a better place?

On the other hand, what's "better"? There are some who would say the world would be better if everyone was unable to say hurtful things, and some might say the world would be better if certain kinds of people simply did not exist. Who gets to decide? I think most of us would have enough of a sense of self-preservation not to trust some wet-behind-the-years kid straight out from University, high on purist ideals, to make that decision. Not that I would necessarily depend on some Bible-thumper Churchgoer for those decisions either, if we're being honest.

In all fairness, however, career choices are highly personal. Still, let's examine the issue of wanting to find purpose in work. Some like to claim that it's all hubris and entitlement.

Is it really entitlement, though?

One might say this comes across as a little selfish. After all, every day one remains without a job is another day that financial slack is taken up by someone else. So holding out for a job with purpose does suggest a disproportionate amount of privilege.

But entitlement?

To be honest, this does come across as awfully entitled if they want the success from the start, without paying their dues or starting small like the rest of us. The people who have changed the world as we know it, were extraordinary people with extraordinary talent, some of whom endured extraordinary hardship. I'm by no means saying that people should not aim to be extraordinary - ambition is a grand thing to have, after all - but the fact is, not everyone can be extraordinary. That's literally what the word means. If everyone can be special, then no one is special.

Everyone should have the right to aim high. Not everyone, however, should expect to be given an easy ride. Chances are, the first several companies won't be a great fit in terms of purpose and values. The pay and hours may (probably will) suck. And expecting not to have to deal with any of that shit because you're special or something, does smack of entitlement. I mean, you better be pretty damn special, sweetheart.

Be willing to start small.

All I'm saying is, as long as one's willing to start small and compromise without expecting instant success, it's not entitlement. It's just a goal like any other.

Now, I could spend this entire blogpost ranting endlessly about how kids these days don't understand the value of hard work and paying your dues, loyalty and all other Boomer values that employers tend to like perpetuating (wink wink, nudge nudge). Or I could unpack my own experiences when I had what the older generation called "unrealistic expectations".

And trust me, there's plenty to unpack.

Back in 2010...

I was in between jobs. Things were looking bleak. I was patiently holding out for a proper software dev job. My dad recommended me a tech support job at some medical company. I declined repeatedly. If anyone's at all familiar with my backstory, they would know how much I absolutely despise the role that I spent the first six years of my career in, and how hard I worked to never have to do another day of tech support in my life, ever again.

Dad didn't agree. To him, a job was just a job. To him, I was this spoilt brat who wasn't even giving it a fair chance.

I didn't understand why my dad, who sometimes couldn't tell a laptop from a lap dance, was presuming to give me any kind of career advice, let alone tech career advice. The truth was that he wasn't. He was just trying to get me to see reason; that any job was better than no job at all.

From a survival standpoint, he was absolutely right. However, from the standpoint of a career in software development, it would have been one hell of a stupid choice.

I didn't want just any job. I wanted a career. And the job I chose needed to at least be a positive step towards that goal, no matter how small. Dad didn't understand the concept of a career. He didn't have much of an education. His work experience consisted of selling used cars, selling insurance, basically selling. He might even have tried to run a garment factory once. As far as he was concerned, he was lucky to have any kind of job, let alone one that just required him to drive around and talk to people. To this day, I simultaneously resent and admire him for his ability to just suck it up.

I have an
education, dammit!

Because I was different. I had an education, and with it, the ability to be choosy. Honestly, if I just wanted to do any job, why waste all those years on tech qualifications?

Thus, I like to think that I had a practical reason to be choosy, as opposed to doing it for "purpose". (Like these young punks. Heh heh.)

It's now 2026!

I won't pretend I know how kids these days think. I know what they think, or at least, what the news reports say they think. But I don't know how they arrive at those conclusions, or how it even makes sense to them.

Purpose? Every job has a purpose. Perhaps not the high-minded noble purpose these young ones pontificate about constantly, but purpose nonetheless.

Hey, some people find purpose in not starving. And if you think that's not a purpose, shut your privileged piehole.

Some people find purpose in a job that enables them to take care of the people who matter. What the job actually entails isn't the issue. What effect it has on the world isn't the issue. The issue is that they have mouths to feed, and by God they are going to feed those mouths.

Still think all that isn't purpose? It might be more actual purpose than these youngsters have ever dealt with. Remember, before you try to save the world, take some time to help Mommy with the dishes.

Too good to do the dishes?

I don't mean to sound condescending (even though I probably do). After all, I've struggled with career choices myself. And the conclusions I came to as a result, some might uncharitably say was due to me not being successful in changing the world.

Newsflash: don't worry too much about changing the world. Leave it alone and it'll sort itself out most of the time, with or without you. Thinking that your input is required, is nothing short of vanity... though it can be argued that said hubris is exactly one of the ingredients required to change the world.

But I can say all that. I have the benefit of hindsight, after all. And perhaps young people need that exact same benefit.

Or it could be that they're right, and my ways are outdated, the same way my father's ways were outdated back in 2010.

My advice (not that anyone asked for it)

There's nothing wrong at all with starting small, or imperfect beginnings. So what if the company you land in doesn't align with your life goals, or at all? You're fresh out of University; you have zero leverage with which to change the world. And expecting to be able to do it right away is mere fantasy.

Clock your experience points. Pay your dues. Do all that, and you'll have a leg to stand on when you eventually pursue those lofty noble goals. Without practical experience, all you have are good intentions. And we all know what the road to hell is paved by.

This blogpost was written on purpose,
T___T

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.