Entries from May 2009 ↓

Dear Railsists, Please Don’t be Obtrusive

obtrusive_or_not.png Update: thanks to Jon Wood aka jellybob, a prototype demonstration has been added, which is even better than my original jQuery btw as it degrades gracefully. Check it out in the ‘prototype-unobtrusive’ directory.

I am guessing 9 out of 10 of you reading the title is prepared for yet-another Rails drama on some obtrusive community members, and because everyone is tired of Rails dramas, I am risking that some of you won’t care to read the article - but I couldn’t resist :-). Actually I’d like to talk about usage of (un)obtrusive Javascript - why is it a bad idea to be obtrusive, especially given that (as you will learn from the article) writing unobtrusive Javascript is not harder, and you get the warm, fuzzy feeling of writing nice and clean code!

The Drill

To demonstrate the differences, I’ll lead you through the creation of a quick AJAXy shout wall both the default/standard (and obtrusive) way, then do the same with unobtrusive Javascript to show you that contrary to the popular belief, you don’t need to memorize the “Tome of Javascript Black Magick Tricks” by heart, use obscure libraries or special coding techniques to achieve clean, unobtrusive code. The shout wall is simply a form for posting a new message, and a list of messages below it, like so:

shout_wall.png

(You can check out the code used in this post from it’s github repository).

The Standard Way

