суббота, января 10, 2009

Specific or default templates in Rails

I want to develop admin controller to do simple CRUD operations on not-so-complex models. I want it to be DRY and to allow adding features to all of them easily. This is my second attempt, the first one was way too ugly: it used inheritance from admin's base controller and descendants passed customization parameters to base class; also, have used custom "render" methods to find template for specific controller or, if the specific template is missing, fall back to general template.

And then it broke. Yeah, this could be fixed, but the whole idea seemed bad.

I just finished the second attempt and it looks promising.

First, it includes only one controller and uses routes to determine the actual model to operate on. Every route includes additional parameter that is passed to controller that contains name of model:
map.resources :users, :controller => 'items',
    :requirements => { :item_type => 'User' }
map.resources :regions, :controller => 'items',
    :requirements => { :item_type => 'Region' }

Second, the most troublesome was to make it work like this: I have default views in app/views/items... I wanted to have subdirectories with name of particular model that would contain overrides to default templates:

Regions doesn't need custom 'item' partial because default one (which outputs only item.to_s) fits well.

Obviously, controller's view_paths should be manipulated to search first in concrete subfolders and then in default place. But the problem was that it always tried to prepend controller name and so find it in corresponding subfolder. It's not very nice being forced to create additional directories like this:

Well, after some digging the way to do it elegantly I found this solution:

1. Redefine controller's controller_path to return nil. After this all templates are searched in app/views/ instead of app/views/items, so it needs to be fixed, so
2. Redefine controller's view_paths to base in app/views/items.
3. Based on current item type prepend corresponding view path.

The result is as follows:
class ItemsController < ApplicationController
  def self.controller_path
  self.view_paths = [ RAILS_ROOT + '/app/views/items' ]

  before_filter :setup_model

  # ... action code


  def setup_model
    @model  = params[:item_type].constantize
    prepend_view_path(RAILS_ROOT + '/app/views/items/' + params[:item_type].underscore)

Happy coding!

пятница, января 09, 2009

Back to Rails

Lately I wasn't doing any Rails development because I was busy re-learning C/C++ "the right way". Turns out it's not that painful as they say.

Then I had to do some Rails coding using latest and greatest Rails 2.2 and I find that that the simple things are broken (or somewhat obscure).

Case 1.

I was developing some general resource controller and wanted to spec out a redirect after #create action. My controller should not be bound to any specific resource url (as it is planned to bind many different resources with that controller), so I decided to make use of Rails' url rewriting. Spec was like this:

it "should redirect to #index after item creation" do
  post :create
  response.should redirect_to(:action => :index)

And this spec failed because of MethodNotAllowed: Only get and post requests are allowed.

WTF? It's the very basic functionality. Why does it fail to work ?

After half a day of investigation I found out that this is a bug in rspec-rails package and redirect_to matcher fails to handle hash-like url specifier.

Case 2.

I wanted to handle AR::RecordInvalid exceptions with one handler to DRY up controller code. I wrote corresponding "rescue_from" call in controller and wrote specs (in fact, the reverse order: specs then code). And those specs failed too.

The other several hours of investigation revealed hidden option: RSpec-rails overrides default Rails' error handling (which is based on ActiveSupport's #rescue_from feature) with pass-through exception handler, and you have to write following code to make YOUR exception handlers work:
before do

Yeah, Rails is nice, but upgrading to a new version is a pain.