Friday, June 27, 2008

Refactoring block helpers with more block helpers


This post shows how to build helpers around create_table and form_for, which are my two most commonly used block helpers in Rails. This stuff is obvious to a Rubyist, but not everyone is a Rubyist.

create_table

If you reuse the same columns between tables, your migrations can end up containing a lot of duplicate code. For example, let's say you are making a content management system for your office supply company. Since each product is sufficiently different, you want to make a different ActiveRecord model for each product.

As a result, your migration to add products will look like this:



 1 create_table :tapes do |t|
2 t.string :product_name
3 t.integer :category
4 t.integer :length, :width
5 t.float :price
6 t.integer :total_sold, :total_in_stock, :default => 0
7 t.timestamps
8 end
9
10 create_table :staplers do |t|
11 t.string :product_name
12 t.integer :capacity, :type
13 t.string :color
14 t.float :price
15 t.integer :total_sold, :total_in_stock, :default => 0
16 t.timestamps
17 end


A polymorphic model can be used in this scenario. But what if you have 15 very unique products, and each needs their own indexing strategy? Nonetheless, that is not the point. The problem here is that duplicate columns exist between each table, and this can be refactored.

Simply create a new helper method create_product_table that wraps create_table
:


 1 def self.create_product_table(*args)
2 create_table(*args) do |t|
3 t.string :product_name
4 t.float :price
5 t.integer :total_sold, :total_in_stock, :default => 0
6 t.timestamps
7 yield(t) if block_given?
8 end
9 end


Then use the helper to refactor your existing migration. It removes the duplication, and makes the code more readable:


 1 create_product_table :tapes do |t|
2 t.integer :category
3 t.integer :length, :width
4 end
5
6 create_product_table :staplers do |t|
7 t.integer :capacity, :type
8 t.string :color
9 end



form_for

The solution for form_for is very similar to create_table.

In this arbitrary but typical example, your awesome web 2.0 website supports tagging of just about anything. Comments, Posts, Images, Movies, all taggable. That means all your forms need a "Tags:" input box.

Rather than duplicating the same code in each form for the tagging input, make a taggable_form_for helper:



1 def taggable_form_for(*args)
2 form_for(*args) do |f|
3 yield(f)
4 concat(f.label :tags, 'Tags')
5 concat(f.text_field :tags)
6 end
7 end


The forms are a bit tricky because you must call concat so that the html is emitted in the correct location.

Broken 'pre' tags with Blogger


Sometime in the last week, a significant change was introduced into Blogger. Previously, any content inside of a <pre> section was left alone. Now, a <br/> tag is insterted for every line break within a <pre> tag, which results in a double line break. As a result, all of my code snippets have an extra line break.

I will write a more relevant post tomorrow. I just wanted to explain why my existing snippets look wrong.

Tuesday, June 24, 2008

Googtaculous



To follow up on my post about Google's Ajax library API, I made a plugin that you can install to make your life easier.



1 script/plugin install git://github.com/matthuhiggins/googtaculous.git



The name is pretty clever, eh?

Friday, June 20, 2008

Clearing memcache without restart



Up until now, I always restarted memcache to clear it. However, you can make a rake task to do so:


1 namespace :cache do
2 desc 'Clear memcache'
3 task :clear => :environment do
4 ActionController::Base.cache_store.clear
5 end
6 end



Drop this into /lib/tasks/cache.rake. Now just run the task:


1 rake cache:clear



This only works if you have configured action_controller.cache_store to be memcache.

Have a good weekend!

Monday, June 16, 2008

table_for @appetizers


My First Plugin
I woke this morning realizing that I suck with git and have never made a public plugin. Finding this unacceptable, I set out today to make those truisms become falsehoods. The result is table_for, an ActionView plugin hosted on github.

The Distasteful Tables
You come up with a brilliant recipe site that focuses on appetizers. The idea seems perfect, but where will you put the appetizers? A table, of course!

Let's make an AppetizerController to retrieve those tasty morsels:



1 class AppetizerController < ApplicationController
2 def index
3 @appetizers = Appetizer.all
4 end
5 end


Then create a table in the view:


 1 <table>
2 <tr>
3 <th>Name</th>
4 <th>Price</th>
5 <th>Tastiness</th>
6 <th>Chef</th>
7 <th>Description</th>
8 </tr>
9
10 <% for appetizer in @appetizers %>
11 <tr>
12 <td><%=h appetizer.name %></td>
13 <td><%=h appetizer.price %></td>
14 <td><%=h appetizer.tastiness %></td>
15 <td><%=h appetizer.chef %></td>
16 <td><%=h appetizer.description %></td>
17 </tr>
18 <% end %>
19 </table>

Mmm, delicious. But wait, I see some duplication.

Can we get a table_for those appetizers?
Let's use table_for to trim down the code. First git it:


1 ruby script/plugin install git://github.com/matthuhiggins/table_for.git


Refactor the table:


1 <% table_for @appetizers do |t| %>
2 <% t.name %>
3 <% t.price %>
4 <% t.tastiness %>
5 <% t.chef %>
6 <% t.description %>
7 <% end %>


There we go. Much better. A week later, you find that your prices should be labelled with '$', descriptions are way too long, and the tastiness field needs to be right aligned. Not to mention, there are multiple tables on the page requiring different class names. Easy:


1 <% table_for @appetizers, :html => {:class => 'appetizers'} do |t| %>
2 <% t.name %>
3 <% t.price, :helper_method => :number_to_currency %>
4 <% t.tastiness, :html => {:class => 'tastiness'} %>
5 <% t.chef %>
6 <% t.description, :helper_method => [:truncate, 100] %>
7 <% end %>


Other options exist, which can be found in the RDoc. I think something similar to table_for should go into ActionView, and with a few rounds of feedback, maybe I will suggest a patch.

Meanwhile, Gregg from RailsEnvy asked me to change my google library post into a plugin. With my newfound plugin skillz, expect that soon.


Saturday, June 14, 2008

Rails: Where to put the 'other' files


Introduction
When starting a Rails project, four golden folders are predefined: Models, Views, Controllers, Helpers. Could we possibly need anything more? In my experience, the answer is yes. This leads to the question of, where do these extra files go? Compared to models, views, controllers and helpers, Rails provides little guidance about where to put this other stuff. Fortunately, Rails comes packaged with two folders to put additional Ruby code: lib and config/initializers. Each have their unique properties, and they can be used together to neatly package your Ruby code.


The weakling lib folder
In Agile Web Development on Rails, the authors describe how to use a naming convention, such that your lib file names match the classes they contain. For example, a class of FooBar must be in the file foo_bar.rb. Classes can even be namespaced. For example, Foo::Bar will be loaded if placed inside of foo/bar.rb.

Files in lib are not loaded when Rails starts. Rails has overridden both Class.const_missing and Module.const_missing to dynamically load the file based on the class name. In fact, this is exactly how Rails loads your models and controllers.

While the functionality of the lib folder is nice, it is extremely limited. It prevents writing modules to extend or override the functionality of another class, because they will never get loaded.


The bloated initializers folder
Rails first loads the framework, then plugins, and finally, it loads the files inside of config/initializers (in alphabetic order, no less). Previous to 2.0, naughty developers pasted code at the bottom of environment.rb, and the initializer folder was a welcome convention to help organize this madness.

I find myself using the initializer folder to configure plugins such as HAML, or loading the action_mailer configuration out of a YML file.

Unfortunately, I see many code snippets showing how to extend the functionality of Rails simply by creating a new file in initializers, and pasting in the code. In a matter of fact, my last post suggested doing this. This is OK in light doses, but after a few months of this practice, you will find yourself with an initializers folder with many files and little structure, not to mention that they get loaded in alphabetic order.


Lib folder, meet initializers
I think I hurt lib and initializer's feelings. I feel bad about this, so maybe it is time to introduce them to each other. A simple example is in order:

We want to add a new 'user_logger' mixin to ActionController::Base. Any controller can choose to enable 'user_logger', and it will put the current user's name in the log file for each request they make. This won't be the last time we extend ActionController's functionality in this project, so let's do it the Right Way.

We first write the module. Start by creating a new folder inside of /lib called rails_extensions. Now paste this code inside the file user_logger.rb:



 1 module ActionControllerExtra
2 module UserLogger
3 def self.included(base)
4 base.extend(ClassMethods)
5 end
6
7 module ClassMethods
8 # Logs that a user requested the current action:
9 #
10 # MyController < ApplicationController
11 # user_logger
12 # end
13 #
14 # The same options for ActionController::Filters are available. For example,
15 # to only log the user in the create and destroy actions:
16 #
17 # MyController < ApplicationController
18 # user_logger :only => [:create, :destroy]
19 # end
20 #
21 def user_logger(options = {})
22 include InstanceMethods
23 before_filter :log_user, options
24 end
25 end
26
27 module InstanceMethods
28 def log_user
29 logger.info("#{session[:username]} requested #{controller_name}/#{action_name}")
30 end
31 end
32 end
33 end



Next, create the file rails_extensions.rb in /config/initializers. Paste this code inside:

1 require 'rails_extensions/user_logger'
2
3 ActionController::Base.class_eval do
4 include ActionController::UserLogger
5 end


You may want to create an even deeper directory structure inside of rails_extensions. In my larger projects, I keep a folder for action_controller, active_record, etc. Nonetheless, using initializers and lib together can help you avoid a bloated initializer folder and weak set of functionality in the lib folder.


Tuesday, June 10, 2008

Using Google Ajax Libraries API with Ruby on Rails


Intro

The new Google Ajax Libraries API can help you offload bandwidth from your site. Their servers are configured with gzip, expires headers and e-tags, which leads to good scores from YSlow. I will show two ways to use Google's new API to offload Ruby on Rails javascript (prototype and scriptaculous). In both solutions, your existing calls to javascript_include_tag can be left the same, because all of the magic is handled beneath the covers.


Implementation #1: Create a module
This first example is a module that chains the expand_javascript_sources
method found in ActionView::Helpers::AssetTagHelper. If prototype or a scriptaculous file is loaded via javascript_include_tag, we will replace the normal functionality with a path to the file on Google's servers. Start by creating the file /config/initializers/google_asset_tag_helper.rb. Paste in the following:

 1 module ActionView
2 module Helpers
3 module GoogleAssetTagHelper
4 GOOGLE_PATHS = {
5 'prototype' => 'http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.2/prototype',
6 'controls' => 'http://ajax.googleapis.com/ajax/libs/scriptaculous/1.8.1/controls',
7 'dragdrop' => 'http://ajax.googleapis.com/ajax/libs/scriptaculous/1.8.1/dragdrop',
8 'effects' => 'http://ajax.googleapis.com/ajax/libs/scriptaculous/1.8.1/effects'
9 }
10
11 def self.included(base)
12 base.send :alias_method_chain, :expand_javascript_sources, :google unless ActionController::Base.consider_all_requests_local
13 end
14
15 def expand_javascript_sources_with_google(sources)
16 google_sources, normal_sources = sources.partition { |source| GOOGLE_PATHS.include? source }
17 GOOGLE_PATHS.values_at(*google_sources) + expand_javascript_sources_without_google(normal_sources)
18 end
19 end
20 end
21 end
22
23 ActionView::Base.class_eval do
24 include ActionView::Helpers::GoogleAssetTagHelper
25 end


This defines GoogleAssetTagHelper, which chains expand_javascript_sources. The module is included into ActionView::Base. Notice that on line 12, the original method is only aliased if ActionController::Base.consider_all_requests_local is false. Since this is set to true in development mode, it is a sufficient way to determine whether or not Google AJAX Library should be used.
If using the remote libraries in development is desirable, the condition can be safely removed.


Implementation #2: Assign lambda to ActionController::Base.asset_host
An alternative solution uses ActionController::Base.asset_host to determine the root path to the javascript files. In /config/environments/production.rb, you find:

1 # config.action_controller.asset_host = "http://some.asset.host/"

As of Rails 2.0.2, asset_host can a string or Proc. Since we want to conditionally change the asset_host based on what file is being include, a Proc is needed:

 1 google_paths = {
2 'prototype' => 'http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.2/',
3 'controls' => 'http://ajax.googleapis.com/ajax/libs/scriptaculous/1.8.1/',
4 'dragdrop' => 'http://ajax.googleapis.com/ajax/libs/scriptaculous/1.8.1/',
5 'effects' => 'http://ajax.googleapis.com/ajax/libs/scriptaculous/1.8.1/'
6 }
7
8 config.action_controller.asset_host = Proc.new do |source, request|
9 google_asset = google_paths.keys.detect { |asset| source.starts_with?("/javascripts/#{asset}") }
10 google_asset ? google_paths[google_asset] : "#{request.protocol}#{request.host_with_port}"
11 end

In this example, the code assumes that you do not already have an asset_host defined. If you do, simply replace the false expression on line 10 with your original asset_host string.

(The variable google_paths is defined inside of production.rb for brevity. However, I recommend defining it as a constant elsewhere in your own project.)

Using the javascript_include_tag
As mentioned in the introduction, your existing javascript_include_tag calls remain the same:


1 javascript_include_tag 'prototype', 'effects', 'controls', 'application'