Note: If you’d like to follow along, please use the provided pastie links - do not try to cut & paste multiple lines from the page (single lines are OK), as it will be b0rk3d.

  1. Creating a new Rails application
    rails obtrusive-shout-wall
      
    1. rails obtrusive-shout-wall
  2. Get into the Rails dir
    cd obtrusive-shout-wall
      
    1. cd obtrusive-shout-wall
  3. Generate the resource message
      
    script/generate resource message
      
    1. script/generate resource message
  4. Add this the following to the generated migration (some_timestamp_create_messages (Get it from pastie):
    t.string :author
    t.text :message 
    
    1. t.string :author
    2. t.text :message
  5. Run the migrations:
    rake db:migrate
    
    1. rake db:migrate
  6. Because we want to view the messages in reverse order (newest one first), we add a default scope to the Message model (in message.rb):
        default_scope :order => 'created_at DESC'
    
    1. default_scope :order => ‘created_at DESC’
  7. Create the application layout - create a new file in app/views/layouts called application.html.erb, and fill it with the following content (Get it from pastie):
    
      
        <%= stylesheet_link_tag "application" %>
            <%= javascript_include_tag :defaults %>     
      
        
        <%= yield %>
        
    
    
    1. <html>
    2.   <head>
    3.     <%= stylesheet_link_tag "application" %>
    4.         <%= javascript_include_tag :defaults %>     
    5.   </head>
    6.     <body>
    7.     <%= yield %>
    8.     </body>
    9. </html>
  8. Create a file application.css and drop it into public/stylesheets. Add the following content (Get it from pastie):
    body {
        background-color:#FFFFFF;
        color:#333333;
        font-family:"Lucida Grande",verdana,arial,helvetica,sans-serif;
        margin:0 auto;
        padding:0;
        text-align:center;
        width:960px;
    }
    
    #messages {
        text-align: left;
        margin-left: 80px;
        margin-top: 50px;
    }
    
    #message-form {
        text-align: left;
    }
    
    #message-form dl {
        margin:10px 0 0 80px;
    }
    
    #message-form dd {
    color:#666666;
    font-size:11px;
    line-height:24px;
    margin:0 0 5px 80px;
    }
    
    #message-form dt {
        float:left;
        font-size:14px;
        line-height:24px;
        width:80px;
      text-align: left; 
    }
    
    #author {
      margin-right: 640px;
    }
    
    #message {
      width: 600px;
        height: 200px;
      margin-right: 194px;
    }
    
    .message {
      margin-bottom: 20px;
    }
    
    .first_row {
      padding-bottom: 10px;
    }
    
    .message-meta {
        font-size: 12px;
    }
    
    .author {
      color: #FF5050;
        font-weight: bold;
    }
    
    .new-message-label {
      text-align: left;
      padding-top: 30px;
      margin-left: 80px;
    }
    
    #submit-button {
      float : right;
      margin-right: 195px;
      margin-top: 10px;
    }
    
    1. body {
    2.     background-color:#FFFFFF;
    3.     color:#333333;
    4.     font-family:"Lucida Grande",verdana,arial,helvetica,sans-serif;
    5.     margin:0 auto;
    6.     padding:0;
    7.     text-align:center;
    8.     width:960px;
    9. }
    10.  
    11. #messages {
    12.     text-align: left;
    13.     margin-left: 80px;
    14.     margin-top: 50px;
    15. }
    16.  
    17. #message-form {
    18.     text-align: left;
    19. }
    20.  
    21. #message-form dl {
    22.     margin:10px 0 0 80px;
    23. }
    24.  
    25. #message-form dd {
    26. color:#666666;
    27. font-size:11px;
    28. line-height:24px;
    29. margin:0 0 5px 80px;
    30. }
    31.  
    32. #message-form dt {
    33.     float:left;
    34.     font-size:14px;
    35.     line-height:24px;
    36.     width:80px;
    37.   text-align: left;
    38. }
    39.  
    40. #author {
    41.   margin-right: 640px;
    42. }
    43.  
    44. #message {
    45.   width: 600px;
    46.     height: 200px;
    47.   margin-right: 194px;
    48. }
    49.  
    50. .message {
    51.   margin-bottom: 20px;
    52. }
    53.  
    54. .first_row {
    55.   padding-bottom: 10px;
    56. }
    57.  
    58. .message-meta {
    59.     font-size: 12px;
    60. }
    61.  
    62. .author {
    63.   color: #FF5050;
    64.     font-weight: bold;
    65. }
    66.  
    67. .new-message-label {
    68.   text-align: left;
    69.   padding-top: 30px;
    70.   margin-left: 80px;
    71. }
    72.  
    73. #submit-button {
    74.   float : right;
    75.   margin-right: 195px;
    76.   margin-top: 10px;
    77. }
  9. Create a new action, index in MessagesController (Get it from pastie):
    def index
      @messages = Message.all    
    end
    
    1. def index
    2.   @messages = Message.all   
    3. end
  10. This goes into app/views/messages/index.html.erb (Get it from pastie):

    Enter new message!

    <% remote_form_for :message, :html => {:id => "message-form"} do |form| %>
    Author:
    <%= text_field_tag 'author' %>
    Message:
    <%= text_area_tag 'message' %>
    <%= submit_tag "Submit!", :id => "submit-button"%> <% end %>
    <%= render :partial => 'message', :collection => @messages %>
    1. <h3 class="new-message-label">Enter new message!</h3>
    2. <% remote_form_for :message, :html => {:id => "message-form"} do |form| %> 
    3.   <dl>
    4.         <dt>Author:</dt>
    5.     <dd><%= text_field_tag ‘author’ %></dd>
    6.         <dt>Message:</dt>
    7.         <dd><%= text_area_tag ‘message’ %></dd>
    8.     </dl>
    9.     <%= submit_tag "Submit!", :id => "submit-button"%>
    10. <% end %>
    11.  
    12. <div id="messages">
    13.     <%= render :partial => ‘message’, :collection => @messages %>
    14. </div>
    We are showing the form for the messages and list the already exiting messages below the list. Note that we are using the _remote_form_for_ Rails helper to create an AJAXy form. This is already obtrusive, since if you observe the generated HTML, you will see that the form has an onsubmit parameter with some horribly looking code attached to it.:

    Obtrusive helper.png

    Sure, you can go ‘meh’ all the way, but slinging Javascript code all over the place is just as bad idea as writing inline CSS (or even worse, using HTML code for styling) or putting Rails code into views. It will work without any problems - but it’s just not the right way of doing things, especially if your code is going to hit a certain size.
  11. You probably noticed that we are rendering a message as a partial - so create a partial file app/views/messages/_message.html.erb with the following content (Get it from pastie):
    ">
    on <%= message.created_at.to_formatted_s(:long_ordinal) %>, <%= message.author %> said:
    <%= message.message %>
    1. <div class="message" id="message-<%=message.id%>">
    2.   <div class="message-meta">on
    3.    <%= message.created_at.to_formatted_s(:long_ordinal) %>,
    4.    <span class="author"><%= message.author %></span>
    5.    said:
    6.   </div>
    7.   <div><%= message.message %></div>
    8. </div>
  12. We need a ‘create’ action in MessagesController in order to process the form submission (Get it from pastie):
    def create
      @message = Message.create(:author => params[:author], :message => params[:message])
    end
    
    1. def create
    2.   @message = Message.create(:author => params[:author], :message => params[:message])
    3. end
  13. And obviously we’ll need to render something to respond to the create action. Using the standard Rails way, RJS, we might come up with something like this (in app/views/messages/create.js.rjs - Get it from pastie):
    page.insert_html :top, "messages", :partial => 'message', :object => @message
    page.visual_effect  :highlight, "message-#{@message.id}"
    
    1. page.insert_html :top, "messages", :partial => ‘message’, :object => @message
    2. page.visual_effect  :highlight, "message-#{@message.id}"
    Here we insert the “messages” partial, using the just created @message, and throw a splash of yellow fade into the mix for good measure. Easy peasy.
  14. We are done! Fire up script/server, hit localhost:3000/messages and voila!

The Good Way

Here I am presenting only the steps that are different from the above - i.e. if step 3 is skipped, use the one from above.

  1. Creating a new Rails application
      rails unobtrusive-shout-wall
      
    1. rails unobtrusive-shout-wall
  2. Get into the Rails dir
    cd unobtrusive-shout-wall
      
    1. cd unobtrusive-shout-wall
  3. Same as above
  4. Same as above
  5. Same as above
  6. Same as above
  7. Since we are going to use jQuery (unobtrusiveness is *not* a property of jQuery, you can be just as unobtrusive with Prorotype - but I switched to jQuery just before learning how, and now I am lazy to go back check out how in the ‘prototype unobtrusive’ directory in the github repository), you have to download jQuery with some basic effects, as well as an AJAX form handling library (still from the directory unobtrusive-shout-wall - Get it from pastie):
  8. curl http://jqueryjs.googlecode.com/files/jquery-1.3.1.min.js > public/javascripts/jquery.js
    curl http://www.malsup.com/jquery/form/jquery.form.js?2.28 > public/javascripts/jquery-form.js
    curl http://view.jquery.com/tags/ui/latest/ui/effects.core.js > public/javascripts/effects.core.js
    curl http://view.jquery.com/tags/ui/latest/ui/effects.highlight.js > public/javascripts/effects.highlight.js
    
    1. curl http://jqueryjs.googlecode.com/files/jquery-1.3.1.min.js > public/javascripts/jquery.js
    2. curl http://www.malsup.com/jquery/form/jquery.form.js?2.28 > public/javascripts/jquery-form.js
    3. curl http://view.jquery.com/tags/ui/latest/ui/effects.core.js > public/javascripts/effects.core.js
    4. curl http://view.jquery.com/tags/ui/latest/ui/effects.highlight.js > public/javascripts/effects.highlight.js
    and replace
    <%= javascript_include_tag :defaults %>
    
    1. <%= javascript_include_tag :defaults %>
    with
    <%= javascript_include_tag 'jquery' %>      
    <%= javascript_include_tag 'jquery-form' %>
    <%= javascript_include_tag 'application' %>
    <%= javascript_include_tag 'effects.core' %>
    <%= javascript_include_tag 'effects.highlight' %>       
    
    1. <%= javascript_include_tag ‘jquery’ %>     
    2. <%= javascript_include_tag ‘jquery-form’ %>
    3. <%= javascript_include_tag ‘application’ %>
    4. <%= javascript_include_tag ‘effects.core’ %>
    5. <%= javascript_include_tag ‘effects.highlight’ %>
    in the layout file.
  9. Same as above
  10. Same as above
  11. Same as above - just delete “remote” from the name of the helper, i.e. use a standard Rails view helper, form_for
  12. Same as above
  13. Since we are not relying on Rails to do the rendering for as via a template file, we return the html chunk that we will render from Javascipt. So your create action should look like (Get it from pastie):
    def create
      @message = Message.create(:author => params[:author], :message => params[:message])
      render :partial => 'message', :object => @message
    end
    
    1. def create
    2.   @message = Message.create(:author => params[:author], :message => params[:message])
    3.   render :partial => ‘message’, :object => @message
    4. end
  14. Now comes the fundamentally different part - instead of using RJS to respond to the create action, we move all our code to application.js (Get if from pastie):
    $(document).ready(function() {      
      $("#message-form").ajaxForm({success: handleNewMessage});
    
      function handleNewMessage(response, statusText) {
        $("#messages").prepend(response).effect("highlight", {}, 1500);
      }    
    });
    
    1. $(document).ready(function() {     
    2.   $("#message-form").ajaxForm({success: handleNewMessage});
    3.  
    4.   function handleNewMessage(response, statusText) {
    5.     $("#messages").prepend(response).effect("highlight", {}, 1500);
    6.   }   
    7. });
    I don’t think so that this code is particularly more complicated or hard to understand that the RJS one. Everything is inside the ready() function, which means that it’s only run once the document is properly loaded. Then we declare that “#message-form” is an AJAX form, and that upon successful submission, the handleNewMessage() method should be called. And if that happens, we add the response (which is the return value of the “create” action) to the “#messages” div, just as we did in RJS. Then we apply the yellow fade! w00t!
  15. Same as above

(You can check out the code used in this post from it’s github repository).

Conclusion

As you can see, the only real difference between the obtrusive and non-obtrusive version is in the last 2 points (downloading and including the jQuery header files can be easily solved with Rails templates): instead of leaving the rendering part to Rails, we return the response as a string and dynamically insert it from jQuery. With about the same effort, we kept all the Javascript code in application.js, which is much cleaner this way (you can open up 1 file and check out all the JS/AJAX behavior in one place), especially after introducing a lot of Javascript functionality into your code - in other words, for the same amount of work we got something much better. Please try to keep this in mind when you are working with Javascript and Rails the next time - believe me, it can save you from a lot of pain!

Rails 2.3 with Ruby 1.9

After the longest time we decided to upgrade the rails version in our applications. The last upgrade was to 1.2.6 year and a half ago. As we have quite a number of applications I am quite reluctant to upgrade unless it's really really worth it. There was quite a number of positive changes in Rails 2.2 (like the database connection pooling, thread safety and built in internationalization) but it wasn't compatible with Ruby 1.9 and I really didn't feel like going through all the apps just to go through them again to upgrade it to Ruby 1.9.

After release of Rails 2.3 compatible with Ruby 1.9 I could finally go ahead with the upgrade. And it turned out to be quite easy - but unfortunately some things haven't been ported to 1.9 yet. One of the main advantages of 1.9 is supposed to be increase in performance. It can be easily demonstrated by running recursive Fibonacci test. Unfortunately not even one 
of my applications needs faster Fibonacci :-). Having run most of our apps on 1.9 for around 2 weeks now I don't really see any tremendous performance increase or lower memory requirements. Several of our systems require extensive calculations (especially bigger accounting systems) but the performance there is dependent on many other factors like database speed, array sorting and grouping and so on and I am still working out the optimizations here.


What really really really helped me during the migration was David Black's book The Well-Grounded Rubyist. I heard him speaking several times but I was quite surprised by how well written the book was. It's really sharp
 and to the point without useless 'technicalities'. At the same time it's very readable and not dry like API reference.


Okay back to Rails. As I said most of the things went smooth BUT here's the list of some that didn't :-)

1) Mongrel doesn't work - not so much of a problem for me as I don't use it in production and in development I switched to Thin. 

2) File upload on nginx doesn't work - it seems to be an issue with Rack that is bundled with Rails 2.3. This is a bit of an issue as I was just starting to like nginx and I used it for some my lighter applications. 

3) Problems with encoding - this a single issue that I didn't manage to resolve or find a workaround. We've done some projects in Europe and they have to support slavic languages with all their funny characters. As 1.9 supports so many encodings I really didn't expect any problems there but I didn't manage to get pass the
incompatible character encodings: UTF-8 and ASCII-8BIT
when data from database contain special characters. There are several patches available in lighthouse here and here but non of them really worked for me - safe for
contact.name.force_enconding('utf-8')
4) send_data doesn't work on binary files with message
invalid byte sequence in UTF-8
. I used to use it for on the fly generated images but had to replace it with creating a temp file and then use
send_file

5) ruby zip hasn't been updated yet and still references ftools that have been replaced by fileutils. I had to change it manually along with File.move reference replaced with FileUtils.move

6) quite a number of plugins stopped working and as many are no longer supported I decided to ditch a few of them. I have never been a fan of the small rails plugins that only adding some syntactic sugar not just because they break on every update of Rails (even worse with Ruby upgrade) but mostly because of their obtrusiveness. Even though they make the code shorter it's usually much less readable as you have to learn and understand the plugin.

RailsBridge is Born

rails_bridge.pngI surely don’t have to introduce last week’s Ruby/Rails earthquake to anyone by now - it has been covered by a boatload of blogs (including this one), analyzed, argued over, rebutted, reddited, dugg and whatnot - suffice to say, it’s time to move on. It was a rather unpleasant drama piece, but fortunately it shed light on some problems the Ruby/Rails communities are facing, and (besides the ensuing trollfest and pointless arguing) it had a pleasant side-effect: a handful of people started to discuss how things could be made better, creating a small (and growing) but determined community: RailsBridge!

Say Hello to RailsBridge!

RailsBridge’s mission is “To create an inclusive and friendly Ruby on Rails community” (check out details on the homepage). Everyone is welcome to participate - with ideas, suggestions, design, code, new project proposals or just about anything that would make Rails a more open and accepted technology and community.

It’s very important to understand that we are not trying to form yet another oh-my-god-we-are-so-awesome Rails group of uber-hackers which looks down on everyone not in it’s ranks - for two reasons, at the very least:

  • RailsBridge is open to anyone! - you don’t have to be a Rails core commiter, coder with 20 years of experience or a renowned Rails blogger - it’s enough if you’d like to make the Ruby/Rails scene more welcoming in any way
  • We are trying to promote the exact opposite philosophy - we acknowledge that Rails’s image is somewhat tarnished because of the “rock star” attitude and we’d like to show by our actions that we are working on this and will eventually turn Ruby and Rails into a very welcoming and enjoyable community

Surely, this is quite a venture - our mission statement sounds great, but everyone can do the talking on just about whichever Gordian Knot out there, be it world hunger, world peace or friendly Rails. We’ll have to demonstrate that we mean serious business, and that’s why we need you - everyone’s ideas, insights and help matters! Check out the RailsBridge homepage to learn more about what are we up to and how to get involved.

File Downloads in Rails on Apache

Over the past half a year we worked on several systems for logistics/courier/delivery companies. It was quite fun as a big logistics and inventory control system was my very first project when I arrived to Asia 9 years ago. Last week we launched one of them. It was slightly different as most of our systems are used mainly internally but this one is also used by clients of the courier company to book deliveries. Each delivery requires printing of confirmation documents, internal routing documents, then signed and stamped documents are uploaded back to the system as a proof of delivery for clients. It has quite a number of other (technically) interesting features like integration with google maps to geo-tag the orders and notifying drivers in the area - I'll write about it soon.

As this courier company handles several hundreds of deliveries every day it requires quite a number of uploads/downloads of documents and images. The issue with uploads and downloads is that they will tie up rails processes for much longer than usual requests. The problem is that rails processes are quite expensive in terms of memory (around 70 - 100MB for simple operations). Do 20 concurrent uploads/downloads and all your rails processes and memory will get tied up - and quite wastefully as there is no 'dynamic' processing during upload or download. The process just sits there and waits for the file to get delivered. Luckily there's a simple way how to move this burden to Apache. This kind of upload/download processes will take only around 2MB on average and your rails processes are free to handle the dynamic requests.

To let apache handle file downloads use x_sendfile. All you need to do is install x_senfile module on apache and adjust you config a little. Here's the installation instructions for Ubuntu 8.04:
cd /usr/local/src
wget http://tn123.ath.cx/mod_xsendfile/mod_xsendfile.c
apxs2 -cia mod_xsendfile.c
Open apache config file and add the module line:
sudo vim /etc/apache2/apache2.conf
LoadModule xsendfile_module /usr/lib/apache2/modules/mod_xsendfile.so
Add the following 2 lines to your virtual host file
sudo vim /etc/apache2/sites-available/my_app
XSendfile on
XSendFileAllowAbove on
You will have to restart the apache and you should be done:
sudo /etc/init.d/apache2 restart
Now in your controller handling download add x_sendfile attribute and set it to true
 send_file("/home/test/big_file.zip",
           :filename => "big_file.zip",
           :type => "application/zip,
           :x_sendfile=> true)
The uploads are equally easy. You just need to apache modules libapreq2 and modporter.
wget http://mirror.nus.edu.sg/apache/httpd/libapreq/libapreq2-2.12.tar.gz
tar -xzf libapreq2-2.12.tar.gz
cd libapreq2-2.12
./configure --with-apache2-apxs=/usr/bin/apxs2
make
sudo make install
Now download and install modporter. Get it from the github. Either use the download link and unzip or just clone the repository
git clone git://github.com/actionrails/modporter.git
cd modporter
On Ubuntu I had to change reference to apxs in Rakefile as the in ubuntu it's apxs2.
vim Rakefile
Change the APXS line to the following:
APXS           = "apxs2"
No just run rake:
sudo rake
add the following to your apache2.conf
sudo vim /etc/apache2/apache2.conf
LoadModule apreq_module /usr/lib/apache2/modules/mod_apreq2.so
LoadModule porter_module /usr/lib/apache2/modules/mod_porter.so
To actually use it in your rails application you will need modporter plugin.
script/plugin install git://github.com/actionrails/modporter-plugin.git
Add the following to the application_controller.rb (application.rb before Rails 2.3)
class ApplicationController < ActionController::Base 
mod_porter_secret = "secret"
.
.
and just enable porter in you vhost file:
sudo vim /etc/apache2/sites-available/my_app
Porter On
PorterSharedSecret secret
And that's all now if you check the uploaded files they will be a ModPorter::UploadedFile Credit for this goes to Koz from theRailsWay.com - you can find more information here.