Converting a php page to Ruby on Rails

Today I converted a partial page from PHP and Smarty into Ruby on Rails, part of a major rewrite of Maia Mailguard.
The information for this screen come from one database table, though some elements do not map one to one to the inputs seen one the screen. Here’s the basic form, sans I18N translations:

The original PHP page can be found on the maiamailguard.com website.

My thoughts about that code? It violates the “Don’t Repeat Yourself” (DRY) principle. There is a lot of repetition of elements, both syntactical and semantic. For example a quick scan shows repetitions of input type=”radio” name=“*_destiny” ; just one of many. It is simply tiresome to read and understand.

Changing gears to Ruby on Rails, my first thought was that a plugin called Formtastic has the ability to make a collection of radio buttons all from one input helper call, a clear improvement to repeating nearly the same code for each radio input. I soon had that included in the project and it works as advertised… except it put the radio in a vertical line rather than a horizontal line.

The solution to this was to extend formtastic to add my own control. Rather than include all of the code here, I’ll just link the github changeset where you can view it. Essentially, I created a control named “radio_group”, based on the original radio_input control, but with a few changes to the html it generated. Once this was done, I was able to produce an entire row like this:

<%= f.input :banned_files_destiny, :as=>:radio_group,
    :label => t(:text_mail_with_attachments),
    :collection=>[[ t(:text_labeled),     'label'],
                              [ t(:text_quarantined), 'quarantine'],
                              [ t(:text_discarded),   'discard']
                            ],
     :wrapper_html => { :class => "banned_filebody2" }
%>

This cut down a lot of repeated html, though one may argue that it is in fact a few lines longer that the plain html. Nevertheless I think the rails version at this point is already far more readable. However, I didn’t stop there.

After finishing with that conversion, I noticed again that there is a lot of repetition. Four destiny lines, four bypass lines and three threshold inputs each share a lot of code. I refactored these into four helper methods, with parameters for all the essential bits of information. The end result is a stunning improvement.

This creates a one to one mapping of each visible row to a single statement such as:

<%= destiny_input(  f,  :banned_files_destiny,    :text_mail_with_attachments,   "banned_filebody2") %>

Some other minor notes about the code you see: The t(:text) is a helper function built into rails to look up keys in a I18N (Internationalization) dictionary to translate the text. At the point of the screenshot above, I hadn’t entered those translations into the dictionary, so it shows placeholders. The good news is that those translations don’t impeded the code process in this page.

Also, if you really dig deep you may discover that the policies table does not in fact have any column named *_destiny, yet the first parameter to f.input is supposed to be the name of a database column. Actually, it is the name of a method on the Policy class. The database schema used by amavisd-new has two sets of columns of interest here, *_lover and discard_*. If *_lover is true it will allow the message through regardless of of its status, and if discard_* is true, it discards it. One wonders what happens if both are set to true!

In the previous versions of Maia, we worked around this by instead asking for a designation of “label”, “quarantine”, or “discard” – and set the real database columns accordingly. To manage this in rails was actually quite easy, easier to read than the php version. I added a set of functions to the Policy class:

def virus_destiny=(destiny)
  self.virus_lover     = (destiny == "label"   ? "Y" : "N");
  self.discard_viruses = (destiny == "discard" ? "Y" : "N");
end

def virus_destiny
  return 'label'      if self.virus_lover
  return 'discard'    if self.discard_viruses
  return 'quarantine'
end

Short and sweet, it accesses and sets the two columns according to the choice made from the form. The form simply sees the virus_destiny methods and doesn’t know that it really represents two columns in the model.

While porting a project like this is never fast and easy, the improvements made to this one little corner of Maia Mailguard make for a beautiful example of the power and readability of Ruby on Rails, and has me quite excited to continue working on “Maia on Rails”.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *