Tuesday, 22 December 2009

The named_scope in Rails

The named_scope method allows you to set up shortcuts to narrow searches within set parameters. It is invoked in the model, something like this:
class Computer < ActiveRecord::Base
named_scope :active, :conditions => {:in_use => true}
named_scope :networked, :conditions => "(network_id <> \"\")"
named_scope :recent, lambda { {:conditions => ["created_at > ?", 1.months.ago ] } }
end

Note the use of the lambda. If you simply compared the created_at to the date, you would be comparing it to the date the named_scope was invoked, rather than the current date (you might object that Rails reloads the model each time, so the difference might not be so much, but that does not seem to affect your named_scopes). You might also want to do this if you want to access another model. Say your database has a list of types, and this model has a field for the ID of one of those types. You want to collect all the records by the name of the type, so you need to access the table for the other model to get the ID. But that model might not be loaded when this one is, so you have to delay getting the ID. Or perhaps you want to send parameters to a named_scope through the lambda.
# Use a lambda to access a later loading model
named_scope :blue, lambda {
{ :conditions => ["(colour_id = #{Colour.find_by_name('blue').id})"] }
}
# Use a lambda to allow parameters
named_scope :located, lambda { |loc|
{ :conditions => {:location_id => loc } }
}

You can also use named_scopes for other things, such as ordering.
named_scope :ordered, :order => 'created_at ASC'
Now to get an array of computers in use, just do this:
Computer.active

You can chain named_scopes together, and also with find. Now I can list all the blue computers at location 5, in ascending order of record creation, or all the networked computers running WinXP just like this:
Computer.blue.located(5).ordered
Computer.networked.find_by_os("winxp")


References
http://ryandaigle.com/articles/2008/8/20/named-scope-it-s-not-just-for-conditions-ya-know
http://snippets.aktagon.com/snippets/210-How-to-use-named-scope-in-Rails
http://jitu-blog.blogspot.com/2009/07/looking-into-rails-namedscope.html
http://stackoverflow.com/questions/137630/encapsulating-sql-in-a-namedscope

Notes
A named_space can be used with will_paginate without a problem:
@samples = Sample.outstanding.ordered.paginate :page => params[:page], :per_page => 16

I found that I had to restart my web server when trying these out to get the named_scopes to reload.

Also, if named_scope does not like your condition, it just fails to define a new method. There is no warning or hint about what could be wrong. All you get is a method_missing complaint when you try to invoke it.


Struggling with Ruby: Contents Page

Wednesday, 16 December 2009

Using Sub-directories in Rails Projects

If you have a big project, you are going to want to break it up into parts, grouping, say, controllers for a certain part in one sub-directory. I found a couple of blog pages saying how to do this (basically you set up a name space in routes.rb, and prefix the controller class names with that name space, with the views in a similarly-named subdirectory):

http://myles.eftos.id.au/blog/2005/11/15/sub-directories-on-rails/
http://www.purpleworkshops.com/articles/grouped-controllers

However, they paint it rather simpler than it really is.

The Namespace for Controllers
Okay, so I have a number of controllers relating to a sample logging system, and I want to put them all inside a directory called sample_log. This corresponds to a Ruby namespace (because I might have a controller called TopController in each part of the system, so Rails needs a way to guarantee they are distinct). All the views need to be in their own subdirectory too, with the same name. Each of my controllers' class name needs to be prefixed with the name of the namespace:
class SampleLog::SamplesController < ApplicationController

Then you need to set up your routes, so that Rails knows you are using a namespace:
map.namespace :sample_log do |submap|
submap.resources :samples
# other controllers
end

At this point, you should be able to get pages, with a URL something like this:
http://localhost:3000/sample_log/samples/


Links
So far so good. The tricky part (especially if you already have a project that you want to do this to) is handling links in and out of the subdirectory. The standard link_to method invocation looks like this:
link_to 'Cylinders', :controller => 'cylinders'

This will generate a link within the sub-directory. How do you link to other subdirectories, or to the top level? Append a slash to your controller name, like this:
link_to 'Cylinders', :controller => '/cylinders'
link_to 'Samples', :controller => '/sample_log/samples'

The various helper methods like new_samples_path and edit_samples_path seem to work fine, but require the directory name to be appended to the method name (run rake routes to see the helper methods listed):
link_to 'List', sample_log_generic_samples_path
link_to 'Show', sample_log_generic_sample_path(@sample)
link_to 'Edit', edit_sample_log_generic_sample_path(@sample)
redirect_to sample_log_samples_path

However, Rails does not seem to be able to cope with links like this (use the helper methods just mentioned instead):
link_to 'Show', @sample
redirect_to @sample

For some reason, Rails does not provide a helper method for destroy, so you will need to given that link through the action:
link_to 'Destroy', { :action => :destroy, :id => sample.id },
:confirm => 'Are you sure?',
:method => :delete

If you use the form_for functionality, or polymophic methods (presumably with STI), you need to send the directory as a parameter (as a symbol or string), wrapped up in an array:
form_for([:sample_log, @sample]) do |f|
link_to 'Edit', edit_polymorphic_path([:sample_log, @samples[1]])


Pointing to templates and partials
Any time you explicitly invoke a template in a controller, you will obviously need to change that so it points to the correct directory, and in your views, your partials will need to be adjusted similarly if you are using the full directory path (which you might do if the partial is in the directory of another controller).
# This works (as does the implicit version, i.e., no render statement at all)
render :action => 'show'

# This
render :template => 'samples/show'
# ...becomes this
render :template => 'sample_log/samples/show'

# This
render :partial => 'samples/list_table_row',
:collection => @samples
# ... becomes this
render :partial => 'sample_log/samples/list_table_row',
:collection => @samples

# But this stays the same
render :partial => 'list_table_row',
:collection => @samples


Functional Testing
You will need to modify your functional tests. Just as with the controllers, they need to go into an identically name directory, and put into the correct name space. Rake will find the tests in subdirectories without any prompting.
class SampleLog::SamplesControllerTest < ActionController::TestCase

Besides that, the other major change is to make sure all your paths are defined using helper methods, as in the controller.

Integration Testing
These tests will need to be modified so your HTTP requests point to the right URL, and the templates you expect are in the right folder
# This
get "/samples/home"
# ... becomes
get "/sample_log/samples/home"

# This
assert_template "samples/home"
# ... becomes
assert_template "sample_log/samples/home"


Models
Controllers and views are closely coupled, so if you want your controllers in a subdirectory, your views must be in an identically named subdirectory. On the other hand, your models can be handled independantly (when I was trying this out, I moved the controllers and views of one section first, and had the project working fine with the corresponding models still in the top app/model directory, then I moved all the models for all the sections, and again had it working fine, then moved the remaining controllers and views). That said, it would seem to me that best practice has to be to have your directory structure identical for controllers, views and models.

You have two choices with the models. The first is to use the same namespace concept as the controllers. In this case, the database name also needs to have the nampespace prepended.
sample_log/sample.rb    # The file name
SampleLog::Sample # The class name
sample_log_samples # The database

That is probably the best way to go if you are starting from scratch, but if you are modifying an existing project, you could find that there are a lot of changes required (and so a lot of potential for errors).

The alternative is to forget the name spaces, and just make sure Rails can find your files. Two (nearly identical) approaches can be seen here:

http://toolmantim.com/articles/keeping_models_in_subdirectories
http://www.paperplanes.de/2007/5/2/namespacing_your_rails_model_an.html

The basic idea is that you tell Rails about the location of your models. Rails keeps an array of paths that it loads from, so you need to add your new paths to that in config/environment.rb. This code snippet adds three folders, sample_log, computer_log and user_log and would go inside the Rails::Initializer.run do |config| block.
ary = %w(sample computer user)
ary.each do |dir|
config.load_paths << "#{RAILS_ROOT}/app/models/#{dir}_log" end

That is all you need to do for your models. The unit tests can be shifted into their own subdirectory if you want (and I think you should), but none of the unit test or model files need to be changed at all.

ActionMailer
One last point. If you put your ActionMailer templates in a subdirectory, you need to tell ActionMailer where to find them. You do that in config/environment.rb, inside the big Rails::Initializer.run block, like this:
config.action_mailer.template_root = "#{RAILS_ROOT}/app/views/#{my_sub_dir}"



Struggling with Ruby: Contents Page